Compare commits

...

89 Commits

Author SHA1 Message Date
Bai
ca29e142f4 feat: update v2.12.2 to latest 2021-08-12 18:48:03 +08:00
xinwen
173c450b25 fix: 将 es 的 doc_type 默认值改为 _doc 2021-08-11 18:12:55 +08:00
xinwen
ff804b2d19 fix: 修复索引不存在时报错 2021-08-11 18:12:55 +08:00
xinwen
bc9bd30203 fix: 无效的 es 报 500 2021-08-11 18:12:55 +08:00
xinwen
998ee2ee14 fix: 修复 es 命令存储过滤不准确 2021-08-11 18:12:55 +08:00
Bai
628b14133d fix: 解决访问api-docs失败的问题 2021-07-26 14:32:34 +08:00
Bai
a69ab682a0 fix: 修复ssh-private-key错误导致系统用户列表加载出现500的问题 2021-07-23 18:57:31 +08:00
Bai
b3499a0675 fix: 修改特权用户文案 2021-07-21 16:46:23 +08:00
feng626
e5d3fe696f 网域网管取消密码不为空校验 2021-07-21 16:32:45 +08:00
ibuler
8f3fb60332 perf: 优化工单推荐资产的数量 2021-07-21 14:39:45 +08:00
xinwen
1116d1d353 fix: xrdp 设置分辨率不生效 2021-07-20 19:29:18 +08:00
feng626
5b239cd340 关闭 网域网关 密码特殊字符校验 2021-07-19 18:26:42 +08:00
Jiangjie.Bai
c0560ad3cc Merge pull request #6464 from jumpserver/dev
v2.12.0 rc5
2021-07-15 19:19:18 +08:00
ibuler
c318762f82 perf: 修改account密码加载 2021-07-15 19:12:58 +08:00
ibuler
5d373c0137 fix: 修复错误格式 2021-07-15 19:12:58 +08:00
Jiangjie.Bai
3aea998bd2 Merge pull request #6462 from jumpserver/dev
v2.12.0 rc5
2021-07-15 18:23:38 +08:00
ibuler
c1ca48a32a perf: 修改i18n 2021-07-15 18:22:53 +08:00
Jiangjie.Bai
2f0fcddc29 Merge pull request #6458 from jumpserver/dev
v2.12.0 rc5
2021-07-15 18:00:27 +08:00
ibuler
329565251a perf: 修改prefetch 2021-07-15 17:58:45 +08:00
ibuler
06a223376c perf: 基本完成 2021-07-15 17:58:45 +08:00
Bai
47e8ad3aac fix: 修复创建资产关联所在节点的系统用户时没有设置组织ID的问题 2021-07-15 17:12:56 +08:00
Jiangjie.Bai
c4fb3a8c04 Merge pull request #6455 from jumpserver/dev
v2.12.0 rc5
2021-07-15 17:04:22 +08:00
ibuler
9d4121c3b7 perf: 优化代码 2021-07-15 11:02:45 +08:00
xinwen
2eb1fe8547 fix: 系统用户与资产关系变化时 AuthBook 表的 org_id 可能是 root 组织 2021-07-15 11:02:45 +08:00
ibuler
e933774e6c fix: 修复创建 authbook 可能没有组织id的问题 2021-07-15 11:01:14 +08:00
Jiangjie.Bai
0b994d1c46 Merge pull request #6450 from jumpserver/dev
v2.12.0 rc4
2021-07-14 21:38:38 +08:00
xinwen
381b150c2b fix: 探测 authbook 在 root 组织下保存的情况 2021-07-14 21:37:48 +08:00
xinwen
53ebac9363 fix: 探测 authbook 在 root 组织下保存的情况 2021-07-14 21:36:53 +08:00
Jiangjie.Bai
a0638dd5c4 Merge pull request #6447 from jumpserver/dev
v2.12.0 rc4
2021-07-14 19:02:45 +08:00
Bai
5b741de896 fix: 修复系统用户资产导出包含组织名称 2021-07-14 18:56:40 +08:00
Bai
d7f587216d fix: 修复测试系统用户可连接性问题 2021-07-14 17:07:55 +08:00
ibuler
019f00a34a perf: 优化特权账号创建和导出
perf: 优化搜索

perf: Huany

perf: 还原

perf: 又改

xxx
2021-07-14 16:56:34 +08:00
Bai
9684b2d4ac fix: 修复测试资产可连接性获取admin_user总是新加载的authbook对象 2021-07-14 15:31:39 +08:00
xinwen
2e190c9ea9 fix: 授权过期自动刷新授权树 2021-07-14 15:00:41 +08:00
xinwen
601a48071f fix: 组织统计中系统用户数量不对 2021-07-14 13:56:44 +08:00
Bai
bf885f94e4 fix: 修复系统用户资产导出文案 2021-07-14 12:27:58 +08:00
Bai
7d4be819b8 fix: 修复系统用户资产导出文案 2021-07-14 12:25:07 +08:00
xinwen
26a7fa836c fix: 网关测试连接 500 2021-07-14 12:24:43 +08:00
Jiangjie.Bai
187329b006 Merge pull request #6429 from jumpserver/dev
v2.12.0 rc3
2021-07-13 20:45:03 +08:00
xinwen
8375008cfa fix: 用户无效时,企业微信&钉钉扫码 500 2021-07-13 20:43:37 +08:00
ibuler
16333fa1aa fix: 修复管理用户批量删除失败的bug 2021-07-13 18:12:19 +08:00
Bai
72deb005a6 fix: 修复改密日志支持模糊搜索 2021-07-13 18:07:48 +08:00
Bai
18509a0ca4 fix: 修复导出系统用户资产列表时包含org_id字段 2021-07-13 17:46:21 +08:00
ibuler
e63d0dcd9e perf: 添加ssh 指纹 2021-07-13 17:45:59 +08:00
Bai
62ba3984bd fix: 修复用户列表角色字段不显示的问题 2021-07-13 16:22:49 +08:00
ibuler
db170aac9e perf: 添加测试多个账号的任务 2021-07-13 13:16:27 +08:00
ibuler
5c7e73e2e0 fix: 修复系统用户详情,测试资产可连接性问题
fix; bug

