mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
669f659843 | ||
|
|
a718862b37 | ||
|
|
52898e951e | ||
|
|
0f03fd1ee1 | ||
|
|
10e06a4533 | ||
|
|
98e38ebfd8 | ||
|
|
9660e20176 | ||
|
|
21a7ec9fec | ||
|
|
7d123ff8c5 | ||
|
|
2af6ac504d | ||
|
|
6c8d1c4e77 | ||
|
|
52d3e1b34b | ||
|
|
bf6fcc9020 | ||
|
|
a0b756ebaa | ||
|
|
5e8a55f949 | ||
|
|
f9218584f4 | ||
|
|
228446979f | ||
|
|
aa37d86959 | ||
|
|
0e9079fa2e | ||
|
|
58c058c1a5 | ||
|
|
f390556a87 | ||
|
|
b7378da46e | ||
|
|
0c8c926aac | ||
|
|
81d8592ee1 | ||
|
|
af827f3626 | ||
|
|
91b269fc36 | ||
|
|
1605a57df6 | ||
|
|
5cd23b843a | ||
|
|
d46f1080f8 | ||
|
|
9a541ebf05 | ||
|
|
dba416f5eb | ||
|
|
7d7da9bf98 | ||
|
|
4425efd3c2 | ||
|
|
c6bb9e97fb | ||
|
|
9c7adb7a14 | ||
|
|
7b4faccf05 | ||
|
|
0cd3419e09 | ||
|
|
e49dedf6b1 | ||
|
|
bee4e05b5f | ||
|
|
a5419b49ee | ||
|
|
84e60283b8 | ||
|
|
96206384c0 | ||
|
|
78c61d5afa | ||
|
|
ee712d9a9d | ||
|
|
a1e8c2849a | ||
|
|
54751a715c | ||
|
|
a2907a6e6d | ||
|
|
33236aaa47 | ||
|
|
cd6c7ce7fa | ||
|
|
363baece4f | ||
|
|
1db0e28346 | ||
|
|
7366bbb197 | ||
|
|
7959f84bba | ||
|
|
0c96bf61ef | ||
|
|
39ce60c93a | ||
|
|
8ad78ffef8 | ||
|
|
66b499b8e3 | ||
|
|
22406f47f7 | ||
|
|
72f782b589 | ||
|
|
cf3df951a9 | ||
|
|
4085df913b | ||
|
|
d93f3aca51 | ||
|
|
b180a162cd | ||
|
|
1bf3ff5e1b | ||
|
|
0def477b63 | ||
|
|
337e1ba206 | ||
|
|
fe2d80046c | ||
|
|
f16a9ddb86 | ||
|
|
5f6c207721 | ||
|
|
988d686418 | ||
|
|
89e654af80 | ||
|
|
2ab1bbaa2c | ||
|
|
b43626b5a2 | ||
|
|
5e4b3e924f | ||
|
|
66b0173e20 | ||
|
|
67f6b1080e | ||
|
|
b56b897260 | ||
|
|
f031f4d560 | ||
|
|
d0e119fb50 | ||
|
|
7892e50aa2 | ||
|
|
bff3582136 | ||
|
|
bdf95903ce | ||
|
|
c1e6bc5d60 | ||
|
|
da588ce0ae | ||
|
|
d0680c3753 | ||
|
|
905d0d5131 | ||
|
|
d347ed9862 | ||
|
|
8611f765a3 | ||
|
|
962f1c0310 | ||
|
|
473a66719b | ||
|
|
aeb43a04f6 | ||
|
|
49a35985a1 | ||
|
|
21b789e08c | ||
|
|
51387ad97e | ||
|
|
290d584ac9 | ||
|
|
160b238058 | ||
|
|
938255df6f | ||
|
|
4230da0fd9 | ||
|
|
fee3715d30 | ||
|
|
689bd093be | ||
|
|
77461d7834 | ||
|
|
ee5894c296 | ||
|
|
07898004b0 | ||
|
|
630164cd51 | ||
|
|
981319e553 | ||
|
|
fedd32ea7a | ||
|
|
e57574f10a | ||
|
|
3f0a0b33b5 | ||
|
|
c21217d50c | ||
|
|
e44c8ae940 | ||
|
|
1da187c373 | ||
|
|
36ad42beb2 | ||
|
|
c0560ad3cc | ||
|
|
c318762f82 | ||
|
|
5d373c0137 | ||
|
|
3aea998bd2 | ||
|
|
c1ca48a32a | ||
|
|
2f0fcddc29 | ||
|
|
329565251a | ||
|
|
06a223376c | ||
|
|
47e8ad3aac | ||
|
|
c4fb3a8c04 | ||
|
|
9d4121c3b7 | ||
|
|
2eb1fe8547 | ||
|
|
e933774e6c | ||
|
|
0b994d1c46 | ||
|
|
381b150c2b | ||
|
|
53ebac9363 | ||
|
|
a0638dd5c4 | ||
|
|
5b741de896 | ||
|
|
d7f587216d | ||
|
|
019f00a34a | ||
|
|
9684b2d4ac | ||
|
|
2e190c9ea9 | ||
|
|
601a48071f | ||
|
|
bf885f94e4 | ||
|
|
7d4be819b8 | ||
|
|
26a7fa836c | ||
|
|
187329b006 | ||
|
|
8375008cfa | ||
|
|
16333fa1aa | ||
|
|
72deb005a6 | ||
|
|
18509a0ca4 | ||
|
|
e63d0dcd9e | ||
|
|
62ba3984bd | ||
|
|
db170aac9e | ||
|
|
5c7e73e2e0 | ||
|
|
f772296dff | ||
|
|
f6a26ac165 | ||
|
|
4e3b3442d2 | ||
|
|
2752770ce2 | ||
|
|
1840609d53 | ||
|
|
4f23090a5c | ||
|
|
898b51c593 | ||
|
|
2494418208 | ||
|
|
0fec70fe69 | ||
|
|
bcf90d71a2 | ||
|
|
f8f7ac0af5 | ||
|
|
d6c2705bd6 | ||
|
|
10f8b9f130 | ||
|
|
1e601288fa | ||
|
|
b1032761c8 | ||
|
|
c532c361c0 | ||
|
|
ec8dca90d6 | ||
|
|
a9f814a515 | ||
|
|
c4bbeaaccc | ||
|
|
0fd5ab02e9 | ||
|
|
745979074a | ||
|
|
8ae6863266 | ||
|
|
4fd7f0e949 | ||
|
|
732f0b55dc | ||
|
|
c0ec0f1343 | ||
|
|
aa6e550ba2 | ||
|
|
2ffaf59238 | ||
|
|
6c13fdbc46 | ||
|
|
35941ddf7f | ||
|
|
3ae976c183 | ||
|
|
999666f0eb | ||
|
|
1812074231 | ||
|
|
53eb32e620 | ||
|
|
50bd0b796d | ||
|
|
a02d80a2ae | ||
|
|
71a7eea8ad | ||
|
|
2b927caa60 | ||
|
|
053d958f9a | ||
|
|
8d25d0a653 | ||
|
|
62eb131f59 | ||
|
|
40eb7c79bb | ||
|
|
dabc9eb09b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,3 +39,4 @@ logs/*
|
||||
.vagrant/
|
||||
release/*
|
||||
releashe
|
||||
/apps/script.py
|
||||
|
||||
288
README.md
288
README.md
@@ -1,28 +1,28 @@
|
||||
# JumpServer 多云环境下更好用的堡垒机
|
||||
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
|
||||
<h3 align="center">多云环境下更好用的堡垒机</h3>
|
||||
|
||||
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
|
||||
[](https://github.com/jumpserver/jumpserver/releases)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0"><img src="https://shields.io/github/license/jumpserver/jumpserver" alt="License: GPL v2"></a>
|
||||
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
|
||||
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
</p>
|
||||
|
||||
--------------------------
|
||||
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|
||||
|
||||
|
||||
|《新一代堡垒机建设指南》开放下载|
|
||||
|------------------|
|
||||
|本白皮书由JumpServer开源项目组编著而成。编写团队从企业实践和技术演进的双重视角出发,结合自身在身份与访问安全领域长期研发及落地经验组织撰写,同时积极听取行业内专家的意见和建议,在此基础上完成了本白皮书的编写任务。下载链接:https://jinshuju.net/f/E0qAl8|
|
||||
|
||||
--------------------------
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
改变世界,从一点点开始。
|
||||
改变世界,从一点点开始 ...
|
||||
|
||||
|
||||
## 特色优势
|
||||
### 特色优势
|
||||
|
||||
- 开源: 零门槛,线上快速获取和安装;
|
||||
- 分布式: 轻松支持大规模并发访问;
|
||||
@@ -33,247 +33,28 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
|
||||
## 功能列表
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td rowspan="11">身份认证<br>Authentication</td>
|
||||
<td rowspan="7">登录认证</td>
|
||||
<td>资源统一登录与认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LDAP/AD 认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS 认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OpenID 认证(实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAS 认证 (实现单点登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>钉钉认证 (扫码登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>企业微信认证 (扫码登录)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">MFA认证</td>
|
||||
<td>MFA 二次认证(Google Authenticator)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS 二次认证</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>登录复核</td>
|
||||
<td>用户登录行为受管理员的监管与控制:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>登录限制</td>
|
||||
<td>用户登录来源 IP 受管理员控制(支持黑/白名单)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="11">账号管理<br>Account</td>
|
||||
<td rowspan="2">集中账号</td>
|
||||
<td>管理用户管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>系统用户管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">统一密码</td>
|
||||
<td>资产密码托管</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>自动生成密码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>自动推送密码</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密码过期设置</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">批量改密</td>
|
||||
<td>定期批量改密:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多种密码策略:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>多云纳管 </td>
|
||||
<td>对私有云、公有云资产自动统一纳管:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>收集用户 </td>
|
||||
<td>自定义任务定期收集主机用户:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>密码匣子 </td>
|
||||
<td>统一对资产主机的用户密码进行查看、更新、测试操作:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="17">授权控制<br>Authorization</td>
|
||||
<td>多维授权</td>
|
||||
<td>对用户、用户组、资产、资产节点、应用以及系统用户进行授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">资产授权</td>
|
||||
<td>资产以树状结构进行展示</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>资产和节点均可灵活授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>节点内资产自动继承授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>子节点自动继承父节点授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">应用授权</td>
|
||||
<td>实现更细粒度的应用级授权</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MySQL 数据库应用、RemoteApp 远程应用:small_orange_diamond: </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>动作授权</td>
|
||||
<td>实现对授权资产的文件上传、下载以及连接动作的控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>时间授权</td>
|
||||
<td>实现对授权资源使用时间段的限制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>特权指令</td>
|
||||
<td>实现对特权指令的使用(支持黑白名单)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>命令过滤</td>
|
||||
<td>实现对授权系统用户所执行的命令进行控制</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件传输</td>
|
||||
<td>SFTP 文件上传/下载</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件管理</td>
|
||||
<td>实现 Web SFTP 文件管理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>工单管理</td>
|
||||
<td>支持对用户登录请求行为进行控制:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">访问控制</td>
|
||||
<td>登录资产复核(通过 SSH/Telnet 协议登录资产):small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>命令执行复核:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>组织管理</td>
|
||||
<td>实现多租户管理与权限隔离:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="8">安全审计<br>Audit</td>
|
||||
<td>操作审计</td>
|
||||
<td>用户操作行为审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">会话审计</td>
|
||||
<td>在线会话内容监控</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>在线会话内容审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>历史会话内容审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">录像审计</td>
|
||||
<td>支持对 Linux、Windows 等资产操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持对 RemoteApp:small_orange_diamond:、MySQL 等应用操作的录像进行回放审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>指令审计</td>
|
||||
<td>支持对资产和应用等操作的命令进行审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件传输</td>
|
||||
<td>可对文件的上传、下载记录进行审计</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="20">数据库审计<br>Database</td>
|
||||
<td rowspan="2">连接方式</td>
|
||||
<td>命令方式</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Web UI方式 :small_orange_diamond:</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td rowspan="4">支持的数据库</td>
|
||||
<td>MySQL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Oracle :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MariaDB :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="6">功能亮点</td>
|
||||
<td>语法高亮</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SQL格式化</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持快捷键</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持选中执行</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SQL历史查询</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>支持页面创建 DB, TABLE</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">会话审计</td>
|
||||
<td>命令记录</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>录像回放</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**说明**: 带 :small_orange_diamond: 后缀的是 X-PACK 插件有的功能
|
||||
|
||||
## 快速开始
|
||||
### 快速开始
|
||||
|
||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||
- [完整文档](https://docs.jumpserver.org)
|
||||
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
|
||||
- [手动安装](https://github.com/jumpserver/installer)
|
||||
|
||||
## 组件项目
|
||||
### 组件项目
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
|
||||
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
## 贡献
|
||||
### 社区
|
||||
|
||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose) 或加入到我们的社区当中进行进一步交流沟通。
|
||||
|
||||
#### 微信交流群
|
||||
|
||||
<img src="https://download.jumpserver.org/images/weixin-group.jpeg" alt="微信群二维码" width="200"/>
|
||||
|
||||
### 贡献
|
||||
如果有你好的想法创意,或者帮助我们修复了 Bug, 欢迎提交 Pull Request
|
||||
|
||||
感谢以下贡献者,让 JumpServer 更加完善
|
||||
@@ -282,16 +63,29 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/koko/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
|
||||
</a>
|
||||
|
||||
## 致谢
|
||||
<a href="https://github.com/jumpserver/lina/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/luna/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
### 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
|
||||
|
||||
## JumpServer 企业版
|
||||
### JumpServer 企业版
|
||||
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
|
||||
|
||||
## 案例研究
|
||||
### 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
@@ -302,7 +96,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
|
||||
## 安全说明
|
||||
### 安全说明
|
||||
|
||||
JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装.
|
||||
|
||||
@@ -312,7 +106,7 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
## License & Copyright
|
||||
### License & Copyright
|
||||
|
||||
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
|
||||
283
README_EN.md
283
README_EN.md
@@ -1,22 +1,18 @@
|
||||
# Jumpserver - The Bastion Host for Multi-Cloud Environment
|
||||
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
|
||||
<h3 align="center">Open Source Bastion Host</h3>
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://hub.docker.com/u/jumpserver)
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0"><img src="https://shields.io/github/license/jumpserver/jumpserver" alt="License: GPL v2"></a>
|
||||
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
|
||||
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
</p>
|
||||
|
||||
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README.md)
|
||||
JumpServer is the world's first open-source Bastion Host and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
|
||||
|
||||
|Security Notice|
|
||||
|------------------|
|
||||
|On 15th January 2021, JumpServer found a critical bug for remote execution vulnerability. Please fix it asap! [For more detail](https://github.com/jumpserver/jumpserver/issues/5533) Thanks for **reactivity of Alibaba Hackerone bug bounty program** report use the bug|
|
||||
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 is the world's first open-source Bastion Host and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
|
||||
|
||||
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 cross-regional 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 by taking every little step
|
||||
|
||||
@@ -31,246 +27,14 @@ Change the world by taking every little step
|
||||
- Multi-Tenant system: multiple subsidiary companies or departments access the same system simultaneously.
|
||||
- Many applications supported: link to databases, windows remote applications, and Kubernetes cluster, etc.
|
||||
|
||||
## Features List
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td rowspan="8">Authentication</td>
|
||||
<td rowspan="5">Login</td>
|
||||
<td>Unified way to access and authenticate resources</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LDAP/AD Authentication</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS Authentication</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>OpenID Authentication(Single Sign-On)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAS Authentication (Single Sign-On)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">MFA (Multi-Factor Authentication)</td>
|
||||
<td>Use Google Authenticator for MFA</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>RADIUS (Remote Authentication Dial In User Service)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Login Supervision</td>
|
||||
<td>Any user’s login behavior is supervised and controlled by the administrator:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="11">Accounting</td>
|
||||
<td rowspan="2">Centralized Accounts Management</td>
|
||||
<td>Admin Users management</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>System Users management</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">Unified Password Management</td>
|
||||
<td>Asset password custody (a matrix storing all asset password with dense security)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Auto-generated passwords</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Automatic password handling (auto login assets)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password expiration settings</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Password change Schedular</td>
|
||||
<td>Support regular batch Linux/Windows assets password changing:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Implement multiple password strategies:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Multi-Cloud Management</td>
|
||||
<td>Automatically manage private cloud and public cloud assets in a unified platform :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Users Acquisition </td>
|
||||
<td>Create regular custom tasks to collect system users in selected assets to identify and track the privileges ownership:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Password Vault </td>
|
||||
<td>Unified operations to check, update, and test system user password to prevent stealing or unauthorised sharing of passwords:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="15">Authorization</td>
|
||||
<td>Multi-Dimensional</td>
|
||||
<td>Granting users or user groups to access assets, asset nodes, or applications through system users. Providing precise access control to different roles of users</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="4">Assets</td>
|
||||
<td>Assets are arranged and displayed in a tree structure </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Assets and Nodes have immense flexibility for authorizing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Assets in nodes inherit authorization automatically</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>child nodes automatically inherit authorization from parent nodes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Application</td>
|
||||
<td>Provides granular access control for privileged users on application level to protect from unauthorized access and unintentional errors</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Database applications (MySQL, Oracle, PostgreSQL, MariaDB, etc.) and Remote App:small_orange_diamond: </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Actions</td>
|
||||
<td>Deeper restriction on the control of file upload, download and connection actions of authorized assets. Control the permission of clipboard copy/paste (from outer terminal to current asset)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Time Bound</td>
|
||||
<td>Sharply limited the available (accessible) time for account access to the authorized resources to reduce the risk and attack surface drastically</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Privileged Assignment</td>
|
||||
<td>Assign the denied/allowed command lists to different system users as privilege elevation, with the latter taking the form of allowing particular commands to be run with a higher level of privileges. (Minimize insider threat)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Command Filtering</td>
|
||||
<td>Creating list of restriction commands that you would like to assign to different authorized system users for filtering purpose</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Transfer and Management</td>
|
||||
<td>Support SFTP file upload/download</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Management</td>
|
||||
<td>Provide a Web UI for SFTP file management</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Workflow Management</td>
|
||||
<td>Manage user login confirmation requests and assets or applications authorization requests for Just-In-Time Privileges functionality:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Group Management </td>
|
||||
<td>Establishing a multi-tenant ecosystem that able authority isolation to keep malicious actors away from sensitive administrative backends:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="8">Auditing</td>
|
||||
<td>Operations</td>
|
||||
<td>Auditing user operation behaviors for any access or usage of given privileged accounts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Session</td>
|
||||
<td>Support real-time session audit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Full history of all previous session audits</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3">Video</td>
|
||||
<td>Complete session audit and playback recordings on assets operation (Linux, Windows)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Full recordings of RemoteApp, MySQL, and Kubernetes:small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Supports uploading recordings to public clouds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Command</td>
|
||||
<td>Command auditing on assets and applications operation. Send warning alerts when executing illegal commands</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Transfer</td>
|
||||
<td>Full recordings of file upload and download</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="20">Database</td>
|
||||
<td rowspan="2">How to connect</td>
|
||||
<td>Command line</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Built-in Web UI:small_orange_diamond:</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td rowspan="4">Supported Database</td>
|
||||
<td>MySQL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Oracle :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MariaDB :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL :small_orange_diamond:</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="6">Feature Highlights</td>
|
||||
<td>Syntax highlights</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Prettier SQL formmating</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Support Shortcuts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Support selected SQL statements</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SQL commands history query</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Support page creation: DB, TABLE</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Session Auditing</td>
|
||||
<td>Full records of command</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Playback videos</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**Note**: Rows with :small_orange_diamond: at the end of the sentence means that it is X-PACK features exclusive ([Apply for X-PACK Trial](https://jinshuju.net/f/kyOYpi))
|
||||
|
||||
### Start
|
||||
|
||||
Quick start [Docker Install](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
|
||||
|
||||
Step by Step deployment. [Docs](http://docs.jumpserver.org/zh/docs/step_by_step.html)
|
||||
|
||||
Full documentation [Docs](http://docs.jumpserver.org)
|
||||
|
||||
### Demo、Video 和 Snapshot
|
||||
|
||||
We provide online demo, demo video and screenshots to get you started quickly.
|
||||
|
||||
[Demo](https://demo.jumpserver.org/auth/login/?next=/)
|
||||
[Video](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
|
||||
[Snapshot](http://docs.jumpserver.org/zh/docs/snapshot.html)
|
||||
|
||||
### SDK
|
||||
|
||||
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) Thanks to 恺珺 for providing his Java SDK vesrion.
|
||||
|
||||
## JumpServer Component Projects
|
||||
### JumpServer Component Projects
|
||||
- [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 Character protocaol Connector, replace original Python Version [Coco](https://github.com/jumpserver/coco)
|
||||
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer Graphics protocol Connector,rely on [Apache Guacamole](https://guacamole.apache.org/)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer Graphics protocol Connector,rely on [Apache Guacamole](https://guacamole.apache.org/)
|
||||
|
||||
## Contribution
|
||||
### Contribution
|
||||
If you have any good ideas or helping us to fix bugs, please submit a Pull Request and accept our thanks :)
|
||||
|
||||
Thanks to the following contributors for making JumpServer better everyday!
|
||||
@@ -279,16 +43,27 @@ Thanks to the following contributors for making JumpServer better everyday!
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/koko/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
|
||||
</a>
|
||||
|
||||
## Thanks to
|
||||
<a href="https://github.com/jumpserver/lina/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/luna/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
|
||||
</a>
|
||||
|
||||
### Thanks to
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web page connection RDP, SSH, VNC protocol equipment. JumpServer graphical connection dependent.
|
||||
- [OmniDB](https://omnidb.org/) Web page connection to databases. JumpServer Web database dependent.
|
||||
|
||||
|
||||
## JumpServer Enterprise Version
|
||||
### JumpServer Enterprise Version
|
||||
- [Apply for it](https://jinshuju.net/f/kyOYpi)
|
||||
|
||||
## Case Study
|
||||
### Case Study
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
@@ -299,7 +74,7 @@ Thanks to the following contributors for making JumpServer better everyday!
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
|
||||
## For safety instructions
|
||||
### For safety instructions
|
||||
|
||||
JumpServer is a security product. Please refer to [Basic Security Recommendations](https://docs.jumpserver.org/zh/master/install/install_security/) for deployment and installation.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from .base import BaseACL, BaseACLQuerySet
|
||||
from ..utils import contains_ip
|
||||
from common.utils.ip import contains_ip
|
||||
|
||||
|
||||
class ACLManager(models.Manager):
|
||||
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
from common.utils.ip import contains_ip
|
||||
|
||||
|
||||
class ACLManager(OrgManager):
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||
|
||||
|
||||
__all__ = ['LoginACLSerializer', ]
|
||||
|
||||
@@ -54,7 +54,7 @@ class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
|
||||
protocol_group = serializers.ListField(
|
||||
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
|
||||
help_text=protocol_group_help_text.format(
|
||||
', '.join([SystemUser.PROTOCOL_SSH, SystemUser.PROTOCOL_TELNET])
|
||||
', '.join([SystemUser.Protocol.ssh, SystemUser.Protocol.telnet])
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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,4 +1,4 @@
|
||||
from .application import *
|
||||
from .application_user import *
|
||||
from .account import *
|
||||
from .mixin import *
|
||||
from .remote_app import *
|
||||
|
||||
75
apps/applications/api/account.py
Normal file
75
apps/applications/api/account.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django_filters import rest_framework as filters
|
||||
from django.db.models import F, Value, CharField
|
||||
from django.db.models.functions import Concat
|
||||
from django.http import Http404
|
||||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.drf.api import JMSModelViewSet
|
||||
from common.utils import unique
|
||||
from perms.models import ApplicationPermission
|
||||
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
||||
from .. import serializers
|
||||
|
||||
|
||||
class AccountFilterSet(BaseFilterSet):
|
||||
username = filters.CharFilter(field_name='username')
|
||||
app = filters.CharFilter(field_name='applications', lookup_expr='exact')
|
||||
app_name = filters.CharFilter(field_name='app_name', lookup_expr='exact')
|
||||
|
||||
class Meta:
|
||||
model = ApplicationPermission
|
||||
fields = ['type', 'category']
|
||||
|
||||
|
||||
class ApplicationAccountViewSet(JMSModelViewSet):
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
search_fields = ['username', 'app_name']
|
||||
filterset_class = AccountFilterSet
|
||||
filterset_fields = ['username', 'app_name', 'type', 'category']
|
||||
serializer_class = serializers.ApplicationAccountSerializer
|
||||
http_method_names = ['get', 'put', 'patch', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ApplicationPermission.objects\
|
||||
.exclude(system_users__isnull=True) \
|
||||
.exclude(applications__isnull=True) \
|
||||
.annotate(uid=Concat(
|
||||
'applications', Value('_'), 'system_users', output_field=CharField()
|
||||
)) \
|
||||
.annotate(systemuser=F('system_users')) \
|
||||
.annotate(systemuser_display=F('system_users__name')) \
|
||||
.annotate(username=F('system_users__username')) \
|
||||
.annotate(password=F('system_users__password')) \
|
||||
.annotate(app=F('applications')) \
|
||||
.annotate(app_name=F("applications__name")) \
|
||||
.values('username', 'password', 'systemuser', 'systemuser_display',
|
||||
'app', 'app_name', 'category', 'type', 'uid', 'org_id')
|
||||
return queryset
|
||||
|
||||
def get_object(self):
|
||||
obj = self.get_queryset().filter(
|
||||
uid=self.kwargs['pk']
|
||||
).first()
|
||||
if not obj:
|
||||
raise Http404()
|
||||
return obj
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset_list = unique(queryset, key=lambda x: (x['app'], x['systemuser']))
|
||||
return queryset_list
|
||||
|
||||
@staticmethod
|
||||
def filter_spm_queryset(resource_ids, queryset):
|
||||
queryset = queryset.filter(uid__in=resource_ids)
|
||||
return queryset
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||
serializer_class = serializers.ApplicationAccountSecretSerializer
|
||||
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
http_method_names = ['get', 'options']
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.tree import TreeNodeSerializer
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
from .. import serializers
|
||||
from ..models import Application
|
||||
@@ -13,7 +16,22 @@ __all__ = ['ApplicationViewSet']
|
||||
|
||||
class ApplicationViewSet(OrgBulkModelViewSet):
|
||||
model = Application
|
||||
filterset_fields = ('name', 'type', 'category')
|
||||
search_fields = filterset_fields
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
'category': ['exact'],
|
||||
'type': ['exact', 'in'],
|
||||
}
|
||||
search_fields = ('name', 'type', 'category')
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.ApplicationSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.ApplicationSerializer,
|
||||
'get_tree': TreeNodeSerializer
|
||||
}
|
||||
|
||||
@action(methods=['GET'], detail=False, url_path='tree')
|
||||
def get_tree(self, request, *args, **kwargs):
|
||||
show_count = request.query_params.get('show_count', '1') == '1'
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
|
||||
serializer = self.get_serializer(tree_nodes, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from rest_framework import generics
|
||||
from django.conf import settings
|
||||
|
||||
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
|
||||
from .. import serializers
|
||||
from ..models import Application, ApplicationUser
|
||||
from perms.models import ApplicationPermission
|
||||
|
||||
|
||||
class ApplicationUserListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin, )
|
||||
filterset_fields = ('name', 'username')
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.ApplicationUserSerializer
|
||||
_application = None
|
||||
|
||||
@property
|
||||
def application(self):
|
||||
if self._application is None:
|
||||
app_id = self.request.query_params.get('application_id')
|
||||
if app_id:
|
||||
self._application = Application.objects.get(id=app_id)
|
||||
return self._application
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context.update({
|
||||
'application': self.application
|
||||
})
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ApplicationUser.objects.none()
|
||||
if not self.application:
|
||||
return queryset
|
||||
system_user_ids = ApplicationPermission.objects.filter(applications=self.application)\
|
||||
.values_list('system_users', flat=True)
|
||||
if not system_user_ids:
|
||||
return queryset
|
||||
queryset = ApplicationUser.objects.filter(id__in=system_user_ids)
|
||||
return queryset
|
||||
|
||||
|
||||
class ApplicationUserAuthInfoListApi(ApplicationUserListApi):
|
||||
serializer_class = serializers.ApplicationUserWithAuthInfoSerializer
|
||||
http_method_names = ['get']
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
|
||||
def get_permissions(self):
|
||||
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
@@ -1,89 +1,53 @@
|
||||
from orgs.models import Organization
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.tree import TreeNode
|
||||
from orgs.models import Organization
|
||||
from ..models import Application
|
||||
|
||||
__all__ = ['SerializeApplicationToTreeNodeMixin']
|
||||
|
||||
|
||||
class SerializeApplicationToTreeNodeMixin:
|
||||
|
||||
@staticmethod
|
||||
def _serialize_db(db):
|
||||
return {
|
||||
'id': db.id,
|
||||
'name': db.name,
|
||||
'title': db.name,
|
||||
'pId': '',
|
||||
'open': False,
|
||||
'iconSkin': 'database',
|
||||
'meta': {'type': 'database_app'}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_remote_app(remote_app):
|
||||
return {
|
||||
'id': remote_app.id,
|
||||
'name': remote_app.name,
|
||||
'title': remote_app.name,
|
||||
'pId': '',
|
||||
'open': False,
|
||||
'isParent': False,
|
||||
'iconSkin': 'chrome',
|
||||
'meta': {'type': 'remote_app'}
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_cloud(cloud):
|
||||
return {
|
||||
'id': cloud.id,
|
||||
'name': cloud.name,
|
||||
'title': cloud.name,
|
||||
'pId': '',
|
||||
'open': False,
|
||||
'isParent': False,
|
||||
'iconSkin': 'k8s',
|
||||
'meta': {'type': 'k8s_app'}
|
||||
}
|
||||
|
||||
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(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]
|
||||
organizations.sort(key=lambda x: x.name)
|
||||
return organizations
|
||||
|
||||
@staticmethod
|
||||
def create_root_node():
|
||||
name = _('My applications')
|
||||
node = TreeNode(**{
|
||||
'id': 'applications',
|
||||
'name': name,
|
||||
'title': name,
|
||||
'pId': '',
|
||||
'open': True,
|
||||
'isParent': True,
|
||||
'meta': {
|
||||
'type': 'root'
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
def serialize_applications_with_org(self, applications):
|
||||
root_node = self.create_root_node()
|
||||
tree_nodes = [root_node]
|
||||
organizations = self.filter_organizations(applications)
|
||||
data_organizations = self.serialize_organizations(organizations)
|
||||
data_applications = self.serialize_applications(applications)
|
||||
data = data_organizations + data_applications
|
||||
return data
|
||||
|
||||
for i, org in enumerate(organizations):
|
||||
# 组织节点
|
||||
org_node = org.as_tree_node(pid=root_node.id)
|
||||
tree_nodes.append(org_node)
|
||||
org_applications = applications.filter(org_id=org.id)
|
||||
count = org_applications.count()
|
||||
org_node.name += '({})'.format(count)
|
||||
|
||||
# 各应用节点
|
||||
apps_nodes = Application.create_tree_nodes(
|
||||
queryset=org_applications, root_node=org_node,
|
||||
show_empty=False
|
||||
)
|
||||
tree_nodes += apps_nodes
|
||||
return tree_nodes
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class ApplicationCategoryChoices(TextChoices):
|
||||
class AppCategory(TextChoices):
|
||||
db = 'db', _('Database')
|
||||
remote_app = 'remote_app', _('Remote app')
|
||||
cloud = 'cloud', 'Cloud'
|
||||
@@ -15,7 +14,7 @@ class ApplicationCategoryChoices(TextChoices):
|
||||
return dict(cls.choices).get(category, '')
|
||||
|
||||
|
||||
class ApplicationTypeChoices(TextChoices):
|
||||
class AppType(TextChoices):
|
||||
# db category
|
||||
mysql = 'mysql', 'MySQL'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
@@ -31,19 +30,38 @@ class ApplicationTypeChoices(TextChoices):
|
||||
# cloud category
|
||||
k8s = 'k8s', 'Kubernetes'
|
||||
|
||||
@classmethod
|
||||
def category_types_mapper(cls):
|
||||
return {
|
||||
AppCategory.db: [cls.mysql, cls.oracle, cls.pgsql, cls.mariadb],
|
||||
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
|
||||
AppCategory.cloud: [cls.k8s]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def type_category_mapper(cls):
|
||||
mapper = {}
|
||||
for category, tps in cls.category_types_mapper().items():
|
||||
for tp in tps:
|
||||
mapper[tp] = category
|
||||
return mapper
|
||||
|
||||
@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]
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.db]]
|
||||
|
||||
@classmethod
|
||||
def remote_app_types(cls):
|
||||
return [cls.chrome.value, cls.mysql_workbench.value, cls.vmware_client.value, cls.custom.value]
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.remote_app]]
|
||||
|
||||
@classmethod
|
||||
def cloud_types(cls):
|
||||
return [cls.k8s.value]
|
||||
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
25
apps/applications/migrations/0009_applicationuser.py
Normal file
25
apps/applications/migrations/0009_applicationuser.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-23 09:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0070_auto_20210426_1515'),
|
||||
('applications', '0008_auto_20210104_0435'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApplicationUser',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('assets.systemuser',),
|
||||
),
|
||||
]
|
||||
0
apps/applications/models/account.py
Normal file
0
apps/applications/models/account.py
Normal file
@@ -1,19 +1,174 @@
|
||||
from collections import defaultdict
|
||||
|
||||
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 common.tree import TreeNode
|
||||
from assets.models import Asset, SystemUser
|
||||
from .. import const
|
||||
|
||||
|
||||
class Application(CommonModelMixin, OrgModelMixin):
|
||||
class ApplicationTreeNodeMixin:
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
category: str
|
||||
|
||||
@classmethod
|
||||
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
|
||||
show_empty=True, show_count=True):
|
||||
count = counts.get(c.value, 0)
|
||||
if count == 0 and not show_empty:
|
||||
return None
|
||||
label = c.label
|
||||
if count is not None and show_count:
|
||||
label = '{} ({})'.format(label, count)
|
||||
data = {
|
||||
'id': id_,
|
||||
'name': label,
|
||||
'title': label,
|
||||
'pId': pid,
|
||||
'isParent': bool(count),
|
||||
'open': opened,
|
||||
'iconSkin': '',
|
||||
'meta': {
|
||||
'type': tp,
|
||||
'data': {
|
||||
'name': c.name,
|
||||
'value': c.value
|
||||
}
|
||||
}
|
||||
}
|
||||
return TreeNode(**data)
|
||||
|
||||
@classmethod
|
||||
def create_root_tree_node(cls, queryset, show_count=True):
|
||||
count = queryset.count() if show_count else None
|
||||
root_id = 'applications'
|
||||
root_name = _('Applications')
|
||||
if count is not None and show_count:
|
||||
root_name = '{} ({})'.format(root_name, count)
|
||||
node = TreeNode(**{
|
||||
'id': root_id,
|
||||
'name': root_name,
|
||||
'title': root_name,
|
||||
'pId': '',
|
||||
'isParent': True,
|
||||
'open': True,
|
||||
'iconSkin': '',
|
||||
'meta': {
|
||||
'type': 'applications_root',
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
@classmethod
|
||||
def create_category_tree_nodes(cls, root_node, counts=None, show_empty=True, show_count=True):
|
||||
nodes = []
|
||||
categories = const.AppType.category_types_mapper().keys()
|
||||
for category in categories:
|
||||
i = root_node.id + '_' + category.value
|
||||
node = cls.create_choice_node(
|
||||
category, i, pid=root_node.id, tp='category',
|
||||
counts=counts, opened=False, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
if not node:
|
||||
continue
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def create_types_tree_nodes(cls, root_node, counts, show_empty=True, show_count=True):
|
||||
nodes = []
|
||||
type_category_mapper = const.AppType.type_category_mapper()
|
||||
for tp in const.AppType.type_category_mapper().keys():
|
||||
category = type_category_mapper.get(tp)
|
||||
pid = root_node.id + '_' + category.value
|
||||
i = root_node.id + '_' + tp.value
|
||||
node = cls.create_choice_node(
|
||||
tp, i, pid, tp='type', counts=counts, opened=False,
|
||||
show_empty=show_empty, show_count=show_count
|
||||
)
|
||||
if not node:
|
||||
continue
|
||||
nodes.append(node)
|
||||
return nodes
|
||||
|
||||
@staticmethod
|
||||
def get_tree_node_counts(queryset):
|
||||
counts = defaultdict(int)
|
||||
values = queryset.values_list('type', 'category')
|
||||
for i in values:
|
||||
tp = i[0]
|
||||
category = i[1]
|
||||
counts[tp] += 1
|
||||
counts[category] += 1
|
||||
return counts
|
||||
|
||||
@classmethod
|
||||
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
|
||||
counts = cls.get_tree_node_counts(queryset)
|
||||
tree_nodes = []
|
||||
|
||||
# 根节点有可能是组织名称
|
||||
if root_node is None:
|
||||
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
|
||||
tree_nodes.append(root_node)
|
||||
|
||||
# 类别的节点
|
||||
tree_nodes += cls.create_category_tree_nodes(
|
||||
root_node, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
|
||||
# 类型的节点
|
||||
tree_nodes += cls.create_types_tree_nodes(
|
||||
root_node, counts, show_empty=show_empty,
|
||||
show_count=show_count
|
||||
)
|
||||
|
||||
# 应用的节点
|
||||
for app in queryset:
|
||||
pid = root_node.id + '_' + app.type
|
||||
tree_nodes.append(app.as_tree_node(pid))
|
||||
return tree_nodes
|
||||
|
||||
def as_tree_node(self, pid):
|
||||
icon_skin_category_mapper = {
|
||||
'remote_app': 'chrome',
|
||||
'db': 'database',
|
||||
'cloud': 'cloud'
|
||||
}
|
||||
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
|
||||
node = TreeNode(**{
|
||||
'id': str(self.id),
|
||||
'name': self.name,
|
||||
'title': self.name,
|
||||
'pId': pid,
|
||||
'isParent': False,
|
||||
'open': False,
|
||||
'iconSkin': icon_skin,
|
||||
'meta': {
|
||||
'type': 'application',
|
||||
'data': {
|
||||
'category': self.category,
|
||||
'type': self.type,
|
||||
}
|
||||
}
|
||||
})
|
||||
return node
|
||||
|
||||
|
||||
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
category = models.CharField(
|
||||
max_length=16, choices=const.ApplicationCategoryChoices.choices, verbose_name=_('Category')
|
||||
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=16, choices=const.ApplicationTypeChoices.choices, verbose_name=_('Type')
|
||||
max_length=16, choices=const.AppType.choices, verbose_name=_('Type')
|
||||
)
|
||||
domain = models.ForeignKey(
|
||||
'assets.Domain', null=True, blank=True, related_name='applications',
|
||||
@@ -35,7 +190,7 @@ class Application(CommonModelMixin, OrgModelMixin):
|
||||
|
||||
@property
|
||||
def category_remote_app(self):
|
||||
return self.category == const.ApplicationCategoryChoices.remote_app.value
|
||||
return self.category == const.AppCategory.remote_app.value
|
||||
|
||||
def get_rdp_remote_app_setting(self):
|
||||
from applications.serializers.attrs import get_serializer_class_by_application_type
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.models import Organization
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
|
||||
from assets.serializers import SystemUserSerializer
|
||||
from .. import models
|
||||
from .. import const
|
||||
|
||||
__all__ = [
|
||||
'ApplicationSerializer', 'ApplicationSerializerMixin',
|
||||
'ApplicationUserSerializer', 'ApplicationUserWithAuthInfoSerializer'
|
||||
'ApplicationAccountSerializer', 'ApplicationAccountSecretSerializer'
|
||||
]
|
||||
|
||||
|
||||
@@ -45,16 +47,15 @@ class ApplicationSerializerMixin(serializers.Serializer):
|
||||
|
||||
|
||||
class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category(Display)'))
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type(Dispaly)'))
|
||||
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
|
||||
class Meta:
|
||||
model = models.Application
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'category', 'category_display', 'type', 'type_display', 'attrs',
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'comment'
|
||||
'category', 'category_display', 'type', 'type_display',
|
||||
'attrs', 'date_created', 'date_updated', 'created_by', 'comment'
|
||||
]
|
||||
fields_fk = ['domain']
|
||||
fields = fields_small + fields_fk
|
||||
@@ -68,41 +69,42 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
|
||||
return _attrs
|
||||
|
||||
|
||||
class ApplicationUserSerializer(SystemUserSerializer):
|
||||
application_name = serializers.SerializerMethodField(label=_('Application name'))
|
||||
application_category = serializers.SerializerMethodField(label=_('Application category'))
|
||||
application_type = serializers.SerializerMethodField(label=_('Application type'))
|
||||
class ApplicationAccountSerializer(serializers.Serializer):
|
||||
id = serializers.ReadOnlyField(label=_("Id"), source='uid')
|
||||
username = serializers.ReadOnlyField(label=_("Username"))
|
||||
password = serializers.CharField(write_only=True, label=_("Password"))
|
||||
systemuser = serializers.ReadOnlyField(label=_('System user'))
|
||||
systemuser_display = serializers.ReadOnlyField(label=_("System user display"))
|
||||
app = serializers.ReadOnlyField(label=_('App'))
|
||||
app_name = serializers.ReadOnlyField(label=_("Application name"), read_only=True)
|
||||
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
|
||||
category_display = serializers.SerializerMethodField(label=_('Category display'))
|
||||
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
|
||||
type_display = serializers.SerializerMethodField(label=_('Type display'))
|
||||
uid = serializers.ReadOnlyField(label=_("Union id"))
|
||||
org_id = serializers.ReadOnlyField(label=_("Organization"))
|
||||
org_name = serializers.SerializerMethodField(label=_("Org name"))
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
model = models.ApplicationUser
|
||||
fields_mini = [
|
||||
'id', 'application_name', 'application_category', 'application_type', 'name', 'username'
|
||||
]
|
||||
fields_small = fields_mini + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
"username_same_with_user", 'comment',
|
||||
]
|
||||
fields = fields_small
|
||||
extra_kwargs = {
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
'created_by': {'read_only': True},
|
||||
}
|
||||
category_mapper = dict(const.AppCategory.choices)
|
||||
type_mapper = dict(const.AppType.choices)
|
||||
|
||||
@property
|
||||
def application(self):
|
||||
return self.context['application']
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
|
||||
def get_application_name(self, obj):
|
||||
return self.application.name
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
||||
def get_application_category(self, obj):
|
||||
return self.application.get_category_display()
|
||||
def get_category_display(self, obj):
|
||||
return self.category_mapper.get(obj['category'])
|
||||
|
||||
def get_application_type(self, obj):
|
||||
return self.application.get_type_display()
|
||||
def get_type_display(self, obj):
|
||||
return self.type_mapper.get(obj['type'])
|
||||
|
||||
@staticmethod
|
||||
def get_org_name(obj):
|
||||
org = Organization.get_instance(obj['org_id'])
|
||||
return org.name
|
||||
|
||||
|
||||
class ApplicationUserWithAuthInfoSerializer(ApplicationUserSerializer):
|
||||
|
||||
class Meta(ApplicationUserSerializer.Meta):
|
||||
fields = ApplicationUserSerializer.Meta.fields + ['password', 'token']
|
||||
class ApplicationAccountSecretSerializer(ApplicationAccountSerializer):
|
||||
password = serializers.CharField(write_only=False, label=_("Password"))
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 common.utils import get_logger, is_uuid, get_object_or_none
|
||||
from assets.models import Asset
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@@ -14,28 +14,37 @@ logger = get_logger(__file__)
|
||||
__all__ = ['RemoteAppSerializer']
|
||||
|
||||
|
||||
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
|
||||
class ExistAssetPrimaryKeyRelatedField(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
|
||||
def to_representation(self, _id):
|
||||
# _id 是 instance.id
|
||||
if self.pk_field is not None:
|
||||
return self.pk_field.to_representation(value)
|
||||
return value
|
||||
return self.pk_field.to_representation(_id)
|
||||
# 解决删除资产后,远程应用更新页面会显示资产ID的问题
|
||||
asset = get_object_or_none(Asset, id=_id)
|
||||
if not asset:
|
||||
return None
|
||||
return _id
|
||||
|
||||
|
||||
class RemoteAppSerializer(serializers.Serializer):
|
||||
asset_info = serializers.SerializerMethodField()
|
||||
asset = CharPrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=False, label=_("Asset"), allow_null=True
|
||||
asset = ExistAssetPrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
|
||||
)
|
||||
path = serializers.CharField(
|
||||
max_length=128, label=_('Application path'), allow_null=True
|
||||
)
|
||||
|
||||
def validate_asset(self, asset):
|
||||
if not asset:
|
||||
raise serializers.ValidationError(_('This field is required.'))
|
||||
return asset
|
||||
|
||||
@staticmethod
|
||||
def get_asset_info(obj):
|
||||
asset_id = obj.get('asset')
|
||||
|
||||
@@ -14,9 +14,9 @@ __all__ = [
|
||||
# ---------------------------------------------------
|
||||
|
||||
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,
|
||||
const.AppCategory.db.value: application_category.DBSerializer,
|
||||
const.AppCategory.remote_app.value: application_category.RemoteAppSerializer,
|
||||
const.AppCategory.cloud.value: application_category.CloudSerializer,
|
||||
}
|
||||
|
||||
# define `attrs` field `type serializers mapping`
|
||||
@@ -24,17 +24,17 @@ category_serializer_classes_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,
|
||||
const.AppType.mysql.value: application_type.MySQLSerializer,
|
||||
const.AppType.mariadb.value: application_type.MariaDBSerializer,
|
||||
const.AppType.oracle.value: application_type.OracleSerializer,
|
||||
const.AppType.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,
|
||||
const.AppType.chrome.value: application_type.ChromeSerializer,
|
||||
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
|
||||
const.AppType.vmware_client.value: application_type.VMwareClientSerializer,
|
||||
const.AppType.custom.value: application_type.CustomSerializer,
|
||||
# cloud
|
||||
const.ApplicationTypeChoices.k8s.value: application_type.K8SSerializer
|
||||
const.AppType.k8s.value: application_type.K8SSerializer
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ app_name = 'applications'
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'applications', api.ApplicationViewSet, 'application')
|
||||
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
|
||||
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
path('application-users/', api.ApplicationUserListApi.as_view(), name='application-user'),
|
||||
path('application-user-auth-infos/', api.ApplicationUserAuthInfoListApi.as_view(), name='application-user-auth-info')
|
||||
# path('accounts/', api.ApplicationAccountViewSet.as_view(), name='application-account'),
|
||||
# path('account-secrets/', api.ApplicationAccountSecretViewSet.as_view(), name='application-account-secret')
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ from .asset import *
|
||||
from .label import *
|
||||
from .system_user import *
|
||||
from .system_user_relation import *
|
||||
from .accounts import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
|
||||
113
apps/assets/api/accounts.py
Normal file
113
apps/assets/api/accounts.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from django.db.models import F, Q
|
||||
from rest_framework.decorators import action
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework.response import Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||
from ..models import AuthBook, Node
|
||||
from .. import serializers
|
||||
|
||||
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
|
||||
|
||||
|
||||
class AccountFilterSet(BaseFilterSet):
|
||||
username = filters.CharFilter(method='do_nothing')
|
||||
ip = filters.CharFilter(field_name='ip', lookup_expr='exact')
|
||||
hostname = filters.CharFilter(field_name='hostname', lookup_expr='exact')
|
||||
node = filters.CharFilter(method='do_nothing')
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
qs = super().qs
|
||||
qs = self.filter_username(qs)
|
||||
qs = self.filter_node(qs)
|
||||
return qs
|
||||
|
||||
def filter_username(self, qs):
|
||||
username = self.get_query_param('username')
|
||||
if not username:
|
||||
return qs
|
||||
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
|
||||
return qs
|
||||
|
||||
def filter_node(self, qs):
|
||||
node_id = self.get_query_param('node')
|
||||
if not node_id:
|
||||
return qs
|
||||
node = get_object_or_404(Node, pk=node_id)
|
||||
node_ids = node.get_all_children(with_self=True).values_list('id', flat=True)
|
||||
node_ids = list(node_ids)
|
||||
qs = qs.filter(asset__nodes__in=node_ids)
|
||||
return qs
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields = [
|
||||
'asset', 'systemuser', 'id',
|
||||
]
|
||||
|
||||
|
||||
class AccountViewSet(OrgBulkModelViewSet):
|
||||
model = AuthBook
|
||||
filterset_fields = ("username", "asset", "systemuser", 'ip', 'hostname')
|
||||
search_fields = ('username', 'ip', 'hostname', 'systemuser__username')
|
||||
filterset_class = AccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSerializer,
|
||||
'verify_account': serializers.AssetTaskSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()\
|
||||
.annotate(ip=F('asset__ip'))\
|
||||
.annotate(hostname=F('asset__hostname'))
|
||||
return queryset
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='verify')
|
||||
def verify_account(self, request, *args, **kwargs):
|
||||
account = super().get_object()
|
||||
task = test_accounts_connectivity_manual.delay([account])
|
||||
return Response(data={'task': task.id})
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSecretSerializer
|
||||
}
|
||||
permission_classes = (IsOrgAdmin, NeedMFAVerify)
|
||||
http_method_names = ['get']
|
||||
|
||||
|
||||
class AccountTaskCreateAPI(CreateAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.AccountTaskSerializer
|
||||
filterset_fields = AccountViewSet.filterset_fields
|
||||
search_fields = AccountViewSet.search_fields
|
||||
filterset_class = AccountViewSet.filterset_class
|
||||
|
||||
def get_accounts(self):
|
||||
queryset = AuthBook.objects.all()
|
||||
queryset = self.filter_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
accounts = self.get_accounts()
|
||||
task = test_accounts_connectivity_manual.delay(accounts)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
@@ -1,109 +1,28 @@
|
||||
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from common.utils import get_logger
|
||||
from ..hands import IsOrgAdmin
|
||||
from ..models import AdminUser, Asset
|
||||
from ..models import SystemUser
|
||||
from .. import serializers
|
||||
from ..tasks import test_admin_user_connectivity_manual
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
|
||||
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
|
||||
'AdminUserAssetsListView',
|
||||
]
|
||||
__all__ = ['AdminUserViewSet']
|
||||
|
||||
|
||||
# 兼容一下老的 api
|
||||
class AdminUserViewSet(OrgBulkModelViewSet):
|
||||
"""
|
||||
Admin user api set, for add,delete,update,list,retrieve resource
|
||||
"""
|
||||
model = AdminUser
|
||||
model = SystemUser
|
||||
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()
|
||||
queryset = super().get_queryset().filter(type=SystemUser.Type.admin)
|
||||
queryset = queryset.annotate(assets_amount=Count('assets'))
|
||||
return queryset
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
has_related_asset = instance.assets.exists()
|
||||
if has_related_asset:
|
||||
data = {'msg': _('Deleted failed, There are related assets')}
|
||||
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
class AdminUserAuthApi(generics.UpdateAPIView):
|
||||
model = AdminUser
|
||||
serializer_class = serializers.AdminUserAuthSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
|
||||
model = AdminUser
|
||||
serializer_class = serializers.ReplaceNodeAdminUserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
admin_user = self.get_object()
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
if serializer.is_valid():
|
||||
nodes = serializer.validated_data['nodes']
|
||||
assets = []
|
||||
for node in nodes:
|
||||
assets.extend([asset.id for asset in node.get_all_assets()])
|
||||
|
||||
with transaction.atomic():
|
||||
Asset.objects.filter(id__in=assets).update(admin_user=admin_user)
|
||||
|
||||
return Response({"msg": "ok"})
|
||||
else:
|
||||
return Response({'error': serializer.errors}, status=400)
|
||||
|
||||
|
||||
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
|
||||
"""
|
||||
Test asset admin user assets_connectivity
|
||||
"""
|
||||
model = AdminUser
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.TaskIDSerializer
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
admin_user = self.get_object()
|
||||
task = test_admin_user_connectivity_manual.delay(admin_user)
|
||||
return Response({"task": task.id})
|
||||
|
||||
|
||||
class AdminUserAssetsListView(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.AssetSimpleSerializer
|
||||
filterset_fields = ("hostname", "ip")
|
||||
search_fields = filterset_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
return get_object_or_404(AdminUser, pk=pk)
|
||||
|
||||
def get_queryset(self):
|
||||
admin_user = self.get_object()
|
||||
return admin_user.get_related_assets()
|
||||
|
||||
@@ -9,10 +9,11 @@ from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.mixins import generics
|
||||
from ..models import Asset, Node, Platform
|
||||
from ..models import Asset, Node, Platform, SystemUser
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
update_assets_hardware_info_manual, test_assets_connectivity_manual
|
||||
update_assets_hardware_info_manual, test_assets_connectivity_manual,
|
||||
test_system_users_connectivity_a_asset, push_system_users_a_asset
|
||||
)
|
||||
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
|
||||
|
||||
@@ -33,8 +34,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
||||
filterset_fields = {
|
||||
'hostname': ['exact'],
|
||||
'ip': ['exact'],
|
||||
'systemuser__id': ['exact'],
|
||||
'admin_user__id': ['exact'],
|
||||
'system_users__id': ['exact'],
|
||||
'platform__base': ['exact'],
|
||||
'is_active': ['exact'],
|
||||
'protocols': ['exact', 'icontains']
|
||||
@@ -43,7 +43,6 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
|
||||
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetSerializer,
|
||||
'display': serializers.AssetDisplaySerializer,
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
|
||||
@@ -96,21 +95,27 @@ class AssetPlatformViewSet(ModelViewSet):
|
||||
|
||||
|
||||
class AssetsTaskMixin:
|
||||
|
||||
def perform_assets_task(self, serializer):
|
||||
data = serializer.validated_data
|
||||
assets = data['assets']
|
||||
action = data['action']
|
||||
assets = data.get('assets', [])
|
||||
if action == "refresh":
|
||||
task = update_assets_hardware_info_manual.delay(assets)
|
||||
else:
|
||||
# action == 'test':
|
||||
task = test_assets_connectivity_manual.delay(assets)
|
||||
return task
|
||||
|
||||
def perform_create(self, serializer):
|
||||
task = self.perform_assets_task(serializer)
|
||||
self.set_task_to_serializer_data(serializer, task)
|
||||
|
||||
def set_task_to_serializer_data(self, serializer, task):
|
||||
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
|
||||
@@ -119,13 +124,37 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
pk = self.kwargs.get('pk')
|
||||
request.data['asset'] = pk
|
||||
request.data['assets'] = [pk]
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def perform_asset_task(self, serializer):
|
||||
data = serializer.validated_data
|
||||
action = data['action']
|
||||
if action not in ['push_system_user', 'test_system_user']:
|
||||
return
|
||||
asset = data['asset']
|
||||
system_users = data.get('system_users')
|
||||
if not system_users:
|
||||
system_users = asset.get_all_systemusers()
|
||||
if action == 'push_system_user':
|
||||
task = push_system_users_a_asset.delay(system_users, asset=asset)
|
||||
elif action == 'test_system_user':
|
||||
task = test_system_users_connectivity_a_asset.delay(system_users, asset=asset)
|
||||
else:
|
||||
task = None
|
||||
return task
|
||||
|
||||
def perform_create(self, serializer):
|
||||
task = self.perform_asset_task(serializer)
|
||||
if not task:
|
||||
task = self.perform_assets_task(serializer)
|
||||
self.set_task_to_serializer_data(serializer, task)
|
||||
|
||||
|
||||
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
|
||||
model = Asset
|
||||
serializer_class = serializers.AssetTaskSerializer
|
||||
serializer_class = serializers.AssetsTaskSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import coreapi
|
||||
from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import generics, filters
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
|
||||
from common.utils import get_object_or_none, get_logger
|
||||
from common.mixins import CommonApiMixin
|
||||
from ..backends import AssetUserManager
|
||||
from ..models import Node
|
||||
from .. import serializers
|
||||
from ..tasks import (
|
||||
test_asset_users_connectivity_manual
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI',
|
||||
]
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AssetUserFilterBackend(filters.BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
kwargs = {}
|
||||
for field in view.filterset_fields:
|
||||
value = request.GET.get(field)
|
||||
if not value:
|
||||
continue
|
||||
if field == "node_id":
|
||||
value = get_object_or_none(Node, pk=value)
|
||||
kwargs["node"] = value
|
||||
continue
|
||||
elif field == "asset_id":
|
||||
field = "asset"
|
||||
kwargs[field] = value
|
||||
if kwargs:
|
||||
queryset = queryset.filter(**kwargs)
|
||||
logger.debug("Filter {}".format(kwargs))
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserSearchBackend(filters.BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
value = request.GET.get('search')
|
||||
if not value:
|
||||
return queryset
|
||||
queryset = queryset.search(value)
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='latest', location='query', required=False,
|
||||
type='string', example='1',
|
||||
description='Only the latest version'
|
||||
)
|
||||
]
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
latest = request.GET.get('latest') == '1'
|
||||
if latest:
|
||||
queryset = queryset.distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
|
||||
serializer_classes = {
|
||||
'default': serializers.AssetUserWriteSerializer,
|
||||
'display': serializers.AssetUserReadSerializer,
|
||||
'retrieve': serializers.AssetUserReadSerializer,
|
||||
}
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
filterset_fields = [
|
||||
"id", "ip", "hostname", "username",
|
||||
"asset_id", "node_id",
|
||||
"prefer", "prefer_id",
|
||||
]
|
||||
search_fields = ["ip", "hostname", "username"]
|
||||
filter_backends = [
|
||||
AssetUserFilterBackend, AssetUserSearchBackend,
|
||||
AssetUserLatestFilterBackend,
|
||||
]
|
||||
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
return False
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
if pk is None:
|
||||
return
|
||||
queryset = self.get_queryset()
|
||||
obj = queryset.get(id=pk)
|
||||
return obj
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
manager = AssetUserManager()
|
||||
manager.delete(instance)
|
||||
|
||||
def get_queryset(self):
|
||||
manager = AssetUserManager()
|
||||
queryset = manager.all()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetUserAuthInfoViewSet(AssetUserViewSet):
|
||||
serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
|
||||
http_method_names = ['get', 'post']
|
||||
permission_classes = [IsOrgAdminOrAppUser]
|
||||
|
||||
def get_permissions(self):
|
||||
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class AssetUserTaskCreateAPI(generics.CreateAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.AssetUserTaskSerializer
|
||||
filter_backends = AssetUserViewSet.filter_backends
|
||||
filterset_fields = AssetUserViewSet.filterset_fields
|
||||
|
||||
def get_asset_users(self):
|
||||
manager = AssetUserManager()
|
||||
queryset = manager.all()
|
||||
for cls in self.filter_backends:
|
||||
queryset = cls().filter_queryset(self.request, queryset, self)
|
||||
return list(queryset)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
asset_users = self.get_asset_users()
|
||||
# action = serializer.validated_data["action"]
|
||||
# only this
|
||||
# if action == "test":
|
||||
task = test_asset_users_connectivity_manual.delay(asset_users)
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
return handler
|
||||
@@ -2,14 +2,12 @@
|
||||
#
|
||||
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.utils import reverse
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from tickets.models import Ticket
|
||||
from tickets.api import GenericTicketStatusRetrieveCloseAPI
|
||||
from ..hands import IsOrgAdmin, IsAppUser
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
|
||||
@@ -33,7 +33,7 @@ class GatewayViewSet(OrgBulkModelViewSet):
|
||||
model = Gateway
|
||||
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
|
||||
search_fields = ("domain__name", "name", "username", "ip")
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.GatewaySerializer
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class SerializeToTreeNodeMixin:
|
||||
'isParent': True,
|
||||
'open': node.is_org_root(),
|
||||
'meta': {
|
||||
'node': {
|
||||
'data': {
|
||||
"id": node.id,
|
||||
"key": node.key,
|
||||
"value": node.value,
|
||||
@@ -65,7 +65,7 @@ class SerializeToTreeNodeMixin:
|
||||
'chkDisabled': not asset.is_active,
|
||||
'meta': {
|
||||
'type': 'asset',
|
||||
'asset': {
|
||||
'data': {
|
||||
'id': asset.id,
|
||||
'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
|
||||
@@ -32,13 +32,13 @@ class SystemUserViewSet(OrgBulkModelViewSet):
|
||||
filterset_fields = {
|
||||
'name': ['exact'],
|
||||
'username': ['exact'],
|
||||
'protocol': ['exact', 'in']
|
||||
'protocol': ['exact', 'in'],
|
||||
'type': ['exact', 'in'],
|
||||
}
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.SystemUserSerializer
|
||||
serializer_classes = {
|
||||
'default': serializers.SystemUserSerializer,
|
||||
'list': serializers.SystemUserListSerializer,
|
||||
}
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.db.models.signals import m2m_changed
|
||||
from django.db.models.functions import Concat
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import current_org
|
||||
from .. import models, serializers
|
||||
@@ -15,6 +16,8 @@ __all__ = [
|
||||
'SystemUserUserRelationViewSet',
|
||||
]
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RelationMixin:
|
||||
def get_queryset(self):
|
||||
@@ -24,8 +27,8 @@ class RelationMixin:
|
||||
queryset = queryset.filter(systemuser__org_id=org_id)
|
||||
|
||||
queryset = queryset.annotate(systemuser_display=Concat(
|
||||
F('systemuser__name'), Value('('), F('systemuser__username'),
|
||||
Value(')')
|
||||
F('systemuser__name'), Value('('),
|
||||
F('systemuser__username'), Value(')')
|
||||
))
|
||||
return queryset
|
||||
|
||||
@@ -41,10 +44,11 @@ class RelationMixin:
|
||||
system_users_objects_map[i.systemuser].append(_id)
|
||||
|
||||
sender = self.get_sender()
|
||||
for system_user, objects in system_users_objects_map.items():
|
||||
for system_user, object_ids in system_users_objects_map.items():
|
||||
logger.debug('System user relation changed, send m2m_changed signals')
|
||||
m2m_changed.send(
|
||||
sender=sender, instance=system_user, action='post_add',
|
||||
reverse=False, model=model, pk_set=objects
|
||||
reverse=False, model=model, pk_set=set(object_ids)
|
||||
)
|
||||
|
||||
def get_sender(self):
|
||||
@@ -71,7 +75,7 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
|
||||
]
|
||||
search_fields = [
|
||||
"id", "asset__hostname", "asset__ip",
|
||||
"systemuser__name", "systemuser__username"
|
||||
"systemuser__name", "systemuser__username",
|
||||
]
|
||||
|
||||
def get_objects_attr(self):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
from .manager import AssetUserManager
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from abc import abstractmethod
|
||||
|
||||
from ..models import Asset
|
||||
|
||||
|
||||
class BaseBackend:
|
||||
@abstractmethod
|
||||
def all(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, username=None, hostname=None, ip=None, assets=None,
|
||||
node=None, prefer_id=None, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def search(self, item):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_queryset(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, union_id):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def qs_to_values(qs):
|
||||
values = qs.values(
|
||||
'hostname', 'ip', "asset_id",
|
||||
'name', 'username', 'password', 'private_key', 'public_key',
|
||||
'score', 'version',
|
||||
"asset_username", "union_id",
|
||||
'date_created', 'date_updated',
|
||||
'org_id', 'backend', 'backend_display'
|
||||
)
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def make_assets_as_ids(assets):
|
||||
if not assets:
|
||||
return []
|
||||
if isinstance(assets[0], Asset):
|
||||
assets = [a.id for a in assets]
|
||||
return assets
|
||||
@@ -1,332 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext as _
|
||||
from functools import reduce
|
||||
from django.db.models import F, CharField, Value, IntegerField, Q, Count
|
||||
from django.db.models.functions import Concat
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from common.utils import get_object_or_none
|
||||
from orgs.utils import current_org
|
||||
from ..models import AuthBook, SystemUser, Asset, AdminUser
|
||||
from .base import BaseBackend
|
||||
|
||||
|
||||
class DBBackend(BaseBackend):
|
||||
union_id_length = 2
|
||||
|
||||
def __init__(self, queryset=None):
|
||||
if queryset is None:
|
||||
queryset = self.all()
|
||||
self.queryset = queryset
|
||||
|
||||
def _clone(self):
|
||||
return self.__class__(self.queryset)
|
||||
|
||||
def all(self):
|
||||
return AuthBook.objects.none()
|
||||
|
||||
def count(self):
|
||||
return self.queryset.count()
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset
|
||||
|
||||
def delete(self, union_id):
|
||||
cleaned_union_id = union_id.split('_')
|
||||
# 如果union_id通不过本检查,代表可能不是本backend, 应该返回空
|
||||
if not self._check_union_id(union_id, cleaned_union_id):
|
||||
return
|
||||
return self._perform_delete_by_union_id(cleaned_union_id)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
pass
|
||||
|
||||
def filter(self, assets=None, node=None, prefer=None, prefer_id=None,
|
||||
union_id=None, id__in=None, **kwargs):
|
||||
clone = self._clone()
|
||||
clone._filter_union_id(union_id)
|
||||
clone._filter_prefer(prefer, prefer_id)
|
||||
clone._filter_node(node)
|
||||
clone._filter_assets(assets)
|
||||
clone._filter_other(kwargs)
|
||||
clone._filter_id_in(id__in)
|
||||
return clone
|
||||
|
||||
def _filter_union_id(self, union_id):
|
||||
if not union_id:
|
||||
return
|
||||
cleaned_union_id = union_id.split('_')
|
||||
# 如果union_id通不过本检查,代表可能不是本backend, 应该返回空
|
||||
if not self._check_union_id(union_id, cleaned_union_id):
|
||||
self.queryset = self.queryset.none()
|
||||
return
|
||||
return self._perform_filter_union_id(union_id, cleaned_union_id)
|
||||
|
||||
def _check_union_id(self, union_id, cleaned_union_id):
|
||||
return union_id and len(cleaned_union_id) == self.union_id_length
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
self.queryset = self.queryset.filter(union_id=union_id)
|
||||
|
||||
def _filter_assets(self, assets):
|
||||
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
|
||||
|
||||
def _filter_id_in(self, ids):
|
||||
if ids and isinstance(ids, list):
|
||||
self.queryset = self.queryset.filter(union_id__in=ids)
|
||||
|
||||
@staticmethod
|
||||
def clean_kwargs(kwargs):
|
||||
return {k: v for k, v in kwargs.items() if v}
|
||||
|
||||
def _filter_other(self, kwargs):
|
||||
kwargs = self.clean_kwargs(kwargs)
|
||||
if kwargs:
|
||||
self.queryset = self.queryset.filter(**kwargs)
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
pass
|
||||
|
||||
def search(self, item):
|
||||
qs = []
|
||||
for i in ['hostname', 'ip', 'username']:
|
||||
kwargs = {i + '__startswith': item}
|
||||
qs.append(Q(**kwargs))
|
||||
q = reduce(lambda x, y: x | y, qs)
|
||||
clone = self._clone()
|
||||
clone.queryset = clone.queryset.filter(q).distinct()
|
||||
return clone
|
||||
|
||||
|
||||
class SystemUserBackend(DBBackend):
|
||||
model = SystemUser.assets.through
|
||||
backend = 'system_user'
|
||||
backend_display = _('System user')
|
||||
prefer = backend
|
||||
base_score = 0
|
||||
union_id_length = 2
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
if prefer and prefer != self.prefer:
|
||||
self.queryset = self.queryset.none()
|
||||
|
||||
if prefer_id:
|
||||
self.queryset = self.queryset.filter(systemuser__id=prefer_id)
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
system_user_id, asset_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
asset_id=asset_id, systemuser__id=system_user_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
system_user_id, asset_id = union_id_cleaned
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
if all((system_user, asset)):
|
||||
system_user.assets.remove(asset)
|
||||
|
||||
def _filter_node(self, node):
|
||||
if node:
|
||||
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
|
||||
|
||||
def get_annotate(self):
|
||||
kwargs = dict(
|
||||
hostname=F("asset__hostname"),
|
||||
ip=F("asset__ip"),
|
||||
name=F("systemuser__name"),
|
||||
username=F("systemuser__username"),
|
||||
password=F("systemuser__password"),
|
||||
private_key=F("systemuser__private_key"),
|
||||
public_key=F("systemuser__public_key"),
|
||||
score=F("systemuser__priority") + self.base_score,
|
||||
version=Value(0, IntegerField()),
|
||||
date_created=F("systemuser__date_created"),
|
||||
date_updated=F("systemuser__date_updated"),
|
||||
asset_username=Concat(F("asset__id"), Value("_"),
|
||||
F("systemuser__username"),
|
||||
output_field=CharField()),
|
||||
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
|
||||
output_field=CharField()),
|
||||
org_id=F("asset__org_id"),
|
||||
backend=Value(self.backend, CharField()),
|
||||
backend_display=Value(self.backend_display, CharField()),
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_filter(self):
|
||||
return dict(
|
||||
systemuser__username_same_with_user=False,
|
||||
)
|
||||
|
||||
def all(self):
|
||||
kwargs = self.get_annotate()
|
||||
filters = self.get_filter()
|
||||
qs = self.model.objects.all().annotate(**kwargs)
|
||||
if not current_org.is_root():
|
||||
filters['org_id'] = current_org.org_id()
|
||||
qs = qs.filter(**filters)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
|
||||
|
||||
class DynamicSystemUserBackend(SystemUserBackend):
|
||||
backend = 'system_user_dynamic'
|
||||
backend_display = _('System user(Dynamic)')
|
||||
prefer = 'system_user'
|
||||
union_id_length = 3
|
||||
|
||||
def get_annotate(self):
|
||||
kwargs = super().get_annotate()
|
||||
kwargs.update(dict(
|
||||
name=Concat(
|
||||
F("systemuser__users__name"), Value('('), F("systemuser__name"), Value(')'),
|
||||
output_field=CharField()
|
||||
),
|
||||
username=F("systemuser__users__username"),
|
||||
asset_username=Concat(
|
||||
F("asset__id"), Value("_"),
|
||||
F("systemuser__users__username"),
|
||||
output_field=CharField()
|
||||
),
|
||||
union_id=Concat(
|
||||
F("systemuser_id"), Value("_"), F("asset_id"),
|
||||
Value("_"), F("systemuser__users__id"),
|
||||
output_field=CharField()
|
||||
),
|
||||
users_count=Count('systemuser__users'),
|
||||
))
|
||||
return kwargs
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
system_user_id, asset_id, user_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
asset_id=asset_id, systemuser_id=system_user_id,
|
||||
union_id=union_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
system_user_id, asset_id, user_id = union_id_cleaned
|
||||
system_user = get_object_or_none(SystemUser, pk=system_user_id)
|
||||
if not system_user:
|
||||
return
|
||||
system_user.users.remove(user_id)
|
||||
if system_user.users.count() == 0:
|
||||
system_user.assets.remove(asset_id)
|
||||
|
||||
def get_filter(self):
|
||||
return dict(
|
||||
users_count__gt=0,
|
||||
systemuser__username_same_with_user=True
|
||||
)
|
||||
|
||||
|
||||
class AdminUserBackend(DBBackend):
|
||||
model = Asset
|
||||
backend = 'admin_user'
|
||||
backend_display = _('Admin user')
|
||||
prefer = backend
|
||||
base_score = 200
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
if prefer and prefer != self.backend:
|
||||
self.queryset = self.queryset.none()
|
||||
if prefer_id:
|
||||
self.queryset = self.queryset.filter(admin_user__id=prefer_id)
|
||||
|
||||
def _filter_node(self, node):
|
||||
if node:
|
||||
self.queryset = self.queryset.filter(nodes__id=node.id)
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
admin_user_id, asset_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
id=asset_id, admin_user_id=admin_user_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
raise PermissionDenied(_("Could not remove asset admin user"))
|
||||
|
||||
def all(self):
|
||||
qs = self.model.objects.all().annotate(
|
||||
asset_id=F("id"),
|
||||
name=F("admin_user__name"),
|
||||
username=F("admin_user__username"),
|
||||
password=F("admin_user__password"),
|
||||
private_key=F("admin_user__private_key"),
|
||||
public_key=F("admin_user__public_key"),
|
||||
score=Value(self.base_score, IntegerField()),
|
||||
version=Value(0, IntegerField()),
|
||||
date_updated=F("admin_user__date_updated"),
|
||||
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
|
||||
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
|
||||
backend=Value(self.backend, CharField()),
|
||||
backend_display=Value(self.backend_display, CharField()),
|
||||
)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
|
||||
|
||||
class AuthbookBackend(DBBackend):
|
||||
model = AuthBook
|
||||
backend = 'db'
|
||||
backend_display = _('Database')
|
||||
prefer = backend
|
||||
base_score = 400
|
||||
|
||||
def _filter_node(self, node):
|
||||
if node:
|
||||
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
|
||||
|
||||
def _filter_prefer(self, prefer, prefer_id):
|
||||
if not prefer or not prefer_id:
|
||||
return
|
||||
if prefer.lower() == "admin_user":
|
||||
model = AdminUser
|
||||
elif prefer.lower() == "system_user":
|
||||
model = SystemUser
|
||||
else:
|
||||
self.queryset = self.queryset.none()
|
||||
return
|
||||
obj = get_object_or_none(model, pk=prefer_id)
|
||||
if obj is None:
|
||||
self.queryset = self.queryset.none()
|
||||
return
|
||||
username = obj.get_username()
|
||||
if isinstance(username, str):
|
||||
self.queryset = self.queryset.filter(username=username)
|
||||
# dynamic system user return more username
|
||||
else:
|
||||
self.queryset = self.queryset.filter(username__in=username)
|
||||
|
||||
def _perform_filter_union_id(self, union_id, union_id_cleaned):
|
||||
authbook_id, asset_id = union_id_cleaned
|
||||
self.queryset = self.queryset.filter(
|
||||
id=authbook_id, asset_id=asset_id,
|
||||
)
|
||||
|
||||
def _perform_delete_by_union_id(self, union_id_cleaned):
|
||||
authbook_id, asset_id = union_id_cleaned
|
||||
authbook = get_object_or_none(AuthBook, pk=authbook_id)
|
||||
if authbook.is_latest:
|
||||
raise PermissionDenied(_("Latest version could not be delete"))
|
||||
AuthBook.objects.filter(id=authbook_id).delete()
|
||||
|
||||
def all(self):
|
||||
qs = self.model.objects.all().annotate(
|
||||
hostname=F("asset__hostname"),
|
||||
ip=F("asset__ip"),
|
||||
score=F('version') + self.base_score,
|
||||
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
|
||||
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
|
||||
backend=Value(self.backend, CharField()),
|
||||
backend_display=Value(self.backend_display, CharField()),
|
||||
)
|
||||
qs = self.qs_to_values(qs)
|
||||
return qs
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from itertools import chain, groupby
|
||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||
|
||||
from orgs.utils import current_org
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.struct import QuerySetChain
|
||||
|
||||
from ..models import AssetUser, AuthBook
|
||||
from .db import (
|
||||
AuthbookBackend, SystemUserBackend, AdminUserBackend,
|
||||
DynamicSystemUserBackend
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class NotSupportError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AssetUserQueryset:
|
||||
ObjectDoesNotExist = ObjectDoesNotExist
|
||||
MultipleObjectsReturned = MultipleObjectsReturned
|
||||
|
||||
def __init__(self, backends=()):
|
||||
self.backends = backends
|
||||
self._distinct_queryset = None
|
||||
|
||||
def backends_queryset(self):
|
||||
return [b.get_queryset() for b in self.backends]
|
||||
|
||||
@lazyproperty
|
||||
def backends_counts(self):
|
||||
return [b.count() for b in self.backends]
|
||||
|
||||
def filter(self, hostname=None, ip=None, username=None,
|
||||
assets=None, asset=None, node=None,
|
||||
id=None, prefer_id=None, prefer=None, id__in=None):
|
||||
if not assets and asset:
|
||||
assets = [asset]
|
||||
|
||||
kwargs = dict(
|
||||
hostname=hostname, ip=ip, username=username,
|
||||
assets=assets, node=node, prefer=prefer, prefer_id=prefer_id,
|
||||
id__in=id__in, union_id=id,
|
||||
)
|
||||
logger.debug("Filter: {}".format(kwargs))
|
||||
backends = []
|
||||
for backend in self.backends:
|
||||
clone = backend.filter(**kwargs)
|
||||
backends.append(clone)
|
||||
return self._clone(backends)
|
||||
|
||||
def _clone(self, backends=None):
|
||||
if backends is None:
|
||||
backends = self.backends
|
||||
return self.__class__(backends)
|
||||
|
||||
def search(self, item):
|
||||
backends = []
|
||||
for backend in self.backends:
|
||||
new = backend.search(item)
|
||||
backends.append(new)
|
||||
return self._clone(backends)
|
||||
|
||||
def distinct(self):
|
||||
logger.debug("Distinct asset user queryset")
|
||||
queryset_chain = chain(*(backend.get_queryset() for backend in self.backends))
|
||||
queryset_sorted = sorted(
|
||||
queryset_chain,
|
||||
key=lambda item: (item["asset_username"], item["score"]),
|
||||
reverse=True,
|
||||
)
|
||||
results = groupby(queryset_sorted, key=lambda item: item["asset_username"])
|
||||
final = [next(result[1]) for result in results]
|
||||
self._distinct_queryset = final
|
||||
return self
|
||||
|
||||
def get(self, latest=False, **kwargs):
|
||||
queryset = self.filter(**kwargs)
|
||||
if latest:
|
||||
queryset = queryset.distinct()
|
||||
queryset = list(queryset)
|
||||
count = len(queryset)
|
||||
if count == 1:
|
||||
data = queryset[0]
|
||||
return data
|
||||
elif count > 1:
|
||||
msg = 'Should return 1 record, but get {}'.format(count)
|
||||
raise MultipleObjectsReturned(msg)
|
||||
else:
|
||||
msg = 'No record found(org is {})'.format(current_org.name)
|
||||
raise ObjectDoesNotExist(msg)
|
||||
|
||||
def get_latest(self, **kwargs):
|
||||
return self.get(latest=True, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def to_asset_user(data):
|
||||
obj = AssetUser()
|
||||
for k, v in data.items():
|
||||
setattr(obj, k, v)
|
||||
return obj
|
||||
|
||||
@property
|
||||
def queryset(self):
|
||||
if self._distinct_queryset is not None:
|
||||
return self._distinct_queryset
|
||||
return QuerySetChain(self.backends_queryset())
|
||||
|
||||
def count(self):
|
||||
if self._distinct_queryset is not None:
|
||||
return len(self._distinct_queryset)
|
||||
else:
|
||||
return sum(self.backends_counts)
|
||||
|
||||
def __getitem__(self, ndx):
|
||||
return self.queryset.__getitem__(ndx)
|
||||
|
||||
def __iter__(self):
|
||||
self._data = iter(self.queryset)
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return self.to_asset_user(next(self._data))
|
||||
|
||||
|
||||
class AssetUserManager:
|
||||
support_backends = (
|
||||
('db', AuthbookBackend),
|
||||
('system_user', SystemUserBackend),
|
||||
('admin_user', AdminUserBackend),
|
||||
('system_user_dynamic', DynamicSystemUserBackend),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.backends = [backend() for name, backend in self.support_backends]
|
||||
self._queryset = AssetUserQueryset(self.backends)
|
||||
|
||||
def all(self):
|
||||
return self._queryset
|
||||
|
||||
def delete(self, obj):
|
||||
name_backends_map = dict(self.support_backends)
|
||||
backend_name = obj.backend
|
||||
backend_cls = name_backends_map.get(backend_name)
|
||||
union_id = obj.union_id
|
||||
if backend_cls:
|
||||
backend_cls().delete(union_id)
|
||||
else:
|
||||
raise ObjectDoesNotExist("Not backend found")
|
||||
|
||||
@staticmethod
|
||||
def create(**kwargs):
|
||||
# 使用create方法创建AuthBook对象,解决并发创建问题(添加锁机制)
|
||||
authbook = AuthBook.create(**kwargs)
|
||||
return authbook
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._queryset, item)
|
||||
@@ -1,7 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
# from django.conf import settings
|
||||
|
||||
# from .vault import VaultBackend
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
|
||||
90
apps/assets/migrations/0071_systemuser_type.py
Normal file
90
apps/assets/migrations/0071_systemuser_type.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-04 16:46
|
||||
|
||||
from django.db import migrations, models, transaction
|
||||
import django.db.models.deletion
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def migrate_admin_user_to_system_user(apps, schema_editor):
|
||||
admin_user_model = apps.get_model("assets", "AdminUser")
|
||||
system_user_model = apps.get_model("assets", "SystemUser")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
admin_users = admin_user_model.objects.using(db_alias).all()
|
||||
print()
|
||||
for admin_user in admin_users:
|
||||
kwargs = {}
|
||||
for attr in [
|
||||
'id', 'org_id', 'username', 'password', 'private_key', 'public_key',
|
||||
'comment', 'date_created', 'date_updated', 'created_by',
|
||||
]:
|
||||
value = getattr(admin_user, attr)
|
||||
kwargs[attr] = value
|
||||
|
||||
name = admin_user.name
|
||||
exist = system_user_model.objects.using(db_alias).filter(
|
||||
name=admin_user.name, org_id=admin_user.org_id
|
||||
).exists()
|
||||
if exist:
|
||||
name = admin_user.name + '_' + str(admin_user.id)[:5]
|
||||
kwargs.update({
|
||||
'name': name,
|
||||
'type': 'admin',
|
||||
'protocol': 'ssh',
|
||||
'auto_push': False,
|
||||
})
|
||||
|
||||
with transaction.atomic():
|
||||
s = system_user_model(**kwargs)
|
||||
s.save()
|
||||
print(" Migrate admin user to system user: {} => {}".format(admin_user.name, s.name))
|
||||
assets = admin_user.assets.all()
|
||||
s.assets.set(assets)
|
||||
|
||||
|
||||
def migrate_assets_admin_user(apps, schema_editor):
|
||||
asset_model = apps.get_model("assets", "Asset")
|
||||
db_alias = schema_editor.connection.alias
|
||||
assets = asset_model.objects.using(db_alias).all()
|
||||
assets.update(admin_user=F('_admin_user'))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0070_auto_20210426_1515'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('common', 'Common user'), ('admin', 'Admin user')], default='common', max_length=16, verbose_name='Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='login_mode',
|
||||
field=models.CharField(choices=[('auto', 'Automatic managed'), ('manual', 'Manually input')], default='auto', max_length=10, verbose_name='Login mode'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.RunPython(migrate_admin_user_to_system_user),
|
||||
migrations.RenameField(
|
||||
model_name='asset',
|
||||
old_name='admin_user',
|
||||
new_name='_admin_user',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='admin_user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='admin_assets', to='assets.systemuser', verbose_name='Admin user'),
|
||||
),
|
||||
migrations.RunPython(migrate_assets_admin_user),
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='_admin_user',
|
||||
),
|
||||
]
|
||||
85
apps/assets/migrations/0072_historicalauthbook.py
Normal file
85
apps/assets/migrations/0072_historicalauthbook.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-05 16:10
|
||||
|
||||
import common.fields.model
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
import uuid
|
||||
from django.utils import timezone
|
||||
from django.db import migrations, transaction
|
||||
|
||||
|
||||
def migrate_old_authbook_to_history(apps, schema_editor):
|
||||
authbook_model = apps.get_model("assets", "AuthBook")
|
||||
history_model = apps.get_model("assets", "HistoricalAuthBook")
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
print()
|
||||
while True:
|
||||
authbooks = authbook_model.objects.using(db_alias).filter(is_latest=False)[:20]
|
||||
if not authbooks:
|
||||
break
|
||||
historys = []
|
||||
authbook_ids = []
|
||||
# Todo: 或许能优化成更新那样
|
||||
for authbook in authbooks:
|
||||
authbook_ids.append(authbook.id)
|
||||
history = history_model()
|
||||
|
||||
for attr in [
|
||||
'id', 'username', 'password', 'private_key', 'public_key', 'version',
|
||||
'comment', 'created_by', 'asset', 'date_created', 'date_updated'
|
||||
]:
|
||||
setattr(history, attr, getattr(authbook, attr))
|
||||
history.history_type = '-'
|
||||
history.history_date = timezone.now()
|
||||
historys.append(history)
|
||||
|
||||
with transaction.atomic():
|
||||
print(" Migrate old auth book to history table: {} items".format(len(authbook_ids)))
|
||||
history_model.objects.bulk_create(historys, ignore_conflicts=True)
|
||||
authbook_model.objects.filter(id__in=authbook_ids).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('assets', '0071_systemuser_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HistoricalAuthBook',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('version', models.IntegerField(default=1, verbose_name='Version')),
|
||||
('is_latest', models.BooleanField(default=False, verbose_name='Latest version')),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('history_date', models.DateTimeField()),
|
||||
('history_change_reason', models.CharField(max_length=100, null=True)),
|
||||
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
|
||||
('asset', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.asset', verbose_name='Asset')),
|
||||
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'historical AuthBook',
|
||||
'ordering': ('-history_date', '-history_id'),
|
||||
'get_latest_by': 'history_date',
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.RunPython(migrate_old_authbook_to_history)
|
||||
]
|
||||
105
apps/assets/migrations/0073_auto_20210606_1142.py
Normal file
105
apps/assets/migrations/0073_auto_20210606_1142.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-06 03:42
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db import migrations, models, transaction
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def migrate_system_assets_to_authbook(apps, schema_editor):
|
||||
system_user_model = apps.get_model("assets", "SystemUser")
|
||||
system_user_asset_model = system_user_model.assets.through
|
||||
authbook_model = apps.get_model('assets', 'AuthBook')
|
||||
history_model = apps.get_model("assets", "HistoricalAuthBook")
|
||||
|
||||
print()
|
||||
system_users = system_user_model.objects.all()
|
||||
for s in system_users:
|
||||
while True:
|
||||
systemuser_asset_relations = system_user_asset_model.objects.filter(systemuser=s)[:20]
|
||||
if not systemuser_asset_relations:
|
||||
break
|
||||
authbooks = []
|
||||
relations_ids = []
|
||||
historys = []
|
||||
for i in systemuser_asset_relations:
|
||||
authbook = authbook_model(asset=i.asset, systemuser=i.systemuser, org_id=s.org_id)
|
||||
authbooks.append(authbook)
|
||||
relations_ids.append(i.id)
|
||||
|
||||
history = history_model(
|
||||
asset=i.asset, systemuser=i.systemuser,
|
||||
date_created=timezone.now(), date_updated=timezone.now(),
|
||||
)
|
||||
history.history_type = '-'
|
||||
history.history_date = timezone.now()
|
||||
historys.append(history)
|
||||
|
||||
with transaction.atomic():
|
||||
print(" Migrate system user assets relations: {} items".format(len(relations_ids)))
|
||||
authbook_model.objects.bulk_create(authbooks, ignore_conflicts=True)
|
||||
history_model.objects.bulk_create(historys)
|
||||
system_user_asset_model.objects.filter(id__in=relations_ids).delete()
|
||||
|
||||
|
||||
def migrate_authbook_secret_to_system_user(apps, schema_editor):
|
||||
authbook_model = apps.get_model('assets', 'AuthBook')
|
||||
history_model = apps.get_model('assets', 'HistoricalAuthBook')
|
||||
|
||||
print()
|
||||
authbooks_without_systemuser = authbook_model.objects.filter(systemuser__isnull=True)
|
||||
for authbook in authbooks_without_systemuser:
|
||||
matched = authbook_model.objects.filter(
|
||||
asset=authbook.asset, systemuser__username=authbook.username
|
||||
)
|
||||
if not matched:
|
||||
continue
|
||||
historys = []
|
||||
for i in matched:
|
||||
history = history_model(
|
||||
asset=i.asset, systemuser=i.systemuser,
|
||||
date_created=timezone.now(), date_updated=timezone.now(),
|
||||
version=authbook.version
|
||||
)
|
||||
history.history_type = '-'
|
||||
history.history_date = timezone.now()
|
||||
historys.append(history)
|
||||
|
||||
with transaction.atomic():
|
||||
print(" Migrate secret to system user assets account: {} items".format(len(historys)))
|
||||
matched.update(password=authbook.password, private_key=authbook.private_key,
|
||||
public_key=authbook.public_key, version=authbook.version)
|
||||
history_model.objects.bulk_create(historys)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0072_historicalauthbook'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='authbook',
|
||||
name='systemuser',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalauthbook',
|
||||
name='systemuser',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='authbook',
|
||||
unique_together={('username', 'asset', 'systemuser')},
|
||||
),
|
||||
migrations.RunPython(migrate_system_assets_to_authbook),
|
||||
migrations.RunPython(migrate_authbook_secret_to_system_user),
|
||||
migrations.RemoveField(
|
||||
model_name='authbook',
|
||||
name='is_latest',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalauthbook',
|
||||
name='is_latest',
|
||||
),
|
||||
]
|
||||
24
apps/assets/migrations/0074_remove_systemuser_assets.py
Normal file
24
apps/assets/migrations/0074_remove_systemuser_assets.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-06 03:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0073_auto_20210606_1142'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='systemuser',
|
||||
name='assets',
|
||||
),
|
||||
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='assets',
|
||||
field=models.ManyToManyField(blank=True, related_name='system_users', through='assets.AuthBook', to='assets.Asset', verbose_name='Assets'),
|
||||
),
|
||||
]
|
||||
53
apps/assets/migrations/0075_auto_20210705_1759.py
Normal file
53
apps/assets/migrations/0075_auto_20210705_1759.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 3.1 on 2021-07-05 09:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0074_remove_systemuser_assets'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='connectivity',
|
||||
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='date_verified',
|
||||
field=models.DateTimeField(null=True, verbose_name='Date verified'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='authbook',
|
||||
name='connectivity',
|
||||
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='authbook',
|
||||
name='date_verified',
|
||||
field=models.DateTimeField(null=True, verbose_name='Date verified'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalauthbook',
|
||||
name='connectivity',
|
||||
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalauthbook',
|
||||
name='date_verified',
|
||||
field=models.DateTimeField(null=True, verbose_name='Date verified'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='asset',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC')], default='ssh', max_length=128, verbose_name='Protocol'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'SSH')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
||||
16
apps/assets/migrations/0076_delete_assetuser.py
Normal file
16
apps/assets/migrations/0076_delete_assetuser.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 3.1.6 on 2021-07-12 02:25
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0075_auto_20210705_1759'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='AssetUser',
|
||||
),
|
||||
]
|
||||
@@ -2,7 +2,6 @@ from .base import *
|
||||
from .asset import *
|
||||
from .label import Label
|
||||
from .user import *
|
||||
from .asset_user import *
|
||||
from .cluster import *
|
||||
from .group import *
|
||||
from .domain import *
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
|
||||
import uuid
|
||||
import logging
|
||||
import random
|
||||
from functools import reduce
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.db import models
|
||||
from common.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from common.fields.model import JsonDictTextField
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
from .base import ConnectivityMixin
|
||||
from .utils import Connectivity
|
||||
|
||||
from .base import AbsConnectivity
|
||||
|
||||
__all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet']
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -57,16 +58,12 @@ class AssetQuerySet(models.QuerySet):
|
||||
|
||||
class ProtocolsMixin:
|
||||
protocols = ''
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_CHOICES = (
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
)
|
||||
|
||||
class Protocol(TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
vnc = 'vnc', 'VNC'
|
||||
|
||||
@property
|
||||
def protocols_as_list(self):
|
||||
@@ -167,7 +164,7 @@ class Platform(models.Model):
|
||||
# ordering = ('name',)
|
||||
|
||||
|
||||
class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
# Important
|
||||
PLATFORM_CHOICES = (
|
||||
('Linux', 'Linux'),
|
||||
@@ -182,8 +179,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
|
||||
hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
|
||||
protocol = models.CharField(max_length=128, default=ProtocolsMixin.PROTOCOL_SSH,
|
||||
choices=ProtocolsMixin.PROTOCOL_CHOICES,
|
||||
protocol = models.CharField(max_length=128, default=ProtocolsMixin.Protocol.ssh,
|
||||
choices=ProtocolsMixin.Protocol.choices,
|
||||
verbose_name=_('Protocol'))
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols"))
|
||||
@@ -193,7 +190,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
|
||||
# Auth
|
||||
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"), related_name='assets')
|
||||
admin_user = models.ForeignKey('assets.SystemUser', on_delete=models.SET_NULL, null=True, verbose_name=_("Admin user"), related_name='admin_assets')
|
||||
|
||||
# Some information
|
||||
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
|
||||
@@ -223,11 +220,26 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
|
||||
|
||||
objects = AssetManager.from_queryset(AssetQuerySet)()
|
||||
_connectivity = None
|
||||
|
||||
def __str__(self):
|
||||
return '{0.hostname}({0.ip})'.format(self)
|
||||
|
||||
def set_admin_user_relation(self):
|
||||
from .authbook import AuthBook
|
||||
if not self.admin_user:
|
||||
return
|
||||
if self.admin_user.type != 'admin':
|
||||
raise ValidationError('System user should be type admin')
|
||||
|
||||
defaults = {'asset': self, 'systemuser': self.admin_user, 'org_id': self.org_id}
|
||||
AuthBook.objects.get_or_create(defaults=defaults, asset=self, systemuser=self.admin_user)
|
||||
|
||||
@property
|
||||
def admin_user_display(self):
|
||||
if not self.admin_user:
|
||||
return ''
|
||||
return str(self.admin_user)
|
||||
|
||||
@property
|
||||
def is_valid(self):
|
||||
warning = ''
|
||||
@@ -276,23 +288,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
else:
|
||||
return ''
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
if self._connectivity:
|
||||
return self._connectivity
|
||||
if not self.admin_user_username:
|
||||
return Connectivity.unknown()
|
||||
connectivity = ConnectivityMixin.get_asset_username_connectivity(
|
||||
self, self.admin_user_username
|
||||
)
|
||||
return connectivity
|
||||
|
||||
@connectivity.setter
|
||||
def connectivity(self, value):
|
||||
if not self.admin_user:
|
||||
return
|
||||
self.admin_user.set_asset_connectivity(self, value)
|
||||
|
||||
def get_auth_info(self):
|
||||
if not self.admin_user:
|
||||
return {}
|
||||
@@ -338,7 +333,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
'iconSkin': icon_skin,
|
||||
'meta': {
|
||||
'type': 'asset',
|
||||
'asset': {
|
||||
'data': {
|
||||
'id': self.id,
|
||||
'hostname': self.hostname,
|
||||
'ip': self.ip,
|
||||
@@ -350,6 +345,13 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
|
||||
tree_node = TreeNode(**data)
|
||||
return tree_node
|
||||
|
||||
def get_all_systemusers(self):
|
||||
from .user import SystemUser
|
||||
system_user_ids = SystemUser.assets.through.objects.filter(asset=self)\
|
||||
.values_list('systemuser_id', flat=True)
|
||||
system_users = SystemUser.objects.filter(id__in=system_user_ids)
|
||||
return system_users
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'hostname')]
|
||||
verbose_name = _("Asset")
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .authbook import AuthBook
|
||||
|
||||
|
||||
class AssetUser(AuthBook):
|
||||
hostname = ""
|
||||
ip = ""
|
||||
backend = ""
|
||||
backend_display = ""
|
||||
union_id = ""
|
||||
asset_username = ""
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
@@ -1,92 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Max
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
from orgs.mixins.models import OrgManager
|
||||
from .base import BaseUser
|
||||
from common.utils import lazyproperty
|
||||
from .base import BaseUser, AbsConnectivity
|
||||
|
||||
__all__ = ['AuthBook']
|
||||
|
||||
|
||||
class AuthBookQuerySet(models.QuerySet):
|
||||
def delete(self):
|
||||
if self.count() > 1:
|
||||
raise PermissionDenied(_("Bulk delete deny"))
|
||||
return super().delete()
|
||||
|
||||
|
||||
class AuthBookManager(OrgManager):
|
||||
pass
|
||||
|
||||
|
||||
class AuthBook(BaseUser):
|
||||
class AuthBook(BaseUser, AbsConnectivity):
|
||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
|
||||
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
|
||||
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
|
||||
version = models.IntegerField(default=1, verbose_name=_('Version'))
|
||||
history = HistoricalRecords()
|
||||
_systemuser_display = ''
|
||||
|
||||
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
|
||||
backend = "db"
|
||||
# 用于system user和admin_user的动态设置
|
||||
_connectivity = None
|
||||
CONN_CACHE_KEY = "ASSET_USER_CONN_{}"
|
||||
auth_attrs = ['username', 'password', 'private_key', 'public_key']
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('AuthBook')
|
||||
unique_together = [('username', 'asset', 'systemuser')]
|
||||
|
||||
def get_related_assets(self):
|
||||
return [self.asset]
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.auth_snapshot = {}
|
||||
|
||||
def generate_id_with_asset(self, asset):
|
||||
return self.id
|
||||
def get_or_systemuser_attr(self, attr):
|
||||
val = getattr(self, attr, None)
|
||||
if val:
|
||||
return val
|
||||
if self.systemuser:
|
||||
return getattr(self.systemuser, attr, '')
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def get_max_version(cls, username, asset):
|
||||
version_max = cls.objects.filter(username=username, asset=asset) \
|
||||
.aggregate(Max('version'))
|
||||
version_max = version_max['version__max'] or 0
|
||||
return version_max
|
||||
def load_auth(self):
|
||||
for attr in self.auth_attrs:
|
||||
value = self.get_or_systemuser_attr(attr)
|
||||
self.auth_snapshot[attr] = [getattr(self, attr), value]
|
||||
setattr(self, attr, value)
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
"""
|
||||
使用并发锁机制创建AuthBook对象, (主要针对并发创建 username, asset 相同的对象时)
|
||||
并更新其他对象的 is_latest=False (其他对象: 与当前对象的 username, asset 相同)
|
||||
同时设置自己的 is_latest=True, version=max_version + 1
|
||||
"""
|
||||
username = kwargs['username']
|
||||
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)
|
||||
instances.filter(is_latest=True).update(is_latest=False)
|
||||
max_version = cls.get_max_version(username, asset)
|
||||
kwargs.update({
|
||||
'version': max_version + 1,
|
||||
'is_latest': True
|
||||
})
|
||||
obj = cls.objects.create(**kwargs)
|
||||
return obj
|
||||
def unload_auth(self):
|
||||
if not self.systemuser:
|
||||
return
|
||||
|
||||
for attr, values in self.auth_snapshot.items():
|
||||
origin_value, loaded_value = values
|
||||
current_value = getattr(self, attr, '')
|
||||
if current_value == loaded_value:
|
||||
setattr(self, attr, origin_value)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.unload_auth()
|
||||
instance = super().save(*args, **kwargs)
|
||||
self.load_auth()
|
||||
return instance
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
return self.get_asset_connectivity(self.asset)
|
||||
def username_display(self):
|
||||
return self.get_or_systemuser_attr('username') or '*'
|
||||
|
||||
@lazyproperty
|
||||
def systemuser_display(self):
|
||||
if self._systemuser_display:
|
||||
return self._systemuser_display
|
||||
if not self.systemuser:
|
||||
return ''
|
||||
return str(self.systemuser)
|
||||
|
||||
@property
|
||||
def keyword(self):
|
||||
return '{}_#_{}'.format(self.username, str(self.asset.id))
|
||||
def smart_name(self):
|
||||
username = self.username_display
|
||||
|
||||
@property
|
||||
def hostname(self):
|
||||
return self.asset.hostname
|
||||
if self.asset:
|
||||
asset = str(self.asset)
|
||||
else:
|
||||
asset = '*'
|
||||
return '{}@{}'.format(username, asset)
|
||||
|
||||
@property
|
||||
def ip(self):
|
||||
return self.asset.ip
|
||||
def sync_to_system_user_account(self):
|
||||
if self.systemuser:
|
||||
return
|
||||
matched = AuthBook.objects.filter(
|
||||
asset=self.asset, systemuser__username=self.username
|
||||
)
|
||||
if not matched:
|
||||
return
|
||||
|
||||
for i in matched:
|
||||
i.password = self.password
|
||||
i.private_key = self.private_key
|
||||
i.public_key = self.public_key
|
||||
i.comment = 'Update triggered by account {}'.format(self.id)
|
||||
i.save(update_fields=['password', 'private_key', 'public_key'])
|
||||
|
||||
def __str__(self):
|
||||
return '{}@{}'.format(self.username, self.asset)
|
||||
return self.smart_name
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ from hashlib import md5
|
||||
import sshpubkeys
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from common.utils import random_string, signer
|
||||
from common.utils import (
|
||||
@@ -19,85 +21,39 @@ from common.utils.encode import ssh_pubkey_gen
|
||||
from common.validators import alphanumeric
|
||||
from common import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from .utils import Connectivity
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class ConnectivityMixin:
|
||||
CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY"
|
||||
CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT"
|
||||
ASSET_USER_CACHE_TIME = 3600 * 24
|
||||
id = ''
|
||||
username = ''
|
||||
class Connectivity(models.TextChoices):
|
||||
unknown = 'unknown', _('Unknown')
|
||||
ok = 'ok', _('Ok')
|
||||
failed = 'failed', _('Failed')
|
||||
|
||||
@property
|
||||
def part_id(self):
|
||||
i = '-'.join(str(self.id).split('-')[:3])
|
||||
return i
|
||||
|
||||
def set_connectivity(self, summary):
|
||||
unreachable = summary.get('dark', {}).keys()
|
||||
reachable = summary.get('contacted', {}).keys()
|
||||
class AbsConnectivity(models.Model):
|
||||
connectivity = models.CharField(
|
||||
choices=Connectivity.choices, default=Connectivity.unknown,
|
||||
max_length=16, verbose_name=_('Connectivity')
|
||||
)
|
||||
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
|
||||
|
||||
assets = self.get_related_assets()
|
||||
if not isinstance(assets, list):
|
||||
assets = assets.only('id', 'hostname', 'admin_user__id')
|
||||
for asset in assets:
|
||||
if asset.hostname in unreachable:
|
||||
self.set_asset_connectivity(asset, Connectivity.unreachable())
|
||||
elif asset.hostname in reachable:
|
||||
self.set_asset_connectivity(asset, Connectivity.reachable())
|
||||
else:
|
||||
self.set_asset_connectivity(asset, Connectivity.unknown())
|
||||
cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id)
|
||||
cache.delete(cache_key)
|
||||
|
||||
@property
|
||||
def connectivity(self):
|
||||
assets = self.get_related_assets()
|
||||
if not isinstance(assets, list):
|
||||
assets = assets.only('id', 'hostname', 'admin_user__id')
|
||||
data = {
|
||||
'unreachable': [],
|
||||
'reachable': [],
|
||||
'unknown': [],
|
||||
}
|
||||
for asset in assets:
|
||||
connectivity = self.get_asset_connectivity(asset)
|
||||
if connectivity.is_reachable():
|
||||
data["reachable"].append(asset.hostname)
|
||||
elif connectivity.is_unreachable():
|
||||
data["unreachable"].append(asset.hostname)
|
||||
else:
|
||||
data["unknown"].append(asset.hostname)
|
||||
return data
|
||||
|
||||
@property
|
||||
def connectivity_amount(self):
|
||||
cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id)
|
||||
amount = cache.get(cache_key)
|
||||
if not amount:
|
||||
amount = {k: len(v) for k, v in self.connectivity.items()}
|
||||
cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME)
|
||||
return amount
|
||||
def set_connectivity(self, val):
|
||||
self.connectivity = val
|
||||
self.date_verified = timezone.now()
|
||||
self.save(update_fields=['connectivity', 'date_verified'])
|
||||
|
||||
@classmethod
|
||||
def get_asset_username_connectivity(cls, asset, username):
|
||||
key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id)
|
||||
return Connectivity.get(key)
|
||||
def bulk_set_connectivity(cls, queryset_or_id, connectivity):
|
||||
if not isinstance(queryset_or_id, QuerySet):
|
||||
queryset = cls.objects.filter(id__in=queryset_or_id)
|
||||
else:
|
||||
queryset = queryset_or_id
|
||||
queryset.update(connectivity=connectivity, date_verified=timezone.now())
|
||||
|
||||
def get_asset_connectivity(self, asset):
|
||||
key = self.get_asset_connectivity_key(asset)
|
||||
return Connectivity.get(key)
|
||||
|
||||
def get_asset_connectivity_key(self, asset):
|
||||
return self.CONNECTIVITY_ASSET_CACHE_KEY.format(self.username, asset.id)
|
||||
|
||||
def set_asset_connectivity(self, asset, c):
|
||||
key = self.get_asset_connectivity_key(asset)
|
||||
Connectivity.set(key, c)
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class AuthMixin:
|
||||
@@ -105,14 +61,16 @@ class AuthMixin:
|
||||
password = ''
|
||||
public_key = ''
|
||||
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)
|
||||
try:
|
||||
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
|
||||
except IOError as e:
|
||||
return str(e)
|
||||
else:
|
||||
return ''
|
||||
|
||||
@@ -173,38 +131,6 @@ class AuthMixin:
|
||||
if update_fields:
|
||||
self.save(update_fields=update_fields)
|
||||
|
||||
def has_special_auth(self, asset=None, username=None):
|
||||
from .authbook import AuthBook
|
||||
if username is None:
|
||||
username = self.username
|
||||
queryset = AuthBook.objects.filter(username=username)
|
||||
if asset:
|
||||
queryset = queryset.filter(asset=asset)
|
||||
return queryset.exists()
|
||||
|
||||
def get_asset_user(self, asset, username=None):
|
||||
from ..backends import AssetUserManager
|
||||
if username is None:
|
||||
username = self.username
|
||||
try:
|
||||
manager = AssetUserManager()
|
||||
other = manager.get_latest(
|
||||
username=username, asset=asset,
|
||||
prefer_id=self.id, prefer=self._prefer,
|
||||
)
|
||||
return other
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
return None
|
||||
|
||||
def load_asset_special_auth(self, asset=None, username=None):
|
||||
if not asset:
|
||||
return self
|
||||
|
||||
instance = self.get_asset_user(asset, username=username)
|
||||
if instance:
|
||||
self._merge_auth(instance)
|
||||
|
||||
def _merge_auth(self, other):
|
||||
if other.password:
|
||||
self.password = other.password
|
||||
@@ -244,7 +170,7 @@ class AuthMixin:
|
||||
)
|
||||
|
||||
|
||||
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
|
||||
class BaseUser(OrgModelMixin, AuthMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
@@ -259,8 +185,6 @@ class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
|
||||
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
|
||||
ASSET_USER_CACHE_TIME = 600
|
||||
|
||||
_prefer = "system_user"
|
||||
|
||||
def get_related_assets(self):
|
||||
assets = self.assets.filter(org_id=self.org_id)
|
||||
return assets
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import socket
|
||||
import uuid
|
||||
import random
|
||||
import re
|
||||
|
||||
from django.core.cache import cache
|
||||
import paramiko
|
||||
from django.db import models
|
||||
from django.db.models import TextChoices
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils.strings import no_special_chars
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from .base import BaseUser
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['Domain', 'Gateway']
|
||||
|
||||
|
||||
@@ -39,19 +42,25 @@ class Domain(OrgModelMixin):
|
||||
return self.gateway_set.filter(is_active=True)
|
||||
|
||||
def random_gateway(self):
|
||||
return random.choice(self.gateways)
|
||||
gateways = [gw for gw in self.gateways if gw.is_connective]
|
||||
if gateways:
|
||||
return random.choice(gateways)
|
||||
else:
|
||||
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
|
||||
return random.choice(self.gateways)
|
||||
|
||||
|
||||
class Gateway(BaseUser):
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_CHOICES = (
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
)
|
||||
UNCONNECTIVE_KEY_TMPL = 'asset_unconnective_gateway_{}'
|
||||
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
|
||||
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
|
||||
|
||||
class Protocol(TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
|
||||
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
|
||||
port = models.IntegerField(default=22, verbose_name=_('Port'))
|
||||
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol"))
|
||||
protocol = models.CharField(choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol"))
|
||||
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
|
||||
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
@@ -63,11 +72,40 @@ class Gateway(BaseUser):
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("Gateway")
|
||||
|
||||
def set_unconnective(self):
|
||||
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
|
||||
|
||||
unconnective_silence_period = cache.get(unconnective_silence_period_key,
|
||||
self.UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE)
|
||||
cache.set(unconnective_silence_period_key, unconnective_silence_period * 2)
|
||||
cache.set(unconnective_key, unconnective_silence_period, unconnective_silence_period)
|
||||
|
||||
def set_connective(self):
|
||||
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
|
||||
|
||||
cache.delete(unconnective_key)
|
||||
cache.delete(unconnective_silence_period_key)
|
||||
|
||||
def get_is_unconnective(self):
|
||||
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
|
||||
return cache.get(unconnective_key, False)
|
||||
|
||||
@property
|
||||
def is_connective(self):
|
||||
return not self.get_is_unconnective()
|
||||
|
||||
@is_connective.setter
|
||||
def is_connective(self, value):
|
||||
if value:
|
||||
self.set_connective()
|
||||
else:
|
||||
self.set_unconnective()
|
||||
|
||||
def test_connective(self, local_port=None):
|
||||
if local_port is None:
|
||||
local_port = self.port
|
||||
if self.password and not no_special_chars(self.password):
|
||||
return False, _("Password should not contains special characters")
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
@@ -82,8 +120,16 @@ class Gateway(BaseUser):
|
||||
except(paramiko.AuthenticationException,
|
||||
paramiko.BadAuthenticationType,
|
||||
paramiko.SSHException,
|
||||
paramiko.ssh_exception.NoValidConnectionsError) as e:
|
||||
return False, str(e)
|
||||
paramiko.ssh_exception.NoValidConnectionsError,
|
||||
socket.gaierror) as e:
|
||||
err = str(e)
|
||||
if err.startswith('[Errno None] Unable to connect to port'):
|
||||
err = _('Unable to connect to port {port} on {ip}')
|
||||
err = err.format(port=self.port, ip=self.ip)
|
||||
elif err == 'Authentication failed.':
|
||||
err = _('Authentication failed')
|
||||
self.is_connective = False
|
||||
return False, err
|
||||
|
||||
try:
|
||||
sock = proxy.get_transport().open_channel(
|
||||
@@ -97,7 +143,9 @@ class Gateway(BaseUser):
|
||||
timeout=5)
|
||||
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
|
||||
paramiko.AuthenticationException, TimeoutError) as e:
|
||||
self.is_connective = False
|
||||
return False, str(e)
|
||||
finally:
|
||||
client.close()
|
||||
self.is_connective = True
|
||||
return True, None
|
||||
|
||||
@@ -608,7 +608,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
|
||||
'isParent': True,
|
||||
'open': self.is_org_root(),
|
||||
'meta': {
|
||||
'node': {
|
||||
'data': {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"value": self.value,
|
||||
|
||||
@@ -10,15 +10,274 @@ from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.cache import cache
|
||||
|
||||
from common.utils import signer, get_object_or_none
|
||||
from common.exceptions import JMSException
|
||||
from common.db.models import TextChoices
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
from .authbook import AuthBook
|
||||
|
||||
|
||||
__all__ = ['AdminUser', 'SystemUser']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProtocolMixin:
|
||||
protocol: str
|
||||
|
||||
class Protocol(TextChoices):
|
||||
ssh = 'ssh', 'SSH'
|
||||
rdp = 'rdp', 'RDP'
|
||||
telnet = 'telnet', 'Telnet'
|
||||
vnc = 'vnc', 'VNC'
|
||||
mysql = 'mysql', 'MySQL'
|
||||
oracle = 'oracle', 'Oracle'
|
||||
mariadb = 'mariadb', 'MariaDB'
|
||||
postgresql = 'postgresql', 'PostgreSQL'
|
||||
k8s = '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
|
||||
]
|
||||
|
||||
@property
|
||||
def is_protocol_support_push(self):
|
||||
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
|
||||
|
||||
@classmethod
|
||||
def get_protocol_by_application_type(cls, app_type):
|
||||
from applications.const import AppType
|
||||
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
|
||||
protocol = app_type
|
||||
elif app_type in AppType.remote_app_types():
|
||||
protocol = cls.Protocol.rdp
|
||||
else:
|
||||
protocol = None
|
||||
return protocol
|
||||
|
||||
@property
|
||||
def can_perm_to_asset(self):
|
||||
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
|
||||
class AuthMixin:
|
||||
username_same_with_user: bool
|
||||
protocol: str
|
||||
ASSET_CATEGORY_PROTOCOLS: list
|
||||
login_mode: str
|
||||
LOGIN_MANUAL: str
|
||||
id: str
|
||||
username: str
|
||||
password: str
|
||||
private_key: str
|
||||
public_key: str
|
||||
|
||||
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
|
||||
if not auth:
|
||||
raise ValueError('Auth not set')
|
||||
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
|
||||
logger.debug(f'Set system user temp auth: {key}')
|
||||
cache.set(key, auth, ttl)
|
||||
|
||||
def get_temp_auth(self, asset_or_app_id, user_id):
|
||||
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
|
||||
logger.debug(f'Get system user temp auth: {key}')
|
||||
password = cache.get(key)
|
||||
return password
|
||||
|
||||
def load_tmp_auth_if_has(self, asset_or_app_id, user):
|
||||
if not asset_or_app_id or not user:
|
||||
return
|
||||
|
||||
if self.login_mode != self.LOGIN_MANUAL:
|
||||
return
|
||||
|
||||
auth = self.get_temp_auth(asset_or_app_id, user)
|
||||
if not auth:
|
||||
return
|
||||
username = auth.get('username')
|
||||
password = auth.get('password')
|
||||
|
||||
if username:
|
||||
self.username = username
|
||||
if password:
|
||||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
if not user_id:
|
||||
return
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if not user:
|
||||
return
|
||||
self.load_tmp_auth_if_has(app_id, user)
|
||||
|
||||
def load_asset_special_auth(self, asset, username=''):
|
||||
"""
|
||||
"""
|
||||
authbooks = list(AuthBook.objects.filter(asset=asset, systemuser=self))
|
||||
if len(authbooks) == 0:
|
||||
return None
|
||||
elif len(authbooks) == 1:
|
||||
authbook = authbooks[0]
|
||||
else:
|
||||
authbooks.sort(key=lambda x: 1 if x.username == username else 0, reverse=True)
|
||||
authbook = authbooks[0]
|
||||
authbook.load_auth()
|
||||
self.password = authbook.password
|
||||
self.private_key = authbook.private_key
|
||||
self.public_key = authbook.public_key
|
||||
|
||||
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
|
||||
asset = None
|
||||
if asset_id:
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
# 没有资产就没有必要继续了
|
||||
if not asset:
|
||||
logger.debug('Asset not found, pass')
|
||||
return
|
||||
|
||||
user = None
|
||||
if user_id:
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
|
||||
_username = self.username
|
||||
if self.username_same_with_user:
|
||||
if user and not username:
|
||||
_username = user.username
|
||||
else:
|
||||
_username = username
|
||||
self.username = _username
|
||||
|
||||
# 加载某个资产的特殊配置认证信息
|
||||
self.load_asset_special_auth(asset, _username)
|
||||
self.load_tmp_auth_if_has(asset_id, user)
|
||||
|
||||
|
||||
class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
|
||||
LOGIN_AUTO = 'auto'
|
||||
LOGIN_MANUAL = 'manual'
|
||||
LOGIN_MODE_CHOICES = (
|
||||
(LOGIN_AUTO, _('Automatic managed')),
|
||||
(LOGIN_MANUAL, _('Manually input'))
|
||||
)
|
||||
|
||||
class Type(TextChoices):
|
||||
common = 'common', _('Common user')
|
||||
admin = 'admin', _('Admin user')
|
||||
|
||||
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
assets = models.ManyToManyField(
|
||||
'assets.Asset', blank=True, verbose_name=_("Assets"),
|
||||
through='assets.AuthBook', through_fields=['systemuser', 'asset'],
|
||||
related_name='system_users'
|
||||
)
|
||||
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
|
||||
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
|
||||
type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type'))
|
||||
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=ProtocolMixin.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'))
|
||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
|
||||
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
|
||||
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
|
||||
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
|
||||
ad_domain = models.CharField(default='', max_length=256)
|
||||
|
||||
def __str__(self):
|
||||
username = self.username
|
||||
if self.username_same_with_user:
|
||||
username = '*'
|
||||
return '{0.name}({1})'.format(self, username)
|
||||
|
||||
@property
|
||||
def nodes_amount(self):
|
||||
return self.nodes.all().count()
|
||||
|
||||
@property
|
||||
def login_mode_display(self):
|
||||
return self.get_login_mode_display()
|
||||
|
||||
def is_need_push(self):
|
||||
if self.auto_push and self.is_protocol_support_push:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_admin_user(self):
|
||||
return self.type == self.Type.admin
|
||||
|
||||
@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 in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
@property
|
||||
def cmd_filter_rules(self):
|
||||
from .cmd_filter import CommandFilterRule
|
||||
rules = CommandFilterRule.objects.filter(
|
||||
filter__in=self.cmd_filters.all()
|
||||
).distinct()
|
||||
return rules
|
||||
|
||||
def is_command_can_run(self, command):
|
||||
for rule in self.cmd_filter_rules:
|
||||
action, matched_cmd = rule.match(command)
|
||||
if action == rule.ActionChoices.allow:
|
||||
return True, None
|
||||
elif action == rule.ActionChoices.deny:
|
||||
return False, matched_cmd
|
||||
return True, None
|
||||
|
||||
def get_all_assets(self):
|
||||
from assets.models import Node
|
||||
nodes_keys = self.nodes.all().values_list('key', flat=True)
|
||||
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
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("System user")
|
||||
|
||||
|
||||
# Todo: 准备废弃
|
||||
class AdminUser(BaseUser):
|
||||
"""
|
||||
A privileged user that ansible can use it to push system user and so on
|
||||
@@ -65,240 +324,3 @@ class AdminUser(BaseUser):
|
||||
ordering = ['name']
|
||||
unique_together = [('name', 'org_id')]
|
||||
verbose_name = _("Admin user")
|
||||
|
||||
|
||||
class SystemUser(BaseUser):
|
||||
PROTOCOL_SSH = 'ssh'
|
||||
PROTOCOL_RDP = 'rdp'
|
||||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_MYSQL = 'mysql'
|
||||
PROTOCOL_ORACLE = 'oracle'
|
||||
PROTOCOL_MARIADB = 'mariadb'
|
||||
PROTOCOL_POSTGRESQL = 'postgresql'
|
||||
PROTOCOL_K8S = 'k8s'
|
||||
PROTOCOL_CHOICES = (
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
(PROTOCOL_MYSQL, 'mysql'),
|
||||
(PROTOCOL_ORACLE, 'oracle'),
|
||||
(PROTOCOL_MARIADB, 'mariadb'),
|
||||
(PROTOCOL_POSTGRESQL, 'postgresql'),
|
||||
(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 = (
|
||||
(LOGIN_AUTO, _('Automatic login')),
|
||||
(LOGIN_MANUAL, _('Manually login'))
|
||||
)
|
||||
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
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=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'))
|
||||
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
|
||||
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
|
||||
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
|
||||
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
|
||||
ad_domain = models.CharField(default='', max_length=256)
|
||||
_prefer = 'system_user'
|
||||
|
||||
def __str__(self):
|
||||
username = self.username
|
||||
if self.username_same_with_user:
|
||||
username = 'dynamic'
|
||||
return '{0.name}({1})'.format(self, username)
|
||||
|
||||
def get_username(self):
|
||||
if self.username_same_with_user:
|
||||
return list(self.users.values_list('username', flat=True))
|
||||
else:
|
||||
return self.username
|
||||
|
||||
@property
|
||||
def nodes_amount(self):
|
||||
return self.nodes.all().count()
|
||||
|
||||
@property
|
||||
def login_mode_display(self):
|
||||
return self.get_login_mode_display()
|
||||
|
||||
def is_need_push(self):
|
||||
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 in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
def has_special_auth(self, asset=None, username=None):
|
||||
if username is None and self.username_same_with_user:
|
||||
raise TypeError('System user is dynamic, username should be pass')
|
||||
return super().has_special_auth(asset=asset, username=username)
|
||||
|
||||
@property
|
||||
def can_perm_to_asset(self):
|
||||
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
|
||||
|
||||
def _merge_auth(self, other):
|
||||
super()._merge_auth(other)
|
||||
if self.username_same_with_user:
|
||||
self.username = other.username
|
||||
|
||||
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
|
||||
if not auth:
|
||||
raise ValueError('Auth not set')
|
||||
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
|
||||
logger.debug(f'Set system user temp auth: {key}')
|
||||
cache.set(key, auth, ttl)
|
||||
|
||||
def get_temp_auth(self, asset_or_app_id, user_id):
|
||||
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
|
||||
logger.debug(f'Get system user temp auth: {key}')
|
||||
password = cache.get(key)
|
||||
return password
|
||||
|
||||
def load_tmp_auth_if_has(self, asset_or_app_id, user):
|
||||
if not asset_or_app_id or not user:
|
||||
return
|
||||
if self.login_mode != self.LOGIN_MANUAL:
|
||||
pass
|
||||
|
||||
auth = self.get_temp_auth(asset_or_app_id, user)
|
||||
if not auth:
|
||||
return
|
||||
username = auth.get('username')
|
||||
password = auth.get('password')
|
||||
|
||||
if username:
|
||||
self.username = username
|
||||
if password:
|
||||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
if not user_id:
|
||||
return
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
if not user:
|
||||
return
|
||||
self.load_tmp_auth_if_has(app_id, user)
|
||||
|
||||
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
|
||||
from users.models import User
|
||||
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self.password = ''
|
||||
self.private_key = ''
|
||||
|
||||
asset = None
|
||||
if asset_id:
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
# 没有资产就没有必要继续了
|
||||
if not asset:
|
||||
logger.debug('Asset not found, pass')
|
||||
return
|
||||
|
||||
user = None
|
||||
if user_id:
|
||||
user = get_object_or_none(User, pk=user_id)
|
||||
|
||||
if self.username_same_with_user:
|
||||
if user and not username:
|
||||
username = user.username
|
||||
|
||||
# 加载某个资产的特殊配置认证信息
|
||||
try:
|
||||
self.load_asset_special_auth(asset, username)
|
||||
except Exception as e:
|
||||
logger.error('Load special auth Error: ', e)
|
||||
pass
|
||||
|
||||
self.load_tmp_auth_if_has(asset_id, user)
|
||||
|
||||
@property
|
||||
def cmd_filter_rules(self):
|
||||
from .cmd_filter import CommandFilterRule
|
||||
rules = CommandFilterRule.objects.filter(
|
||||
filter__in=self.cmd_filters.all()
|
||||
).distinct()
|
||||
return rules
|
||||
|
||||
def is_command_can_run(self, command):
|
||||
for rule in self.cmd_filter_rules:
|
||||
action, matched_cmd = rule.match(command)
|
||||
if action == rule.ActionChoices.allow:
|
||||
return True, None
|
||||
elif action == rule.ActionChoices.deny:
|
||||
return False, matched_cmd
|
||||
return True, None
|
||||
|
||||
def get_all_assets(self):
|
||||
from assets.models import Node
|
||||
nodes_keys = self.nodes.all().values_list('key', flat=True)
|
||||
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')]
|
||||
verbose_name = _("System user")
|
||||
|
||||
@@ -11,7 +11,7 @@ from common.utils import validate_ssh_private_key
|
||||
|
||||
|
||||
__all__ = [
|
||||
'init_model', 'generate_fake', 'private_key_validator', 'Connectivity',
|
||||
'init_model', 'generate_fake', 'private_key_validator',
|
||||
]
|
||||
|
||||
|
||||
@@ -35,74 +35,3 @@ def private_key_validator(value):
|
||||
_('%(value)s is not an even number'),
|
||||
params={'value': value},
|
||||
)
|
||||
|
||||
|
||||
class Connectivity:
|
||||
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
|
||||
CONNECTIVITY_CHOICES = (
|
||||
(UNREACHABLE, _("Unreachable")),
|
||||
(REACHABLE, _('Reachable')),
|
||||
(UNKNOWN, _("Unknown")),
|
||||
)
|
||||
|
||||
status = UNKNOWN
|
||||
datetime = timezone.now()
|
||||
|
||||
def __init__(self, status, datetime):
|
||||
self.status = status
|
||||
self.datetime = datetime
|
||||
|
||||
def display(self):
|
||||
return dict(self.__class__.CONNECTIVITY_CHOICES).get(self.status)
|
||||
|
||||
def is_reachable(self):
|
||||
return self.status == self.REACHABLE
|
||||
|
||||
def is_unreachable(self):
|
||||
return self.status == self.UNREACHABLE
|
||||
|
||||
def is_unknown(self):
|
||||
return self.status == self.UNKNOWN
|
||||
|
||||
@classmethod
|
||||
def unreachable(cls):
|
||||
return cls(cls.UNREACHABLE, timezone.now())
|
||||
|
||||
@classmethod
|
||||
def reachable(cls):
|
||||
return cls(cls.REACHABLE, timezone.now())
|
||||
|
||||
@classmethod
|
||||
def unknown(cls):
|
||||
return cls(cls.UNKNOWN, timezone.now())
|
||||
|
||||
@classmethod
|
||||
def set(cls, key, value, ttl=None):
|
||||
cache.set(key, value, ttl)
|
||||
|
||||
@classmethod
|
||||
def get(cls, key):
|
||||
value = cache.get(key, cls.unknown())
|
||||
if not isinstance(value, cls):
|
||||
value = cls.unknown()
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def set_unreachable(cls, key, ttl=0):
|
||||
cls.set(key, cls.unreachable(), ttl)
|
||||
|
||||
@classmethod
|
||||
def set_reachable(cls, key, ttl=0):
|
||||
cls.set(key, cls.reachable(), ttl)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.status == other.status
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.status > other.status
|
||||
|
||||
def __lt__(self, other):
|
||||
return not self.__gt__(other)
|
||||
|
||||
def __str__(self):
|
||||
return self.display()
|
||||
|
||||
@@ -8,6 +8,6 @@ from .system_user import *
|
||||
from .node import *
|
||||
from .domain import *
|
||||
from .cmd_filter import *
|
||||
from .asset_user import *
|
||||
from .gathered_user import *
|
||||
from .favorite_asset import *
|
||||
from .account import *
|
||||
|
||||
61
apps/assets/serializers/account.py
Normal file
61
apps/assets/serializers/account.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from assets.models import AuthBook
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .base import AuthSerializerMixin
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
|
||||
|
||||
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
ip = serializers.ReadOnlyField(label=_("IP"))
|
||||
hostname = serializers.ReadOnlyField(label=_("Hostname"))
|
||||
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
fields_mini = ['id', 'username', 'ip', 'hostname', 'version']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
|
||||
fields_small = fields_mini + fields_write_only + fields_other
|
||||
fields_fk = ['asset', 'systemuser', 'systemuser_display']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {
|
||||
'write_only': True,
|
||||
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||
},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
'systemuser_display': {'label': _('System user display')}
|
||||
}
|
||||
ref_name = 'AssetAccountSerializer'
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('systemuser', 'asset')
|
||||
return queryset
|
||||
|
||||
def to_representation(self, instance):
|
||||
instance.load_auth()
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AccountSecretSerializer(AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
'public_key': {'write_only': False},
|
||||
'systemuser_display': {'label': _('System user display')}
|
||||
}
|
||||
|
||||
|
||||
class AccountTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('test', 'test'),
|
||||
)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
task = serializers.CharField(read_only=True)
|
||||
@@ -1,72 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Node, AdminUser
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .base import AuthSerializer, AuthSerializerMixin
|
||||
from ..models import SystemUser
|
||||
from .system_user import SystemUserSerializer as SuS
|
||||
|
||||
|
||||
class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class AdminUserSerializer(SuS):
|
||||
"""
|
||||
管理用户
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'private_key', 'public_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by'
|
||||
]
|
||||
fields_fk = ['assets_amount']
|
||||
fields = fields_small + fields_fk
|
||||
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
|
||||
class Meta(SuS.Meta):
|
||||
fields = SuS.Meta.fields_mini + \
|
||||
SuS.Meta.fields_write_only + \
|
||||
SuS.Meta.fields_m2m + \
|
||||
[
|
||||
'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint',
|
||||
'date_created', 'date_updated', 'comment', 'created_by',
|
||||
]
|
||||
|
||||
extra_kwargs = {
|
||||
'username': {"required": True},
|
||||
'password': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
}
|
||||
def validate_type(self, val):
|
||||
return SystemUser.Type.admin
|
||||
|
||||
|
||||
class AdminUserDetailSerializer(AdminUserSerializer):
|
||||
class Meta(AdminUserSerializer.Meta):
|
||||
fields = AdminUserSerializer.Meta.fields + ['ssh_key_fingerprint']
|
||||
|
||||
|
||||
class AdminUserAuthSerializer(AuthSerializer):
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = ['password', 'private_key']
|
||||
|
||||
|
||||
class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
管理用户更新关联到的集群
|
||||
"""
|
||||
nodes = serializers.PrimaryKeyRelatedField(
|
||||
many=True, queryset=Node.objects
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = ['id', 'nodes']
|
||||
|
||||
|
||||
class TaskIDSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class AssetUserTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('test', 'test'),
|
||||
)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
task = serializers.CharField(read_only=True)
|
||||
def validate_protocol(self, val):
|
||||
return 'ssh'
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
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
|
||||
from ..models import Asset, Node, Platform
|
||||
from .base import ConnectivitySerializer
|
||||
from ..models import Asset, Node, Platform, SystemUser
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer',
|
||||
'AssetDisplaySerializer',
|
||||
'ProtocolsField', 'PlatformSerializer',
|
||||
'AssetDetailSerializer', 'AssetTaskSerializer',
|
||||
'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField'
|
||||
]
|
||||
|
||||
|
||||
class ProtocolField(serializers.RegexField):
|
||||
protocols = '|'.join(dict(Asset.PROTOCOL_CHOICES).keys())
|
||||
protocols = '|'.join(dict(Asset.Protocol.choices).keys())
|
||||
default_error_messages = {
|
||||
'invalid': _('Protocol format should {}/{}'.format(protocols, '1-65535'))
|
||||
'invalid': _('Protocol format should {}/{}').format(protocols, '1-65535')
|
||||
}
|
||||
regex = r'^(%s)/(\d{1,5})$' % protocols
|
||||
|
||||
@@ -67,7 +64,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
)
|
||||
protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22'])
|
||||
domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
|
||||
admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name'))
|
||||
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
|
||||
|
||||
"""
|
||||
@@ -75,43 +71,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
"""
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields_mini = ['id', 'hostname', 'ip']
|
||||
fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols']
|
||||
fields_small = fields_mini + [
|
||||
'protocol', 'port', 'protocols', 'is_active', 'public_ip',
|
||||
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
|
||||
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
|
||||
'os', 'os_version', 'os_arch', 'hostname_raw', 'comment',
|
||||
'created_by', 'date_created', 'hardware_info',
|
||||
'hardware_info', 'connectivity', 'date_verified'
|
||||
]
|
||||
fields_fk = [
|
||||
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
|
||||
'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display'
|
||||
]
|
||||
fk_only_fields = {
|
||||
'platform': ['name']
|
||||
}
|
||||
fields_m2m = [
|
||||
'nodes', 'nodes_display', 'labels',
|
||||
]
|
||||
annotates_fields = {
|
||||
# 'admin_user_display': 'admin_user__name'
|
||||
}
|
||||
fields_as = list(annotates_fields.keys())
|
||||
fields = fields_small + fields_fk + fields_m2m + fields_as
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created',
|
||||
] + fields_as
|
||||
]
|
||||
fields = fields_small + fields_fk + fields_m2m + read_only_fields
|
||||
|
||||
extra_kwargs = {
|
||||
'protocol': {'write_only': True},
|
||||
'port': {'write_only': True},
|
||||
'hardware_info': {'label': _('Hardware info')},
|
||||
'org_name': {'label': _('Org name')}
|
||||
'org_name': {'label': _('Org name')},
|
||||
'admin_user_display': {'label': _('Admin user display')}
|
||||
}
|
||||
|
||||
def get_fields(self):
|
||||
fields = super().get_fields()
|
||||
|
||||
admin_user_field = fields.get('admin_user')
|
||||
# 因为 mixin 中对 fields 有处理,可能不需要返回 admin_user
|
||||
if admin_user_field:
|
||||
admin_user_field.queryset = SystemUser.objects.filter(type=SystemUser.Type.admin)
|
||||
return fields
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('admin_user', 'domain', 'platform')
|
||||
queryset = queryset.prefetch_related('domain', 'platform', 'admin_user')
|
||||
queryset = queryset.prefetch_related('nodes', 'labels')
|
||||
return queryset
|
||||
|
||||
@@ -158,15 +157,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
|
||||
return instance
|
||||
|
||||
|
||||
class AssetDisplaySerializer(AssetSerializer):
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
|
||||
class Meta(AssetSerializer.Meta):
|
||||
fields = AssetSerializer.Meta.fields + [
|
||||
'connectivity',
|
||||
]
|
||||
|
||||
|
||||
class PlatformSerializer(serializers.ModelSerializer):
|
||||
meta = serializers.DictField(required=False, allow_null=True, label=_('Meta'))
|
||||
|
||||
@@ -186,19 +176,14 @@ class PlatformSerializer(serializers.ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class AssetDetailSerializer(AssetSerializer):
|
||||
platform = PlatformSerializer(read_only=True)
|
||||
|
||||
|
||||
class AssetSimpleSerializer(serializers.ModelSerializer):
|
||||
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'hostname', 'ip', 'connectivity', 'port']
|
||||
fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']
|
||||
|
||||
|
||||
class AssetTaskSerializer(serializers.Serializer):
|
||||
class AssetsTaskSerializer(serializers.Serializer):
|
||||
ACTION_CHOICES = (
|
||||
('refresh', 'refresh'),
|
||||
('test', 'test'),
|
||||
@@ -208,3 +193,17 @@ class AssetTaskSerializer(serializers.Serializer):
|
||||
assets = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
|
||||
|
||||
class AssetTaskSerializer(AssetsTaskSerializer):
|
||||
ACTION_CHOICES = tuple(list(AssetsTaskSerializer.ACTION_CHOICES) + [
|
||||
('push_system_user', 'push_system_user'),
|
||||
('test_system_user', 'test_system_user')
|
||||
])
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
asset = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Asset.objects, required=False, allow_empty=True, many=False
|
||||
)
|
||||
system_users = serializers.PrimaryKeyRelatedField(
|
||||
queryset=SystemUser.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import AuthBook, Asset
|
||||
from ..backends import AssetUserManager
|
||||
|
||||
from .base import ConnectivitySerializer, AuthSerializerMixin
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AssetUserWriteSerializer', 'AssetUserReadSerializer',
|
||||
'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer',
|
||||
]
|
||||
|
||||
|
||||
class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = AuthBook
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields_mini = ['id', 'username']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_small = fields_mini + fields_write_only + ['comment']
|
||||
fields_fk = ['asset']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {'write_only': True},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
if not validated_data.get("name") and validated_data.get("username"):
|
||||
validated_data["name"] = validated_data["username"]
|
||||
instance = AssetUserManager.create(**validated_data)
|
||||
return instance
|
||||
|
||||
|
||||
class AssetUserReadSerializer(AssetUserWriteSerializer):
|
||||
id = serializers.CharField(read_only=True, source='union_id', label=_("ID"))
|
||||
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
|
||||
ip = serializers.CharField(read_only=True, label=_("IP"))
|
||||
asset = serializers.CharField(source='asset_id', label=_('Asset'))
|
||||
backend = serializers.CharField(read_only=True, label=_("Backend"))
|
||||
backend_display = serializers.CharField(read_only=True, label=_("Source"))
|
||||
|
||||
class Meta(AssetUserWriteSerializer.Meta):
|
||||
read_only_fields = (
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'version',
|
||||
)
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'private_key', "public_key"]
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'backend', 'backend_display', 'version',
|
||||
'date_created', "date_updated",
|
||||
'comment'
|
||||
]
|
||||
fields_fk = ['asset', 'hostname', 'ip']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'name': {'required': False},
|
||||
'username': {'required': True},
|
||||
'password': {'write_only': True},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
}
|
||||
|
||||
|
||||
class AssetUserAuthInfoSerializer(AssetUserReadSerializer):
|
||||
password = serializers.CharField(
|
||||
max_length=256, allow_blank=True, allow_null=True,
|
||||
required=False, label=_('Password')
|
||||
)
|
||||
public_key = serializers.CharField(
|
||||
max_length=4096, allow_blank=True, allow_null=True,
|
||||
required=False, label=_('Public key')
|
||||
)
|
||||
private_key = serializers.CharField(
|
||||
max_length=4096, allow_blank=True, allow_null=True,
|
||||
required=False, label=_('Private key')
|
||||
)
|
||||
|
||||
|
||||
class AssetUserPushSerializer(serializers.Serializer):
|
||||
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset"))
|
||||
username = serializers.CharField(max_length=1024)
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
@@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_pubkey_gen, validate_ssh_private_key
|
||||
from ..models import AssetUser
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
@@ -29,11 +28,6 @@ class AuthSerializer(serializers.ModelSerializer):
|
||||
return self.instance
|
||||
|
||||
|
||||
class ConnectivitySerializer(serializers.Serializer):
|
||||
status = serializers.IntegerField()
|
||||
datetime = serializers.DateTimeField()
|
||||
|
||||
|
||||
class AuthSerializerMixin:
|
||||
def validate_password(self, password):
|
||||
return password
|
||||
@@ -64,15 +58,3 @@ class AuthSerializerMixin:
|
||||
def update(self, instance, validated_data):
|
||||
self.clean_auth_fields(validated_data)
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class AuthInfoSerializer(serializers.ModelSerializer):
|
||||
private_key = serializers.ReadOnlyField(source='get_private_key')
|
||||
|
||||
class Meta:
|
||||
model = AssetUser
|
||||
fields = [
|
||||
'username', 'password',
|
||||
'private_key', 'public_key',
|
||||
'date_updated',
|
||||
]
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import re
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from ..models import CommandFilter, CommandFilterRule
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from orgs.utils import tmp_to_root_org
|
||||
@@ -15,7 +14,6 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandFilter
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'org_id', 'org_name',
|
||||
@@ -48,7 +46,6 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
|
||||
]
|
||||
fields_fk = ['filter']
|
||||
fields = '__all__'
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.validators import NoSpecialChars
|
||||
from ..models import Domain, Gateway
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
|
||||
class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
asset_count = serializers.SerializerMethodField(label=_('Assets count'))
|
||||
application_count = serializers.SerializerMethodField(label=_('Applications count'))
|
||||
asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
|
||||
application_count = serializers.SerializerMethodField(label=_('Applications amount'))
|
||||
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
|
||||
|
||||
class Meta:
|
||||
@@ -29,7 +27,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
extra_kwargs = {
|
||||
'assets': {'required': False, 'label': _('Assets')},
|
||||
}
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
def get_asset_count(obj):
|
||||
@@ -45,42 +42,33 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
|
||||
|
||||
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
is_connective = serializers.BooleanField(required=False)
|
||||
|
||||
class Meta:
|
||||
model = Gateway
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields_mini = ['id', 'name']
|
||||
fields_write_only = [
|
||||
'password', 'private_key', 'public_key',
|
||||
]
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'username', 'ip', 'port', 'protocol',
|
||||
'is_active',
|
||||
'is_active', 'is_connective',
|
||||
'date_created', 'date_updated',
|
||||
'created_by', 'comment',
|
||||
]
|
||||
fields_fk = ['domain']
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': True, 'validators': [NoSpecialChars()]},
|
||||
'password': {'write_only': True},
|
||||
'private_key': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.protocol_limit_to_ssh()
|
||||
|
||||
def protocol_limit_to_ssh(self):
|
||||
protocol_field = self.fields['protocol']
|
||||
choices = protocol_field.choices
|
||||
choices.pop('rdp')
|
||||
protocol_field._choices = choices
|
||||
|
||||
|
||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
class Meta(GatewaySerializer.Meta):
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False, 'validators': [NoSpecialChars()]},
|
||||
'password': {'write_only': False},
|
||||
'private_key': {"write_only": False},
|
||||
'public_key': {"write_only": False},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from ..models import FavoriteAsset
|
||||
|
||||
@@ -18,6 +17,5 @@ class FavoriteAssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
model = FavoriteAsset
|
||||
fields = ['user', 'asset']
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from ..models import Label
|
||||
@@ -30,7 +29,6 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
|
||||
extra_kwargs = {
|
||||
'assets': {'required': False}
|
||||
}
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
@staticmethod
|
||||
def get_asset_count(obj):
|
||||
|
||||
@@ -2,15 +2,15 @@ from rest_framework import serializers
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.models import Count
|
||||
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
from common.utils import ssh_pubkey_gen
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import SystemUser, Asset
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
'SystemUserSerializer', 'SystemUserListSerializer',
|
||||
'SystemUserSerializer',
|
||||
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
|
||||
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
|
||||
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
|
||||
@@ -23,24 +23,28 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
系统用户
|
||||
"""
|
||||
auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True)
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint'))
|
||||
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'sftp_root', 'token',
|
||||
'type', 'type_display', 'protocol', 'login_mode', 'login_mode_display',
|
||||
'priority', 'sudo', 'shell', 'sftp_root', 'token', 'ssh_key_fingerprint',
|
||||
'home', 'system_groups', 'ad_domain',
|
||||
'username_same_with_user', 'auto_push', 'auto_generate_key',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = [ 'cmd_filters', 'assets_amount']
|
||||
fields_m2m = ['cmd_filters', 'assets_amount']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'password': {
|
||||
"write_only": True,
|
||||
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||
},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'token': {"write_only": True},
|
||||
@@ -52,18 +56,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
}
|
||||
|
||||
def validate_auto_push(self, value):
|
||||
login_mode = self.initial_data.get("login_mode")
|
||||
protocol = self.initial_data.get("protocol")
|
||||
login_mode = self.get_initial_value("login_mode")
|
||||
protocol = self.get_initial_value("protocol")
|
||||
|
||||
if login_mode == SystemUser.LOGIN_MANUAL or \
|
||||
protocol in [SystemUser.PROTOCOL_TELNET,
|
||||
SystemUser.PROTOCOL_VNC]:
|
||||
if login_mode == SystemUser.LOGIN_MANUAL:
|
||||
value = False
|
||||
elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
|
||||
value = False
|
||||
return value
|
||||
|
||||
def validate_auto_generate_key(self, value):
|
||||
login_mode = self.initial_data.get("login_mode")
|
||||
protocol = self.initial_data.get("protocol")
|
||||
login_mode = self.get_initial_value("login_mode")
|
||||
protocol = self.get_initial_value("protocol")
|
||||
|
||||
if self.context["request"].method.lower() != "post":
|
||||
value = False
|
||||
@@ -71,16 +75,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
value = False
|
||||
elif login_mode == SystemUser.LOGIN_MANUAL:
|
||||
value = False
|
||||
elif protocol in [SystemUser.PROTOCOL_TELNET, SystemUser.PROTOCOL_VNC]:
|
||||
elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
|
||||
value = False
|
||||
return value
|
||||
|
||||
def validate_username_same_with_user(self, username_same_with_user):
|
||||
if not username_same_with_user:
|
||||
return username_same_with_user
|
||||
protocol = self.initial_data.get("protocol", "ssh")
|
||||
protocol = self.get_initial_value("protocol", "ssh")
|
||||
queryset = SystemUser.objects.filter(
|
||||
protocol=protocol, username_same_with_user=True
|
||||
protocol=protocol,
|
||||
username_same_with_user=True
|
||||
)
|
||||
if self.instance:
|
||||
queryset = queryset.exclude(id=self.instance.id)
|
||||
@@ -93,19 +98,20 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
def validate_username(self, username):
|
||||
if username:
|
||||
return username
|
||||
login_mode = self.initial_data.get("login_mode")
|
||||
protocol = self.initial_data.get("protocol")
|
||||
username_same_with_user = self.initial_data.get("username_same_with_user")
|
||||
login_mode = self.get_initial_value("login_mode")
|
||||
protocol = self.get_initial_value("protocol")
|
||||
username_same_with_user = self.get_initial_value("username_same_with_user")
|
||||
|
||||
if username_same_with_user:
|
||||
return ''
|
||||
if login_mode == SystemUser.LOGIN_AUTO and \
|
||||
protocol != SystemUser.PROTOCOL_VNC:
|
||||
|
||||
if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc:
|
||||
msg = _('* Automatic login mode must fill in the username.')
|
||||
raise serializers.ValidationError(msg)
|
||||
return username
|
||||
|
||||
def validate_home(self, home):
|
||||
username_same_with_user = self.initial_data.get("username_same_with_user")
|
||||
username_same_with_user = self.get_initial_value("username_same_with_user")
|
||||
if username_same_with_user:
|
||||
return ''
|
||||
return home
|
||||
@@ -118,66 +124,58 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
raise serializers.ValidationError(error)
|
||||
return value
|
||||
|
||||
def validate_admin_user(self, attrs):
|
||||
if self.instance:
|
||||
tp = self.instance.type
|
||||
else:
|
||||
tp = attrs.get('type')
|
||||
if tp != SystemUser.Type.admin:
|
||||
return attrs
|
||||
attrs['protocol'] = SystemUser.Protocol.ssh
|
||||
attrs['login_mode'] = SystemUser.LOGIN_AUTO
|
||||
attrs['username_same_with_user'] = False
|
||||
attrs['auto_push'] = False
|
||||
return attrs
|
||||
|
||||
def validate_password(self, password):
|
||||
super().validate_password(password)
|
||||
auto_gen_key = self.initial_data.get("auto_generate_key", False)
|
||||
private_key = self.initial_data.get("private_key")
|
||||
login_mode = self.initial_data.get("login_mode")
|
||||
auto_gen_key = self.get_initial_value("auto_generate_key", False)
|
||||
private_key = self.get_initial_value("private_key")
|
||||
login_mode = self.get_initial_value("login_mode")
|
||||
|
||||
if not self.instance and not auto_gen_key and not password and \
|
||||
not private_key and login_mode == SystemUser.LOGIN_AUTO:
|
||||
raise serializers.ValidationError(_("Password or private key required"))
|
||||
return password
|
||||
|
||||
def validate(self, attrs):
|
||||
def validate_gen_key(self, attrs):
|
||||
username = attrs.get("username", "manual")
|
||||
auto_gen_key = attrs.pop("auto_generate_key", False)
|
||||
protocol = attrs.get("protocol")
|
||||
|
||||
if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]:
|
||||
if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
|
||||
return attrs
|
||||
|
||||
if auto_gen_key:
|
||||
# 自动生成
|
||||
if auto_gen_key and not self.instance:
|
||||
password = SystemUser.gen_password()
|
||||
attrs["password"] = password
|
||||
if protocol == SystemUser.PROTOCOL_SSH:
|
||||
if protocol == SystemUser.Protocol.ssh:
|
||||
private_key, public_key = SystemUser.gen_key(username)
|
||||
attrs["private_key"] = private_key
|
||||
attrs["public_key"] = public_key
|
||||
# 如果设置了private key,没有设置public key则生成
|
||||
# 如果设置了private key,没有设置public key则生成
|
||||
elif attrs.get("private_key", None):
|
||||
private_key = attrs["private_key"]
|
||||
password = attrs.get("password")
|
||||
public_key = ssh_pubkey_gen(private_key, password=password,
|
||||
username=username)
|
||||
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
|
||||
attrs["public_key"] = public_key
|
||||
return attrs
|
||||
|
||||
|
||||
class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
fields_small = fields_mini + fields_write_only + [
|
||||
'protocol', 'login_mode', 'login_mode_display', 'priority',
|
||||
'sudo', 'shell', 'home', 'system_groups',
|
||||
'ad_domain', 'sftp_root',
|
||||
"username_same_with_user", 'auto_push', 'auto_generate_key',
|
||||
'date_created', 'date_updated',
|
||||
'comment', 'created_by',
|
||||
]
|
||||
fields_m2m = ["assets_amount",]
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'nodes_amount': {'label': _('Nodes amount')},
|
||||
'assets_amount': {'label': _('Assets amount')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
'created_by': {'read_only': True},
|
||||
'ad_domain': {'label': _('Ad domain')},
|
||||
}
|
||||
def validate(self, attrs):
|
||||
attrs = self.validate_admin_user(attrs)
|
||||
attrs = self.validate_gen_key(attrs)
|
||||
return attrs
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
@@ -215,31 +213,34 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class RelationMixin(BulkSerializerMixin, serializers.Serializer):
|
||||
systemuser_display = serializers.ReadOnlyField()
|
||||
systemuser_display = serializers.ReadOnlyField(label=_("System user name"))
|
||||
org_name = serializers.ReadOnlyField(label=_("Org name"))
|
||||
|
||||
def get_field_names(self, declared_fields, info):
|
||||
fields = super().get_field_names(declared_fields, info)
|
||||
fields.extend(['systemuser', "systemuser_display"])
|
||||
fields.extend(['systemuser', "systemuser_display", "org_name"])
|
||||
return fields
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
||||
|
||||
class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
||||
asset_display = serializers.ReadOnlyField()
|
||||
asset_display = serializers.ReadOnlyField(label=_('Asset hostname'))
|
||||
|
||||
class Meta(RelationMixin.Meta):
|
||||
class Meta:
|
||||
model = SystemUser.assets.through
|
||||
fields = [
|
||||
'id', "asset", "asset_display",
|
||||
"id", "asset", "asset_display", 'systemuser', 'systemuser_display',
|
||||
"connectivity", 'date_verified', 'org_id'
|
||||
]
|
||||
use_model_bulk_create = True
|
||||
model_bulk_create_kwargs = {
|
||||
'ignore_conflicts': True
|
||||
}
|
||||
|
||||
|
||||
class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
||||
node_display = serializers.SerializerMethodField()
|
||||
|
||||
class Meta(RelationMixin.Meta):
|
||||
class Meta:
|
||||
model = SystemUser.nodes.through
|
||||
fields = [
|
||||
'id', 'node', "node_display",
|
||||
@@ -252,7 +253,7 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
|
||||
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
|
||||
user_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta(RelationMixin.Meta):
|
||||
class Meta:
|
||||
model = SystemUser.users.through
|
||||
fields = [
|
||||
'id', "user", "user_display",
|
||||
|
||||
9
apps/assets/serializers/utils.py
Normal file
9
apps/assets/serializers/utils.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def validate_password_contains_left_double_curly_bracket(password):
|
||||
# validate password contains left double curly bracket
|
||||
# check password not contains `{{`
|
||||
if '{{' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
||||
@@ -1,3 +1,5 @@
|
||||
from .common import *
|
||||
from .asset import *
|
||||
from .system_user import *
|
||||
from .authbook import *
|
||||
from .node_assets_amount import *
|
||||
from .node_assets_mapping import *
|
||||
|
||||
131
apps/assets/signals_handler/asset.py
Normal file
131
apps/assets/signals_handler/asset.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, pre_delete, post_delete, pre_save
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE
|
||||
from common.utils import get_logger
|
||||
from common.decorator import on_transaction_commit
|
||||
from assets.models import Asset, SystemUser, Node
|
||||
from assets.tasks import (
|
||||
update_assets_hardware_info_util,
|
||||
test_asset_connectivity_util,
|
||||
push_system_user_to_assets,
|
||||
)
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def update_asset_hardware_info_on_created(asset):
|
||||
logger.debug("Update asset `{}` hardware info".format(asset))
|
||||
update_assets_hardware_info_util.delay([asset])
|
||||
|
||||
|
||||
def test_asset_conn_on_created(asset):
|
||||
logger.debug("Test asset `{}` connectivity".format(asset))
|
||||
test_asset_connectivity_util.delay([asset])
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Node)
|
||||
def on_node_pre_save(sender, instance: Node, **kwargs):
|
||||
instance.parent_key = instance.compute_parent_key()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Asset)
|
||||
@on_transaction_commit
|
||||
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
|
||||
"""
|
||||
当资产创建时,更新硬件信息,更新可连接性
|
||||
确保资产必须属于一个节点
|
||||
"""
|
||||
if created:
|
||||
logger.info("Asset create signal recv: {}".format(instance))
|
||||
|
||||
# 获取资产硬件信息
|
||||
update_asset_hardware_info_on_created(instance)
|
||||
test_asset_conn_on_created(instance)
|
||||
|
||||
# 确保资产存在一个节点
|
||||
has_node = instance.nodes.all().exists()
|
||||
if not has_node:
|
||||
instance.nodes.add(Node.org_root())
|
||||
|
||||
instance.set_admin_user_relation()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
|
||||
"""
|
||||
本操作共访问 4 次数据库
|
||||
|
||||
当资产的节点发生变化时,或者 当节点的资产关系发生变化时,
|
||||
节点下新增的资产,添加到节点关联的系统用户中
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.debug("Assets node add signal recv: {}".format(action))
|
||||
if reverse:
|
||||
nodes = [instance.key]
|
||||
asset_ids = pk_set
|
||||
else:
|
||||
nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True)
|
||||
asset_ids = [instance.id]
|
||||
|
||||
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
|
||||
nodes_ancestors_keys = set()
|
||||
for node in nodes:
|
||||
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
|
||||
|
||||
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
|
||||
system_user_ids = SystemUser.objects.filter(
|
||||
nodes__key__in=nodes_ancestors_keys
|
||||
).distinct().values_list('id', flat=True)
|
||||
|
||||
# 查询所有已存在的关系
|
||||
m2m_model = SystemUser.assets.through
|
||||
exist = set(m2m_model.objects.filter(
|
||||
systemuser_id__in=system_user_ids, asset_id__in=asset_ids
|
||||
).values_list('systemuser_id', 'asset_id'))
|
||||
# TODO 优化
|
||||
to_create = []
|
||||
for system_user_id in system_user_ids:
|
||||
asset_ids_to_push = []
|
||||
for asset_id in asset_ids:
|
||||
if (system_user_id, asset_id) in exist:
|
||||
continue
|
||||
asset_ids_to_push.append(asset_id)
|
||||
to_create.append(m2m_model(
|
||||
systemuser_id=system_user_id,
|
||||
asset_id=asset_id,
|
||||
org_id=instance.org_id
|
||||
))
|
||||
if asset_ids_to_push:
|
||||
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
|
||||
m2m_model.objects.bulk_create(to_create)
|
||||
|
||||
|
||||
RELATED_NODE_IDS = '_related_node_ids'
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Asset)
|
||||
def on_asset_delete(instance: Asset, using, **kwargs):
|
||||
node_ids = set(Node.objects.filter(
|
||||
assets=instance
|
||||
).distinct().values_list('id', flat=True))
|
||||
setattr(instance, RELATED_NODE_IDS, node_ids)
|
||||
m2m_changed.send(
|
||||
sender=Asset.nodes.through, instance=instance, reverse=False,
|
||||
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Asset)
|
||||
def on_asset_post_delete(instance: Asset, using, **kwargs):
|
||||
node_ids = getattr(instance, RELATED_NODE_IDS, None)
|
||||
if node_ids:
|
||||
m2m_changed.send(
|
||||
sender=Asset.nodes.through, instance=instance, reverse=False,
|
||||
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
|
||||
)
|
||||
42
apps/assets/signals_handler/authbook.py
Normal file
42
apps/assets/signals_handler/authbook.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django.dispatch import receiver
|
||||
from django.apps import apps
|
||||
from simple_history.signals import pre_create_historical_record
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
|
||||
from common.utils import get_logger
|
||||
from ..models import AuthBook, SystemUser
|
||||
|
||||
AuthBookHistory = apps.get_model('assets', 'HistoricalAuthBook')
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@receiver(pre_create_historical_record, sender=AuthBookHistory)
|
||||
def pre_create_historical_record_callback(sender, history_instance=None, **kwargs):
|
||||
attrs_to_copy = ['username', 'password', 'private_key']
|
||||
|
||||
for attr in attrs_to_copy:
|
||||
if getattr(history_instance, attr):
|
||||
continue
|
||||
try:
|
||||
system_user = history_instance.systemuser
|
||||
except SystemUser.DoesNotExist:
|
||||
continue
|
||||
if not system_user:
|
||||
continue
|
||||
system_user_attr_value = getattr(history_instance.systemuser, attr)
|
||||
if system_user_attr_value:
|
||||
setattr(history_instance, attr, system_user_attr_value)
|
||||
|
||||
|
||||
@receiver(post_save, sender=AuthBook)
|
||||
def on_authbook_post_create(sender, instance, **kwargs):
|
||||
if not instance.systemuser:
|
||||
instance.sync_to_system_user_account()
|
||||
|
||||
|
||||
@receiver(pre_save, sender=AuthBook)
|
||||
def on_authbook_pre_create(sender, instance, **kwargs):
|
||||
# 升级版本号
|
||||
instance.version += 1
|
||||
# 即使在 root 组织也不怕
|
||||
instance.org_id = instance.asset.org_id
|
||||
@@ -1,223 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, pre_delete, post_delete, pre_save
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.exceptions import M2MReverseNotAllowed
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE
|
||||
from common.utils import get_logger
|
||||
from common.decorator import on_transaction_commit
|
||||
from assets.models import Asset, SystemUser, Node
|
||||
from users.models import User
|
||||
from assets.tasks import (
|
||||
update_assets_hardware_info_util,
|
||||
test_asset_connectivity_util,
|
||||
push_system_user_to_assets_manual,
|
||||
push_system_user_to_assets,
|
||||
add_nodes_assets_to_system_users
|
||||
)
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def update_asset_hardware_info_on_created(asset):
|
||||
logger.debug("Update asset `{}` hardware info".format(asset))
|
||||
update_assets_hardware_info_util.delay([asset])
|
||||
|
||||
|
||||
def test_asset_conn_on_created(asset):
|
||||
logger.debug("Test asset `{}` connectivity".format(asset))
|
||||
test_asset_connectivity_util.delay([asset])
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Node)
|
||||
def on_node_pre_save(sender, instance: Node, **kwargs):
|
||||
instance.parent_key = instance.compute_parent_key()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Asset)
|
||||
@on_transaction_commit
|
||||
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
|
||||
"""
|
||||
当资产创建时,更新硬件信息,更新可连接性
|
||||
确保资产必须属于一个节点
|
||||
"""
|
||||
if created:
|
||||
logger.info("Asset create signal recv: {}".format(instance))
|
||||
|
||||
# 获取资产硬件信息
|
||||
update_asset_hardware_info_on_created(instance)
|
||||
test_asset_conn_on_created(instance)
|
||||
|
||||
# 确保资产存在一个节点
|
||||
has_node = instance.nodes.all().exists()
|
||||
if not has_node:
|
||||
instance.nodes.add(Node.org_root())
|
||||
|
||||
|
||||
@receiver(post_save, sender=SystemUser, dispatch_uid="jms")
|
||||
@on_transaction_commit
|
||||
def on_system_user_update(instance: SystemUser, created, **kwargs):
|
||||
"""
|
||||
当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上,
|
||||
其实应该当 用户名,密码,秘钥 sudo等更新时再推送,这里偷个懒,
|
||||
这里直接取了 instance.assets 因为nodes和系统用户发生变化时,会自动将nodes下的资产
|
||||
关联到上面
|
||||
"""
|
||||
if instance and not created:
|
||||
logger.info("System user update signal recv: {}".format(instance))
|
||||
assets = instance.assets.all().valid()
|
||||
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.assets.through)
|
||||
@on_transaction_commit
|
||||
def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
|
||||
"""
|
||||
当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.debug("System user assets change signal recv: {}".format(instance))
|
||||
if model == Asset:
|
||||
system_user_ids = [instance.id]
|
||||
asset_ids = pk_set
|
||||
else:
|
||||
system_user_ids = pk_set
|
||||
asset_ids = [instance.id]
|
||||
for system_user_id in system_user_ids:
|
||||
push_system_user_to_assets.delay(system_user_id, asset_ids)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.users.through)
|
||||
@on_transaction_commit
|
||||
def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs):
|
||||
"""
|
||||
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
|
||||
if reverse:
|
||||
raise M2MReverseNotAllowed
|
||||
|
||||
if not instance.username_same_with_user:
|
||||
return
|
||||
|
||||
logger.debug("System user users change signal recv: {}".format(instance))
|
||||
usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
|
||||
|
||||
for username in usernames:
|
||||
push_system_user_to_assets_manual.delay(instance, username)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.nodes.through)
|
||||
@on_transaction_commit
|
||||
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
|
||||
"""
|
||||
当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.info("System user nodes update signal recv: {}".format(instance))
|
||||
|
||||
queryset = model.objects.filter(pk__in=pk_set)
|
||||
if model == Node:
|
||||
nodes_keys = queryset.values_list('key', flat=True)
|
||||
system_users = [instance]
|
||||
else:
|
||||
nodes_keys = [instance.key]
|
||||
system_users = queryset
|
||||
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.groups.through)
|
||||
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
|
||||
"""
|
||||
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
if reverse:
|
||||
raise M2MReverseNotAllowed
|
||||
logger.info("System user groups update signal recv: {}".format(instance))
|
||||
|
||||
users = User.objects.filter(groups__id__in=pk_set).distinct()
|
||||
instance.users.add(*users)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Asset.nodes.through)
|
||||
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
|
||||
"""
|
||||
本操作共访问 4 次数据库
|
||||
|
||||
当资产的节点发生变化时,或者 当节点的资产关系发生变化时,
|
||||
节点下新增的资产,添加到节点关联的系统用户中
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.debug("Assets node add signal recv: {}".format(action))
|
||||
if reverse:
|
||||
nodes = [instance.key]
|
||||
asset_ids = pk_set
|
||||
else:
|
||||
nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True)
|
||||
asset_ids = [instance.id]
|
||||
|
||||
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
|
||||
nodes_ancestors_keys = set()
|
||||
for node in nodes:
|
||||
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
|
||||
|
||||
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
|
||||
system_user_ids = SystemUser.objects.filter(
|
||||
nodes__key__in=nodes_ancestors_keys
|
||||
).distinct().values_list('id', flat=True)
|
||||
|
||||
# 查询所有已存在的关系
|
||||
m2m_model = SystemUser.assets.through
|
||||
exist = set(m2m_model.objects.filter(
|
||||
systemuser_id__in=system_user_ids, asset_id__in=asset_ids
|
||||
).values_list('systemuser_id', 'asset_id'))
|
||||
# TODO 优化
|
||||
to_create = []
|
||||
for system_user_id in system_user_ids:
|
||||
asset_ids_to_push = []
|
||||
for asset_id in asset_ids:
|
||||
if (system_user_id, asset_id) in exist:
|
||||
continue
|
||||
asset_ids_to_push.append(asset_id)
|
||||
to_create.append(m2m_model(
|
||||
systemuser_id=system_user_id,
|
||||
asset_id=asset_id
|
||||
))
|
||||
if asset_ids_to_push:
|
||||
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
|
||||
m2m_model.objects.bulk_create(to_create)
|
||||
|
||||
|
||||
RELATED_NODE_IDS = '_related_node_ids'
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Asset)
|
||||
def on_asset_delete(instance: Asset, using, **kwargs):
|
||||
node_ids = set(Node.objects.filter(
|
||||
assets=instance
|
||||
).distinct().values_list('id', flat=True))
|
||||
setattr(instance, RELATED_NODE_IDS, node_ids)
|
||||
m2m_changed.send(
|
||||
sender=Asset.nodes.through, instance=instance, reverse=False,
|
||||
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Asset)
|
||||
def on_asset_post_delete(instance: Asset, using, **kwargs):
|
||||
node_ids = getattr(instance, RELATED_NODE_IDS, None)
|
||||
if node_ids:
|
||||
m2m_changed.send(
|
||||
sender=Asset.nodes.through, instance=instance, reverse=False,
|
||||
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
|
||||
)
|
||||
|
||||
@@ -48,7 +48,6 @@ def expire_node_assets_mapping_for_memory(org_id):
|
||||
Node.expire_node_all_asset_ids_mapping_from_cache(root_org_id)
|
||||
|
||||
node_assets_mapping_for_memory_pub_sub.publish(org_id)
|
||||
node_assets_mapping_for_memory_pub_sub.publish(root_org_id)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Node)
|
||||
@@ -86,7 +85,9 @@ def subscribe_node_assets_mapping_expire(sender, **kwargs):
|
||||
if message["type"] != "message":
|
||||
continue
|
||||
org_id = message['data'].decode()
|
||||
root_org_id = Organization.ROOT_ID
|
||||
Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
|
||||
Node.expire_node_all_asset_ids_mapping_from_memory(root_org_id)
|
||||
logger.debug(
|
||||
"Expire node assets id mapping from memory of org={}, pid={}"
|
||||
"".format(str(org_id), os.getpid())
|
||||
|
||||
142
apps/assets/signals_handler/system_user.py
Normal file
142
apps/assets/signals_handler/system_user.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, pre_save, pre_delete, post_delete
|
||||
)
|
||||
from django.dispatch import receiver
|
||||
|
||||
from common.exceptions import M2MReverseNotAllowed
|
||||
from common.const.signals import POST_ADD
|
||||
from common.utils import get_logger
|
||||
from common.decorator import on_transaction_commit
|
||||
from assets.models import Asset, SystemUser, Node, AuthBook
|
||||
from users.models import User
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from assets.tasks import (
|
||||
push_system_user_to_assets_manual,
|
||||
push_system_user_to_assets,
|
||||
add_nodes_assets_to_system_users
|
||||
)
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.assets.through)
|
||||
@on_transaction_commit
|
||||
def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
|
||||
"""
|
||||
当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中
|
||||
"""
|
||||
logger.debug("System user assets change signal recv: {}".format(instance))
|
||||
|
||||
if not instance:
|
||||
logger.debug('No system user found')
|
||||
return
|
||||
|
||||
if model == Asset:
|
||||
system_user_ids = [instance.id]
|
||||
asset_ids = pk_set
|
||||
else:
|
||||
system_user_ids = pk_set
|
||||
asset_ids = [instance.id]
|
||||
|
||||
org_id = instance.org_id
|
||||
|
||||
# 关联创建的 authbook 没有系统用户id
|
||||
with tmp_to_root_org():
|
||||
authbooks = AuthBook.objects.filter(
|
||||
asset_id__in=asset_ids,
|
||||
systemuser_id__in=system_user_ids
|
||||
)
|
||||
if action == POST_ADD:
|
||||
authbooks.update(org_id=org_id)
|
||||
|
||||
save_action_mapper = {
|
||||
'pre_add': pre_save,
|
||||
'post_add': post_save,
|
||||
'pre_remove': pre_delete,
|
||||
'post_remove': post_delete
|
||||
}
|
||||
|
||||
for ab in authbooks:
|
||||
ab.org_id = org_id
|
||||
|
||||
save_action = save_action_mapper[action]
|
||||
logger.debug('Send AuthBook post save signal: {} -> {}'.format(action, ab.id))
|
||||
save_action.send(sender=AuthBook, instance=ab, created=True)
|
||||
|
||||
if action == POST_ADD:
|
||||
for system_user_id in system_user_ids:
|
||||
push_system_user_to_assets.delay(system_user_id, asset_ids)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.users.through)
|
||||
@on_transaction_commit
|
||||
def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs):
|
||||
"""
|
||||
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
|
||||
if reverse:
|
||||
raise M2MReverseNotAllowed
|
||||
|
||||
if not instance.username_same_with_user:
|
||||
return
|
||||
|
||||
logger.debug("System user users change signal recv: {}".format(instance))
|
||||
usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
|
||||
|
||||
for username in usernames:
|
||||
push_system_user_to_assets_manual.delay(instance, username)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.nodes.through)
|
||||
@on_transaction_commit
|
||||
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
|
||||
"""
|
||||
当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
logger.info("System user nodes update signal recv: {}".format(instance))
|
||||
|
||||
queryset = model.objects.filter(pk__in=pk_set)
|
||||
if model == Node:
|
||||
nodes_keys = queryset.values_list('key', flat=True)
|
||||
system_users = [instance]
|
||||
else:
|
||||
nodes_keys = [instance.key]
|
||||
system_users = queryset
|
||||
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=SystemUser.groups.through)
|
||||
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
|
||||
"""
|
||||
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
|
||||
"""
|
||||
if action != POST_ADD:
|
||||
return
|
||||
if reverse:
|
||||
raise M2MReverseNotAllowed
|
||||
logger.info("System user groups update signal recv: {}".format(instance))
|
||||
|
||||
users = User.objects.filter(groups__id__in=pk_set).distinct()
|
||||
instance.users.add(*users)
|
||||
|
||||
|
||||
@receiver(post_save, sender=SystemUser, dispatch_uid="jms")
|
||||
@on_transaction_commit
|
||||
def on_system_user_update(instance: SystemUser, created, **kwargs):
|
||||
"""
|
||||
当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上,
|
||||
其实应该当 用户名,密码,秘钥 sudo等更新时再推送,这里偷个懒,
|
||||
这里直接取了 instance.assets 因为nodes和系统用户发生变化时,会自动将nodes下的资产
|
||||
关联到上面
|
||||
"""
|
||||
if instance and not created:
|
||||
logger.info("System user update signal recv: {}".format(instance))
|
||||
assets = instance.assets.all().valid()
|
||||
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])
|
||||
@@ -2,9 +2,8 @@
|
||||
#
|
||||
from .utils import *
|
||||
from .common import *
|
||||
from .admin_user_connectivity import *
|
||||
from .asset_connectivity import *
|
||||
from .asset_user_connectivity import *
|
||||
from .account_connectivity import *
|
||||
from .gather_asset_users import *
|
||||
from .gather_asset_hardware_info import *
|
||||
from .push_system_user import *
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import org_aware_func
|
||||
from ..models import Asset
|
||||
from ..models import Connectivity
|
||||
from . import const
|
||||
from .utils import check_asset_can_run_ansible
|
||||
|
||||
@@ -14,13 +14,13 @@ logger = get_logger(__file__)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual',
|
||||
'get_test_asset_user_connectivity_tasks', 'test_user_connectivity',
|
||||
'test_account_connectivity_util', 'test_accounts_connectivity_manual',
|
||||
'get_test_account_connectivity_tasks', 'test_user_connectivity',
|
||||
'run_adhoc',
|
||||
]
|
||||
|
||||
|
||||
def get_test_asset_user_connectivity_tasks(asset):
|
||||
def get_test_account_connectivity_tasks(asset):
|
||||
if asset.is_unixlike():
|
||||
tasks = const.PING_UNIXLIKE_TASKS
|
||||
elif asset.is_windows():
|
||||
@@ -57,7 +57,7 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke
|
||||
"""
|
||||
from ops.inventory import JMSCustomInventory
|
||||
|
||||
tasks = get_test_asset_user_connectivity_tasks(asset)
|
||||
tasks = get_test_account_connectivity_tasks(asset)
|
||||
if not tasks:
|
||||
logger.debug("No tasks ")
|
||||
return {}, {}
|
||||
@@ -71,62 +71,39 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke
|
||||
return raw, summary
|
||||
|
||||
|
||||
@org_aware_func("asset_user")
|
||||
def test_asset_user_connectivity_util(asset_user, task_name):
|
||||
@org_aware_func("account")
|
||||
def test_account_connectivity_util(account, task_name):
|
||||
"""
|
||||
:param asset_user: <AuthBook>对象
|
||||
:param account: <AuthBook>对象
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
if not check_asset_can_run_ansible(asset_user.asset):
|
||||
if not check_asset_can_run_ansible(account.asset):
|
||||
return
|
||||
|
||||
account.load_auth()
|
||||
try:
|
||||
raw, summary = test_user_connectivity(
|
||||
task_name=task_name, asset=asset_user.asset,
|
||||
username=asset_user.username, password=asset_user.password,
|
||||
private_key=asset_user.private_key_file
|
||||
task_name=task_name, asset=account.asset,
|
||||
username=account.username, password=account.password,
|
||||
private_key=account.private_key_file
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
|
||||
return
|
||||
asset_user.set_connectivity(summary)
|
||||
|
||||
if summary.get('success'):
|
||||
account.set_connectivity(Connectivity.ok)
|
||||
else:
|
||||
account.set_connectivity(Connectivity.failed)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def test_asset_users_connectivity_manual(asset_users):
|
||||
def test_accounts_connectivity_manual(accounts):
|
||||
"""
|
||||
:param asset_users: <AuthBook>对象
|
||||
:param accounts: <AuthBook>对象
|
||||
"""
|
||||
for asset_user in asset_users:
|
||||
task_name = _("Test asset user connectivity: {}").format(asset_user)
|
||||
test_asset_user_connectivity_util(asset_user, task_name)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def push_asset_user_util(asset_user):
|
||||
"""
|
||||
:param asset_user: <Asset user>对象
|
||||
"""
|
||||
from .push_system_user import push_system_user_util
|
||||
if not asset_user.backend.startswith('system_user'):
|
||||
logger.error("Asset user is not from system user")
|
||||
return
|
||||
union_id = asset_user.union_id
|
||||
union_id_list = union_id.split('_')
|
||||
if len(union_id_list) < 2:
|
||||
logger.error("Asset user union id length less than 2")
|
||||
return
|
||||
system_user_id = union_id_list[0]
|
||||
asset_id = union_id_list[1]
|
||||
asset = get_object_or_none(Asset, pk=asset_id)
|
||||
system_user = None
|
||||
if not asset:
|
||||
return
|
||||
hosts = check_asset_can_run_ansible([asset])
|
||||
if asset.is_unixlike:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
for account in accounts:
|
||||
task_name = _("Test account connectivity: {}").format(account)
|
||||
test_account_connectivity_util(account, task_name)
|
||||
print(".\n")
|
||||
@@ -1,69 +0,0 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.core.cache import cache
|
||||
|
||||
from orgs.utils import tmp_to_root_org, org_aware_func
|
||||
from common.utils import get_logger
|
||||
from ops.celery.decorator import register_as_period_task
|
||||
|
||||
from ..models import AdminUser
|
||||
from .utils import clean_ansible_task_hosts
|
||||
from .asset_connectivity import test_asset_connectivity_util
|
||||
from . import const
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'test_admin_user_connectivity_util', 'test_admin_user_connectivity_manual',
|
||||
'test_admin_user_connectivity_period'
|
||||
]
|
||||
|
||||
|
||||
@org_aware_func("admin_user")
|
||||
def test_admin_user_connectivity_util(admin_user, task_name):
|
||||
"""
|
||||
Test asset admin user can connect or not. Using ansible api do that
|
||||
:param admin_user:
|
||||
:param task_name:
|
||||
:return:
|
||||
"""
|
||||
assets = admin_user.get_related_assets()
|
||||
hosts = clean_ansible_task_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
summary = test_asset_connectivity_util(hosts, task_name)
|
||||
return summary
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@register_as_period_task(interval=3600)
|
||||
def test_admin_user_connectivity_period():
|
||||
"""
|
||||
A period task that update the ansible task period
|
||||
"""
|
||||
if not const.PERIOD_TASK_ENABLED:
|
||||
logger.debug('Period task off, skip')
|
||||
return
|
||||
key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD'
|
||||
prev_execute_time = cache.get(key)
|
||||
if prev_execute_time:
|
||||
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
|
||||
return
|
||||
cache.set(key, 1, 60*40)
|
||||
with tmp_to_root_org():
|
||||
admin_users = AdminUser.objects.all()
|
||||
for admin_user in admin_users:
|
||||
task_name = _("Test admin user connectivity period: {}").format(
|
||||
admin_user.name
|
||||
)
|
||||
test_admin_user_connectivity_util(admin_user, task_name)
|
||||
cache.set(key, 1, 60*40)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def test_admin_user_connectivity_manual(admin_user):
|
||||
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
|
||||
test_admin_user_connectivity_util(admin_user, task_name)
|
||||
return True
|
||||
@@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import org_aware_func
|
||||
from ..models.utils import Connectivity
|
||||
from ..models import Asset, Connectivity, AuthBook
|
||||
from . import const
|
||||
from .utils import clean_ansible_task_hosts, group_asset_by_platform
|
||||
|
||||
@@ -18,6 +18,28 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def set_assets_accounts_connectivity(assets, results_summary):
|
||||
asset_ids_ok = set()
|
||||
asset_ids_failed = set()
|
||||
|
||||
asset_hostnames_ok = results_summary.get('contacted', {}).keys()
|
||||
|
||||
for asset in assets:
|
||||
if asset.hostname in asset_hostnames_ok:
|
||||
asset_ids_ok.add(asset.id)
|
||||
else:
|
||||
asset_ids_failed.add(asset.id)
|
||||
|
||||
Asset.bulk_set_connectivity(asset_ids_ok, Connectivity.ok)
|
||||
Asset.bulk_set_connectivity(asset_ids_failed, Connectivity.failed)
|
||||
|
||||
accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser__type='admin')
|
||||
accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser__type='admin')
|
||||
|
||||
AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok)
|
||||
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@org_aware_func("assets")
|
||||
def test_asset_connectivity_util(assets, task_name=None):
|
||||
@@ -60,14 +82,7 @@ def test_asset_connectivity_util(assets, task_name=None):
|
||||
results_summary['contacted'].update(contacted)
|
||||
results_summary['dark'].update(dark)
|
||||
continue
|
||||
|
||||
for asset in assets:
|
||||
if asset.hostname in results_summary.get('dark', {}).keys():
|
||||
asset.connectivity = Connectivity.unreachable()
|
||||
elif asset.hostname in results_summary.get('contacted', {}).keys():
|
||||
asset.connectivity = Connectivity.reachable()
|
||||
else:
|
||||
asset.connectivity = Connectivity.unknown()
|
||||
set_assets_accounts_connectivity(assets, results_summary)
|
||||
return results_summary
|
||||
|
||||
|
||||
|
||||
@@ -60,9 +60,12 @@ def parse_windows_result_to_users(result):
|
||||
task_result.pop()
|
||||
|
||||
for line in task_result:
|
||||
user = space.split(line)
|
||||
if user[0]:
|
||||
users[user[0]] = {}
|
||||
username_list = space.split(line)
|
||||
# such as: ['Admini', 'appadm', 'DefaultAccount', '']
|
||||
for username in username_list:
|
||||
if not username:
|
||||
continue
|
||||
users[username] = {}
|
||||
return users
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from itertools import groupby
|
||||
from celery import shared_task
|
||||
from common.db.utils import get_object_if_need, get_objects
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Empty
|
||||
from django.db.models import Empty, Q
|
||||
|
||||
from common.utils import encrypt_password, get_logger
|
||||
from assets.models import SystemUser, Asset, AuthBook
|
||||
@@ -17,6 +17,7 @@ logger = get_logger(__file__)
|
||||
__all__ = [
|
||||
'push_system_user_util', 'push_system_user_to_assets',
|
||||
'push_system_user_to_assets_manual', 'push_system_user_a_asset_manual',
|
||||
'push_system_users_a_asset'
|
||||
]
|
||||
|
||||
|
||||
@@ -238,9 +239,12 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
||||
no_special_auth = []
|
||||
special_auth_set = set()
|
||||
|
||||
auth_books = AuthBook.objects.filter(username__in=usernames, asset_id__in=asset_ids)
|
||||
auth_books = AuthBook.objects.filter(asset_id__in=asset_ids).filter(
|
||||
Q(username__in=usernames) | Q(systemuser__username__in=usernames)
|
||||
).prefetch_related('systemuser')
|
||||
|
||||
for auth_book in auth_books:
|
||||
auth_book.load_auth()
|
||||
special_auth_set.add((auth_book.username, auth_book.asset_id))
|
||||
|
||||
for _username in usernames:
|
||||
@@ -280,14 +284,21 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
|
||||
"""
|
||||
将系统用户推送到一个资产上
|
||||
"""
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
# if username is None:
|
||||
# username = system_user.username
|
||||
task_name = _("Push system users to asset: {}({}) => {}").format(
|
||||
system_user.name, username, asset
|
||||
)
|
||||
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@tmp_to_root_org()
|
||||
def push_system_users_a_asset(system_users, asset):
|
||||
for system_user in system_users:
|
||||
push_system_user_a_asset_manual(system_user, asset)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
@tmp_to_root_org()
|
||||
def push_system_user_to_assets(system_user_id, asset_ids, username=None):
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.utils.translation import ugettext as _
|
||||
from assets.models import Asset
|
||||
from common.utils import get_logger
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from ..models import SystemUser
|
||||
from ..models import SystemUser, Connectivity, AuthBook
|
||||
from . import const
|
||||
from .utils import (
|
||||
clean_ansible_task_hosts, group_asset_by_platform
|
||||
@@ -18,9 +18,29 @@ logger = get_logger(__name__)
|
||||
__all__ = [
|
||||
'test_system_user_connectivity_util', 'test_system_user_connectivity_manual',
|
||||
'test_system_user_connectivity_period', 'test_system_user_connectivity_a_asset',
|
||||
'test_system_users_connectivity_a_asset'
|
||||
]
|
||||
|
||||
|
||||
def set_assets_accounts_connectivity(system_user, assets, results_summary):
|
||||
asset_ids_ok = set()
|
||||
asset_ids_failed = set()
|
||||
|
||||
asset_hostnames_ok = results_summary.get('contacted', {}).keys()
|
||||
|
||||
for asset in assets:
|
||||
if asset.hostname in asset_hostnames_ok:
|
||||
asset_ids_ok.add(asset.id)
|
||||
else:
|
||||
asset_ids_failed.add(asset.id)
|
||||
|
||||
accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser=system_user)
|
||||
accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser=system_user)
|
||||
|
||||
AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok)
|
||||
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed)
|
||||
|
||||
|
||||
@org_aware_func("system_user")
|
||||
def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
"""
|
||||
@@ -32,9 +52,13 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
"""
|
||||
from ops.utils import update_or_create_ansible_task
|
||||
|
||||
if system_user.username_same_with_user:
|
||||
logger.error(_("Dynamic system user not support test"))
|
||||
return
|
||||
|
||||
# hosts = clean_ansible_task_hosts(assets, system_user=system_user)
|
||||
# TODO: 这里不传递系统用户,因为clean_ansible_task_hosts会通过system_user来判断是否可以推送,
|
||||
# 不符合测试可连接性逻辑, 后面需要优化此逻辑
|
||||
# 不符合测试可连接性逻辑, 后面需要优化此逻辑
|
||||
hosts = clean_ansible_task_hosts(assets)
|
||||
if not hosts:
|
||||
return {}
|
||||
@@ -60,7 +84,7 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
_task, created = update_or_create_ansible_task(
|
||||
task_name=_task_name, hosts=_hosts, tasks=_tasks,
|
||||
pattern='all', options=const.TASK_OPTIONS,
|
||||
run_as=_username,
|
||||
run_as=_username, system_user=system_user
|
||||
)
|
||||
raw, summary = _task.run()
|
||||
success = summary.get('success', False)
|
||||
@@ -81,17 +105,10 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
|
||||
print(_("Start test system user connectivity for platform: [{}]").format(platform))
|
||||
print(_("Hosts count: {}").format(len(_hosts)))
|
||||
# 用户名不是动态的,用户名则是一个
|
||||
if not system_user.username_same_with_user:
|
||||
logger.debug("System user not has special auth")
|
||||
run_task(tasks, _hosts, system_user.username)
|
||||
# 否则需要多个任务
|
||||
else:
|
||||
users = system_user.users.all().values_list('username', flat=True)
|
||||
print(_("System user is dynamic: {}").format(list(users)))
|
||||
for username in users:
|
||||
run_task(tasks, _hosts, username)
|
||||
logger.debug("System user not has special auth")
|
||||
run_task(tasks, _hosts, system_user.username)
|
||||
|
||||
system_user.set_connectivity(results_summary)
|
||||
set_assets_accounts_connectivity(system_user, hosts, results_summary)
|
||||
return results_summary
|
||||
|
||||
|
||||
@@ -115,6 +132,12 @@ def test_system_user_connectivity_a_asset(system_user, asset):
|
||||
test_system_user_connectivity_util(system_user, [asset], task_name)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def test_system_users_connectivity_a_asset(system_users, asset):
|
||||
for system_user in system_users:
|
||||
test_system_user_connectivity_a_asset(system_user, asset)
|
||||
|
||||
|
||||
@shared_task(queue="ansible")
|
||||
def test_system_user_connectivity_period():
|
||||
if not const.PERIOD_TASK_ENABLED:
|
||||
|
||||
@@ -11,16 +11,16 @@ app_name = 'assets'
|
||||
|
||||
router = BulkRouter()
|
||||
router.register(r'assets', api.AssetViewSet, 'asset')
|
||||
router.register(r'accounts', api.AccountViewSet, 'account')
|
||||
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
||||
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
|
||||
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')
|
||||
router.register(r'system-users', api.SystemUserViewSet, 'system-user')
|
||||
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')
|
||||
router.register(r'labels', api.LabelViewSet, 'label')
|
||||
router.register(r'nodes', api.NodeViewSet, 'node')
|
||||
router.register(r'domains', api.DomainViewSet, 'domain')
|
||||
router.register(r'gateways', api.GatewayViewSet, 'gateway')
|
||||
router.register(r'cmd-filters', api.CommandFilterViewSet, 'cmd-filter')
|
||||
router.register(r'asset-users', api.AssetUserViewSet, 'asset-user')
|
||||
router.register(r'asset-user-auth-infos', api.AssetUserAuthInfoViewSet, 'asset-user-auth-info')
|
||||
router.register(r'gathered-users', api.GatheredUserViewSet, 'gathered-user')
|
||||
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
|
||||
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
|
||||
@@ -37,13 +37,6 @@ urlpatterns = [
|
||||
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
|
||||
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
|
||||
|
||||
path('asset-users/tasks/', api.AssetUserTaskCreateAPI.as_view(), name='asset-user-task-create'),
|
||||
|
||||
path('admin-users/<uuid:pk>/nodes/', api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
|
||||
path('admin-users/<uuid:pk>/auth/', api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
|
||||
path('admin-users/<uuid:pk>/connective/', api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
|
||||
path('admin-users/<uuid:pk>/assets/', api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
|
||||
|
||||
path('system-users/<uuid:pk>/auth-info/', api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
|
||||
path('system-users/<uuid:pk>/assets/', api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
|
||||
path('system-users/<uuid:pk>/assets/<uuid:asset_id>/auth-info/', api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
|
||||
@@ -52,6 +45,8 @@ urlpatterns = [
|
||||
path('system-users/<uuid:pk>/tasks/', api.SystemUserTaskApi.as_view(), name='system-user-task-create'),
|
||||
path('system-users/<uuid:pk>/cmd-filter-rules/', api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
|
||||
|
||||
path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'),
|
||||
|
||||
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
|
||||
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
|
||||
path('nodes/<uuid:pk>/children/', api.NodeChildrenApi.as_view(), name='node-children'),
|
||||
|
||||
@@ -76,6 +76,7 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
|
||||
('datetime', ('date_from', 'date_to'))
|
||||
]
|
||||
filterset_fields = ['user', 'change_by', 'remote_addr']
|
||||
search_fields = filterset_fields
|
||||
ordering = ['-datetime']
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from orgs.mixins.models import OrgModelMixin, Organization
|
||||
from orgs.utils import current_org
|
||||
|
||||
__all__ = [
|
||||
@@ -63,6 +63,11 @@ class OperateLog(OrgModelMixin):
|
||||
def __str__(self):
|
||||
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if current_org.is_root() and not self.org_id:
|
||||
self.org_id = Organization.ROOT_ID
|
||||
return super(OperateLog, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PasswordChangeLog(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
|
||||
@@ -5,14 +5,13 @@ from rest_framework import serializers
|
||||
from django.db.models import F
|
||||
|
||||
from common.mixins import BulkSerializerMixin
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
from terminal.models import Session
|
||||
from ops.models import CommandExecution
|
||||
from . import models
|
||||
|
||||
|
||||
class FTPLogSerializer(serializers.ModelSerializer):
|
||||
operate_display = serializers.ReadOnlyField(source='get_operate_display', label=_('Operate for display'))
|
||||
operate_display = serializers.ReadOnlyField(source='get_operate_display', label=_('Operate display'))
|
||||
|
||||
class Meta:
|
||||
model = models.FTPLog
|
||||
@@ -27,9 +26,9 @@ class FTPLogSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class UserLoginLogSerializer(serializers.ModelSerializer):
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type for display'))
|
||||
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status for display'))
|
||||
mfa_display = serializers.ReadOnlyField(source='get_mfa_display', label=_('MFA for display'))
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display'))
|
||||
mfa_display = serializers.ReadOnlyField(source='get_mfa_display', label=_('MFA display'))
|
||||
|
||||
class Meta:
|
||||
model = models.UserLoginLog
|
||||
@@ -75,7 +74,7 @@ class SessionAuditSerializer(serializers.ModelSerializer):
|
||||
class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
is_success = serializers.BooleanField(read_only=True, label=_('Is success'))
|
||||
hosts_display = serializers.ListSerializer(
|
||||
child=serializers.CharField(), source='hosts', read_only=True, label=_('Hosts for display')
|
||||
child=serializers.CharField(), source='hosts', read_only=True, label=_('Hosts display')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -92,8 +91,8 @@ class CommandExecutionSerializer(serializers.ModelSerializer):
|
||||
'hosts': {'label': _('Hosts')}, # 外键,会生成 sql。不在 model 上修改
|
||||
'run_as': {'label': _('Run as')},
|
||||
'user': {'label': _('User')},
|
||||
'run_as_display': {'label': _('Run as for display')},
|
||||
'user_display': {'label': _('User for display')},
|
||||
'run_as_display': {'label': _('Run as display')},
|
||||
'user_display': {'label': _('User display')},
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@@ -108,7 +107,6 @@ class CommandExecutionHostsRelationSerializer(BulkSerializerMixin, serializers.M
|
||||
commandexecution_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
model = CommandExecution.hosts.through
|
||||
fields = [
|
||||
'id', 'asset', 'asset_display', 'commandexecution', 'commandexecution_display'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.db.models.signals import post_save, post_delete, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
@@ -11,6 +11,8 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.request import Request
|
||||
|
||||
from assets.models import Asset, SystemUser
|
||||
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
|
||||
from jumpserver.utils import current_request
|
||||
from common.utils import get_request_ip, get_logger, get_syslogger
|
||||
from users.models import User
|
||||
@@ -20,6 +22,9 @@ from terminal.models import Session, Command
|
||||
from common.utils.encode import model_to_json
|
||||
from .utils import write_login_log
|
||||
from . import models
|
||||
from .models import OperateLog
|
||||
from orgs.utils import current_org
|
||||
from perms.models import AssetPermission, ApplicationPermission
|
||||
|
||||
logger = get_logger(__name__)
|
||||
sys_logger = get_syslogger(__name__)
|
||||
@@ -33,7 +38,7 @@ MODELS_NEED_RECORD = (
|
||||
'LoginACL', 'LoginAssetACL',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform',
|
||||
'CommandFilter', 'Platform', 'AuthBook',
|
||||
# applications
|
||||
'Application',
|
||||
# orgs
|
||||
@@ -90,6 +95,124 @@ def create_operate_log(action, sender, resource):
|
||||
logger.error("Create operate log error: {}".format(e))
|
||||
|
||||
|
||||
M2M_NEED_RECORD = {
|
||||
'OrganizationMember': (
|
||||
_('User and Organization'),
|
||||
_('{User} *JOINED* {Organization}'),
|
||||
_('{User} *LEFT* {Organization}')
|
||||
),
|
||||
User.groups.through._meta.object_name: (
|
||||
_('User and Group'),
|
||||
_('{User} *JOINED* {UserGroup}'),
|
||||
_('{User} *LEFT* {UserGroup}')
|
||||
),
|
||||
SystemUser.assets.through._meta.object_name: (
|
||||
_('Asset and SystemUser'),
|
||||
_('{Asset} *ADD* {SystemUser}'),
|
||||
_('{Asset} *REMOVE* {SystemUser}')
|
||||
),
|
||||
Asset.nodes.through._meta.object_name: (
|
||||
_('Node and Asset'),
|
||||
_('{Node} *ADD* {Asset}'),
|
||||
_('{Node} *REMOVE* {Asset}')
|
||||
),
|
||||
AssetPermission.users.through._meta.object_name: (
|
||||
_('User asset permissions'),
|
||||
_('{AssetPermission} *ADD* {User}'),
|
||||
_('{AssetPermission} *REMOVE* {User}'),
|
||||
),
|
||||
AssetPermission.user_groups.through._meta.object_name: (
|
||||
_('User group asset permissions'),
|
||||
_('{AssetPermission} *ADD* {UserGroup}'),
|
||||
_('{AssetPermission} *REMOVE* {UserGroup}'),
|
||||
),
|
||||
AssetPermission.assets.through._meta.object_name: (
|
||||
_('Asset permission'),
|
||||
_('{AssetPermission} *ADD* {Asset}'),
|
||||
_('{AssetPermission} *REMOVE* {Asset}'),
|
||||
),
|
||||
AssetPermission.nodes.through._meta.object_name: (
|
||||
_('Node permission'),
|
||||
_('{AssetPermission} *ADD* {Node}'),
|
||||
_('{AssetPermission} *REMOVE* {Node}'),
|
||||
),
|
||||
AssetPermission.system_users.through._meta.object_name: (
|
||||
_('Asset permission and SystemUser'),
|
||||
_('{AssetPermission} *ADD* {SystemUser}'),
|
||||
_('{AssetPermission} *REMOVE* {SystemUser}'),
|
||||
),
|
||||
ApplicationPermission.users.through._meta.object_name: (
|
||||
_('User application permissions'),
|
||||
_('{ApplicationPermission} *ADD* {User}'),
|
||||
_('{ApplicationPermission} *REMOVE* {User}'),
|
||||
),
|
||||
ApplicationPermission.user_groups.through._meta.object_name: (
|
||||
_('User group application permissions'),
|
||||
_('{ApplicationPermission} *ADD* {UserGroup}'),
|
||||
_('{ApplicationPermission} *REMOVE* {UserGroup}'),
|
||||
),
|
||||
ApplicationPermission.applications.through._meta.object_name: (
|
||||
_('Application permission'),
|
||||
_('{ApplicationPermission} *ADD* {Application}'),
|
||||
_('{ApplicationPermission} *REMOVE* {Application}'),
|
||||
),
|
||||
ApplicationPermission.system_users.through._meta.object_name: (
|
||||
_('Application permission and SystemUser'),
|
||||
_('{ApplicationPermission} *ADD* {SystemUser}'),
|
||||
_('{ApplicationPermission} *REMOVE* {SystemUser}'),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
M2M_ACTION = {
|
||||
POST_ADD: 'add',
|
||||
POST_REMOVE: 'remove',
|
||||
POST_CLEAR: 'remove',
|
||||
}
|
||||
|
||||
|
||||
@receiver(m2m_changed)
|
||||
def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
|
||||
if action not in M2M_ACTION:
|
||||
return
|
||||
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
|
||||
sender_name = sender._meta.object_name
|
||||
if sender_name in M2M_NEED_RECORD:
|
||||
action = M2M_ACTION[action]
|
||||
org_id = current_org.id
|
||||
remote_addr = get_request_ip(current_request)
|
||||
user = str(user)
|
||||
resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name]
|
||||
if action == 'add':
|
||||
resource_tmpl = resource_tmpl_add
|
||||
elif action == 'remove':
|
||||
resource_tmpl = resource_tmpl_remove
|
||||
|
||||
to_create = []
|
||||
objs = model.objects.filter(pk__in=pk_set)
|
||||
|
||||
instance_name = instance._meta.object_name
|
||||
instance_value = str(instance)
|
||||
|
||||
model_name = model._meta.object_name
|
||||
|
||||
for obj in objs:
|
||||
resource = resource_tmpl.format(**{
|
||||
instance_name: instance_value,
|
||||
model_name: str(obj)
|
||||
})[:128] # `resource` 字段只有 128 个字符长 😔
|
||||
|
||||
to_create.append(OperateLog(
|
||||
user=user, action=action, resource_type=resource_type,
|
||||
resource=resource, remote_addr=remote_addr, org_id=org_id
|
||||
))
|
||||
OperateLog.objects.bulk_create(to_create)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs):
|
||||
# last_login 改变是最后登录日期, 每次登录都会改变
|
||||
|
||||
@@ -9,4 +9,5 @@ from .login_confirm import *
|
||||
from .sso import *
|
||||
from .wecom import *
|
||||
from .dingtalk import *
|
||||
from .feishu import *
|
||||
from .password import *
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import urllib.parse
|
||||
import json
|
||||
import base64
|
||||
from typing import Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
@@ -8,6 +11,7 @@ from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
@@ -15,92 +19,42 @@ from rest_framework import serializers
|
||||
|
||||
from authentication.signals import post_auth_failed, post_auth_success
|
||||
from common.utils import get_logger, random_string
|
||||
from common.drf.api import SerializerMixin2
|
||||
from common.drf.api import SerializerMixin
|
||||
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
|
||||
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
from common.http import is_true
|
||||
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
RDPFileSerializer
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserConnectionTokenViewSet']
|
||||
|
||||
|
||||
class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericViewSet):
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
'get_rdp_file': RDPFileSerializer
|
||||
}
|
||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||
|
||||
@staticmethod
|
||||
def check_resource_permission(user, asset, application, system_user):
|
||||
from perms.utils.asset import has_asset_system_permission
|
||||
from perms.utils.application import has_application_system_permission
|
||||
if asset and not has_asset_system_permission(user, asset, system_user):
|
||||
error = f'User not has this asset and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} asset={asset.id}'
|
||||
raise PermissionDenied(error)
|
||||
if application and not has_application_system_permission(user, application, system_user):
|
||||
error = f'User not has this application and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} application={application.id}'
|
||||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5*60):
|
||||
if not self.request.user.is_superuser and user != self.request.user:
|
||||
raise PermissionDenied('Only super user can create user token')
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
value = {
|
||||
'user': str(user.id),
|
||||
'username': user.username,
|
||||
'system_user': str(system_user.id),
|
||||
'system_user_name': system_user.name
|
||||
}
|
||||
|
||||
if asset:
|
||||
value.update({
|
||||
'type': 'asset',
|
||||
'asset': str(asset.id),
|
||||
'hostname': asset.hostname,
|
||||
})
|
||||
elif application:
|
||||
value.update({
|
||||
'type': 'application',
|
||||
'application': application.id,
|
||||
'application_name': str(application)
|
||||
})
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
return token
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
class ClientProtocolMixin:
|
||||
request: Request
|
||||
get_serializer: Callable
|
||||
create_token: Callable
|
||||
|
||||
def get_request_resource(self, serializer):
|
||||
asset = serializer.validated_data.get('asset')
|
||||
application = serializer.validated_data.get('application')
|
||||
system_user = serializer.validated_data['system_user']
|
||||
user = serializer.validated_data.get('user')
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
return Response({"token": token}, status=201)
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
user = serializer.validated_data.get('user')
|
||||
if not user or not self.request.user.is_superuser:
|
||||
user = self.request.user
|
||||
return asset, application, system_user, user
|
||||
|
||||
def get_rdp_file_content(self, serializer):
|
||||
options = {
|
||||
'full address:s': '',
|
||||
'username:s': '',
|
||||
'screen mode id:i': '0',
|
||||
# 'screen mode id:i': '1',
|
||||
# 'desktopwidth:i': '1280',
|
||||
# 'desktopheight:i': '800',
|
||||
'use multimon:i': '1',
|
||||
'use multimon:i': '0',
|
||||
'session bpp:i': '32',
|
||||
'audiomode:i': '0',
|
||||
'disable wallpaper:i': '0',
|
||||
@@ -125,24 +79,17 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
# 'remoteapplicationname:s': 'Firefox',
|
||||
# 'remoteapplicationcmdline:s': '',
|
||||
}
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
asset = serializer.validated_data.get('asset')
|
||||
application = serializer.validated_data.get('application')
|
||||
system_user = serializer.validated_data['system_user']
|
||||
height = serializer.validated_data.get('height')
|
||||
width = serializer.validated_data.get('width')
|
||||
user = request.user
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
height = self.request.query_params.get('height')
|
||||
width = self.request.query_params.get('width')
|
||||
full_screen = is_true(self.request.query_params.get('full_screen'))
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
|
||||
options['screen mode id:i'] = '2' if full_screen else '1'
|
||||
address = settings.TERMINAL_RDP_ADDR
|
||||
if not address or address == 'localhost:3389':
|
||||
address = request.get_host().split(':')[0] + ':3389'
|
||||
address = self.request.get_host().split(':')[0] + ':3389'
|
||||
options['full address:s'] = address
|
||||
options['username:s'] = '{}|{}'.format(user.username, token)
|
||||
if system_user.ad_domain:
|
||||
@@ -152,15 +99,73 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
options['desktopheight:i'] = height
|
||||
else:
|
||||
options['smart sizing:i'] = '1'
|
||||
data = ''
|
||||
content = ''
|
||||
for k, v in options.items():
|
||||
data += f'{k}:{v}\n'
|
||||
content += f'{k}:{v}\n'
|
||||
if asset:
|
||||
name = asset.hostname
|
||||
elif application:
|
||||
name = application.name
|
||||
else:
|
||||
name = '*'
|
||||
return name, content
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name, data = self.get_rdp_file_content(serializer)
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname)
|
||||
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
def get_valid_serializer(self):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return serializer
|
||||
|
||||
def get_client_protocol_data(self, serializer):
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
protocol = system_user.protocol
|
||||
if protocol == 'rdp':
|
||||
name, config = self.get_rdp_file_content(serializer)
|
||||
elif protocol == 'vnc':
|
||||
raise HttpResponse(status=404, data={"error": "VNC not support"})
|
||||
else:
|
||||
config = 'ssh://system_user@asset@user@jumpserver-ssh'
|
||||
data = {
|
||||
"protocol": system_user.protocol,
|
||||
"username": user.username,
|
||||
"config": config
|
||||
}
|
||||
return data
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
|
||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||
serializer = self.get_valid_serializer()
|
||||
protocol_data = self.get_client_protocol_data(serializer)
|
||||
protocol_data = base64.b64encode(json.dumps(protocol_data).encode()).decode()
|
||||
data = {
|
||||
'url': 'jms://{}'.format(protocol_data),
|
||||
}
|
||||
return Response(data=data)
|
||||
|
||||
|
||||
class SecretDetailMixin:
|
||||
valid_token: Callable
|
||||
request: Request
|
||||
get_serializer: Callable
|
||||
|
||||
@staticmethod
|
||||
def _get_application_secret_detail(application):
|
||||
from perms.models import Action
|
||||
@@ -206,43 +211,11 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
'actions': actions,
|
||||
}
|
||||
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
from assets.models import SystemUser, Asset
|
||||
from applications.models import Application
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
value = cache.get(key, None)
|
||||
if not value:
|
||||
raise serializers.ValidationError('Token not found')
|
||||
|
||||
user = get_object_or_404(User, id=value.get('user'))
|
||||
if not user.is_valid:
|
||||
raise serializers.ValidationError("User not valid, disabled or expired")
|
||||
|
||||
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
||||
|
||||
asset = None
|
||||
app = None
|
||||
if value.get('type') == 'asset':
|
||||
asset = get_object_or_404(Asset, id=value.get('asset'))
|
||||
else:
|
||||
app = get_object_or_404(Application, id=value.get('application'))
|
||||
|
||||
if asset and not asset.is_active:
|
||||
raise serializers.ValidationError("Asset disabled")
|
||||
|
||||
try:
|
||||
self.check_resource_permission(user, asset, app, system_user)
|
||||
except PermissionDenied:
|
||||
raise serializers.ValidationError('Permission expired or invalid')
|
||||
return value, user, system_user, asset, app
|
||||
|
||||
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
|
||||
def get_secret_detail(self, request, *args, **kwargs):
|
||||
token = request.data.get('token', '')
|
||||
try:
|
||||
value, user, system_user, asset, app = self.valid_token(token)
|
||||
value, user, system_user, asset, app, expired_at = self.valid_token(token)
|
||||
except serializers.ValidationError as e:
|
||||
post_auth_failed.send(
|
||||
sender=self.__class__, username='', request=self.request,
|
||||
@@ -250,7 +223,7 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
)
|
||||
raise e
|
||||
|
||||
data = dict(user=user, system_user=system_user)
|
||||
data = dict(user=user, system_user=system_user, expired_at=expired_at)
|
||||
if asset:
|
||||
asset_detail = self._get_asset_secret_detail(asset, user=user, system_user=system_user)
|
||||
system_user.load_asset_more_auth(asset.id, user.username, user.id)
|
||||
@@ -268,6 +241,104 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
|
||||
serializer = self.get_serializer(data)
|
||||
return Response(data=serializer.data, status=200)
|
||||
|
||||
|
||||
class UserConnectionTokenViewSet(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
SecretDetailMixin, GenericViewSet
|
||||
):
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||
|
||||
@staticmethod
|
||||
def check_resource_permission(user, asset, application, system_user):
|
||||
from perms.utils.asset import has_asset_system_permission
|
||||
from perms.utils.application import has_application_system_permission
|
||||
|
||||
if asset and not has_asset_system_permission(user, asset, system_user):
|
||||
error = f'User not has this asset and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} asset={asset.id}'
|
||||
raise PermissionDenied(error)
|
||||
if application and not has_application_system_permission(user, application, system_user):
|
||||
error = f'User not has this application and system user permission: ' \
|
||||
f'user={user.id} system_user={system_user.id} application={application.id}'
|
||||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
|
||||
if not self.request.user.is_superuser and user != self.request.user:
|
||||
raise PermissionDenied('Only super user can create user token')
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
value = {
|
||||
'user': str(user.id),
|
||||
'username': user.username,
|
||||
'system_user': str(system_user.id),
|
||||
'system_user_name': system_user.name
|
||||
}
|
||||
|
||||
if asset:
|
||||
value.update({
|
||||
'type': 'asset',
|
||||
'asset': str(asset.id),
|
||||
'hostname': asset.hostname,
|
||||
})
|
||||
elif application:
|
||||
value.update({
|
||||
'type': 'application',
|
||||
'application': application.id,
|
||||
'application_name': str(application)
|
||||
})
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
return token
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
asset, application, system_user, user = self.get_request_resource(serializer)
|
||||
token = self.create_token(user, asset, application, system_user)
|
||||
return Response({"token": token}, status=201)
|
||||
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
from assets.models import SystemUser, Asset
|
||||
from applications.models import Application
|
||||
from perms.utils.asset.permission import validate_permission as asset_validate_permission
|
||||
from perms.utils.application.permission import validate_permission as app_validate_permission
|
||||
|
||||
key = self.CACHE_KEY_PREFIX.format(token)
|
||||
value = cache.get(key, None)
|
||||
if not value:
|
||||
raise serializers.ValidationError('Token not found')
|
||||
|
||||
user = get_object_or_404(User, id=value.get('user'))
|
||||
if not user.is_valid:
|
||||
raise serializers.ValidationError("User not valid, disabled or expired")
|
||||
|
||||
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
|
||||
|
||||
asset = None
|
||||
app = None
|
||||
if value.get('type') == 'asset':
|
||||
asset = get_object_or_404(Asset, id=value.get('asset'))
|
||||
if not asset.is_active:
|
||||
raise serializers.ValidationError("Asset disabled")
|
||||
|
||||
has_perm, expired_at = asset_validate_permission(user, asset, system_user, 'connect')
|
||||
else:
|
||||
app = get_object_or_404(Application, id=value.get('application'))
|
||||
has_perm, expired_at = app_validate_permission(user, app, system_user)
|
||||
|
||||
if not has_perm:
|
||||
raise serializers.ValidationError('Permission expired or invalid')
|
||||
return value, user, system_user, asset, app, expired_at
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action in ["create", "get_rdp_file"]:
|
||||
if self.request.data.get('user', None):
|
||||
|
||||
45
apps/authentication/api/feishu.py
Normal file
45
apps/authentication/api/feishu.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.permissions import IsOrgAdmin
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
from authentication import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class FeiShuQRUnBindBase(APIView):
|
||||
user: User
|
||||
|
||||
def post(self, request: Request, **kwargs):
|
||||
user = self.user
|
||||
|
||||
if not user.feishu_id:
|
||||
raise errors.FeiShuNotBound
|
||||
|
||||
user.feishu_id = None
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
|
||||
class FeiShuEventSubscriptionCallback(APIView):
|
||||
"""
|
||||
# https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM
|
||||
"""
|
||||
permission_classes = ()
|
||||
|
||||
def post(self, request: Request, *args, **kwargs):
|
||||
return Response(data=request.data)
|
||||
@@ -2,12 +2,13 @@
|
||||
#
|
||||
import time
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf import settings
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from common.permissions import IsValidUser, NeedMFAVerify
|
||||
from ..serializers import OtpVerifySerializer
|
||||
from .. import serializers
|
||||
from .. import errors
|
||||
@@ -48,6 +49,9 @@ class UserOtpVerifyApi(CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = OtpVerifySerializer
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return Response({'code': 'valid', 'msg': 'verified'})
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
@@ -58,3 +62,8 @@ class UserOtpVerifyApi(CreateAPIView):
|
||||
return Response({"ok": "1"})
|
||||
else:
|
||||
return Response({"error": _("Code is invalid")}, status=400)
|
||||
|
||||
def get_permissions(self):
|
||||
if self.request.method.lower() == 'get' and settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
self.permission_classes = [NeedMFAVerify]
|
||||
return super().get_permissions()
|
||||
|
||||
@@ -11,7 +11,7 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
from common.utils.timezone import utcnow
|
||||
from common.const.http import POST, GET
|
||||
from common.drf.api import JmsGenericViewSet
|
||||
from common.drf.api import JMSGenericViewSet
|
||||
from common.drf.serializers import EmptySerializer
|
||||
from common.permissions import IsSuperUser
|
||||
from common.utils import reverse
|
||||
@@ -26,7 +26,7 @@ NEXT_URL = 'next'
|
||||
AUTH_KEY = 'authkey'
|
||||
|
||||
|
||||
class SSOViewSet(AuthMixin, JmsGenericViewSet):
|
||||
class SSOViewSet(AuthMixin, JMSGenericViewSet):
|
||||
queryset = SSOToken.objects.all()
|
||||
serializer_classes = {
|
||||
'login_url': SSOTokenSerializer,
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.core.cache import cache
|
||||
from django.utils.translation import ugettext as _
|
||||
from six import text_type
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend as DJModelBackend
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from common.auth import signature
|
||||
@@ -17,6 +17,9 @@ from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||
from ..models import AccessKey, PrivateToken
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
def get_request_date_header(request):
|
||||
date = request.META.get('HTTP_DATE', b'')
|
||||
if isinstance(date, text_type):
|
||||
@@ -25,9 +28,16 @@ def get_request_date_header(request):
|
||||
return date
|
||||
|
||||
|
||||
class ModelBackend(DJModelBackend):
|
||||
class JMSModelBackend(ModelBackend):
|
||||
def user_can_authenticate(self, user):
|
||||
return user.is_valid
|
||||
return True
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
user = UserModel._default_manager.get(pk=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
return None
|
||||
return user if user.is_valid else None
|
||||
|
||||
|
||||
class AccessKeyAuthentication(authentication.BaseAuthentication):
|
||||
@@ -203,7 +213,7 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||
return None, None
|
||||
|
||||
|
||||
class SSOAuthentication(ModelBackend):
|
||||
class SSOAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
@@ -212,7 +222,7 @@ class SSOAuthentication(ModelBackend):
|
||||
pass
|
||||
|
||||
|
||||
class WeComAuthentication(ModelBackend):
|
||||
class WeComAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
@@ -221,7 +231,7 @@ class WeComAuthentication(ModelBackend):
|
||||
pass
|
||||
|
||||
|
||||
class DingTalkAuthentication(ModelBackend):
|
||||
class DingTalkAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
@@ -230,7 +240,16 @@ class DingTalkAuthentication(ModelBackend):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationTokenAuthentication(ModelBackend):
|
||||
class FeiShuAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationTokenAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
@@ -315,6 +315,11 @@ class DingTalkNotBound(JMSException):
|
||||
default_detail = 'DingTalk is not bound'
|
||||
|
||||
|
||||
class FeiShuNotBound(JMSException):
|
||||
default_code = 'feishu_not_bound'
|
||||
default_detail = 'FeiShu is not bound'
|
||||
|
||||
|
||||
class PasswdInvalid(JMSException):
|
||||
default_code = 'passwd_invalid'
|
||||
default_detail = _('Your password is invalid')
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from captcha.fields import CaptchaField, CaptchaTextInput
|
||||
|
||||
|
||||
@@ -23,12 +23,17 @@ class UserLoginForm(forms.Form):
|
||||
max_length=1024, strip=False
|
||||
)
|
||||
auto_login = forms.BooleanField(
|
||||
label=_("{} days auto login").format(days_auto_login or 1),
|
||||
required=False, initial=False, widget=forms.CheckboxInput(
|
||||
required=False, initial=False,
|
||||
widget=forms.CheckboxInput(
|
||||
attrs={'disabled': disable_days_auto_login}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
auto_login_field = self.fields['auto_login']
|
||||
auto_login_field.label = _("{} days auto login").format(self.days_auto_login or 1)
|
||||
|
||||
def confirm_login_allowed(self, user):
|
||||
if not user.is_staff:
|
||||
raise forms.ValidationError(
|
||||
|
||||
@@ -237,6 +237,12 @@ class AuthMixin:
|
||||
request = self.request
|
||||
|
||||
self._set_partial_credential_error(user.username, ip, request)
|
||||
|
||||
if user.is_expired:
|
||||
self.raise_credential_error(errors.reason_user_expired)
|
||||
elif not user.is_active:
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
|
||||
self._check_is_local_user(user)
|
||||
self._check_is_block(user.username)
|
||||
self._check_login_acl(user, ip)
|
||||
|
||||
@@ -196,8 +196,5 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
|
||||
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
|
||||
gateway = ConnectionTokenGatewaySerializer(read_only=True)
|
||||
actions = ActionsField()
|
||||
expired_at = serializers.IntegerField()
|
||||
|
||||
|
||||
class RDPFileSerializer(ConnectionTokenSerializer):
|
||||
width = serializers.IntegerField(allow_null=True, max_value=3112, min_value=100, required=False)
|
||||
height = serializers.IntegerField(allow_null=True, max_value=4096, min_value=100, required=False)
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK %}
|
||||
{% if AUTH_OPENID or AUTH_CAS or AUTH_WECOM or AUTH_DINGTALK or AUTH_FEISHU %}
|
||||
<div class="hr-line-dashed"></div>
|
||||
<div style="display: inline-block; float: left">
|
||||
<b class="text-muted text-left" >{% trans "More login options" %}</b>
|
||||
@@ -215,6 +215,11 @@
|
||||
<i class="fa"><img src="{{ LOGIN_DINGTALK_LOGO_URL }}" height="13" width="13"></i> {% trans 'DingTalk' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if AUTH_FEISHU %}
|
||||
<a href="{% url 'authentication:feishu-qr-login' %}" class="more-login-item">
|
||||
<i class="fa"><img src="{{ LOGIN_FEISHU_LOGO_URL }}" height="13" width="13"></i> {% trans 'FeiShu' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@@ -20,6 +20,10 @@ urlpatterns = [
|
||||
path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'),
|
||||
path('dingtalk/qr/unbind/<uuid:user_id>/', api.DingTalkQRUnBindForAdminApi.as_view(), name='dingtalk-qr-unbind-for-admin'),
|
||||
|
||||
path('feishu/qr/unbind/', api.FeiShuQRUnBindForUserApi.as_view(), name='feishu-qr-unbind'),
|
||||
path('feishu/qr/unbind/<uuid:user_id>/', api.FeiShuQRUnBindForAdminApi.as_view(), name='feishu-qr-unbind-for-admin'),
|
||||
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
|
||||
|
||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/challenge/', api.MFAChallengeApi.as_view(), name='mfa-challenge'),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#
|
||||
|
||||
from django.urls import path, include
|
||||
from django.db.transaction import non_atomic_requests
|
||||
|
||||
from .. import views
|
||||
from users import views as users_view
|
||||
@@ -10,7 +11,7 @@ app_name = 'authentication'
|
||||
|
||||
urlpatterns = [
|
||||
# login
|
||||
path('login/', views.UserLoginView.as_view(), name='login'),
|
||||
path('login/', non_atomic_requests(views.UserLoginView.as_view()), name='login'),
|
||||
path('login/otp/', views.UserLoginOtpView.as_view(), name='login-otp'),
|
||||
path('login/wait-confirm/', views.UserLoginWaitConfirmView.as_view(), name='login-wait-confirm'),
|
||||
path('login/guard/', views.UserLoginGuardView.as_view(), name='login-guard'),
|
||||
@@ -37,6 +38,14 @@ urlpatterns = [
|
||||
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
|
||||
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
|
||||
|
||||
path('feishu/bind/success-flash-msg/', views.FlashDingTalkBindSucceedMsgView.as_view(), name='feishu-bind-success-flash-msg'),
|
||||
path('feishu/bind/failed-flash-msg/', views.FlashDingTalkBindFailedMsgView.as_view(), name='feishu-bind-failed-flash-msg'),
|
||||
path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
|
||||
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'),
|
||||
path('feishu/qr/login/', views.FeiShuQRLoginView.as_view(), name='feishu-qr-login'),
|
||||
path('feishu/qr/bind/callback/', views.FeiShuQRBindCallbackView.as_view(), name='feishu-qr-bind-callback'),
|
||||
path('feishu/qr/login/callback/', views.FeiShuQRLoginCallbackView.as_view(), name='feishu-qr-login-callback'),
|
||||
|
||||
# Profile
|
||||
path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
|
||||
path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'),
|
||||
|
||||
@@ -4,3 +4,4 @@ from .login import *
|
||||
from .mfa import *
|
||||
from .wecom import *
|
||||
from .dingtalk import *
|
||||
from .feishu import *
|
||||
|
||||
253
apps/authentication/views/feishu.py
Normal file
253
apps/authentication/views/feishu.py
Normal file
@@ -0,0 +1,253 @@
|
||||
import urllib
|
||||
|
||||
from django.http.response import HttpResponseRedirect, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import TemplateView
|
||||
from django.views import View
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
from django.db.utils import IntegrityError
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
from common.mixins.views import PermissionsMixin
|
||||
from common.message.backends.feishu import FeiShu, URL
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
FEISHU_STATE_SESSION_KEY = '_feishu_state'
|
||||
|
||||
|
||||
class FeiShuQRMixin(PermissionsMixin, View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
except APIException as e:
|
||||
msg = str(e.detail)
|
||||
return self.get_failed_reponse(
|
||||
'/',
|
||||
_('FeiShu Error'),
|
||||
msg
|
||||
)
|
||||
|
||||
def verify_state(self):
|
||||
state = self.request.GET.get('state')
|
||||
session_state = self.request.session.get(FEISHU_STATE_SESSION_KEY)
|
||||
if state != session_state:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_verify_state_failed_response(self, redirect_uri):
|
||||
msg = _("You've been hacked")
|
||||
return self.get_failed_reponse(redirect_uri, msg, msg)
|
||||
|
||||
def get_qr_url(self, redirect_uri):
|
||||
state = random_string(16)
|
||||
self.request.session[FEISHU_STATE_SESSION_KEY] = state
|
||||
|
||||
params = {
|
||||
'app_id': settings.FEISHU_APP_ID,
|
||||
'state': state,
|
||||
'redirect_uri': redirect_uri,
|
||||
}
|
||||
url = URL.AUTHEN + '?' + urllib.parse.urlencode(params)
|
||||
return url
|
||||
|
||||
def get_success_reponse(self, redirect_url, title, msg):
|
||||
ok_flash_msg_url = reverse('authentication:feishu-bind-success-flash-msg')
|
||||
ok_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(ok_flash_msg_url)
|
||||
|
||||
def get_failed_reponse(self, redirect_url, title, msg):
|
||||
failed_flash_msg_url = reverse('authentication:feishu-bind-failed-flash-msg')
|
||||
failed_flash_msg_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'title': title,
|
||||
'msg': msg
|
||||
})
|
||||
return HttpResponseRedirect(failed_flash_msg_url)
|
||||
|
||||
def get_already_bound_response(self, redirect_url):
|
||||
msg = _('FeiShu is already bound')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
class FeiShuQRBindView(FeiShuQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class FeiShuQRBindCallbackView(FeiShuQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
user = request.user
|
||||
|
||||
if user.feishu_id:
|
||||
response = self.get_already_bound_response(redirect_url)
|
||||
return response
|
||||
|
||||
feishu = FeiShu(
|
||||
app_id=settings.FEISHU_APP_ID,
|
||||
app_secret=settings.FEISHU_APP_SECRET
|
||||
)
|
||||
user_id = feishu.get_user_id_by_code(code)
|
||||
|
||||
if not user_id:
|
||||
msg = _('FeiShu query user failed')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
user.feishu_id = user_id
|
||||
user.save()
|
||||
except IntegrityError as e:
|
||||
if e.args[0] == 1062:
|
||||
msg = _('The FeiShu is already bound to another user')
|
||||
response = self.get_failed_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
raise e
|
||||
|
||||
msg = _('Binding FeiShu successfully')
|
||||
response = self.get_success_reponse(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
|
||||
class FeiShuEnableStartView(UserVerifyPasswordView):
|
||||
|
||||
def get_success_url(self):
|
||||
referer = self.request.META.get('HTTP_REFERER')
|
||||
redirect_url = self.request.GET.get("redirect_url")
|
||||
|
||||
success_url = reverse('authentication:feishu-qr-bind')
|
||||
|
||||
success_url += '?' + urllib.parse.urlencode({
|
||||
'redirect_url': redirect_url or referer
|
||||
})
|
||||
|
||||
return success_url
|
||||
|
||||
|
||||
class FeiShuQRLoginView(FeiShuQRMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urllib.parse.urlencode({'redirect_url': redirect_url})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
class FeiShuQRLoginCallbackView(AuthMixin, FeiShuQRMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
code = request.GET.get('code')
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
login_url = reverse('authentication:login')
|
||||
|
||||
if not self.verify_state():
|
||||
return self.get_verify_state_failed_response(redirect_url)
|
||||
|
||||
feishu = FeiShu(
|
||||
app_id=settings.FEISHU_APP_ID,
|
||||
app_secret=settings.FEISHU_APP_SECRET
|
||||
)
|
||||
user_id = feishu.get_user_id_by_code(code)
|
||||
if not user_id:
|
||||
# 正常流程不会出这个错误,hack 行为
|
||||
msg = _('Failed to get user from FeiShu')
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
user = get_object_or_none(User, feishu_id=user_id)
|
||||
if user is None:
|
||||
title = _('FeiShu is not bound')
|
||||
msg = _('Please login with a password and then bind the FeiShu')
|
||||
response = self.get_failed_reponse(login_url, title=title, msg=msg)
|
||||
return response
|
||||
|
||||
try:
|
||||
self.check_oauth2_auth(user, settings.AUTH_BACKEND_FEISHU)
|
||||
except errors.AuthFailedError as e:
|
||||
self.set_login_failed_mark()
|
||||
msg = e.msg
|
||||
response = self.get_failed_reponse(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashFeiShuBindSucceedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding FeiShu successfully'),
|
||||
'messages': msg or _('Binding FeiShu successfully'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashFeiShuBindFailedMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
title = request.GET.get('title')
|
||||
msg = request.GET.get('msg')
|
||||
|
||||
context = {
|
||||
'title': title or _('Binding FeiShu failed'),
|
||||
'messages': msg or _('Binding FeiShu failed'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
@@ -4,6 +4,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import reverse, redirect
|
||||
@@ -17,6 +18,7 @@ from django.views.generic.edit import FormView
|
||||
from django.conf import settings
|
||||
from django.urls import reverse_lazy
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
from django.db.transaction import atomic
|
||||
|
||||
from common.utils import get_request_ip, FlashMessageUtil
|
||||
from users.utils import (
|
||||
@@ -44,25 +46,46 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
# show jumpserver login page if request http://{JUMP-SERVER}/?admin=1
|
||||
if self.request.GET.get("admin", 0):
|
||||
return None
|
||||
next_url = request.GET.get('next') or '/'
|
||||
auth_type = ''
|
||||
auth_url = ''
|
||||
|
||||
if settings.AUTH_OPENID:
|
||||
auth_type = 'OIDC'
|
||||
auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME)
|
||||
elif settings.AUTH_CAS:
|
||||
openid_auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + f'?next={next_url}'
|
||||
else:
|
||||
openid_auth_url = None
|
||||
|
||||
if settings.AUTH_CAS:
|
||||
auth_type = 'CAS'
|
||||
auth_url = reverse(settings.CAS_LOGIN_URL_NAME)
|
||||
if not auth_url:
|
||||
cas_auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + f'?next={next_url}'
|
||||
else:
|
||||
cas_auth_url = None
|
||||
|
||||
if not any([openid_auth_url, cas_auth_url]):
|
||||
return None
|
||||
|
||||
message_data = {
|
||||
'title': _('Redirecting'),
|
||||
'message': _("Redirecting to {} authentication").format(auth_type),
|
||||
'redirect_url': auth_url,
|
||||
'has_cancel': True,
|
||||
'cancel_url': reverse('authentication:login') + '?admin=1'
|
||||
}
|
||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||
if settings.LOGIN_REDIRECT_TO_BACKEND == 'OPENID' and openid_auth_url:
|
||||
auth_url = openid_auth_url
|
||||
|
||||
elif settings.LOGIN_REDIRECT_TO_BACKEND == 'CAS' and cas_auth_url:
|
||||
auth_url = cas_auth_url
|
||||
|
||||
else:
|
||||
auth_url = openid_auth_url or cas_auth_url
|
||||
|
||||
if settings.LOGIN_REDIRECT_TO_BACKEND:
|
||||
redirect_url = auth_url
|
||||
else:
|
||||
message_data = {
|
||||
'title': _('Redirecting'),
|
||||
'message': _("Redirecting to {} authentication").format(auth_type),
|
||||
'redirect_url': auth_url,
|
||||
'interval': 3,
|
||||
'has_cancel': True,
|
||||
'cancel_url': reverse('authentication:login') + '?admin=1'
|
||||
}
|
||||
redirect_url = FlashMessageUtil.gen_message_url(message_data)
|
||||
|
||||
query_string = request.GET.urlencode()
|
||||
redirect_url = "{}&{}".format(redirect_url, query_string)
|
||||
return redirect_url
|
||||
@@ -86,7 +109,8 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
self.request.session.delete_test_cookie()
|
||||
|
||||
try:
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
with atomic():
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
self.set_login_failed_mark()
|
||||
@@ -133,6 +157,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
'AUTH_CAS': settings.AUTH_CAS,
|
||||
'AUTH_WECOM': settings.AUTH_WECOM,
|
||||
'AUTH_DINGTALK': settings.AUTH_DINGTALK,
|
||||
'AUTH_FEISHU': settings.AUTH_FEISHU,
|
||||
'rsa_public_key': rsa_public_key,
|
||||
'forgot_password_url': forgot_password_url
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class Cache(metaclass=CacheType):
|
||||
return data
|
||||
|
||||
def save_data_to_db(self, data):
|
||||
logger.info(f'Set data to cache: key={self.key} data={data}')
|
||||
logger.debug(f'Set data to cache: key={self.key} data={data}')
|
||||
self.redis.hset(self.key, mapping=data)
|
||||
self.load_data_from_db()
|
||||
|
||||
@@ -143,10 +143,10 @@ class Cache(metaclass=CacheType):
|
||||
|
||||
def init_all_values(self):
|
||||
t_start = time.time()
|
||||
logger.info(f'Start init cache: key={self.key}')
|
||||
logger.debug(f'Start init cache: key={self.key}')
|
||||
data = self.compute_values(*self.field_names)
|
||||
self.save_data_to_db(data)
|
||||
logger.info(f'End init cache: cost={time.time()-t_start} key={self.key}')
|
||||
logger.debug(f'End init cache: cost={time.time()-t_start} key={self.key}')
|
||||
return data
|
||||
|
||||
def refresh(self, *fields):
|
||||
@@ -173,11 +173,11 @@ class Cache(metaclass=CacheType):
|
||||
def expire(self, *fields):
|
||||
self._data = None
|
||||
if not fields:
|
||||
logger.info(f'Delete cached key: key={self.key}')
|
||||
logger.debug(f'Delete cached key: key={self.key}')
|
||||
self.redis.delete(self.key)
|
||||
else:
|
||||
self.redis.hdel(self.key, *fields)
|
||||
logger.info(f'Expire cached fields: key={self.key} fields={fields}')
|
||||
logger.debug(f'Expire cached fields: key={self.key} fields={fields}')
|
||||
|
||||
|
||||
class CacheValueDesc:
|
||||
@@ -201,7 +201,7 @@ class CacheValueDesc:
|
||||
|
||||
def compute_value(self, instance: Cache):
|
||||
t_start = time.time()
|
||||
logger.info(f'Start compute cache field: field={self.field_name} key={instance.key}')
|
||||
logger.debug(f'Start compute cache field: field={self.field_name} key={instance.key}')
|
||||
if self.field_type.queryset is not None:
|
||||
new_value = self.field_type.queryset.count()
|
||||
else:
|
||||
@@ -214,7 +214,7 @@ class CacheValueDesc:
|
||||
new_value = compute_func()
|
||||
|
||||
new_value = self.field_type.field_type(new_value)
|
||||
logger.info(f'End compute cache field: cost={time.time()-t_start} field={self.field_name} value={new_value} key={instance.key}')
|
||||
logger.debug(f'End compute cache field: cost={time.time()-t_start} field={self.field_name} value={new_value} key={instance.key}')
|
||||
return new_value
|
||||
|
||||
def to_internal_value(self, value):
|
||||
|
||||
@@ -10,3 +10,15 @@ def on_transaction_commit(func):
|
||||
def inner(*args, **kwargs):
|
||||
transaction.on_commit(lambda: func(*args, **kwargs))
|
||||
return inner
|
||||
|
||||
|
||||
class Singleton(object):
|
||||
""" 单例类 """
|
||||
def __init__(self, cls):
|
||||
self._cls = cls
|
||||
self._instance = {}
|
||||
|
||||
def __call__(self):
|
||||
if self._cls not in self._instance:
|
||||
self._instance[self._cls] = self._cls()
|
||||
return self._instance[self._cls]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user