perf: 还原migrations
2021-07-13 11:31:39 +08:00
ibuler
f772296dff fix(assets): 修复patch system user的问题
perf: 去掉debug
2021-07-13 11:30:38 +08:00
xinwen
f6a26ac165 fix: 全局组织命令记录无数据 2021-07-13 10:58:48 +08:00
ibuler
4e3b3442d2 perf: 修改添加翻译
perf: 优化翻译

perf: 修改i18n

perf: 编译
2021-07-13 10:35:42 +08:00
Jiangjie.Bai
2752770ce2 Merge pull request #6416 from jumpserver/dev
v2.12.0 rc2
2021-07-12 18:26:56 +08:00
ibuler
1840609d53 fix: 修复动态系统用户无法提交的问题
fix: 修复动态系统用户
2021-07-12 18:21:04 +08:00
fit2bot
4f23090a5c fix: 修复账号搜索 5xx (#6413)
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2021-07-12 18:20:32 +08:00
ibuler
898b51c593 fix: 修复账号搜索问题 2021-07-12 18:17:21 +08:00
xinwen
2494418208 fix: 用户组删除时,授权树不会自动更新 2021-07-12 18:16:53 +08:00
Bai
0fec70fe69 feat: 添加移除AssetUser Model的migrations 2021-07-12 13:10:40 +08:00
Jiangjie.Bai
bcf90d71a2 Merge pull request #6405 from jumpserver/dev
v2.12.0 rc1
2021-07-08 16:55:46 +08:00
ibuler
f8f7ac0af5 fix(assets): 修复创建资产报错 2021-07-08 16:52:31 +08:00
Jiangjie.Bai
d6c2705bd6 Merge pull request #6402 from jumpserver/dev
v2.12 rc1
2021-07-08 15:19:21 +08:00
ibuler
10f8b9f130 perf: 优化ansible执行命令 2021-07-08 14:54:05 +08:00
Bai
1e601288fa fix: 修改CAS配置默认值 2021-07-08 14:47:03 +08:00
Tommy.chen
b1032761c8 add cas CAS_USERNAME_ATTRIBUTE CAS_RENAME_ATTRIBUTES CAS_CREATE_USER read 2021-07-08 14:34:30 +08:00
Z000000
c532c361c0 批量命令支持更广泛的设备如思科等网络设备,docker等 (#6356)
* feat: Update README (#6182)

* feat: Update README

* feat: Update README

* Update README.md

* feat: update README

* Update README.md

* docs: 修改英文版本

* Update README.md

* 批量命令支持更广泛的设备如思科等网络设备,docker等

Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
Co-authored-by: 老广 <ibuler@qq.com>
2021-07-08 14:32:03 +08:00
fit2bot
ec8dca90d6 refactor: 整合系统用户和管理用户 (#6236)
* perf: 整合系统用户和管理用户

* stash

stash

perf: 优化系统用户和资产的表结构

* perf: 添加信号

* perf: 添加算法

* perf: 去掉 asset user backends

* perf: 整理系统用户api

* perfF: 暂存一下

* stash

* perf: 暂存一下

* perf: 暂存

* xxx

* perf: ...

* stash it

* xxx

* xxx

* xxx

* xxx

* xxx

* stash it

* 修改Protocols

* perf: 修改创建authbook信号

* perf: 添加auth info

* .stash

* perf: 基本完成

* perf: 修复完成

* perf: 修复更改的id

* perf: 修复迁移过去数量不对的问题

* perf: 修改systemuser

* fix: 修复批量编辑近期的问题

* fix: 修复authbook加载的问题

* xxx

Co-authored-by: ibuler <ibuler@qq.com>
2021-07-08 14:23:18 +08:00
xinwen
a9f814a515 fix: 过期用户登录提示不明确 2021-07-08 10:27:15 +08:00
xinwen
c4bbeaaccc feat: rdp 添加授权过期自动断开 2021-07-07 11:09:17 +08:00
xinwen
0fd5ab02e9 fix: 修复 interval 周期任务不执行问题 2021-07-01 16:42:22 +08:00
老广
745979074a Update README.md 2021-06-29 15:12:10 +08:00
Bai
8ae6863266 fix: 修复终端更新存储失败的问题 2021-06-29 13:33:16 +08:00
Bai
4fd7f0e949 fix: 修复自动生成系统用户密码中包含 {{ 双字符时测试可连接性失败的问题 2021-06-29 13:22:59 +08:00
xinwen
732f0b55dc refactor: 更改系统消息初始化策略 2021-06-28 15:57:49 +08:00
Jiangjie.Bai
c0ec0f1343 feat: 支持设置默认存储(命令、录像) (#6336)
* fix: 修改LDAP用户导入的组织为当前组织

* fix: 修改翻译信息

* feat: 支持设置默认存储

* feat: 支持设置默认存储(2)

* feat: 支持设置默认存储(3)
2021-06-28 10:32:59 +08:00
xinwen
aa6e550ba2 fix: 系统消息通知升级错误 2021-06-25 23:47:13 +08:00
Jiangjie.Bai
2ffaf59238 Merge pull request #6328 from jumpserver/ibuler-patch-1
Update README.md
2021-06-25 14:47:36 +08:00
ibuler
6c13fdbc46 perf: 优化图片大小
perf: ...
2021-06-25 10:16:26 +08:00
fghbng@qq.com
35941ddf7f feat: 优化缓存,将会话的缓存拿出来 2021-06-25 10:13:51 +08:00
fghbng@qq.com
3ae976c183 优化缓存,将会话的缓存拿出来 2021-06-25 10:13:51 +08:00
ibuler
999666f0eb docs: 修改英文版本 2021-06-23 17:16:01 +08:00
老广
1812074231 Update README.md 2021-06-23 11:31:51 +08:00
Bai
53eb32e620 fix: 修改翻译信息 2021-06-22 19:17:44 +08:00
Bai
50bd0b796d fix: 修改LDAP用户导入的组织为当前组织 2021-06-22 19:17:44 +08:00
wojiushixiaobai
a02d80a2ae feat: arm64 支持 2021-06-22 14:44:47 +08:00
ibuler
71a7eea8ad perf: 修复next为空可能会导致的bug 2021-06-22 11:13:44 +08:00
ibuler
2b927caa60 fix: 修复oidc登录的问题
..
2021-06-22 11:04:12 +08:00
ibuler
053d958f9a fix: 修复app无法下载xrdp文件 2021-06-22 10:20:37 +08:00
ibuler
8d25d0a653 fix: 修复登录页面的 i18n 问题 2021-06-22 10:18:48 +08:00
Bai
62eb131f59 fix: 修改创建用户时如果没有在任何组织内默认添加到default组织 2021-06-21 18:59:07 +08:00
jiangweidong
40eb7c79bb feat: 添加青云SDK 2021-06-21 15:57:21 +08:00
Bai
dabc9eb09b fix: 修改获取系统用户认证信息时username的选择逻辑;(单独设置过的系统用户认证信息登录资产失败) 2021-06-18 18:15:58 +08:00
126 changed files with 2581 additions and 3343 deletions

288
README.md
View File

@@ -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>
[![License](https://shields.io/github/license/jumpserver/jumpserver)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html)
[![Release Downloads](https://shields.io/github/downloads/jumpserver/jumpserver/total)](https://github.com/jumpserver/jumpserver/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](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.

View File

@@ -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>
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](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.
|![notification](https://raw.githubusercontent.com/goharbor/website/master/docs/img/readme/bell-outline-badged.svg)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 AuthenticationSingle 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 users 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 Connectorrely on [Apache Guacamole](https://guacamole.apache.org/)
- [Lion](https://github.com/jumpserver/lion-release) JumpServer Graphics protocol Connectorrely 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.

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#

View File

@@ -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])
)
)

View 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',),
),
]

View File

@@ -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 *

106
apps/assets/api/accounts.py Normal file
View File

@@ -0,0 +1,106 @@
from django.db.models import F, Q
from django.conf import settings
from rest_framework.decorators import action
from django_filters import rest_framework as filters
from rest_framework.response import Response
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
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')
@property
def qs(self):
qs = super().qs
qs = self.filter_username(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
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']
def get_permissions(self):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser]
return super().get_permissions()
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

View File

@@ -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()

View File

@@ -33,8 +33,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 +42,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]

View File

@@ -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

View File

@@ -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

View File

@@ -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,)

View File

@@ -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):

View File

@@ -1 +0,0 @@
from .manager import AssetUserManager

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
#
# from django.conf import settings
# from .vault import VaultBackend

View File

@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
#

View 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',
),
]

View 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)
]

View 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',
),
]

View 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'),
),
]

View 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'),
),
]

View 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',
),
]

View File

@@ -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 *

View File

@@ -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 {}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
#
import socket
import uuid
import random
import re
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 orgs.mixins.models import OrgModelMixin
from .base import BaseUser
@@ -43,15 +43,12 @@ class Domain(OrgModelMixin):
class Gateway(BaseUser):
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
)
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"))
@@ -66,8 +63,6 @@ class Gateway(BaseUser):
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,7 +77,8 @@ class Gateway(BaseUser):
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
paramiko.SSHException,
paramiko.ssh_exception.NoValidConnectionsError) as e:
paramiko.ssh_exception.NoValidConnectionsError,
socket.gaierror) as e:
return False, str(e)
try:

View File

@@ -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 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
@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")

View File

@@ -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()

View File

@@ -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 *

View File

@@ -0,0 +1,55 @@
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
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},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
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},
}
class AccountTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
task = serializers.CharField(read_only=True)

View File

@@ -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'

View File

@@ -1,24 +1,21 @@
# -*- 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',
]
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'))
}
@@ -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)
"""
@@ -81,37 +77,40 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
'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,16 +176,11 @@ 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):

View File

@@ -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

View File

@@ -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',
]

View File

@@ -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)

View File

@@ -3,9 +3,7 @@
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
@@ -29,7 +27,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
extra_kwargs = {
'assets': {'required': False, 'label': _('Assets')},
}
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
def get_asset_count(obj):
@@ -47,7 +44,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = Gateway
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name']
fields_write_only = [
'password', 'private_key', 'public_key',
@@ -61,26 +57,16 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
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},
}

View File

@@ -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']

View File

@@ -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):

View File

@@ -2,7 +2,6 @@ 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
@@ -10,7 +9,7 @@ from ..models import SystemUser, Asset
from .base import AuthSerializerMixin
__all__ = [
'SystemUserSerializer', 'SystemUserListSerializer',
'SystemUserSerializer',
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
@@ -23,21 +22,22 @@ 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},
@@ -52,18 +52,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 +71,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 +94,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 +120,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 +209,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 +249,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",

View File

@@ -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 *

View 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
)

View 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 = instance.history.all().count() + 1
# 即使在 root 组织也不怕
instance.org_id = instance.asset.org_id

View File

@@ -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
)

View 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])

View File

@@ -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 *

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -21,6 +21,25 @@ __all__ = [
]
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 +51,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 +83,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 +104,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

View File

@@ -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'),

View File

@@ -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):

View File

@@ -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'

View File

@@ -15,7 +15,7 @@ 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
@@ -29,7 +29,7 @@ logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet']
class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericViewSet):
class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin, GenericViewSet):
permission_classes = (IsSuperUserOrAppUser,)
serializer_classes = {
'default': ConnectionTokenSerializer,
@@ -97,10 +97,10 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
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',
@@ -155,8 +155,14 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
data = ''
for k, v in options.items():
data += f'{k}:{v}\n'
if asset:
name = asset.hostname
elif application:
name = application.name
else:
name = '*'
response = HttpResponse(data, content_type='application/octet-stream')
filename = "{}-{}-jumpserver.rdp".format(user.username, asset.hostname)
filename = "{}-{}-jumpserver.rdp".format(user.username, name)
filename = urllib.parse.quote(filename)
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@@ -210,6 +216,8 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
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)
@@ -226,23 +234,24 @@ class UserConnectionTokenViewSet(RootOrgViewMixin, SerializerMixin2, GenericView
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 asset and not asset.is_active:
raise serializers.ValidationError("Asset disabled")
try:
self.check_resource_permission(user, asset, app, system_user)
except PermissionDenied:
if not has_perm:
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app
return value, user, system_user, asset, app, expired_at
@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 +259,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)

View File

@@ -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()

View File

@@ -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,7 @@ class DingTalkAuthentication(ModelBackend):
pass
class AuthorizationTokenAuthentication(ModelBackend):
class AuthorizationTokenAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""

View File

@@ -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(

View File

@@ -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)

View File

@@ -196,6 +196,7 @@ class ConnectionTokenSecretSerializer(serializers.Serializer):
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
actions = ActionsField()
expired_at = serializers.IntegerField()
class RDPFileSerializer(ConnectionTokenSerializer):

View File

@@ -44,14 +44,15 @@ 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)
auth_url = reverse(settings.AUTH_OPENID_AUTH_LOGIN_URL_NAME) + f'?next={next_url}'
elif settings.AUTH_CAS:
auth_type = 'CAS'
auth_url = reverse(settings.CAS_LOGIN_URL_NAME)
auth_url = reverse(settings.CAS_LOGIN_URL_NAME) + f'?next={next_url}'
if not auth_url:
return None

View File

@@ -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):

View File

@@ -2,12 +2,12 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework_bulk import BulkModelViewSet
from ..mixins.api import (
SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin,
RelationMixin, AllowBulkDestoryMixin, RenderToJsonMixin,
SerializerMixin, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin,
RelationMixin, AllowBulkDestroyMixin, RenderToJsonMixin,
)
class CommonMixin(SerializerMixin2,
class CommonMixin(SerializerMixin,
QuerySetMixin,
ExtraFilterFieldsMixin,
PaginatedResponseMixin,
@@ -26,13 +26,13 @@ class JMSModelViewSet(CommonMixin,
class JMSBulkModelViewSet(CommonMixin,
AllowBulkDestoryMixin,
AllowBulkDestroyMixin,
BulkModelViewSet):
pass
class JMSBulkRelationModelViewSet(CommonMixin,
RelationMixin,
AllowBulkDestoryMixin,
AllowBulkDestroyMixin,
BulkModelViewSet):
pass

View File

@@ -23,7 +23,7 @@ from ..utils import lazyproperty
__all__ = [
'JSONResponseMixin', 'CommonApiMixin', 'AsyncApiMixin', 'RelationMixin',
'SerializerMixin2', 'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin',
'QuerySetMixin', 'ExtraFilterFieldsMixin', 'RenderToJsonMixin',
]
@@ -62,21 +62,27 @@ class RenderToJsonMixin:
class SerializerMixin:
""" 根据用户请求动作的不同,获取不同的 `serializer_class `"""
action: str
request: Request
serializer_classes = None
single_actions = ['put', 'retrieve', 'patch']
def get_serializer_class_by_view_action(self):
if not hasattr(self, 'serializer_classes'):
return None
if not isinstance(self.serializer_classes, dict):
return None
action = self.request.query_params.get('action')
serializer_class = None
if action:
# metadata方法 使用 action 参数获取
serializer_class = self.serializer_classes.get(action)
view_action = self.request.query_params.get('action') or self.action or 'list'
serializer_class = self.serializer_classes.get(view_action)
if serializer_class is None:
serializer_class = self.serializer_classes.get(self.action)
view_method = self.request.method.lower()
serializer_class = self.serializer_classes.get(view_method)
if serializer_class is None and view_action in self.single_actions:
serializer_class = self.serializer_classes.get('single')
if serializer_class is None:
serializer_class = self.serializer_classes.get('display')
if serializer_class is None:
@@ -301,36 +307,18 @@ class RelationMixin:
self.send_m2m_changed_signal(instance, 'post_remove')
class SerializerMixin2:
serializer_classes = {}
def get_serializer_class(self):
if self.serializer_classes:
serializer_class = self.serializer_classes.get(
self.action, self.serializer_classes.get('default')
)
if isinstance(serializer_class, dict):
serializer_class = serializer_class.get(
self.request.method.lower, serializer_class.get('default')
)
assert serializer_class, '`serializer_classes` config error'
return serializer_class
return super().get_serializer_class()
class QuerySetMixin:
def get_queryset(self):
queryset = super().get_queryset()
serializer_class = self.get_serializer_class()
if serializer_class and hasattr(serializer_class, 'setup_eager_loading'):
queryset = serializer_class.setup_eager_loading(queryset)
return queryset
class AllowBulkDestoryMixin:
class AllowBulkDestroyMixin:
def allow_bulk_destroy(self, qs, filtered):
"""
我们规定,批量删除的情况必须用 `id` 指定要删除的数据。

View File

@@ -293,7 +293,14 @@ class EagerLoadQuerySetFields:
class CommonSerializerMixin(DynamicFieldsMixin, DefaultValueFieldsMixin):
pass
instance: None
initial_data: dict
def get_initial_value(self, attr, default=None):
if self.instance:
return getattr(self.instance, attr, default)
else:
return self.initial_data.get(attr)
class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin):

View File

@@ -6,7 +6,7 @@ import socket
import string
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_{}~'
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
def random_datetime(date_start, date_end):

View File

@@ -212,6 +212,10 @@ class Config(dict):
'CAS_ROOT_PROXIED_AS': '',
'CAS_LOGOUT_COMPLETELY': True,
'CAS_VERSION': 3,
'CAS_USERNAME_ATTRIBUTE': 'uid',
'CAS_APPLY_ATTRIBUTES_TO_USER': False,
'CAS_RENAME_ATTRIBUTES': {},
'CAS_CREATE_USER': True,
'AUTH_SSO': False,
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,

View File

@@ -11,11 +11,11 @@ def jumpserver_processor(request):
'DEFAULT_PK': '00000000-0000-0000-0000-000000000000',
'LOGO_URL': static('img/logo.png'),
'LOGO_TEXT_URL': static('img/logo_text.png'),
'LOGIN_IMAGE_URL': static('img/login_image.png'),
'LOGIN_IMAGE_URL': static('img/login_image.jpg'),
'FAVICON_URL': static('img/facio.ico'),
'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'),
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_log.png'),
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_log.png'),
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'),
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'),
'JMS_TITLE': _('JumpServer Open Source Bastion Host'),
'VERSION': settings.VERSION,
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2021',

View File

@@ -96,6 +96,10 @@ CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY
CAS_VERSION = CONFIG.CAS_VERSION
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
CAS_CHECK_NEXT = lambda _next_page: True
CAS_USERNAME_ATTRIBUTE = CONFIG.CAS_USERNAME_ATTRIBUTE
CAS_APPLY_ATTRIBUTES_TO_USER = CONFIG.CAS_APPLY_ATTRIBUTES_TO_USER
CAS_RENAME_ATTRIBUTES = CONFIG.CAS_RENAME_ATTRIBUTES
CAS_CREATE_USER = CONFIG.CAS_CREATE_USER
# SSO Auth
AUTH_SSO = CONFIG.AUTH_SSO
@@ -120,7 +124,7 @@ LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
AUTH_BACKEND_MODEL = 'authentication.backends.api.ModelBackend'
AUTH_BACKEND_MODEL = 'authentication.backends.api.JMSModelBackend'
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend'

View File

@@ -67,6 +67,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django.forms',
'simple_history',
]
@@ -86,6 +87,7 @@ MIDDLEWARE = [
'orgs.middleware.OrgMiddleware',
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
'authentication.backends.cas.middleware.CASMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
]

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -73,7 +73,7 @@ msgid ""
"User list、User group、Asset list、Domain list、Admin user、System user、"
"Labels、Asset permission"
msgstr ""
"用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权"
"用户列表、用户组、资产列表、网域列表、特权用户、系统用户、标签管理、资产授权"
"规则"
#: static/js/jumpserver.js:416

View File

@@ -32,16 +32,6 @@ class MessageType(type):
}
if issubclass(clz, SystemMessage):
system_msgs.append(msg)
try:
if not SystemMsgSubscription.objects.filter(message_type=message_type).exists():
sub = SystemMsgSubscription.objects.create(message_type=message_type)
clz.post_insert_to_db(sub)
except ProgrammingError as e:
if e.args[0] == 1146:
# 表不存在
pass
else:
raise
elif issubclass(clz, UserMessage):
user_msgs.append(msg)

View File

@@ -1,13 +1,20 @@
import json
from importlib import import_module
import inspect
from django.utils.functional import LazyObject
from django.db.models.signals import post_save
from django.db.models.signals import post_migrate
from django.dispatch import receiver
from django.db.utils import DEFAULT_DB_ALIAS
from django.apps import apps as global_apps
from django.apps import AppConfig
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from common.decorator import on_transaction_commit
from .models import SiteMessage
from .models import SiteMessage, SystemMsgSubscription
from .notifications import SystemMessage
logger = get_logger(__name__)
@@ -41,3 +48,37 @@ def on_site_message_create(sender, instance, created, **kwargs):
}
data = json.dumps(data)
new_site_msg_chan.publish(data)
@receiver(post_migrate, dispatch_uid='notifications.signals_handler.create_system_messages')
def create_system_messages(app_config: AppConfig, **kwargs):
try:
notifications_module = import_module('.notifications', app_config.module.__package__)
for name, obj in notifications_module.__dict__.items():
if name.startswith('_'):
continue
if not inspect.isclass(obj):
continue
if not issubclass(obj, SystemMessage):
continue
attrs = obj.__dict__
if 'message_type_label' not in attrs:
continue
if 'category' not in attrs:
continue
if 'category_label' not in attrs:
continue
message_type = obj.get_message_type()
sub, created = SystemMsgSubscription.objects.get_or_create(message_type=message_type)
if created:
obj.post_insert_to_db(sub)
logger.info(f'Create SystemMsgSubscription: package={app_config.module.__package__} type={message_type}')
except ModuleNotFoundError:
pass

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
#
import json
import os
import redis_lock
import redis
@@ -12,6 +11,7 @@ from django_celery_beat.models import (
PeriodicTask, IntervalSchedule, CrontabSchedule, PeriodicTasks
)
from common.utils.timezone import now
from common.utils import get_logger
logger = get_logger(__name__)
@@ -36,6 +36,8 @@ def create_or_update_celery_periodic_tasks(tasks):
for name, detail in tasks.items():
interval = None
crontab = None
last_run_at = None
try:
IntervalSchedule.objects.all().count()
except (ProgrammingError, OperationalError):
@@ -50,6 +52,7 @@ def create_or_update_celery_periodic_tasks(tasks):
interval = IntervalSchedule.objects.filter(**kwargs).first()
if interval is None:
interval = IntervalSchedule.objects.create(**kwargs)
last_run_at = now()
elif isinstance(detail.get("crontab"), str):
try:
minute, hour, day, month, week = detail["crontab"].split()
@@ -75,7 +78,8 @@ def create_or_update_celery_periodic_tasks(tasks):
enabled=detail.get('enabled', True),
args=json.dumps(detail.get('args', [])),
kwargs=json.dumps(detail.get('kwargs', {})),
description=detail.get('description') or ''
description=detail.get('description') or '',
last_run_at=last_run_at,
)
task = PeriodicTask.objects.update_or_create(
defaults=defaults, name=name,

View File

@@ -31,7 +31,11 @@ class JMSBaseInventory(BaseInventory):
if run_as_admin:
info.update(asset.get_auth_info())
if asset.is_unixlike():
info["become"] = asset.admin_user.become_info
info["become"] = {
"method": 'sudo',
"user": 'root',
"pass": ''
}
if asset.is_windows():
info["vars"].update({
"ansible_connection": "ssh",
@@ -103,8 +107,6 @@ class JMSInventory(JMSBaseInventory):
super().__init__(host_list=host_list)
def get_run_user_info(self, host):
from assets.backends import AssetUserManager
if not self.run_as and not self.system_user:
return {}
@@ -112,17 +114,12 @@ class JMSInventory(JMSBaseInventory):
asset = self.assets.filter(id=asset_id).first()
if not asset:
logger.error('Host not found: ', asset_id)
return {}
if self.system_user:
self.system_user.load_asset_special_auth(asset=asset, username=self.run_as)
return self.system_user._to_secret_json()
try:
manager = AssetUserManager()
run_user = manager.get_latest(username=self.run_as, asset=asset, prefer='system_user')
return run_user._to_secret_json()
except Exception as e:
logger.error(e, exc_info=True)
else:
return {}

View File

@@ -0,0 +1,20 @@
# Generated by Django 3.1.6 on 2021-07-14 08:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0076_delete_assetuser'),
('ops', '0019_adhocexecution_celery_task_id'),
]
operations = [
migrations.AddField(
model_name='adhoc',
name='run_system_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser'),
),
]

View File

@@ -146,6 +146,7 @@ class AdHoc(OrgModelMixin):
hosts = models.ManyToManyField('assets.Asset', verbose_name=_("Host"))
run_as_admin = models.BooleanField(default=False, verbose_name=_('Run as admin'))
run_as = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Username'))
run_system_user = models.ForeignKey('assets.SystemUser', null=True, on_delete=models.CASCADE)
become = EncryptJsonDictCharField(max_length=1024, default='', blank=True, null=True, verbose_name=_("Become"))
created_by = models.CharField(max_length=64, default='', blank=True, null=True, verbose_name=_('Create by'))
date_created = models.DateTimeField(auto_now_add=True, db_index=True)
@@ -167,7 +168,7 @@ class AdHoc(OrgModelMixin):
inventory = JMSInventory(
self.hosts.all(), run_as_admin=self.run_as_admin,
run_as=self.run_as, become_info=become_info
run_as=self.run_as, become_info=become_info, system_user=self.run_system_user
)
return inventory
@@ -286,18 +287,12 @@ class AdHocExecution(OrgModelMixin):
raw = ''
try:
date_start_s = timezone.now().now().strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Start task: {}").format(date_start_s, self.task.name))
raw, summary = self.start_runner()
except Exception as e:
logger.error(e, exc_info=True)
raw = {"dark": {"all": str(e)}, "contacted": []}
finally:
self.clean_up(summary, time_start)
date_end = timezone.now().now()
date_end_s = date_end.strftime('%Y-%m-%d %H:%M:%S')
print(_("{} Task finish").format(date_end_s))
print('.\n\n.')
return raw, summary
def clean_up(self, summary, time_start):

View File

@@ -86,8 +86,10 @@ class CommandExecution(OrgModelMixin):
host = self.hosts.first()
if host and host.is_windows():
shell = 'win_shell'
else:
elif host and host.is_unixlike():
shell = 'shell'
else:
shell = 'raw'
result = runner.execute(self.command, 'all', module=shell)
self.result = result.results_command
except SoftTimeLimitExceeded as e:

View File

@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from notifications.notifications import SystemMessage
from notifications.models import SystemMsgSubscription
from users.models import User
from notifications.backends import BACKEND
__all__ = ('ServerPerformanceMessage',)
@@ -24,3 +25,5 @@ class ServerPerformanceMessage(SystemMessage):
def post_insert_to_db(cls, subscription: SystemMsgSubscription):
admins = User.objects.filter(role=User.ROLE.ADMIN)
subscription.users.add(*admins)
subscription.receive_backends = [BACKEND.EMAIL]
subscription.save()

View File

@@ -28,7 +28,7 @@ def update_or_create_ansible_task(
task_name, hosts, tasks,
interval=None, crontab=None, is_periodic=False,
callback=None, pattern='all', options=None,
run_as_admin=False, run_as=None, become_info=None,
run_as_admin=False, run_as=None, system_user=None, become_info=None,
):
if not hosts or not tasks or not task_name:
return None, None
@@ -49,7 +49,7 @@ def update_or_create_ansible_task(
adhoc = task.get_latest_adhoc()
new_adhoc = AdHoc(task=task, pattern=pattern,
run_as_admin=run_as_admin,
run_as=run_as)
run_as=run_as, run_system_user=system_user)
new_adhoc.tasks = tasks
new_adhoc.options = options
new_adhoc.become = become_info

View File

@@ -38,7 +38,7 @@ class OrgRelatedCache(Cache):
在事务提交之后再发送信号,防止因事务的隔离性导致未获得最新的数据
"""
def func():
logger.info(f'CACHE: Send refresh task {self}.{fields}')
logger.debug(f'CACHE: Send refresh task {self}.{fields}')
refresh_org_cache_task.delay(self, *fields)
on_commit(func)
@@ -54,8 +54,8 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
assets_amount = IntegerField()
nodes_amount = IntegerField(queryset=Node.objects)
admin_users_amount = IntegerField(queryset=AdminUser.objects)
system_users_amount = IntegerField(queryset=SystemUser.objects)
admin_users_amount = IntegerField(queryset=SystemUser.objects.filter(type=SystemUser.Type.admin))
system_users_amount = IntegerField(queryset=SystemUser.objects.filter(type=SystemUser.Type.common))
domains_amount = IntegerField(queryset=Domain.objects)
gateways_amount = IntegerField(queryset=Gateway.objects)
@@ -93,7 +93,7 @@ class OrgResourceStatisticsCache(OrgRelatedCache):
return node.assets_amount
def compute_total_count_online_users(self):
return len(set(Session.objects.filter(is_finished=False).values_list('user_id', flat=True)))
return Session.objects.filter(is_finished=False).values_list('user_id').distinct().count()
def compute_total_count_online_sessions(self):
return Session.objects.filter(is_finished=False).count()

View File

@@ -9,10 +9,10 @@ from django.utils.translation import ugettext_lazy as _
from common.utils import lazyproperty, settings
from common.const import choices
from common.db.models import ChoiceSet
from common.db.models import TextChoices
class ROLE(ChoiceSet):
class ROLE(TextChoices):
ADMIN = choices.ADMIN, _('Organization administrator')
AUDITOR = choices.AUDITOR, _("Organization auditor")
USER = choices.USER, _('User')

View File

@@ -4,7 +4,6 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from users.models.user import User
from common.drf.serializers import AdaptedBulkListSerializer
from common.drf.serializers import BulkModelSerializer
from common.db.models import concated_display as display
from .models import Organization, OrganizationMember, ROLE
@@ -35,7 +34,6 @@ class OrgSerializer(ModelSerializer):
class Meta:
model = Organization
list_serializer_class = AdaptedBulkListSerializer
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'resource_statistics',

View File

@@ -1,5 +1,5 @@
from django.db.models.signals import m2m_changed
from django.db.models.signals import post_save, pre_delete, pre_save
from django.db.models.signals import post_save, pre_delete, pre_save, post_delete
from django.dispatch import receiver
from orgs.models import Organization, OrganizationMember
@@ -55,12 +55,10 @@ class OrgResourceStatisticsRefreshUtil:
Application: ['applications_amount'],
Gateway: ['gateways_amount'],
Domain: ['domains_amount'],
SystemUser: ['system_users_amount'],
AdminUser: ['admin_users_amount'],
SystemUser: ['system_users_amount', 'admin_users_amount'],
Node: ['nodes_amount'],
Asset: ['assets_amount'],
UserGroup: ['groups_amount'],
Session: ['total_count_online_users', 'total_count_online_sessions']
}
@classmethod
@@ -72,11 +70,40 @@ class OrgResourceStatisticsRefreshUtil:
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
@receiver(pre_save)
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, **kwargs):
@receiver(post_save)
def on_post_save_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs):
if created:
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
@receiver(post_delete)
def on_post_delete_refresh_org_resource_statistics_cache(sender, instance, **kwargs):
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
@receiver(pre_delete)
def on_pre_delete_refresh_org_resource_statistics_cache(sender, instance, **kwargs):
OrgResourceStatisticsRefreshUtil.refresh_if_need(instance)
def _refresh_session_org_resource_statistics_cache(instance: Session):
cache_field_name = ['total_count_online_users', 'total_count_online_sessions']
org_cache = OrgResourceStatisticsCache(instance.org)
org_cache.expire(*cache_field_name)
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
@receiver(pre_save, sender=Session)
def on_session_pre_save(sender, instance: Session, **kwargs):
old = Session.objects.filter(id=instance.id).values_list('is_finished', flat=True)
if old:
instance._signal_old_is_finished = old[0]
else:
instance._signal_old_is_finished = None
@receiver(post_save, sender=Session)
def on_session_changed_refresh_org_resource_statistics_cache(sender, instance, created, **kwargs):
if created or instance.is_finished != instance._signal_old_is_finished:
_refresh_session_org_resource_statistics_cache(instance)
@receiver(post_delete, sender=Session)
def on_session_deleted_refresh_org_resource_statistics_cache(sender, instance, **kwargs):
_refresh_session_org_resource_statistics_cache(instance)

View File

@@ -15,6 +15,7 @@ from orgs.hands import set_current_org, Node, get_current_org
from perms.models import (AssetPermission, ApplicationPermission)
from users.models import UserGroup, User
from common.const.signals import PRE_REMOVE, POST_REMOVE
from common.decorator import on_transaction_commit
from common.signals import django_ready
from common.utils import get_logger
from common.utils.connection import RedisPubSub
@@ -35,8 +36,8 @@ class OrgsMappingForMemoryPubSub(LazyObject):
orgs_mapping_for_memory_pub_sub = OrgsMappingForMemoryPubSub()
def expire_orgs_mapping_for_memory():
orgs_mapping_for_memory_pub_sub.publish('expire_orgs_mapping')
def expire_orgs_mapping_for_memory(org_id):
orgs_mapping_for_memory_pub_sub.publish(str(org_id))
@receiver(django_ready)
@@ -53,7 +54,7 @@ def subscribe_orgs_mapping_expire(sender, **kwargs):
if message['data'] == b'error':
raise ValueError
Organization.expire_orgs_mapping()
logger.debug('Expire orgs mapping')
logger.debug('Expire orgs mapping: ' + str(message['data']))
except Exception as e:
logger.exception(f'subscribe_orgs_mapping_expire: {e}')
Organization.expire_orgs_mapping()
@@ -64,22 +65,21 @@ def subscribe_orgs_mapping_expire(sender, **kwargs):
@receiver(post_save, sender=Organization)
def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
def on_org_create_or_update(sender, instance, created=False, **kwargs):
# 必须放到最开始, 因为下面调用Node.save方法时会获取当前组织的org_id(即instance.org_id), 如果不过期会找不到
expire_orgs_mapping_for_memory()
if instance:
old_org = get_current_org()
set_current_org(instance)
node_root = Node.org_root()
if node_root.value != instance.name:
node_root.value = instance.name
node_root.save()
set_current_org(old_org)
expire_orgs_mapping_for_memory(instance.id)
old_org = get_current_org()
set_current_org(instance)
node_root = Node.org_root()
if node_root.value != instance.name:
node_root.value = instance.name
node_root.save()
set_current_org(old_org)
@receiver(post_delete, sender=Organization)
def on_org_delete(sender, **kwargs):
expire_orgs_mapping_for_memory()
@receiver(pre_delete, sender=Organization)
def on_org_delete(sender, instance, **kwargs):
expire_orgs_mapping_for_memory(instance.id)
@receiver(pre_delete, sender=Organization)
@@ -167,3 +167,13 @@ def on_org_user_changed(action, instance, reverse, pk_set, **kwargs):
leaved_users = set(pk_set) - set(org.members.filter(id__in=user_pk_set).values_list('id', flat=True))
_clear_users_from_org(org, leaved_users)
@receiver(post_save, sender=User)
@on_transaction_commit
def on_user_created_set_default_org(sender, instance, created, **kwargs):
if not created:
return
if instance.orgs.count() > 0:
return
Organization.default().members.add(instance)

View File

@@ -4,7 +4,7 @@ from functools import reduce
from django.utils.translation import ugettext_lazy as _
from django.db.models import F
from common.db.models import ChoiceSet
from common.db.models import TextChoices
from orgs.mixins.models import OrgModelMixin
from common.db import models
from common.utils import lazyproperty
@@ -165,7 +165,7 @@ class AssetPermission(BasePermission):
class UserAssetGrantedTreeNodeRelation(OrgModelMixin, FamilyMixin, models.JMSBaseModel):
class NodeFrom(ChoiceSet):
class NodeFrom(TextChoices):
granted = 'granted', 'Direct node granted'
child = 'child', 'Have children node'
asset = 'asset', 'Direct asset granted'

View File

@@ -3,7 +3,6 @@
from rest_framework import serializers
from common.mixins import BulkSerializerMixin
from common.drf.serializers import AdaptedBulkListSerializer
from perms.models import ApplicationPermission
__all__ = [
@@ -24,14 +23,11 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer):
fields.extend(['applicationpermission', "applicationpermission_display"])
return fields
class Meta:
list_serializer_class = AdaptedBulkListSerializer
class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = ApplicationPermission.users.through
fields = [
'id', 'user', 'user_display',
@@ -41,7 +37,7 @@ class ApplicationPermissionUserRelationSerializer(RelationMixin, serializers.Mod
class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer):
usergroup_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = ApplicationPermission.user_groups.through
fields = [
'id', 'usergroup', "usergroup_display",
@@ -51,7 +47,7 @@ class ApplicationPermissionUserGroupRelationSerializer(RelationMixin, serializer
class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializers.ModelSerializer):
application_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = ApplicationPermission.applications.through
fields = [
'id', "application", "application_display",
@@ -61,7 +57,7 @@ class ApplicationPermissionApplicationRelationSerializer(RelationMixin, serializ
class ApplicationPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
systemuser_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = ApplicationPermission.system_users.through
fields = [
'id', 'systemuser', 'systemuser_display'

View File

@@ -3,7 +3,6 @@
from rest_framework import serializers
from common.mixins import BulkSerializerMixin
from common.drf.serializers import AdaptedBulkListSerializer
from assets.models import Asset, Node
from perms.models import AssetPermission
from users.models import User
@@ -37,14 +36,11 @@ class RelationMixin(BulkSerializerMixin, serializers.Serializer):
fields.extend(['assetpermission', "assetpermission_display"])
return fields
class Meta:
list_serializer_class = AdaptedBulkListSerializer
class AssetPermissionUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = AssetPermission.users.through
fields = [
'id', 'user', 'user_display',
@@ -66,7 +62,7 @@ class AssetPermissionAllUserSerializer(serializers.Serializer):
class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.ModelSerializer):
usergroup_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = AssetPermission.user_groups.through
fields = [
'id', 'usergroup', "usergroup_display",
@@ -76,7 +72,7 @@ class AssetPermissionUserGroupRelationSerializer(RelationMixin, serializers.Mode
class AssetPermissionAssetRelationSerializer(RelationMixin, serializers.ModelSerializer):
asset_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = AssetPermission.assets.through
fields = [
'id', "asset", "asset_display",
@@ -98,7 +94,7 @@ class AssetPermissionAllAssetSerializer(serializers.Serializer):
class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSerializer):
node_display = serializers.CharField(source='node.full_value', read_only=True)
class Meta(RelationMixin.Meta):
class Meta:
model = AssetPermission.nodes.through
fields = [
'id', 'node', "node_display",
@@ -108,7 +104,7 @@ class AssetPermissionNodeRelationSerializer(RelationMixin, serializers.ModelSeri
class AssetPermissionSystemUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
systemuser_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = AssetPermission.system_users.through
fields = [
'id', 'systemuser', 'systemuser_display'

View File

@@ -16,6 +16,17 @@ from perms.utils.asset.user_permission import UserGrantedTreeRefreshController
logger = get_logger(__file__)
@receiver(pre_delete, sender=UserGroup)
def on_user_group_delete(sender, instance: UserGroup, using, **kwargs):
exists = AssetPermission.user_groups.through.objects.filter(usergroup_id=instance.id).exists()
if not exists:
return
org_id = instance.org_id
user_ids = UserGroup.users.through.objects.filter(usergroup_id=instance.id).values_list('user_id', flat=True)
UserGrantedTreeRefreshController.add_need_refresh_orgs_for_users([org_id], list(user_ids))
@receiver(m2m_changed, sender=User.groups.through)
def on_user_groups_change(sender, instance, action, reverse, pk_set, **kwargs):
if not action.startswith('post'):

View File

@@ -15,7 +15,7 @@ logger = get_logger(__file__)
@register_as_period_task(interval=settings.PERM_EXPIRED_CHECK_PERIODIC)
@shared_task(queue='celery_check_asset_perm_expired')
@shared_task()
@atomic()
def check_asset_permission_expired():
"""

View File

@@ -81,7 +81,7 @@ class PublicSettingApi(generics.RetrieveAPIView):
logo_urls = {
'logo_logout': static('img/logo.png'),
'logo_index': static('img/logo_text.png'),
'login_image': static('img/login_image.png'),
'login_image': static('img/login_image.jpg'),
'favicon': static('img/facio.ico')
}
if not settings.XPACK_ENABLED:

View File

@@ -7,8 +7,7 @@ from collections.abc import Iterable
from smtplib import SMTPSenderRefused
from rest_framework import generics
from rest_framework.views import Response, APIView
from django.conf import settings
from django.core.mail import send_mail, get_connection
from orgs.models import Organization
from django.utils.translation import ugettext_lazy as _
from ..utils import (
@@ -17,11 +16,12 @@ from ..utils import (
)
from ..tasks import sync_ldap_user
from common.permissions import IsOrgAdmin, IsSuperUser
from common.utils import get_logger
from common.utils import get_logger, is_uuid
from ..serializers import (
MailTestSerializer, LDAPTestConfigSerializer, LDAPUserSerializer,
PublicSettingSerializer, LDAPTestLoginSerializer, SettingsSerializer
)
from orgs.utils import current_org
from users.models import User
logger = get_logger(__file__)
@@ -170,6 +170,14 @@ class LDAPUserListApi(generics.ListAPIView):
class LDAPUserImportAPI(APIView):
permission_classes = (IsSuperUser,)
def get_org(self):
org_id = self.request.data.get('org_id')
if is_uuid(org_id):
org = Organization.objects.get(id=org_id)
else:
org = current_org
return org
def get_ldap_users(self):
username_list = self.request.data.get('username_list', [])
cache_police = self.request.query_params.get('cache_police', True)
@@ -188,12 +196,15 @@ class LDAPUserImportAPI(APIView):
if users is None:
return Response({'msg': _('Get ldap users is None')}, status=400)
errors = LDAPImportUtil().perform_import(users)
org = self.get_org()
errors = LDAPImportUtil().perform_import(users, org)
if errors:
return Response({'errors': errors}, status=400)
count = users if users is None else len(users)
return Response({'msg': _('Imported {} users successfully').format(count)})
return Response({
'msg': _('Imported {} users successfully (Organization: {})').format(count, org)
})
class LDAPCacheRefreshAPI(generics.RetrieveAPIView):

View File

@@ -362,20 +362,19 @@ class LDAPImportUtil(object):
)
return obj, created
def perform_import(self, users):
def perform_import(self, users, org=None):
logger.info('Start perform import ldap users, count: {}'.format(len(users)))
errors = []
instances = []
objs = []
for user in users:
try:
obj, created = self.update_or_create(user)
if created:
instances.append(obj)
objs.append(obj)
except Exception as e:
errors.append({user['username']: str(e)})
logger.error(e)
# 默认添加用户到 Default 组织
Organization.default().members.add(*instances)
if org and not org.is_root():
org.members.add(*objs)
logger.info('End perform import ldap users')
return errors

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Some files were not shown because too many files have changed in this diff Show More