Compare commits

...

322 Commits

Author SHA1 Message Date
xinwen
eba7a7288a fix: 修复 test cookie 问题,删掉key=0迁移脚本 2021-04-09 03:13:56 -05:00
xinwen
bd11018cef fix: 管理用户输入带密码的秘钥报错 2021-04-06 19:45:40 +08:00
xinwen
8a2b0a7845 fix: 修正 key 为 0 的节点 2021-04-06 18:27:18 +08:00
xinwen
b3dbdd5389 fix: revert 一些代码 2021-03-30 10:44:48 +08:00
xinwen
a4643fe4b4 fix: Default 组织下出现 app user 2021-03-30 10:23:13 +08:00
xinwen
dce9b50ac0 feat: 增加 es 忽略 https 证书验证 2021-03-30 10:23:13 +08:00
xinwen
e2b7c902a5 perf: delete_test_cookie 2021-03-30 10:23:13 +08:00
ibuler
4055306cf8 perf: 修改表结构迁移,增加rdp terminal 2021-03-24 10:24:09 +08:00
xinwen
9e1c5bb64d fix: 授权树节点排序 2021-03-24 10:13:45 +08:00
ibuler
32df722e9d perf: 合并 2021-03-23 18:46:30 +08:00
ibuler
22f6f5c34c perf: session add rdp terminal login from 2021-03-23 18:31:42 +08:00
ibuler
6fb819ca53 perf: 优化登录ip限制提示 2021-03-23 18:31:24 +08:00
老广
043c4a7a0b Merge pull request #5813 from jumpserver/master
v2.8.1
2021-03-19 20:05:29 +08:00
老广
61d4311e24 Merge pull request #5808 from jumpserver/dev
Dev
2021-03-19 20:01:03 +08:00
xinwen
370e1628be fix: 禁用的资产限制访问 2021-03-19 20:00:25 +08:00
xinwen
adf5c4a7b9 fix: LDAP 自动创建的用户有多余的空格 2021-03-19 17:39:06 +08:00
Bai
9fc1ae7b6d perf: 修改翻译 2021-03-19 14:45:05 +08:00
xinwen
313757dbe9 fix: 修复用户与用户组关系变化时 500 2021-03-19 14:33:39 +08:00
Jiangjie.Bai
b32e352b24 Merge pull request #5799 from jumpserver/dev
v2.8 发版
2021-03-18 21:04:37 +08:00
Bai
b950b48112 fix: 隐藏azure-sdk的日志 2021-03-18 21:03:35 +08:00
Jiangjie.Bai
519eb3bef2 Merge pull request #5797 from jumpserver/dev
v2.8 发版
2021-03-18 20:41:28 +08:00
Bai
4e55d0f1e4 添加依赖: (azure-identity==1.5.0)(azure-mgmt-subscription==1.0.0) 2021-03-18 20:30:59 +08:00
Jiangjie.Bai
2b3bb65114 Merge pull request #5794 from jumpserver/dev
v2.8 发版 (rc6)
2021-03-18 17:39:02 +08:00
xinwen
b597cfcd19 fix: 修复一些 Root 组织没数据的问题 2021-03-18 17:25:11 +08:00
xinwen
33952b2333 fix: 有无效的 ES 时,创建 Session 失败 2021-03-18 14:34:00 +08:00
xinwen
a47a9c0345 fix: Root 组织用户从用户组移除报错 2021-03-18 10:53:31 +08:00
xinwen
4e0c056867 fix: 日志审计-> 批量命令 全局组织无数据 2021-03-18 10:42:52 +08:00
ibuler
a9b5599db5 perf: 更换登录页图片 2021-03-18 10:30:23 +08:00
ibuler
8a2eb70ad2 revert: 还原api限制 2021-03-18 10:25:40 +08:00
老广
776234e8cc Merge pull request #5785 from jumpserver/dev
v2.8 发版 (rc4)
2021-03-17 20:15:31 +08:00
ibuler
e2406955bc perf: 优化license判断
perf: 优化license 过期
2021-03-17 18:47:38 +08:00
Bai
dba9550bc0 perf: 修改翻译 2021-03-17 18:25:39 +08:00
xinwen
6ad1362a3f perf: 批量命令日志导出增加 hosts_display 字段 2021-03-17 18:16:19 +08:00
Bai
dfa2f7d6c9 fix: 修复用户登录复核工单org_id为ROOT 2021-03-17 17:23:39 +08:00
ibuler
c55e2db75e perf: 优化license判断 2021-03-17 17:18:39 +08:00
xinwen
fd3a4d887e fix: Root 组织查不到命令记录 2021-03-17 17:17:15 +08:00
ibuler
42afc1e0bf fix(perms): 修复删除 资产或授权引起的bug 2021-03-17 17:15:38 +08:00
ibuler
50c89431df perf: 修复ansible summary显示两遍的问题 2021-03-17 17:13:07 +08:00
xinwen
f1f5017be3 fix: 无效的 ES 会卡住 2021-03-17 17:09:52 +08:00
ibuler
9b85aafa52 perf: 全局组织仅支持删除和查看 2021-03-17 17:09:11 +08:00
xinwen
817268d7cd perf: 资产与节点变化的时候,优化发送推送系统用户信号 2021-03-17 17:04:53 +08:00
ibuler
d3bbfdc458 perf: 不修改原来的默认配置 2021-03-17 17:02:44 +08:00
Bai
18a390d66a perf: 修改迁移文件(default_org_name: DEFAULT => Default) 2021-03-17 15:22:21 +08:00
老广
73b57a662e Merge pull request #5766 from jumpserver/dev
v2.8 发版 (rc4)
2021-03-16 20:49:04 +08:00
ibuler
ea325f6e52 perf(users): 优化用户认证来源 2021-03-16 20:48:00 +08:00
Bai
1216f15e45 fix: 修复新旧版本对于default_node节点变更冲突的问题(旧版本会将新版本迁移后的default_node节点的key修改为非1) 2021-03-16 20:45:50 +08:00
ibuler
cc3911d2f1 fix: 修复 user profile all orgs 的bug 2021-03-16 20:30:46 +08:00
xinwen
36c083f674 fix: 会话里查不到命令记录 2021-03-16 20:29:05 +08:00
xinwen
98c6a93658 fix: 任务应该分组织 2021-03-16 19:26:13 +08:00
Bai
adc607dafe fix: 修复用户角色由组织用户->组织管理员时从组织清除用户 2021-03-16 19:21:01 +08:00
xinwen
1e85805ea3 fix: 用户授权资产过滤失效 2021-03-16 19:19:36 +08:00
老广
957d3660ce Merge pull request #5754 from jumpserver/dev
v2.8 发版 (rc3)
2021-03-15 19:59:31 +08:00
xinwen
049f6dca67 fix: 删除组织时,确保没有跟节点之外的其他节点。以及组织删除后,将跟节点删除 2021-03-15 19:58:46 +08:00
ibuler
7f4377b0e8 fix(auditor): 修复审计员无法访问命令列表的bug 2021-03-15 19:29:47 +08:00
ibuler
7dfd0ee8fe fix(orgs): 修复访问 current org api 错误
perf(users): 优化用户删除和移除行为

perf: 优化组织权限判断
2021-03-15 19:29:21 +08:00
xinwen
41f375a4f7 fix: 修正多个 DEFAULT 节点 2021-03-15 19:09:29 +08:00
Bai
a50dfe9c18 perf: 管理用户创建/更新username字段设置为required 2021-03-15 19:05:47 +08:00
Bai
bd8a1a7d0e fix: 修复MFA绑定失败的问题(通过修改session中auth_backend的key实现;django.auth.get_user时校验backends路径失败返回AnonymousUser) 2021-03-15 19:05:10 +08:00
Bai
5546719712 fix: 修改get_instance逻辑;二次构建org_mapping;订阅失效速度慢于读取速度; 2021-03-15 11:40:54 +08:00
xinwen
068b39d922 perf: 优化 jms 脚本 2021-03-15 10:19:29 +08:00
Jiangjie.Bai
2e1763cce7 Merge pull request #5739 from jumpserver/dev
v2.8 发版 (2)
2021-03-12 20:56:23 +08:00
xinwen
ff9e470ce2 fix: 命令执行 500 2021-03-12 19:17:06 +08:00
xinwen
3080bf3647 fix: 修复获取整个授权树缺少节点 bug 2021-03-12 19:12:18 +08:00
Jiangjie.Bai
0b04821794 Merge pull request #5736 from jumpserver/dev
v2.8 发版 (2)
2021-03-12 18:11:25 +08:00
Bai
296bb88834 fix: 修改celery健康检测worker数量 2021-03-12 18:09:47 +08:00
Bai
c57cce8881 perf: 修改system_user.priority默认值为81 2021-03-12 18:07:52 +08:00
Jiangjie.Bai
174cc16980 Merge pull request #5728 from jumpserver/dev
v2.8 发版
2021-03-11 21:17:39 +08:00
fit2bot
5b2649f775 fix: 修改用户序列类read_only字段 (#5729)
* fix: 修改用户序列类read_only字段

Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
Co-authored-by: Bai <bugatti_it@163.com>
2021-03-11 21:15:45 +08:00
xinwen
83829df70c fix: 干掉 dockerfile 行内注释 2021-03-11 20:31:38 +08:00
Jiangjie.Bai
64641a18e6 feat: ACL (#5696)
* feature: acl (v0.1)

* feature: acl (v0.2)

* feature: acl (v0.3)

* feature: acl (v0.4)

* feature: acl (v0.5)

* feature: acl (v0.6)

* feature: acl (v0.7)

* feature: acl (v0.8)

* feature: acl (v0.9)

* feature: acl (v1.0)

* feature: acl (v1.1)

* feature: acl (v1.2)

* feature: acl (v1.3)

* feature: acl (v1.4)

* feature: acl (v1.5)

* feature: acl (v1.6)

* feature: acl (v1.7)

* feature: acl (v1.8)

* feature: acl (v1.9)

* feature: acl (v2.0)

* feature: acl (v2.1)

* feature: acl (v2.2)

* feature: acl (v2.3)

* feature: acl (v2.4)

* feature: acl (v2.5)

* feature: acl (v2.6)

* feature: acl (v2.7)

* feature: acl (v2.8)

* feature: acl (v2.9)

* feature: acl (v3.0)

* feature: acl (v3.1)

* feature: acl (v3.2)

* feature: acl (v3.3)

* feature: acl (v3.4)

* feature: acl (v3.5)

* feature: acl (v3.6)

* feature: acl (v3.7)

* feature: acl (v3.8)

* feature: acl (v3.9)

* feature: acl (v4.0)

* feature: acl (v4.1)

* feature: acl (v4.2)

* feature: acl (v4.3)

* feature: acl (v4.4)
2021-03-11 20:17:44 +08:00
ibuler
09303ecc56 perf: 优化各serializer字段翻译 2021-03-11 20:15:27 +08:00
xinwen
5f48e7aeb2 perf: Dockerfile 安装软件包时添加 rm -rf /var/lib/apt/lists/* 清理 2021-03-11 20:13:41 +08:00
ibuler
25dfce621b perf: 优化,添加登录页面图片链接到 github 2021-03-11 18:45:50 +08:00
xinwen
102d3b590b perf: luna 页面获取资产系统用户按 name 排序 2021-03-11 15:07:06 +08:00
fit2bot
a45f581b0e fix: 将 user 添加到 default 组织时进度数量显示不准确 (#5720)
Co-authored-by: xinwen <coderWen@126.com>
2021-03-11 11:09:47 +08:00
ibuler
b3991d0388 fix: 修复Migrate的问题
fix: 修复 org migrations 依赖
2021-03-11 10:59:13 +08:00
xinwen
184e8b31e6 perf: 优化下 orgid_nodekey_assetsid_mapping 2021-03-10 18:15:49 +08:00
xinwen
615bcadf62 fix: 资产授权规则过滤添加 distinct 2021-03-10 17:51:31 +08:00
fit2bot
7b2f813e7f feat: 支持修改忘记密码重置密码的连接 (#5700)
perf: 优化代码暗示

Co-authored-by: ibuler <ibuler@qq.com>
2021-03-10 11:21:12 +08:00
ibuler
81170b4b7b perf: 优化登录页面,非常给力
perf: 优化报错

perf: 优化忘记密码

perf: 添加注释
2021-03-10 10:49:11 +08:00
xinwen
c4eacbabc6 refactor: 重构缓存框架 2021-03-10 10:33:45 +08:00
xinwen
ccb0509d85 feat: 批量导入解析为Json的接口添加 title 字段 2021-03-10 10:14:06 +08:00
xinwen
886393c539 feat: 添加批量导入时将其他格式(csv, excel)解析为Json的接口 2021-03-09 14:24:13 +08:00
xinwen
15b0ad9c12 refactor: 清理代码 orgs.mixins.api.OrgMembershipModelViewSetMixin 2021-03-09 12:44:28 +08:00
wojiushixiaobai
19e2a5b9f9 fix(terminal): 修正录像接口返回状态码 2021-03-08 10:14:01 +08:00
fit2bot
0aa2c2016f perf(project): 优化命名的风格 (#5693)
perf: 修改错误的地

perf: 优化写错的几个

Co-authored-by: ibuler <ibuler@qq.com>
2021-03-08 10:08:51 +08:00
xinwen
935947c97a fix: 用户详情页的资产授权列表慢 2021-03-08 07:56:58 +08:00
xinwen
3e7e01418d perf: 优化命令记录慢的问题 2021-03-08 07:56:28 +08:00
xinwen
7f42e59714 fix: 修复生成假数据脚本错误 2021-03-05 15:59:38 +08:00
xinwen
840e5e8863 fix: 修复 default 组织迁移脚本问题 2021-03-05 15:56:08 +08:00
xinwen
24fb8b2a89 feat: 管理用户详情页添加认证方式与秘钥指纹 2021-03-05 15:26:14 +08:00
Bai
c1bf854824 perf: 添加依赖包(pyvmomi==7.0.1)(termcolor==1.1.0) 2021-03-04 17:12:51 +08:00
xinwen
ab23a357f7 feat: 推送动态系统用户,系统上用户的 comment 字段是 userdisplayname 2021-03-04 11:28:24 +08:00
xinwen
78bf6f5817 refactor: 获取授权树或者资产列表时避免读时锁 2021-03-04 11:26:54 +08:00
ibuler
91a26abf9e perf: 优化default节点 2021-03-04 11:26:37 +08:00
ibuler
d7e7c62c7a perf: 优化表结构迁移 2021-03-04 10:42:57 +08:00
xinwen
09bdff4a67 fix: 缓存框架 expire_fields 可能报错 2021-03-04 10:40:29 +08:00
Bai
56328e112a perf: 移除资源创建时对于Auditor用户的限制 2021-03-03 15:57:13 +08:00
Bai
1d15f7125e perf: 优化org获取逻辑 - 采用redis订阅机制实现orgs_mapping数据的维护;删除get_org_by_id等方法;
perf: 优化get_instance接口
2021-03-03 13:19:37 +08:00
ibuler
e6b17da57d perf: 去掉pycrypto库
perf: 显示添加pycryptodome
2021-03-03 11:48:45 +08:00
xinwen
1870fc97d5 refactor: 适配新的 default 组织 2021-03-03 10:52:09 +08:00
fit2bot
f548b4bd2b feat: serializer 添加默认值,前端可以调用 (#5666)
perf: 优化默认值

Co-authored-by: ibuler <ibuler@qq.com>
2021-03-02 19:18:25 +08:00
fit2bot
a56ac7b34e perf(orgs): 默认组织改为实体组织,并支持全局组织 (#5617)
* perf(orgs): 默认组织改为实体组织

* perf: 添加获取当前组织信息的api

* perf: 资产列表在 root 组织下的表现

* fix: 修复 root 组织引起的问题

* perf: 优化OrgModelMixin save; org_root获取; org_roles获取; UserCanUseCurrentOrg权限类

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <bugatti_it@163.com>
2021-03-02 14:57:48 +08:00
fit2bot
51c9a89b1f fix: 用户页面授权资产列表获取系统用户慢 (#5663)
Co-authored-by: xinwen <coderWen@126.com>
2021-03-02 14:48:47 +08:00
fit2bot
6f3ead3c42 perf: 优化系统用户生成密码的复杂度 (#5648)
* perf: 优化系统用户生成密码的复杂度

* perf: 修改 common.random_string

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <bugatti_it@163.com>
2021-03-01 18:40:07 +08:00
xinwen
1036d1c132 fix: 修复授权树一些问题 2021-03-01 18:09:41 +08:00
ibuler
5de5fa2e96 fix: 修复获取不到 org 的问题 2021-03-01 14:35:44 +08:00
ibuler
19043d0a66 perf: 添加 xrdp type 2021-03-01 14:34:08 +08:00
ibuler
bc3e50a529 fix: 修复代码更改引起的bug 2021-03-01 14:32:30 +08:00
fit2bot
a7ab7da61c feat: 添加限制用户只能从source登录的功能 (#5592)
* stash it

* feat: 添加限制用户只能从source登录的功能

* fix: 修复小错误

Co-authored-by: ibuler <ibuler@qq.com>
2021-02-26 17:33:11 +08:00
fit2bot
b483f78d52 fix(assets): 系统用户支持 OPENSSH 格式的私钥 (#5604)
* fix(assets): 系统用户支持 OPENSSH 格式的私钥

* fix: 升级paramiko

Co-authored-by: ibuler <ibuler@qq.com>
2021-02-26 16:34:15 +08:00
ibuler
88d8a3326f perf(ops): 优化定期检查磁盘,添加开关控制 2021-02-26 16:33:39 +08:00
ibuler
8f7dcd512a perf(ops): ansible 增加 summary 汇总 2021-02-26 16:33:27 +08:00
xinwen
d795867916 perf: 优化批量更新会查询全部数据的问题 2021-02-26 16:32:46 +08:00
xinwen
4c4f544f0d fix: 修复禁用 MFA 后还可以用 MFA 查看密码匣子 2021-02-26 16:28:45 +08:00
xinwen
8ec26dea43 feat: 重置 MFA 发个邮件 #754 2021-02-26 16:25:55 +08:00
xinwen
799d1e4043 feat: 资产授权规则添加是否有效的过滤条件 2021-02-25 14:48:47 +08:00
ibuler
b03642847e perf: 去掉 data_tree 2021-02-24 16:22:26 +08:00
fit2bot
a4e635bff0 feat: 添加下载rdp文件的api (#5637)
* feat: 添加下载rdp文件的api

* perf: 优化一些权限

* perf: 优化一波token

Co-authored-by: ibuler <ibuler@qq.com>
2021-02-24 15:31:22 +08:00
xinwen
83cc339d4b refactor: 调整组织统计数据缓存的更新策略为懒更新模式 2021-02-23 17:48:14 +08:00
ibuler
bb9790a50f feat: 为rdp 添加一个api 2021-02-23 16:22:37 +08:00
xinwen
9be3cbb936 perf: 优化用户详情页授权列表加载速度&添加可重入锁 2021-02-20 10:25:35 +08:00
xinwen
e599bca951 fix: 命令存储 es 类型主机带用户名密码报错 2021-02-18 16:41:27 +08:00
fit2bot
501ad698b7 添加 UnionQuertSet (#5578)
* 添加 UnionQuertSet

* 跑通了

* 改变了 count 这类方法的代理模式

* 使用了老广的

Co-authored-by: xinwen <coderWen@126.com>
2021-02-07 10:15:39 +08:00
Bai
50e6c96358 fix: 修复 component status 获取key error 问题 2021-02-05 17:07:21 +08:00
Jiangjie.Bai
7cf6e54f01 refactor tree (重构&优化资产树/用户授权树加载速度) (#5548) (#5549)
* Bai reactor tree ( 重构获取完整资产树中节点下资产总数的逻辑) (#5548)

* tree: v0.1

* tree: v0.2

* tree: v0.3

* tree: v0.4

* tree: 添加并发锁未请求到时的debug日志

* 以空间换时间的方式优化资产树

* Reactor tree togther v2 (#5576)

* Bai reactor tree ( 重构获取完整资产树中节点下资产总数的逻辑) (#5548)

* tree: v0.1

* tree: v0.2

* tree: v0.3

* tree: v0.4

* tree: 添加并发锁未请求到时的debug日志

* 以空间换时间的方式优化资产树

* 修改授权适配新方案

* 添加树处理工具

* 完成新的用户授权树计算以及修改一些信号

* 重构了获取资产的一些 api

* 重构了一些节点的api

* 整理了一些代码

* 完成了api 的重构

* 重构检查节点数量功能

* 完成重构授权树工具类

* api 添加强制刷新参数

* 整理一些信号

* 处理一些信号的问题

* 完成了信号的处理

* 重构了资产树相关的锁机制

* RebuildUserTreeTask 还得添加回来

* 优化下不能在root组织的检查函数

* 优化资产树变化时锁的使用

* 修改一些算法的小工具

* 资产树锁不再校验是否在具体组织里

* 整理了一些信号的位置

* 修复资产与节点关系维护的bug

* 去掉一些调试代码

* 修复资产授权过期检查刷新授权树的 bug

* 添加了可重入锁

* 添加一些计时,优化一些sql

* 增加 union 查询的支持

* 尝试用 sql 解决节点资产数量问题

* 开始优化计算授权树节点资产数量不用冗余表

* 新代码能跑起来了,修复一下bug

* 去掉 UserGrantedMappingNode 换成 UserAssetGrantedTreeNodeRelation

* 修了些bug,做了些优化

* 优化QuerySetStage 执行逻辑

* 与小白的内存结合了

* 删掉老的表,迁移新的 assets_amount 字段

* 优化用户授权页面资产列表 count 慢

* 修复批量命令数量不对

* 修改获取非直接授权节点的 children 的逻辑

* 获取整棵树的节点

* 回退锁

* 整理迁移脚本

* 改变更新树策略

* perf: 修改一波缩进

* fix: 修改handler名称

* 修复授权树获取资产sql 泛滥

* 修复授权规则有效bug

* 修复一些bug

* 修复一些bug

* 又修了一些小bug

* 去掉了老的 get_nodes_all_assets

* 修改一些写法

* Reactor tree togther b2 (#5570)

* fix: 修改handler名称

* perf: 优化生成树

* perf: 去掉注释

* 优化了一些

* 重新生成迁移脚本

* 去掉周期检查节点资产数量的任务

* Pr@reactor tree togther guang@perf mapping (#5573)

* fix: 修改handler名称

* perf: mapping 拆分出来

* 修改名称

* perf: 修改锁名

* perf: 去掉检查节点任务

* perf: 修改一下名称

* perf: 优化一波

Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
Co-authored-by: Bai <bugatti_it@163.com>
Co-authored-by: xinwen <coderWen@126.com>

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: 老广 <ibuler@qq.com>
2021-02-05 13:29:29 +08:00
Bai
709e7af953 perf: 修改依赖版本jumpserver-django-oidc-rp=0.3.7.6 2021-02-03 14:38:38 +08:00
ibuler
93474766f6 perf(permission): 优化权限控制,显式的声明权限 2021-02-03 14:28:47 +08:00
fit2bot
542eb25e7b fix(perms): 修复权限校验时的组织切换问题 (#5546)
* fix(perms): 修复权限校验时的组织切换问题

* fix(perms): 修复获取actions的切换组织问题

* perf: 继续添加 application 的验证组织

Co-authored-by: ibuler <ibuler@qq.com>
2021-02-03 12:01:18 +08:00
Bai
609d2710fa perf: 会话列表添加search_fields字段 2021-02-03 11:55:19 +08:00
ibuler
d852d2f670 perf: 还原回原来的用户来源字段 2021-02-02 16:54:51 +08:00
Bai
087a3f2914 fix: 注释fake数据生成模块的导入(system) 2021-02-02 02:11:30 -06:00
fit2bot
36f113e307 chore: 添加贡献者图片 (#5532)
* chore: 添加贡献者图片

* chore: 优化通知样式
Co-authored-by: ibuler <ibuler@qq.com>
2021-01-27 13:03:20 +08:00
ibuler
23afe81ff5 perf(authentication): 优化connection token的使用 2021-01-26 18:07:11 +08:00
ibuler
dd5b2b9101 perf: 去掉 v2 的api 2021-01-26 18:04:47 +08:00
fit2bot
d363118911 perf(settings): 优化settings配置 (#5515)
* stash

* perf: 优化 动态seting

* perf(settings): 优化settings配置

* perf: 完成终端和安全setting

* perf: 修改翻译

* perf: 去掉其他位置的DYNAMIC

* perf: 还原回来原来的一些代码

* perf: 优化ldap

* perf: 移除dynmic config

* perf: 去掉debug消息

* perf: 优化 refresh 命名

Co-authored-by: ibuler <ibuler@qq.com>
2021-01-26 17:54:12 +08:00
fit2bot
351d4d8123 refactor(celery): 重构celery,使用 threads 模型,避免 占用太多内存 (#5525)
* refactor(celery): 重构celery,使用 threads 模型,避免 占用太多内存

* fix: 修复无法关闭fd的bug

Co-authored-by: ibuler <ibuler@qq.com>
2021-01-25 05:34:41 -06:00
Bai
efb9f48c6f perf: 删除pycryptodome依赖包安装(因为pycryptodomepycrypto安装包目录冲突);只安装 pycryptodomex依赖包; 修改 from cryptofrom cryptodome 2021-01-25 11:59:20 +08:00
Bai
d04b90b8e8 feat: 修改copyright 2014-2021 2021-01-22 10:29:33 +08:00
Jiangjie.Bai
66f57fdb27 Merge pull request #5504 from jumpserver/dev
Dev
2021-01-21 15:56:09 +08:00
Bai
c949589564 perf: 修改翻译信息; 添加Domain迁移文件 2021-01-21 15:54:45 +08:00
ibuler
992708abe8 chore: 添加ping等工具 2021-01-21 15:43:17 +08:00
xinwen
f63f8d085d fix: Web页面-> 命令执行 高危命令没有告警 2021-01-21 15:39:24 +08:00
Jiangjie.Bai
3e55447327 Merge pull request #5497 from jumpserver/dev
Dev
2021-01-20 19:31:33 +08:00
xinwen
3ac20d80d1 fix: 登录日志的登录方式不准确 2021-01-20 19:21:29 +08:00
fit2bot
3e38e4fc59 i18n: 优化翻译 (#5492)
* i18n: 优化翻译

* i18n: 优化翻译(2)

* i18n: 优化翻译(3)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-20 19:08:39 +08:00
xinwen
1090887b2b fix: 管理用户-> 资产用户列表 更新密码报错 2021-01-20 00:58:20 -06:00
Bai
7c55d462cd fix: 修复工单翻译和登录确认工单开启问题 2021-01-20 13:57:05 +08:00
fit2bot
ea16088c08 fix: 修改翻译内容 (#5489)
* fix: 修改翻译内容

* fix: 修改翻译内容(2)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-20 11:38:09 +08:00
Bai
4de9e608b1 fix: 修改命令存储配置DOC_TYPE字段类型为ReadableHiddenField 2021-01-20 11:24:08 +08:00
xinwen
dd9a55bd5f fix: 用户名超过64字符,命令无法记录问题 2021-01-19 20:13:20 -06:00
Jiangjie.Bai
ee22006683 Merge pull request #5485 from jumpserver/dev
Dev
2021-01-19 20:10:24 +08:00
xinwen
09d91d8bf3 fix: 修复系统平台创建名称报错信息 2021-01-19 20:08:33 +08:00
Bai
46fbc19697 i18n: 修改翻译 2021-01-19 20:05:44 +08:00
Bai
44a42e4739 fix: 修改用户相关tickets为自己申请的或者待受理的 2021-01-19 19:43:17 +08:00
xinwen
4de2ae607d perf: 优化 日志审计-> 批量命令-> 用户显示格式 2021-01-19 05:06:43 -06:00
Bai
9fee82cd14 fix: 添加录像存储endpoint校验逻辑 2021-01-19 19:05:16 +08:00
fit2bot
9126c7780d perf: 工单优化(审批人可以填写工单对应的授权规则名称) (#5468)
* perf: 工单优化(审批人可以填写工单对应的授权规则名称)

* perf: 工单优化(优化推荐的资产、应用、系统用户等逻辑)

* perf: 工单优化(优化工单邮件内容)

* perf: MethodSerializer优化(优化当Serializer不需要时, 默认可以不传递对应字段)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-19 15:44:19 +08:00
fit2bot
0842553f8a fix: 修复 celery 等日志文件的访问漏洞 (#5469)
Co-authored-by: xinwen <coderWen@126.com>
2021-01-19 14:36:41 +08:00
xinwen
5fae919499 fix: 跳板机危险命令告警发送邮件失败 2021-01-18 06:57:57 -06:00
noon
1243546627 Update README_EN.md
style: change some sentences in the critical bug warning
2021-01-18 04:13:11 -06:00
Bai
a0cb16e5c4 perf: 修改关闭工单API权限, 申请人有权限关闭工单 2021-01-18 18:06:30 +08:00
ibuler
7b8f932dcd perf: 去掉几个不用的api 2021-01-18 17:18:05 +08:00
Bai
243eedc4f9 perf: 优化工单body html显示格式及翻译信息 2021-01-18 17:14:15 +08:00
xinwen
230ef2f662 fix: 修复用户离开组织信号被覆盖问题 2021-01-18 01:20:47 -06:00
xinwen
42019c9e8a fix: 修复 AssetUserFilterBackend 2021-01-18 01:19:01 -06:00
Bai
f6622f5e01 fix: 修改翻译 2021-01-18 15:06:36 +08:00
Bai
31f098449f perf: 修改 OPTION 获取 choices 字段选项; 修改display字段翻译 (显示名称) 2021-01-18 15:01:09 +08:00
ibuler
0d4e346210 chore: 修改readme 2021-01-18 00:26:51 -06:00
fit2bot
df193162f7 chore: 修改readme 英文版本 (#5448)
* chore: 修改readme 英文版本

Co-authored-by: ibuler <ibuler@qq.com>
2021-01-18 13:46:55 +08:00
ibuler
646f0a568b chore: 修改readme 2021-01-17 21:21:33 -06:00
Jiangjie.Bai
be5b4a5f71 Merge pull request #5440 from jumpserver/dev
Dev
2021-01-17 19:29:37 +08:00
xinwen
e61511372c fix: 修复缓存框架组织切换问题&组织的 resource_statistics 字段是只读 2021-01-17 19:28:00 +08:00
Jiangjie.Bai
d4f3280427 Merge pull request #5437 from jumpserver/dev
Dev
2021-01-17 17:58:30 +08:00
fit2bot
083f061665 perf: 更新翻译 (#5438)
* perf: 更新翻译

* perf: 更新翻译

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-17 16:22:06 +08:00
fit2bot
be7a93d81a feat: 在登录页面添加CAS/OpenID等第三方登录链接;不再自动跳转登录地址;统一开源/企业版登录页面; (#5389)
* feat: 在登录页面添加CAS/OpenID等第三方登录链接;不再自动跳转登录地址;统一开源/企业版登录页面;

* feat: 登录页面<忘记密码>链接,不限制第三方用户; 在忘记密码页面进行判断与限制

* feat: 登录页面<忘记密码>链接,不限制第三方用户; 在忘记密码页面进行判断与限制 (2)

* fix: 调整样式

Co-authored-by: Bai <bugatti_it@163.com>
Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-17 15:28:22 +08:00
xinwen
156be0a64e fix: 网域列表添加默认 name 排序 2021-01-17 13:06:00 +08:00
fit2bot
a7fa2331bd feat: 添加缓存模块,添加组织资源统计 (#5407)
* feat: 添加缓存模块,添加组织资源统计

* refactor

* recover .gitkeep

* refactor

* 合并信号处理

* 修复组织添加用户没有发信号

* 修改了一个log级别

Co-authored-by: xinwen <coderWen@126.com>
2021-01-17 12:08:21 +08:00
老广
9e0d731a0c Update README.md (#5432)
* Update README.md

* Update README.md
2021-01-16 16:23:30 +08:00
Orange
1b184db956 Merge pull request #5427 from jumpserver/ibuler-patch-1
Update README.md
2021-01-15 18:06:09 +08:00
老广
4b9ed47cda Update README.md 2021-01-15 18:05:09 +08:00
ibuler
f04e2fa090 fix: bug 2021-01-14 10:27:49 +08:00
Bai
83d12d02fb perf: 重构工单处理流程 (7) 2021-01-13 18:48:38 +08:00
ibuler
64257823c5 pref(common): 优化drf options的filterset 可能引起的问题 2021-01-13 18:06:15 +08:00
fit2bot
a7468a243d perf: 重构工单处理流程 (#5408)
* perf: 重构工单处理流程

* perf: 重构工单处理流程 (1)

* perf: 重构工单处理流程 (2)

* perf: 重构工单处理流程 (3)

* perf: 重构工单处理流程 (4)

* perf: 重构工单处理流程 (5)

* perf: 重构工单处理流程 (6)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-13 17:49:03 +08:00
fit2bot
528e251f31 perf: 日志增加请求耗时 (#5406)
Co-authored-by: Eric <xplzv@126.com>
2021-01-12 18:15:59 +08:00
fit2bot
86a055638c reactor: 重构命令&录像存储模块的Serializer及相关模块 (#5392)
* reactor: 重构命令&录像存储模块的Serializer及相关模块


Co-authored-by: Bai <bugatti_it@163.com>
2021-01-12 18:06:42 +08:00
fit2bot
b3f359d47b perf(assets): 优化节点生成子节点key生成逻辑 (#5405)
* perf(assets): 优化节点生成子节点key生成逻辑

* perf(assets): 优化写法

* perf(assets): 优化获取子节点的mark

* perf(assets): 再优化一波

* perf(asset): 继续优化这里的写法

Co-authored-by: ibuler <ibuler@qq.com>
2021-01-12 12:59:05 +08:00
Bai
dbe969b064 perf: 解决MethodSerializer被swagger调用时parent.Serializer会互相影响所需字段显示的问题 2021-01-11 15:37:29 +08:00
Bai
b9258878fe fix: 修复celery日志清除问题 2021-01-11 10:30:10 +08:00
Bai
19c2973501 perf: 可以获取多种协议类型的系统用户列表 2021-01-07 19:09:31 +08:00
ibuler
e7a3c5a822 perf(api): filter_fields被filterset_fields取代
https://django-filter.readthedocs.io/en/stable/guide/migration.html
2021-01-07 18:36:17 +08:00
老广
ff4748f9f4 Merge pull request #5385 from jumpserver/pr@dev@feat_asset_task
feat: 添加批量执行资产任务的接口
2021-01-06 16:54:21 +08:00
xinwen
60c19148dc feat: 添加批量执行资产任务的接口 2021-01-06 16:51:25 +08:00
老广
7eedc0635e Merge pull request #5362 from jumpserver/pr@dev@fix_adhoc_excution
fix: 修复多个 AdHocExecution 在一个 celery task 执行时日志错误
2021-01-06 15:57:09 +08:00
xinwen
f5fd40978e fix: 修复多个 AdHocExecution 在一个 celery task 执行时日志错误 2021-01-06 15:53:38 +08:00
Bai
72dd23dcce perf: ticket 申请添加 comment 2021-01-06 15:19:43 +08:00
老广
5b5c33116a Merge pull request #5350 from hctech/dev
fix:资产自动推送UUIDD数组格式化字符串失败
2021-01-06 14:41:24 +08:00
fit2bot
7167515a53 feat: 实现MethodSerializer, 满足serializer中SerializerField动态更改的需求 (#5382)
* feat: 实现MethodSerializer, 满足serializer中SerializerField动态更改的需求

* feat: 实现MethodSerializer, 满足serializer中SerializerField动态更改的需求 (2)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-06 12:44:12 +08:00
fit2bot
17a01a12db reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能 (#5379)
* reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能

* reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能 (2)

* reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能 (3)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-05 23:39:38 +08:00
Bai
3188692691 perf: 修改swagger问题 2021-01-04 16:25:16 +08:00
Bai
aab59403e1 perf: 修改工单创建授权规则的字段(created_by) 2021-01-04 14:36:54 +08:00
fit2bot
7e7e24f51f reactor&remove: 重构applications模块 & 移除applications、perms中已不再使用的模块 (#5374)
* reactor: 重构applications模块 & 删除applications、perms中已不再使用的模块

 * reactor: 1. 针对application.attrs字段的view-serializer映射逻辑,采用DynamicMapping的方案重写;
 * reactor: 2. 删除applications和perms模块中已不再使用的database-app/k8s-app/remote-app模块;

* reactor: 添加迁移文件(删除perms/databaseperrmission/remoteapppermission/k8sapppermission)

* reactor: 修改细节

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-04 05:27:03 +08:00
fit2bot
428e8bf2a0 perf: 修改 View dynamic mapping include dynamic mapping fields Serializer Class 方案的说明 (#5373)
* perf: 修改 View dynamic mapping `include dynamic mapping fields Serializer Class` 方案的说明

* perf: 修改 View dynamic mapping `include dynamic mapping fields Serializer Class` 方案的说明 (2)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-03 14:17:02 +08:00
Bai
24b1c87121 perf: 修改细节 2021-01-02 08:00:05 +08:00
fit2bot
cef93abb2f feat: 抽象View Mapping Serializer架构设计; 重构工单View、Serializer模块 (#5371)
* perf: 优化工单模块(修改迁移文件->Model assignees_display 字段类型为list)

* ignore: try `view` `serializer jsonfields` Map design (1)

* ignore: try `view` `serializer jsonfields` Map design (2)

* ignore: try `view` `serializer jsonfields` Map design (3)

* ignore: try `view` `serializer jsonfields` Map design (4)

* ignore: try `view` `serializer jsonfields` Map design (5)

* ignore: try `view` `serializer.DynamicMappingField` Mapping design (6)

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块(2)

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块(3)

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块(4)

Co-authored-by: Bai <bugatti_it@163.com>
2021-01-02 07:25:23 +08:00
Bai
5c483084b7 feat: 优化工单模块 2020-12-31 18:39:40 +08:00
fit2bot
167734ca5d feat: 优化工单模块 (#5365)
* feat: 优化工单模块

* feat: 优化工单模块2

Co-authored-by: Bai <bugatti_it@163.com>
2020-12-31 05:47:27 +08:00
Bai
1a9a5c28f5 feat: 优化工单模块1 2020-12-31 05:09:46 +08:00
fit2bot
430e20a49c feat: 优化工单模块 (#5361)
* feat: 优化工单模块1

* feat: 优化工单模块2

* feat: 优化工单模块3

Co-authored-by: Bai <bugatti_it@163.com>
2020-12-30 18:14:06 +08:00
Jiangjie.Bai
3b056ff953 reactor&feat: 重构工单模块 & 支持申请应用工单 (#5352)
* reactor: 修改工单Model,添加工单迁移文件

* reactor: 修改工单Model,添加工单迁移文件

* reactor: 重构工单模块

* reactor: 重构工单模块2

* reactor: 重构工单模块3

* reactor: 重构工单模块4

* reactor: 重构工单模块5

* reactor: 重构工单模块6

* reactor: 重构工单模块7

* reactor: 重构工单模块8

* reactor: 重构工单模块9

* reactor: 重构工单模块10

* reactor: 重构工单模块11

* reactor: 重构工单模块12

* reactor: 重构工单模块13

* reactor: 重构工单模块14

* reactor: 重构工单模块15

* reactor: 重构工单模块16

* reactor: 重构工单模块17

* reactor: 重构工单模块18

* reactor: 重构工单模块19

* reactor: 重构工单模块20

* reactor: 重构工单模块21

* reactor: 重构工单模块22

* reactor: 重构工单模块23

* reactor: 重构工单模块24

* reactor: 重构工单模块25

* reactor: 重构工单模块26

* reactor: 重构工单模块27

* reactor: 重构工单模块28

* reactor: 重构工单模块29

* reactor: 重构工单模块30

* reactor: 重构工单模块31

* reactor: 重构工单模块32

* reactor: 重构工单模块33

* reactor: 重构工单模块34

* reactor: 重构工单模块35

* reactor: 重构工单模块36

* reactor: 重构工单模块37

* reactor: 重构工单模块38

* reactor: 重构工单模块39
2020-12-30 00:19:59 +08:00
huangchao
795d1b59e0 fix:资产自动推送UUIDD数组格式化字符串失败 2020-12-27 23:00:45 +08:00
Bai
9d4f1a01fd perf: 升级依赖 python-ldap==3.3.1 2020-12-22 17:18:42 +08:00
xinwen
332f65cf2f fix: 将节点的资产添加到系统用户时固定组织 2020-12-22 15:14:01 +08:00
Bai
b79e6799c4 fix: 修复资产导入携带disk_info信息时失败的问题 2020-12-21 17:10:44 +08:00
Bai
4eef425e2a fix: 修复提交系统设置失败的问题 2020-12-21 14:30:12 +08:00
Jiangjie.Bai
3f4877f26b Merge pull request #5295 from jumpserver/dev
fix: 修复命令列表过滤字段文案`会话ID`
2020-12-17 18:26:54 +08:00
Bai
0e4d778335 fix: 修复命令列表过滤字段文案会话ID 2020-12-17 18:25:48 +08:00
Jiangjie.Bai
52d20080ff Merge pull request #5293 from jumpserver/dev
fix: 修改系统监控问题
2020-12-17 17:36:06 +08:00
Bai
ed8d72c06b fix: 修改系统监控问题 2020-12-17 17:34:37 +08:00
Jiangjie.Bai
5e9e3ec6f6 Merge pull request #5288 from jumpserver/dev
chore: Merge master from dev
2020-12-17 14:56:25 +08:00
xinwen
4f5f92deb8 fix: 批量删除管理用户报错信息太丑 2020-12-17 14:47:37 +08:00
xinwen
d2a15ee702 fix: 申请资产工单如果系统用户没填内容不推荐系统用户 2020-12-17 14:35:08 +08:00
Bai
a3a591da4b fix: 修复命令导出excel格式报错 2020-12-17 14:31:34 +08:00
Bai
3f2925116e fix: 修复metrics获取terminal过滤is_deleted字段 2020-12-17 11:30:29 +08:00
xinwen
c3e2e536e0 fix: 【用户管理】-创建用户组-可将系统审计员加入到用户组 #579 2020-12-17 10:33:45 +08:00
Jiangjie.Bai
8c133d5fdb Merge pull request #5278 from jumpserver/dev
chore: Merge master from dev
2020-12-16 18:50:49 +08:00
xinwen
89d8efe0f1 fix: perms.signals_handler.on_application_permission_applications_changed 修改名字 2020-12-16 18:49:20 +08:00
Bai
54303ea33f fix: 修复节点创建时更新孩子full_value日志输出问题 2020-12-16 18:37:54 +08:00
Bai
4dcd8dd8dd fix: 修复节点创建时更新孩子full_value日志输出问题 2020-12-16 18:37:54 +08:00
老广
4f04a7d258 Merge pull request #5280 from jumpserver/pr@dev@fix_auto_push
fix: 推送系统用户时 AdHocExecution id 重复
2020-12-16 17:36:45 +08:00
xinwen
bf308e24b6 fix: 推送系统用户时 AdHocExecution id 重复 2020-12-16 17:31:37 +08:00
Bai
b3642f3ff4 fix: 修复LDAP用户登录(未找到)时循环调用问题 2020-12-16 12:01:57 +08:00
xinwen
3aed4955c8 fix: 远程应用授权的一些问题 2020-12-16 12:00:53 +08:00
fit2bot
9a5f9a9c92 fix: 应用授权不会自动推送的bug (#5271)
Co-authored-by: xinwen <coderWen@126.com>
2020-12-16 10:23:37 +08:00
Orange
6d5bec1ef2 Merge pull request #5269 from jumpserver/dev
chore: Merge master from dev
2020-12-15 20:35:51 +08:00
Bai
e93fd1fd44 fix: 删除终端列表state的默认值0 2020-12-15 20:34:47 +08:00
xinwen
7bf37611bd fix: 系统审计员不应该能添加到组 2020-12-15 19:24:25 +08:00
xinwen
b8ec4bfaa5 fix: 日志审计-操作日志中搜索-按动作搜索:需修改文字 删除文件-->删除 #524 2020-12-15 18:38:23 +08:00
Bai
58b6293b76 fix: 组件监控添加offline数量 2020-12-15 18:37:16 +08:00
xinwen
8e12eebceb fix: 获得 oidc acs 等认证方式失败 2020-12-15 18:36:37 +08:00
Bai
72d6ea43fa fix: 修改命令列表过滤参数session 2020-12-15 18:34:05 +08:00
Bai
deedd49dc5 fix: 修复命令记录导出excel文件格式未定义的问题 2020-12-15 18:07:11 +08:00
fit2bot
a36e6fbf84 fix: 修改判断会话活跃逻辑;不必要判断协议 (#5262)
* fix: 修改判断会话活跃逻辑;不必要判断协议

* fix: 修改导入task问题

Co-authored-by: Bai <bugatti_it@163.com>
2020-12-15 18:06:35 +08:00
Bai
b57453cc3c fix: 修复命令列表过滤使用session_id字段 2020-12-15 18:05:42 +08:00
Jiangjie.Bai
62f2909d59 Merge pull request #5256 from jumpserver/dev
chore: Merge master from dev
2020-12-15 14:20:23 +08:00
xinwen
0d469ff95b fix(orgs): 用户离开组织后授权的资产没主动刷新 2020-12-15 14:00:51 +08:00
xinwen
ca883f1fb4 fix: 工单申请资产审批时系统用户没有推荐 2020-12-15 13:06:55 +08:00
fit2bot
6e0fbd78e7 fix: 修复prometheus_metricsAPI数据获取bug;修复组件注册type为空bug (#5253)
* fix: 修复prometheus_metricsAPI数据获取bug;修复组件注册type为空bug

* fix: 修改审计migrations/userloginlog/backend的verbose_name字段

Co-authored-by: Bai <bugatti_it@163.com>
2020-12-15 13:00:59 +08:00
ibuler
0813cff74f perf: 优化 docs swagger,不再要求debug 2020-12-14 11:39:02 +08:00
ibuler
ff428b84f9 fix(assets): 修复asset更新子节点时的日志错误 2020-12-14 11:33:34 +08:00
ibuler
d0c9aa2c55 fix(assets): 修复更新孩子节点时的log error 2020-12-14 11:33:34 +08:00
老广
1d5e603c0d Merge pull request #5231 from jumpserver/dev
chore(merge): 合并 dev 到 master
2020-12-11 19:29:45 +08:00
ibuler
ddbbc8df17 fix(docker): 修复Dockerfile中 echo引起的sh和bash换行兼容问题 2020-12-11 19:26:46 +08:00
Bai
90df404931 fix: 修复swagger问题 2020-12-11 19:02:33 +08:00
Bai
b9cbff1a5f del: 删除测试prometheus相关代码 2020-12-11 19:02:33 +08:00
ibuler
b9717eece3 fix: 修改访问swagger会产生的错误 2020-12-11 18:30:20 +08:00
Bai
f9cf2a243b fix: 修复settings中搜索LDAP用户重复问题 2020-12-11 18:26:10 +08:00
Bai
e056430fce fix: 修复只配置DC域时,LDAP用户认证失败的问题 2020-12-11 18:26:10 +08:00
Jiangjie.Bai
2b2821c0a1 Merge pull request #5223 from jumpserver/dev
chore(merge): 合并 dev 到 master
2020-12-11 16:53:36 +08:00
Bai
213221beae perf: 修改BasePermissionViewSet的custom_filter_fields 2020-12-11 16:19:18 +08:00
Bai
2db9c90a74 feat: 修改翻译:认证方式 2020-12-11 15:49:39 +08:00
Bai
8ced6f1168 fix: 用户ProfileAPI设置is_first_login不是可读写 2020-12-11 15:44:17 +08:00
Bai
6703ab9a77 perf: 添加BasePermissionsViewSet,支持搜索过滤 2020-12-11 15:44:17 +08:00
老广
2fc6e6cd54 Merge pull request #5213 from jumpserver/dev
chore(merge): 合并dev到master
2020-12-10 23:03:35 +08:00
Bai
2176fd8fac feat: 更新翻译 2020-12-10 21:30:56 +08:00
fit2bot
856e7c16e5 feat: 添加组件监控;TerminalModel添加type字段; (#5206)
* feat: 添加组件监控;TerminalModel添加type字段;

* feat: Terminal序列类添加type字段

* feat: Terminal序列类添加type字段为只读

* feat: 修改组件status文案

* feat: 取消上传组件状态序列类count字段

* reactor: 修改termina/models目录结构

* feat: 修改ComponentTypeChoices

* feat: 取消考虑CoreComponent类型

* feat: 修改Terminal status判断逻辑

* feat: 终端列表添加status过滤; 组件状态序列类添加default值

* feat: 添加PrometheusMetricsAPI

* feat: 修改PrometheusMetricsAPI

Co-authored-by: Bai <bugatti_it@163.com>
2020-12-10 20:50:22 +08:00
fit2bot
d4feaf1e08 fix: 修复由于更新django captch版本引起的css丢失问题 (#5204)
* fix: 修复由于更新django captch版本引起的css丢失问题

* perf: 优化验证码的高度

Co-authored-by: ibuler <ibuler@qq.com>
2020-12-10 20:48:10 +08:00
fit2bot
5aee2ce3db chore: 升级依赖库版本 (#5205)
* chore: 升级依赖库版本

* fix: 几个库回退几个版本

Co-authored-by: ibuler <ibuler@qq.com>
2020-12-10 20:46:45 +08:00
xinwen
4424c4bde2 perf(asset): 手动启动节点资产数量自检程序时区分组织 2020-12-10 18:17:01 +08:00
fit2bot
5863e3e008 perf(asset): 资产树,右击增加计算节点数量的菜单,可以让后台去计算 #527 (#5207)
Co-authored-by: xinwen <coderWen@126.com>
2020-12-10 17:12:39 +08:00
xinwen
79a371eb6c perf(auth): 密码过期后,走重置密码流程 #530 2020-12-10 16:06:26 +08:00
fit2bot
7c7de96158 feat(login): 登录日志要体现用哪个backend登录的 #4472 (#5199)
Co-authored-by: xinwen <coderWen@126.com>
2020-12-09 18:43:13 +08:00
ibuler
80b03e73f6 feat(celery): 添加celery的health check接口 2020-12-09 18:06:34 +08:00
fit2bot
32dbab2e34 perf: 数据库应用database字段添加allow_null=True (#5196)
* perf: 数据库应用database字段修改为required

* perf: 数据库应用database字段添加allow_null=True

Co-authored-by: Bai <bugatti_it@163.com>
2020-12-09 13:44:07 +08:00
ibuler
b189e363cc revert(system): 暂时去掉system组件 2020-12-09 11:24:44 +08:00
Bai
4c3a655239 perf: 用户序列类禁止修改source字段 2020-12-08 20:54:33 +08:00
Bai
5533114db5 feat: 用户授权应用树按组织节点进行区分 2020-12-08 20:37:07 +08:00
Jiangjie.Bai
4c469afa95 feat: 取消资产配置相关字段只读模式 (#5182)
* feat: 取消资产配置相关字段只读模式

* feat: 取消资产配置相关字段只读模式
2020-12-08 20:32:48 +08:00
fit2bot
2ccc5beeda perf(Dockerfile): 不再使用zh_CN.UTF-8, en_US.UTF-8 应该也是可以的 (#5190)
* perf(Dockerfile): 不再使用zh_CN.UTF-8, en_US.UTF-8 应该也是可以的

* fix: 还原回原来的LANG设置

* perf: 合并层数

* feat: 修改Dockerfile

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <bugatti_it@163.com>
2020-12-08 20:22:48 +08:00
xinwen
4b67d6925e feat(asset): api 添加推送系统用户到多个资产 2020-12-08 18:08:44 +08:00
fit2bot
dd979f582a stash (#5178)
* Dev (#4791)

* fix(xpack): 修复last login太长的问题 (#4786)

Co-authored-by: ibuler <ibuler@qq.com>

* perf: 更新密码中也发送邮件 (#4789)

Co-authored-by: ibuler <ibuler@qq.com>

* fix(terminal): 修复获取螺旋的异步api

* fix(terminal): 修复有的录像存储有问题的导致下载录像的bug

* fix(orgs): 修复组织添加用户bug

* perf(requirements): 修改jms-storage==0.0.34 (#4797)

Co-authored-by: Bai <bugatti_it@163.com>

Co-authored-by: fit2bot <68588906+fit2bot@users.noreply.github.com>
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <bugatti_it@163.com>

* stash

* feat(system): 添加系统app

* stash

* fix: 修复一些bug

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <bugatti_it@163.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2020-12-08 14:26:18 +08:00
Bai
042ea5e137 feat: 授权应用树API返回org_name字段 2020-12-08 11:01:09 +08:00
ibuler
2a6f68c7ba revert: 还原原来的jms,自动运行migraionts 2020-12-07 19:16:27 +08:00
fit2bot
43b5e97b95 feat(excel): 添加Excel导入/导出 (#5124)
* refactor(drf_renderer): 添加 ExcelRenderer 支持导出excel文件格式; 优化CSVRenderer, 抽象 BaseRenderer

* perf(renderer): 支持导出资源详情

* refactor(drf_parser): 添加 ExcelParser 支持导入excel文件格式; 优化CSVParser, 抽象 BaseParser

* refactor(drf_parser): 添加 ExcelParser 支持导入excel文件格式; 优化CSVParser, 抽象 BaseParser 2

* perf(renderer): 捕获renderer处理异常

* perf: 添加excel依赖包

* perf(drf): 优化导入导出错误日志

* perf: 添加依赖包 pyexcel-io==0.6.4

* perf: 添加依赖包pyexcel-xlsx==0.6.0

* feat: 修改drf/renderer&parser变量命名

* feat: 修改drf/renderer的bug

* feat: 修改drf/renderer&parser变量命名

Co-authored-by: Bai <bugatti_it@163.com>
2020-12-07 15:23:05 +08:00
ibuler
619b521ea1 fix: 修改语言i18n 2020-12-05 13:22:22 +08:00
fit2bot
3447eeda68 fix(applications): 修改attrs不能为null (#5172)
Co-authored-by: Bai <bugatti_it@163.com>
2020-12-04 13:10:59 +08:00
fit2bot
75ef413ea5 fix(applications): 修改attrs不能为null (#5171)
Co-authored-by: Bai <bugatti_it@163.com>
2020-12-04 10:24:10 +08:00
fit2bot
662c9092dc reactor(dockerfile): 使用debian构建docker (#5169)
Co-authored-by: ibuler <ibuler@qq.com>
2020-12-03 19:14:28 +08:00
ibuler
c8d54b28e2 perf: 优化变量命名 2020-12-03 14:23:16 +08:00
ibuler
96cd307d1f perf: 优化entrypoint.sh 2020-12-03 14:23:16 +08:00
ibuler
6385cb3f86 perf: 优化启动,不再自动运行migrations 2020-12-03 14:23:16 +08:00
Bai
36e9d8101a fix: 添加迁移文件: Node ordering 2020-12-03 11:16:27 +08:00
ibuler
3354ab8ce9 fix(req): fix wheel version 2020-12-03 10:47:21 +08:00
Bai
89ec6ba6ef fix: Node ordering [parent_key, value]; 修复默认组织Default节点显示问题(存在key为0的Default节点) 2020-12-03 10:45:25 +08:00
Bai
af40e46a75 fix: 优化迁移Default节点 2020-12-03 10:29:00 +08:00
Bai
86fcd3c251 fix: 添加迁移文件(如果需要,将Default节点的key从0修改为1) 2020-12-03 10:29:00 +08:00
fit2bot
c2d5928273 build(pip): 锁定pip版本 (#5152)
* build(pip): 锁定pip版本

* fix: 锁定pip版本

* fix(req): 锁定加密库版本

* fix(build): 引用pip缓存

Co-authored-by: ibuler <ibuler@qq.com>
2020-12-02 11:09:39 +08:00
xinwen
e656ba70ec fix(assets): 推送动态系统用户未指定 username 取全部 usernames 2020-12-01 20:08:54 +08:00
xinwen
bb807e6251 fix(perms): 新建授权时动态用户可能推送不成功 2020-12-01 20:08:54 +08:00
Bai
bbd6cae3d7 perf(org): 优化获取org_name字段 2020-11-30 15:21:56 +08:00
Bai
c3b09dd800 perf(perms): 优化用户授权树返回org_name字段;添加thread_local属性org_mapper减少查询次数 2020-11-30 15:21:56 +08:00
Bai
6d427b9834 fix: 禁止删除组织根节点 2020-11-30 14:19:20 +08:00
xinwen
610aaf5244 fix(assets): 动态系统用户和用户关系变化时没有推送到资产 2020-11-26 15:20:09 +08:00
xinwen
df2f1b3e6e perf(User): 用户列表在大规模数据情况下慢 2020-11-26 12:32:18 +08:00
fit2bot
f26b7a470a perf(celery-task): 优化检查节点资产数量的 Celery 任务 (#5052)
Co-authored-by: xinwen <coderWen@126.com>
2020-11-25 17:30:07 +08:00
xinwen
a4667f3312 fix(Node): Node 保存的时候,在信号里设置 parent_key 2020-11-25 16:35:12 +08:00
xinwen
91081d9423 refactor(perms): 在动态用户所绑定的授权规则中,如授权给用户组,当用户组增加成员后,动态系统用户下没有相应增加用户,因此也不会自动推送 (#5084) (#5086) 2020-11-24 19:31:45 +08:00
fit2bot
3041697edc fix(orgs): 兼容旧的组织用户关系接口 (#5088)
Co-authored-by: xinwen <coderWen@126.com>
2020-11-24 19:09:14 +08:00
xinwen
75d7530ea5 fix(perms): 在动态用户所绑定的授权规则中,如授权给用户组,当用户组增加成员后,动态系统用户下没有相应增加用户,因此也不会自动推送 2020-11-24 10:27:03 +08:00
ibuler
975cc41bce perf(build): 优化使用pip mirror 2020-11-22 18:16:45 +08:00
ibuler
439999381d perf(build): 优化构建时用的mirror 2020-11-22 17:51:24 +08:00
xinwen
39ab5978be perf(perms): 获取用户所有授权时转换成 list 2020-11-22 17:24:00 +08:00
ibuler
7be7c8cee1 fix(perms): 修复我的资产页面问题 2020-11-22 16:55:21 +08:00
xinwen
68b22cbdec fix(perms): 修复用户组授权树与资产问题 2020-11-22 15:03:03 +08:00
xinwen
a7c704bea3 perf(celery-task): 优化检查节点资产数量的 Celery 任务 2020-11-22 11:36:10 +08:00
xinwen
21993b0d89 perf(perms): 优化用户授权资产列表加载速度 2020-11-22 11:35:13 +08:00
xinwen
73ccf3be5f fix(perms): 当用户授权为空时,清空旧的授权树 2020-11-22 11:26:39 +08:00
ibuler
bf3056abc4 fix(django3): 修复django3兼容问题 2020-11-20 15:25:37 +08:00
xinwen
f2fd9f5990 perf(assets): 限制搜索授权资产返回的条数 2020-11-20 15:24:33 +08:00
fit2bot
6d39a51c36 [fix]: 兼容django 3 (#5038)
* chore(django): 修改版本依赖

* [fix]: 兼容django 3

* fix(merge): 去掉不用的JSONField

* fix(requirements): 修改加密库的版本

Co-authored-by: ibuler <ibuler@qq.com>
2020-11-19 15:50:31 +08:00
xinwen
7fa94008c9 fix(old-api): 调整旧的组织与用户关联接口 2020-11-19 15:21:16 +08:00
464 changed files with 14708 additions and 12854 deletions

View File

@@ -1,5 +1,6 @@
FROM registry.fit2cloud.com/public/python:v3 as stage-build
MAINTAINER Jumpserver Team <ibuler@qq.com>
# 编译代码
FROM python:3.8.6-slim as stage-build
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
@@ -8,33 +9,39 @@ ADD . .
RUN cd utils && bash -ixeu build.sh
FROM registry.fit2cloud.com/public/python:v3
# 构建运行时环境
FROM python:3.8.6-slim
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
ENV MYSQL_MIRROR=$MYSQL_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
WORKDIR /opt/jumpserver
COPY ./requirements ./requirements
RUN useradd jumpserver
RUN yum -y install epel-release && \
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
RUN yum -y install $(cat requirements/rpm_requirements.txt)
RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \
pip config set global.index-url ${PIP_MIRROR}
RUN pip install $(grep 'jms' requirements/requirements.txt) -i https://pypi.org/simple
RUN pip install -r requirements/requirements.txt
COPY ./requirements/deb_buster_requirements.txt ./requirements/deb_buster_requirements.txt
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \
&& grep -v '^#' ./requirements/deb_buster_requirements.txt | xargs apt -y install \
&& rm -rf /var/lib/apt/lists/* \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY ./requirements/requirements.txt ./requirements/requirements.txt
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip config set global.index-url ${PIP_MIRROR} \
&& pip install --no-cache-dir $(grep 'jms' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
RUN mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
RUN echo > config.yml
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8
EXPOSE 8070
EXPOSE 8080

View File

@@ -4,9 +4,13 @@
[![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)
|Developer Wanted|
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
|![notification](https://raw.githubusercontent.com/goharbor/website/master/docs/img/readme/bell-outline-badged.svg)安全通知|
|------------------|
|JumpServer 正在寻找开发者,一起为改变世界做些贡献吧,哪怕一点点,联系我 <ibuler@fit2cloud.com> |
|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533) 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG|
--------------------------
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
@@ -16,7 +20,6 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
改变世界,从一点点开始。
> 注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
## 特色优势
@@ -28,21 +31,6 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- 多租户: 一套系统,多个子公司和部门同时使用;
- 多应用支持: 数据库Windows远程应用Kubernetes。
## 版本说明
自 v2.0.0 发布后, JumpServer 版本号命名将变更为v大版本.功能版本.Bug修复版本。比如
```
v2.0.1 是 v2.0.0 之后的Bug修复版本
v2.1.0 是 v2.0.0 之后的功能版本。
```
像其它优秀开源项目一样JumpServer 每个月会发布一个功能版本,并同时维护 3 个功能版本。比如:
```
在 v2.4 发布前,我们会同时维护 v2.1、v2.2、v2.3
在 v2.4 发布后,我们会同时维护 v2.2、v2.3、v2.4v2.1 会停止维护。
```
## 功能列表
@@ -72,8 +60,8 @@ v2.1.0 是 v2.0.0 之后的功能版本。
<td>RADIUS 二次认证</td>
</tr>
<tr>
<td>登录复核X-PACK</td>
<td>用户登录行为受管理员的监管与控制</td>
<td>登录复核</td>
<td>用户登录行为受管理员的监管与控制:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="11">账号管理<br>Account</td>
@@ -97,23 +85,23 @@ v2.1.0 是 v2.0.0 之后的功能版本。
<td>密码过期设置</td>
</tr>
<tr>
<td rowspan="2">批量改密X-PACK</td>
<td>定期批量改密</td>
<td rowspan="2">批量改密</td>
<td>定期批量改密:small_orange_diamond:</td>
</tr>
<tr>
<td>多种密码策略</td>
<td>多种密码策略:small_orange_diamond:</td>
</tr>
<tr>
<td>多云纳管X-PACK</td>
<td>对私有云、公有云资产自动统一纳管</td>
<td>多云纳管 </td>
<td>对私有云、公有云资产自动统一纳管:small_orange_diamond:</td>
</tr>
<tr>
<td>收集用户X-PACK</td>
<td>自定义任务定期收集主机用户</td>
<td>收集用户 </td>
<td>自定义任务定期收集主机用户:small_orange_diamond:</td>
</tr>
<tr>
<td>密码匣子X-PACK</td>
<td>统一对资产主机的用户密码进行查看、更新、测试操作</td>
<td>密码匣子 </td>
<td>统一对资产主机的用户密码进行查看、更新、测试操作:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="15">授权控制<br>Authorization</td>
@@ -138,7 +126,7 @@ v2.1.0 是 v2.0.0 之后的功能版本。
<td>实现更细粒度的应用级授权</td>
</tr>
<tr>
<td>MySQL 数据库应用、RemoteApp 远程应用X-PACK</td>
<td>MySQL 数据库应用、RemoteApp 远程应用:small_orange_diamond: </td>
</tr>
<tr>
<td>动作授权</td>
@@ -165,12 +153,12 @@ v2.1.0 是 v2.0.0 之后的功能版本。
<td>实现 Web SFTP 文件管理</td>
</tr>
<tr>
<td>工单管理X-PACK</td>
<td>支持对用户登录请求行为进行控制</td>
<td>工单管理</td>
<td>支持对用户登录请求行为进行控制:small_orange_diamond:</td>
</tr>
<tr>
<td>组织管理X-PACK</td>
<td>实现多租户管理与权限隔离</td>
<td>组织管理</td>
<td>实现多租户管理与权限隔离:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="7">安全审计<br>Audit</td>
@@ -189,7 +177,7 @@ v2.1.0 是 v2.0.0 之后的功能版本。
<td>支持对 Linux、Windows 等资产操作的录像进行回放审计</td>
</tr>
<tr>
<td>支持对 RemoteAppX-PACK、MySQL 等应用操作的录像进行回放审计</td>
<td>支持对 RemoteApp:small_orange_diamond:、MySQL 等应用操作的录像进行回放审计</td>
</tr>
<tr>
<td>指令审计</td>
@@ -205,7 +193,7 @@ v2.1.0 是 v2.0.0 之后的功能版本。
<td>命令方式</td>
</tr>
<tr>
<td>Web UI方式 (X-PACK)</td>
<td>Web UI方式 :small_orange_diamond:</td>
</tr>
<tr>
@@ -213,13 +201,13 @@ v2.1.0 是 v2.0.0 之后的功能版本。
<td>MySQL</td>
</tr>
<tr>
<td>Oracle (X-PACK)</td>
<td>Oracle :small_orange_diamond:</td>
</tr>
<tr>
<td>MariaDB (X-PACK)</td>
<td>MariaDB :small_orange_diamond:</td>
</tr>
<tr>
<td>PostgreSQL (X-PACK)</td>
<td>PostgreSQL :small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="6">功能亮点</td>
@@ -249,6 +237,8 @@ v2.1.0 是 v2.0.0 之后的功能版本。
</tr>
</table>
**说明**: 带 :small_orange_diamond: 后缀的是 X-PACK 插件有的功能
## 快速开始
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
@@ -258,9 +248,19 @@ v2.1.0 是 v2.0.0 之后的功能版本。
## 组件项目
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
- [Koko](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
## 贡献
如果有你好的想法创意,或者帮助我们修复了 Bug, 欢迎提交 Pull Request
感谢以下贡献者,让 JumpServer 更加完善
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
## 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化连接依赖
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库JumpServer Web数据库依赖
@@ -268,7 +268,6 @@ v2.1.0 是 v2.0.0 之后的功能版本。
## JumpServer 企业版
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
> 注:企业版支持离线安装,申请通过后会提供高速下载链接。
## 案例研究

View File

@@ -1,22 +1,135 @@
## Jumpserver
![Total visitor](https://visitor-count-badge.herokuapp.com/total.svg?repo_id=jumpserver)
![Visitors in today](https://visitor-count-badge.herokuapp.com/today.svg?repo_id=jumpserver)
[![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.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Ansible](https://img.shields.io/badge/ansible-2.4.2.0-blue.svg?style=plastic)](https://www.ansible.com/)
[![Paramiko](https://img.shields.io/badge/paramiko-2.4.1-green.svg?style=plastic)](http://www.paramiko.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)
----
## CRITICAL BUG WARNING
Recently we have found a critical bug for remote execution vulnerability which leads to pre-auth and info leak, please fix it as soon as possible.
Thanks for **reactivity from Alibaba Hackerone bug bounty program** report us this bug
**Vulnerable version:**
```
< v2.6.2
< v2.5.4
< v2.4.5
= v1.5.9
>= v1.5.3
```
**Safe and Stable version:**
```
>= v2.6.2
>= v2.5.4
>= v2.4.5
= v1.5.9 version tag didn't change
< v1.5.3
```
**Bug Fix Solution:**
Upgrade to the latest version or the version mentioned above
**Temporary Solution (upgrade asap):**
Modify the Nginx config file and disable the vulnerable api listed below
```
/api/v1/authentication/connection-token/
/api/v1/users/connection-token/
```
Path to Nginx config file
```
# Previous Community version
/etc/nginx/conf.d/jumpserver.conf
# Previous Enterprise version
jumpserver-release/nginx/http_server.conf
# Latest version
jumpserver-release/compose/config_static/http_server.conf
```
Changes in Nginx config file
```
### Put the following code on top of location server, or before /api and /
location /api/v1/authentication/connection-token/ {
return 403;
}
location /api/v1/users/connection-token/ {
return 403;
}
### End right here
location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://core:8080;
}
...
```
Save the file and restart Nginx
```
docker deployment:
$ docker restart jms_nginx
rpm or other deployment:
$ systemctl restart nginx
```
**Bug Fix Verification**
```
# Download the following script to check if it is fixed
$ wget https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_bug_check.sh
# Run the code to verify it
$ bash jms_bug_check.sh demo.jumpserver.org
漏洞已修复 (It means the bug is fixed)
漏洞未修复 (It means the bug is not fixed and the system is still vulnerable)
```
**Attack Simulation**
Go to the logs directory which should contain gunicorn.log file. Then download the "attack" script and execute it
```
$ pwd
/opt/jumpserver/core/logs
$ ls gunicorn.log
gunicorn.log
$ wget 'https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_check_attack.sh'
$ bash jms_check_attack.sh
系统未被入侵 (It means the system is safe)
系统已被入侵 (It means the system is being attacked)
```
--------------------------
----
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README.md)
Jumpserver is the first fully open source bastion in the world, based on the GNU GPL v2.0 open source protocol. Jumpserver is a professional operation and maintenance audit system conforms to 4A specifications.
Jumpserver is the world's first open-source PAM (Privileged Access Management System) and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
Jumpserver is developed using Python / Django, conforms to the Web 2.0 specification, and is equipped with the industry-leading Web Terminal solution which have beautiful interface and great user experience.
Jumpserver uses Python / Django for development, follows Web 2.0 specifications, and is equipped with an industry-leading Web Terminal solution that provides a beautiful user interface and great user experience
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple cross-regional areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
Change the world, starting from little things.
@@ -47,7 +160,7 @@ We provide online demo, demo video and screenshots to get you started quickly.
We provide the SDK for your other systems to quickly interact with the Jumpserver API.
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK thanks to 恺珺 for provide Java SDK
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) Thanks to 恺珺 for providing his Java SDK vesrion.
### License & Copyright

3
apps/acls/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,3 @@
from .login_acl import *
from .login_asset_acl import *
from .login_asset_check import *

View File

@@ -0,0 +1,19 @@
from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember
from common.drf.api import JMSBulkModelViewSet
from ..models import LoginACL
from .. import serializers
__all__ = ['LoginACLViewSet', ]
class LoginACLViewSet(JMSBulkModelViewSet):
queryset = LoginACL.objects.all()
filterset_fields = ('name', 'user', )
search_fields = filterset_fields
permission_classes = (IsOrgAdmin, )
serializer_class = serializers.LoginACLSerializer
def get_permissions(self):
if self.action in ["retrieve", "list"]:
self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember)
return super().get_permissions()

View File

@@ -0,0 +1,15 @@
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin
from .. import models, serializers
__all__ = ['LoginAssetACLViewSet']
class LoginAssetACLViewSet(OrgBulkModelViewSet):
model = models.LoginAssetACL
filterset_fields = ('name', )
search_fields = filterset_fields
permission_classes = (IsOrgAdmin, )
serializer_class = serializers.LoginAssetACLSerializer

View File

@@ -0,0 +1,105 @@
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView, RetrieveDestroyAPIView
from common.permissions import IsAppUser
from common.utils import reverse, lazyproperty
from orgs.utils import tmp_to_org, tmp_to_root_org
from tickets.models import Ticket
from ..models import LoginAssetACL
from .. import serializers
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
class LoginAssetCheckAPI(CreateAPIView):
permission_classes = (IsAppUser, )
serializer_class = serializers.LoginAssetCheckSerializer
def create(self, request, *args, **kwargs):
is_need_confirm, response_data = self.check_if_need_confirm()
return Response(data=response_data, status=200)
def check_if_need_confirm(self):
queries = {
'user': self.serializer.user, 'asset': self.serializer.asset,
'system_user': self.serializer.system_user,
'action': LoginAssetACL.ActionChoices.login_confirm
}
with tmp_to_org(self.serializer.org):
acl = LoginAssetACL.filter(**queries).valid().first()
if not acl:
is_need_confirm = False
response_data = {}
else:
is_need_confirm = True
response_data = self._get_response_data_of_need_confirm(acl)
response_data['need_confirm'] = is_need_confirm
return is_need_confirm, response_data
def _get_response_data_of_need_confirm(self, acl):
ticket = LoginAssetACL.create_login_asset_confirm_ticket(
user=self.serializer.user,
asset=self.serializer.asset,
system_user=self.serializer.system_user,
assignees=acl.reviewers.all(),
org_id=self.serializer.org.id
)
confirm_status_url = reverse(
view_name='acls:login-asset-confirm-status',
kwargs={'pk': str(ticket.id)}
)
ticket_detail_url = reverse(
view_name='api-tickets:ticket-detail',
kwargs={'pk': str(ticket.id)},
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
'ticket_detail_url': ticket_detail_url,
'reviewers': [str(user) for user in ticket.assignees.all()],
}
return data
@lazyproperty
def serializer(self):
serializer = self.get_serializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
return serializer
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
permission_classes = (IsAppUser, )
def retrieve(self, request, *args, **kwargs):
if self.ticket.action_open:
status = 'await'
elif self.ticket.action_approve:
status = 'approve'
else:
status = 'reject'
data = {
'status': status,
'action': self.ticket.action,
'processor': self.ticket.processor_display
}
return Response(data=data, status=200)
def destroy(self, request, *args, **kwargs):
if self.ticket.status_open:
self.ticket.close(processor=self.ticket.applicant)
data = {
'action': self.ticket.action,
'status': self.ticket.status,
'processor': self.ticket.processor_display
}
return Response(data=data, status=200)
@lazyproperty
def ticket(self):
with tmp_to_root_org():
return get_object_or_404(Ticket, pk=self.kwargs['pk'])

5
apps/acls/apps.py Normal file
View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AclsConfig(AppConfig):
name = 'acls'

9
apps/acls/const.py Normal file
View File

@@ -0,0 +1,9 @@
from django.utils.translation import ugettext as _
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
ip_group_help_text = common_help_text + _(
'Such as: '
'192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 '
)

View File

@@ -0,0 +1,61 @@
# Generated by Django 3.1 on 2021-03-11 09:53
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='LoginACL',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('ip_group', models.JSONField(default=list, verbose_name='Login IP')),
('action', models.CharField(choices=[('reject', 'Reject'), ('allow', 'Allow')], default='reject', max_length=64, verbose_name='Action')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_acls', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'ordering': ('priority', '-date_updated', 'name'),
},
),
migrations.CreateModel(
name='LoginAssetACL',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('users', models.JSONField(verbose_name='User')),
('system_users', models.JSONField(verbose_name='System User')),
('assets', models.JSONField(verbose_name='Asset')),
('action', models.CharField(choices=[('login_confirm', 'Login confirm')], default='login_confirm', max_length=64, verbose_name='Action')),
('reviewers', models.ManyToManyField(blank=True, related_name='review_login_asset_acls', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
],
options={
'ordering': ('priority', '-date_updated', 'name'),
'unique_together': {('name', 'org_id')},
},
),
]

View File

View File

@@ -0,0 +1,2 @@
from .login_acl import *
from .login_asset_acl import *

35
apps/acls/models/base.py Normal file
View File

@@ -0,0 +1,35 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from common.mixins import CommonModelMixin
__all__ = ['BaseACL', 'BaseACLQuerySet']
class BaseACLQuerySet(models.QuerySet):
def active(self):
return self.filter(is_active=True)
def inactive(self):
return self.filter(is_active=False)
def valid(self):
return self.active()
def invalid(self):
return self.inactive()
class BaseACL(CommonModelMixin):
name = models.CharField(max_length=128, verbose_name=_('Name'))
priority = models.IntegerField(
default=50, verbose_name=_("Priority"),
help_text=_("1-100, the lower the value will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)]
)
is_active = models.BooleanField(default=True, verbose_name=_("Active"))
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
class Meta:
abstract = True

View File

@@ -0,0 +1,54 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .base import BaseACL, BaseACLQuerySet
from ..utils import contains_ip
class ACLManager(models.Manager):
def valid(self):
return self.get_queryset().valid()
class LoginACL(BaseACL):
class ActionChoices(models.TextChoices):
reject = 'reject', _('Reject')
allow = 'allow', _('Allow')
# 条件
ip_group = models.JSONField(default=list, verbose_name=_('Login IP'))
# 动作
action = models.CharField(
max_length=64, choices=ActionChoices.choices, default=ActionChoices.reject,
verbose_name=_('Action')
)
# 关联
user = models.ForeignKey(
'users.User', on_delete=models.CASCADE, related_name='login_acls', verbose_name=_('User')
)
objects = ACLManager.from_queryset(BaseACLQuerySet)()
class Meta:
ordering = ('priority', '-date_updated', 'name')
@property
def action_reject(self):
return self.action == self.ActionChoices.reject
@property
def action_allow(self):
return self.action == self.ActionChoices.allow
@staticmethod
def allow_user_to_login(user, ip):
acl = user.login_acls.valid().first()
if not acl:
return True
is_contained = contains_ip(ip, acl.ip_group)
if acl.action_allow and is_contained:
return True
if acl.action_reject and not is_contained:
return True
return False

View File

@@ -0,0 +1,99 @@
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import BaseACL, BaseACLQuerySet
from ..utils import contains_ip
class ACLManager(OrgManager):
def valid(self):
return self.get_queryset().valid()
class LoginAssetACL(BaseACL, OrgModelMixin):
class ActionChoices(models.TextChoices):
login_confirm = 'login_confirm', _('Login confirm')
# 条件
users = models.JSONField(verbose_name=_('User'))
system_users = models.JSONField(verbose_name=_('System User'))
assets = models.JSONField(verbose_name=_('Asset'))
# 动作
action = models.CharField(
max_length=64, choices=ActionChoices.choices, default=ActionChoices.login_confirm,
verbose_name=_('Action')
)
# 动作: 附加字段
# - login_confirm
reviewers = models.ManyToManyField(
'users.User', related_name='review_login_asset_acls', blank=True,
verbose_name=_("Reviewers")
)
objects = ACLManager.from_queryset(BaseACLQuerySet)()
class Meta:
unique_together = ('name', 'org_id')
ordering = ('priority', '-date_updated', 'name')
@classmethod
def filter(cls, user, asset, system_user, action):
queryset = cls.objects.filter(action=action)
queryset = cls.filter_user(user, queryset)
queryset = cls.filter_asset(asset, queryset)
queryset = cls.filter_system_user(system_user, queryset)
return queryset
@classmethod
def filter_user(cls, user, queryset):
queryset = queryset.filter(
Q(users__username_group__contains=user.username) |
Q(users__username_group__contains='*')
)
return queryset
@classmethod
def filter_asset(cls, asset, queryset):
queryset = queryset.filter(
Q(assets__hostname_group__contains=asset.hostname) |
Q(assets__hostname_group__contains='*')
)
ids = [q.id for q in queryset if contains_ip(asset.ip, q.assets.get('ip_group', []))]
queryset = cls.objects.filter(id__in=ids)
return queryset
@classmethod
def filter_system_user(cls, system_user, queryset):
queryset = queryset.filter(
Q(system_users__name_group__contains=system_user.name) |
Q(system_users__name_group__contains='*')
).filter(
Q(system_users__username_group__contains=system_user.username) |
Q(system_users__username_group__contains='*')
).filter(
Q(system_users__protocol_group__contains=system_user.protocol) |
Q(system_users__protocol_group__contains='*')
)
return queryset
@classmethod
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
from tickets.const import TicketTypeChoices
from tickets.models import Ticket
data = {
'title': _('Login asset confirm') + ' ({})'.format(user),
'type': TicketTypeChoices.login_asset_confirm,
'meta': {
'apply_login_user': str(user),
'apply_login_asset': str(asset),
'apply_login_system_user': str(system_user),
},
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
ticket.assignees.set(assignees)
ticket.open(applicant=user)
return ticket

View File

@@ -0,0 +1,3 @@
from .login_acl import *
from .login_asset_acl import *
from .login_asset_check import *

View File

@@ -0,0 +1,49 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from orgs.utils import current_org
from ..models import LoginACL
from ..utils import is_ip_address, is_ip_network, is_ip_segment
from .. import const
__all__ = ['LoginACLSerializer', ]
def ip_group_child_validator(ip_group_child):
is_valid = ip_group_child == '*' \
or is_ip_address(ip_group_child) \
or is_ip_network(ip_group_child) \
or is_ip_segment(ip_group_child)
if not is_valid:
error = _('IP address invalid: `{}`').format(ip_group_child)
raise serializers.ValidationError(error)
class LoginACLSerializer(BulkModelSerializer):
ip_group = serializers.ListField(
default=['*'], label=_('IP'), help_text=const.ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
)
user_display = serializers.ReadOnlyField(source='user.name', label=_('User'))
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
class Meta:
model = LoginACL
fields = [
'id', 'name', 'priority', 'ip_group', 'user', 'user_display', 'action',
'action_display', 'is_active', 'comment', 'created_by', 'date_created', 'date_updated'
]
extra_kwargs = {
'priority': {'default': 50},
'is_active': {'default': True},
}
@staticmethod
def validate_user(user):
if user not in current_org.get_members():
error = _('The user `{}` is not in the current organization: `{}`').format(
user, current_org
)
raise serializers.ValidationError(error)
return user

View File

@@ -0,0 +1,87 @@
from rest_framework import serializers
from django.utils.translation import ugettext as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import SystemUser
from acls import models
from orgs.models import Organization
from .. import const
__all__ = ['LoginAssetACLSerializer']
class LoginAssetACLUsersSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
help_text=const.common_help_text
)
class LoginAssetACLAssestsSerializer(serializers.Serializer):
ip_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'),
help_text=const.ip_group_help_text + _('(Domain name support)')
)
hostname_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'),
help_text=const.common_help_text
)
class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
name_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Name'),
help_text=const.common_help_text
)
username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
help_text=const.common_help_text
)
protocol_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=16), label=_('Protocol'),
help_text=const.common_help_text + _('Protocol options: {}').format(
', '.join(SystemUser.ASSET_CATEGORY_PROTOCOLS)
)
)
@staticmethod
def validate_protocol_group(protocol_group):
unsupported_protocols = set(protocol_group) - set(SystemUser.ASSET_CATEGORY_PROTOCOLS + ['*'])
if unsupported_protocols:
error = _('Unsupported protocols: {}').format(unsupported_protocols)
raise serializers.ValidationError(error)
return protocol_group
class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
users = LoginAssetACLUsersSerializer()
assets = LoginAssetACLAssestsSerializer()
system_users = LoginAssetACLSystemUsersSerializer()
reviewers_amount = serializers.IntegerField(read_only=True, source='reviewers.count')
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
class Meta:
model = models.LoginAssetACL
fields = [
'id', 'name', 'priority', 'users', 'system_users', 'assets', 'action', 'action_display',
'is_active', 'comment', 'reviewers', 'reviewers_amount', 'created_by', 'date_created',
'date_updated', 'org_id'
]
extra_kwargs = {
"reviewers": {'allow_null': False, 'required': True},
'priority': {'default': 50},
'is_active': {'default': True},
}
def validate_reviewers(self, reviewers):
org_id = self.fields['org_id'].default()
org = Organization.get_instance(org_id)
if not org:
error = _('The organization `{}` does not exist'.format(org_id))
raise serializers.ValidationError(error)
users = org.get_members()
valid_reviewers = list(set(reviewers) & set(users))
if not valid_reviewers:
error = _('None of the reviewers belong to Organization `{}`'.format(org.name))
raise serializers.ValidationError(error)
return valid_reviewers

View File

@@ -0,0 +1,71 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orgs.utils import tmp_to_root_org
from common.utils import get_object_or_none, lazyproperty
from users.models import User
from assets.models import Asset, SystemUser
__all__ = ['LoginAssetCheckSerializer']
class LoginAssetCheckSerializer(serializers.Serializer):
user_id = serializers.UUIDField(required=True, allow_null=False)
asset_id = serializers.UUIDField(required=True, allow_null=False)
system_user_id = serializers.UUIDField(required=True, allow_null=False)
system_user_username = serializers.CharField(max_length=128, default='')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
self.asset = None
self._system_user = None
self._system_user_username = None
def validate_user_id(self, user_id):
self.user = self.validate_object_exist(User, user_id)
return user_id
def validate_asset_id(self, asset_id):
self.asset = self.validate_object_exist(Asset, asset_id)
return asset_id
def validate_system_user_id(self, system_user_id):
self._system_user = self.validate_object_exist(SystemUser, system_user_id)
return system_user_id
def validate_system_user_username(self, system_user_username):
system_user_id = self.initial_data.get('system_user_id')
system_user = self.validate_object_exist(SystemUser, system_user_id)
if self._system_user.login_mode == SystemUser.LOGIN_MANUAL \
and not system_user.username \
and not system_user.username_same_with_user \
and not system_user_username:
error = 'Missing parameter: system_user_username'
raise serializers.ValidationError(error)
self._system_user_username = system_user_username
return system_user_username
@staticmethod
def validate_object_exist(model, field_id):
with tmp_to_root_org():
obj = get_object_or_none(model, pk=field_id)
if not obj:
error = '{} Model object does not exist'.format(model.__name__)
raise serializers.ValidationError(error)
return obj
@lazyproperty
def system_user(self):
if self._system_user.username_same_with_user:
username = self.user.username
elif self._system_user.login_mode == SystemUser.LOGIN_MANUAL:
username = self._system_user_username
else:
username = self._system_user.username
self._system_user.username = username
return self._system_user
@lazyproperty
def org(self):
return self.asset.org

3
apps/acls/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1 @@
from .api_urls import *

View File

@@ -0,0 +1,18 @@
from django.urls import path
from rest_framework_bulk.routes import BulkRouter
from .. import api
app_name = 'acls'
router = BulkRouter()
router.register(r'login-acls', api.LoginACLViewSet, 'login-acl')
router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl')
urlpatterns = [
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
path('login-asset-confirm/<uuid:pk>/status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status')
]
urlpatterns += router.urls

68
apps/acls/utils.py Normal file
View File

@@ -0,0 +1,68 @@
from ipaddress import ip_network, ip_address
def is_ip_address(address):
""" 192.168.10.1 """
try:
ip_address(address)
except ValueError:
return False
else:
return True
def is_ip_network(ip):
""" 192.168.1.0/24 """
try:
ip_network(ip)
except ValueError:
return False
else:
return True
def is_ip_segment(ip):
""" 10.1.1.1-10.1.1.20 """
if '-' not in ip:
return False
ip_address1, ip_address2 = ip.split('-')
return is_ip_address(ip_address1) and is_ip_address(ip_address2)
def in_ip_segment(ip, ip_segment):
ip1, ip2 = ip_segment.split('-')
ip1 = int(ip_address(ip1))
ip2 = int(ip_address(ip2))
ip = int(ip_address(ip))
return min(ip1, ip2) <= ip <= max(ip1, ip2)
def contains_ip(ip, ip_group):
"""
ip_group:
[192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64.]
"""
if '*' in ip_group:
return True
for _ip in ip_group:
if is_ip_address(_ip):
# 192.168.10.1
if ip == _ip:
return True
elif is_ip_network(_ip) and is_ip_address(ip):
# 192.168.1.0/24
if ip_address(ip) in ip_network(_ip):
return True
elif is_ip_segment(_ip) and is_ip_address(ip):
# 10.1.1.1-10.1.1.20
if in_ip_segment(ip, _ip):
return True
else:
# is domain name
if ip == _ip:
return True
return False

View File

@@ -1,5 +1,3 @@
from .application import *
from .mixin import *
from .remote_app import *
from .database_app import *
from .k8s_app import *

View File

@@ -3,18 +3,17 @@
from orgs.mixins.api import OrgBulkModelViewSet
from .mixin import ApplicationAttrsSerializerViewMixin
from ..hands import IsOrgAdminOrAppUser
from .. import models, serializers
__all__ = [
'ApplicationViewSet',
]
__all__ = ['ApplicationViewSet']
class ApplicationViewSet(ApplicationAttrsSerializerViewMixin, OrgBulkModelViewSet):
class ApplicationViewSet(OrgBulkModelViewSet):
model = models.Application
filter_fields = ('name', 'type', 'category')
search_fields = filter_fields
filterset_fields = ('name', 'type', 'category')
search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationSerializer

View File

@@ -1,20 +0,0 @@
# coding: utf-8
#
from orgs.mixins.api import OrgBulkModelViewSet
from .. import models
from .. import serializers
from ..hands import IsOrgAdminOrAppUser
__all__ = [
'DatabaseAppViewSet',
]
class DatabaseAppViewSet(OrgBulkModelViewSet):
model = models.DatabaseApp
filter_fields = ('name',)
search_fields = filter_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.DatabaseAppSerializer

View File

@@ -1,20 +0,0 @@
# coding: utf-8
#
from orgs.mixins.api import OrgBulkModelViewSet
from .. import models
from .. import serializers
from ..hands import IsOrgAdminOrAppUser
__all__ = [
'K8sAppViewSet',
]
class K8sAppViewSet(OrgBulkModelViewSet):
model = models.K8sApp
filter_fields = ('name',)
search_fields = filter_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.K8sAppSerializer

View File

@@ -1,48 +1,7 @@
from common.exceptions import JMSException
from .. import models
from orgs.models import Organization
class ApplicationAttrsSerializerViewMixin:
def get_serializer_class(self):
serializer_class = super().get_serializer_class()
app_type = self.request.query_params.get('type')
app_category = self.request.query_params.get('category')
type_options = list(dict(models.Category.get_all_type_serializer_mapper()).keys())
category_options = list(dict(models.Category.get_category_serializer_mapper()).keys())
# ListAPIView 没有 action 属性
# 不使用method属性因为options请求时为method为post
action = getattr(self, 'action', 'list')
if app_type and app_type not in type_options:
raise JMSException(
'Invalid query parameter `type`, select from the following options: {}'
''.format(type_options)
)
if app_category and app_category not in category_options:
raise JMSException(
'Invalid query parameter `category`, select from the following options: {}'
''.format(category_options)
)
if action in [
'create', 'update', 'partial_update', 'bulk_update', 'partial_bulk_update'
] and not app_type:
# action: create / update
raise JMSException(
'The `{}` action must take the `type` query parameter'.format(action)
)
if app_type:
# action: create / update / list / retrieve / metadata
attrs_cls = models.Category.get_type_serializer_cls(app_type)
elif app_category:
# action: list / retrieve / metadata
attrs_cls = models.Category.get_category_serializer_cls(app_category)
else:
attrs_cls = models.Category.get_no_password_serializer_cls()
return type('ApplicationDynamicSerializer', (serializer_class,), {'attrs': attrs_cls()})
__all__ = ['SerializeApplicationToTreeNodeMixin']
class SerializeApplicationToTreeNodeMixin:
@@ -85,11 +44,46 @@ class SerializeApplicationToTreeNodeMixin:
'meta': {'type': 'k8s_app'}
}
def _serialize(self, application):
def _serialize_application(self, application):
method_name = f'_serialize_{application.category}'
data = getattr(self, method_name)(application)
data.update({
'pId': application.org.id,
'org_name': application.org_name
})
return data
def serialize_applications(self, applications):
data = [self._serialize(application) for application in applications]
data = [self._serialize_application(application) for application in applications]
return data
@staticmethod
def _serialize_organization(org):
return {
'id': org.id,
'name': org.name,
'title': org.name,
'pId': '',
'open': True,
'isParent': True,
'meta': {
'type': 'node'
}
}
def serialize_organizations(self, organizations):
data = [self._serialize_organization(org) for org in organizations]
return data
@staticmethod
def filter_organizations(applications):
organization_ids = set(applications.values_list('org_id', flat=True))
organizations = [Organization.get_instance(org_id) for org_id in organization_ids]
return organizations
def serialize_applications_with_org(self, applications):
organizations = self.filter_organizations(applications)
data_organizations = self.serialize_organizations(organizations)
data_applications = self.serialize_applications(applications)
data = data_organizations + data_applications
return data

View File

@@ -1,40 +1,19 @@
# coding: utf-8
#
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from common.exceptions import JMSException
from ..hands import IsOrgAdmin, IsAppUser
from ..hands import IsAppUser
from .. import models
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
from ..serializers import RemoteAppConnectionInfoSerializer
from ..permissions import IsRemoteApp
__all__ = [
'RemoteAppViewSet', 'RemoteAppConnectionInfoApi',
'RemoteAppConnectionInfoApi',
]
class RemoteAppViewSet(OrgBulkModelViewSet):
model = models.RemoteApp
filter_fields = ('name', 'type', 'comment')
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = RemoteAppSerializer
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
model = models.Application
permission_classes = (IsAppUser, )
permission_classes = (IsAppUser, IsRemoteApp)
serializer_class = RemoteAppConnectionInfoSerializer
@staticmethod
def check_category_allowed(obj):
if not obj.category_is_remote_app:
raise JMSException(
'The request instance(`{}`) is not of category `remote_app`'.format(obj.category)
)
def get_object(self):
obj = super().get_object()
self.check_category_allowed(obj)
return obj

View File

@@ -1,64 +1,49 @@
# coding: utf-8
#
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
# RemoteApp
class ApplicationCategoryChoices(TextChoices):
db = 'db', _('Database')
remote_app = 'remote_app', _('Remote app')
cloud = 'cloud', 'Cloud'
REMOTE_APP_BOOT_PROGRAM_NAME = '||jmservisor'
REMOTE_APP_TYPE_CHROME = 'chrome'
REMOTE_APP_TYPE_MYSQL_WORKBENCH = 'mysql_workbench'
REMOTE_APP_TYPE_VMWARE_CLIENT = 'vmware_client'
REMOTE_APP_TYPE_CUSTOM = 'custom'
# Fields attribute write_only default => False
REMOTE_APP_TYPE_CHROME_FIELDS = [
{'name': 'chrome_target'},
{'name': 'chrome_username'},
{'name': 'chrome_password', 'write_only': True}
]
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
{'name': 'mysql_workbench_ip'},
{'name': 'mysql_workbench_name'},
{'name': 'mysql_workbench_port'},
{'name': 'mysql_workbench_username'},
{'name': 'mysql_workbench_password', 'write_only': True}
]
REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS = [
{'name': 'vmware_target'},
{'name': 'vmware_username'},
{'name': 'vmware_password', 'write_only': True}
]
REMOTE_APP_TYPE_CUSTOM_FIELDS = [
{'name': 'custom_cmdline'},
{'name': 'custom_target'},
{'name': 'custom_username'},
{'name': 'custom_password', 'write_only': True}
]
REMOTE_APP_TYPE_FIELDS_MAP = {
REMOTE_APP_TYPE_CHROME: REMOTE_APP_TYPE_CHROME_FIELDS,
REMOTE_APP_TYPE_MYSQL_WORKBENCH: REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS,
REMOTE_APP_TYPE_VMWARE_CLIENT: REMOTE_APP_TYPE_VMWARE_CLIENT_FIELDS,
REMOTE_APP_TYPE_CUSTOM: REMOTE_APP_TYPE_CUSTOM_FIELDS
}
REMOTE_APP_TYPE_CHOICES = (
(REMOTE_APP_TYPE_CHROME, 'Chrome'),
(REMOTE_APP_TYPE_MYSQL_WORKBENCH, 'MySQL Workbench'),
(REMOTE_APP_TYPE_VMWARE_CLIENT, 'vSphere Client'),
(REMOTE_APP_TYPE_CUSTOM, _('Custom')),
)
@classmethod
def get_label(cls, category):
return dict(cls.choices).get(category, '')
# DatabaseApp
class ApplicationTypeChoices(TextChoices):
# db category
mysql = 'mysql', 'MySQL'
oracle = 'oracle', 'Oracle'
pgsql = 'postgresql', 'PostgreSQL'
mariadb = 'mariadb', 'MariaDB'
# remote-app category
chrome = 'chrome', 'Chrome'
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
vmware_client = 'vmware_client', 'vSphere Client'
custom = 'custom', _('Custom')
DATABASE_APP_TYPE_MYSQL = 'mysql'
# cloud category
k8s = 'k8s', 'Kubernetes'
@classmethod
def get_label(cls, tp):
return dict(cls.choices).get(tp, '')
@classmethod
def db_types(cls):
return [cls.mysql.value, cls.oracle.value, cls.pgsql.value, cls.mariadb.value]
@classmethod
def remote_app_types(cls):
return [cls.chrome.value, cls.mysql_workbench.value, cls.vmware_client.value, cls.custom.value]
@classmethod
def cloud_types(cls):
return [cls.k8s.value]
DATABASE_APP_TYPE_CHOICES = (
(DATABASE_APP_TYPE_MYSQL, 'MySQL'),
)

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1 on 2020-11-19 03:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0006_application'),
]
operations = [
migrations.AlterField(
model_name='application',
name='attrs',
field=models.JSONField(),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.1 on 2021-01-03 20:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('perms', '0017_auto_20210104_0435'),
('applications', '0007_auto_20201119_1110'),
]
operations = [
migrations.DeleteModel(
name='DatabaseApp',
),
migrations.DeleteModel(
name='K8sApp',
),
migrations.AlterField(
model_name='application',
name='attrs',
field=models.JSONField(default=dict, verbose_name='Attrs'),
),
migrations.DeleteModel(
name='RemoteApp',
),
]

View File

@@ -1,4 +1 @@
from .application import *
from .remote_app import *
from .database_app import *
from .k8s_app import *

View File

@@ -1,129 +1,25 @@
from itertools import chain
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django_mysql.models import JSONField, QuerySet
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from common.db.models import ChoiceSet
class DBType(ChoiceSet):
mysql = 'mysql', 'MySQL'
oracle = 'oracle', 'Oracle'
pgsql = 'postgresql', 'PostgreSQL'
mariadb = 'mariadb', 'MariaDB'
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import database_app
mapper = {
cls.mysql: database_app.MySQLAttrsSerializer,
cls.oracle: database_app.OracleAttrsSerializer,
cls.pgsql: database_app.PostgreAttrsSerializer,
cls.mariadb: database_app.MariaDBAttrsSerializer,
}
return mapper
class RemoteAppType(ChoiceSet):
chrome = 'chrome', 'Chrome'
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
vmware_client = 'vmware_client', 'vSphere Client'
custom = 'custom', _('Custom')
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import remote_app
mapper = {
cls.chrome: remote_app.ChromeAttrsSerializer,
cls.mysql_workbench: remote_app.MySQLWorkbenchAttrsSerializer,
cls.vmware_client: remote_app.VMwareClientAttrsSerializer,
cls.custom: remote_app.CustomRemoteAppAttrsSeralizers,
}
return mapper
class CloudType(ChoiceSet):
k8s = 'k8s', 'Kubernetes'
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import k8s_app
mapper = {
cls.k8s: k8s_app.K8sAttrsSerializer,
}
return mapper
class Category(ChoiceSet):
db = 'db', _('Database')
remote_app = 'remote_app', _('Remote app')
cloud = 'cloud', 'Cloud'
@classmethod
def get_category_type_mapper(cls):
return {
cls.db: DBType,
cls.remote_app: RemoteAppType,
cls.cloud: CloudType
}
@classmethod
def get_category_type_choices_mapper(cls):
return {
name: tp.choices
for name, tp in cls.get_category_type_mapper().items()
}
@classmethod
def get_type_choices(cls, category):
return cls.get_category_type_choices_mapper().get(category, [])
@classmethod
def get_all_type_choices(cls):
all_grouped_choices = tuple(cls.get_category_type_choices_mapper().values())
return tuple(chain(*all_grouped_choices))
@classmethod
def get_all_type_serializer_mapper(cls):
mapper = {}
for tp in cls.get_category_type_mapper().values():
mapper.update(tp.get_type_serializer_cls_mapper())
return mapper
@classmethod
def get_type_serializer_cls(cls, tp):
mapper = cls.get_all_type_serializer_mapper()
return mapper.get(tp, None)
@classmethod
def get_category_serializer_mapper(cls):
from ..serializers import remote_app, database_app, k8s_app
return {
cls.db: database_app.DBAttrsSerializer,
cls.remote_app: remote_app.RemoteAppAttrsSerializer,
cls.cloud: k8s_app.CloudAttrsSerializer,
}
@classmethod
def get_category_serializer_cls(cls, cg):
mapper = cls.get_category_serializer_mapper()
return mapper.get(cg, None)
@classmethod
def get_no_password_serializer_cls(cls):
from ..serializers import common
return common.NoPasswordSerializer
from assets.models import Asset
from .. import const
class Application(CommonModelMixin, OrgModelMixin):
name = models.CharField(max_length=128, verbose_name=_('Name'))
domain = models.ForeignKey('assets.Domain', null=True, blank=True, related_name='applications', verbose_name=_("Domain"), on_delete=models.SET_NULL)
category = models.CharField(max_length=16, choices=Category.choices, verbose_name=_('Category'))
type = models.CharField(max_length=16, choices=Category.get_all_type_choices(), verbose_name=_('Type'))
attrs = JSONField()
category = models.CharField(
max_length=16, choices=const.ApplicationCategoryChoices.choices, verbose_name=_('Category')
)
type = models.CharField(
max_length=16, choices=const.ApplicationTypeChoices.choices, verbose_name=_('Type')
)
domain = models.ForeignKey(
'assets.Domain', null=True, blank=True, related_name='applications',
on_delete=models.SET_NULL, verbose_name=_("Domain"),
)
attrs = models.JSONField(default=dict, verbose_name=_('Attrs'))
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
@@ -137,5 +33,38 @@ class Application(CommonModelMixin, OrgModelMixin):
type_display = self.get_type_display()
return f'{self.name}({type_display})[{category_display}]'
def category_is_remote_app(self):
return self.category == Category.remote_app
@property
def category_remote_app(self):
return self.category == const.ApplicationCategoryChoices.remote_app.value
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app:
raise ValueError(f"Not a remote app application: {self.name}")
serializer_class = get_serializer_class_by_application_type(self.type)
fields = serializer_class().get_fields()
parameters = [self.type]
for field_name in list(fields.keys()):
if field_name in ['asset']:
continue
value = self.attrs.get(field_name)
if not value:
continue
if field_name == 'path':
value = '\"%s\"' % value
parameters.append(str(value))
parameters = ' '.join(parameters)
return {
'program': '||jmservisor',
'working_directory': '',
'parameters': parameters
}
def get_remote_app_asset(self):
asset_id = self.attrs.get('asset')
if not asset_id:
raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first()
return asset

View File

@@ -1,42 +0,0 @@
# coding: utf-8
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from .. import const
__all__ = ['DatabaseApp']
class DatabaseApp(CommonModelMixin, OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
type = models.CharField(
default=const.DATABASE_APP_TYPE_MYSQL,
choices=const.DATABASE_APP_TYPE_CHOICES,
max_length=128, verbose_name=_('Type')
)
host = models.CharField(
max_length=128, verbose_name=_('Host'), db_index=True
)
port = models.IntegerField(default=3306, verbose_name=_('Port'))
database = models.CharField(
max_length=128, blank=True, null=True, verbose_name=_('Database'),
db_index=True
)
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
def __str__(self):
return self.name
class Meta:
unique_together = [('org_id', 'name'), ]
verbose_name = _("DatabaseApp")
ordering = ('name', )

View File

@@ -1,27 +0,0 @@
from django.utils.translation import gettext_lazy as _
from common.db import models
from orgs.mixins.models import OrgModelMixin
class K8sApp(OrgModelMixin, models.JMSModel):
class TYPE(models.ChoiceSet):
K8S = 'k8s', _('Kubernetes')
name = models.CharField(max_length=128, verbose_name=_('Name'))
type = models.CharField(
default=TYPE.K8S, choices=TYPE.choices,
max_length=128, verbose_name=_('Type')
)
cluster = models.CharField(max_length=1024, verbose_name=_('Cluster'))
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
def __str__(self):
return self.name
class Meta:
unique_together = [('org_id', 'name'), ]
verbose_name = _('KubernetesApp')
ordering = ('name', )

View File

@@ -1,78 +0,0 @@
# coding: utf-8
#
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.fields.model import EncryptJsonDictTextField
from .. import const
__all__ = [
'RemoteApp',
]
class RemoteApp(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
asset = models.ForeignKey(
'assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset')
)
type = models.CharField(
default=const.REMOTE_APP_TYPE_CHROME,
choices=const.REMOTE_APP_TYPE_CHOICES,
max_length=128, verbose_name=_('App type')
)
path = models.CharField(
max_length=128, blank=False, null=False,
verbose_name=_('App path')
)
params = EncryptJsonDictTextField(
max_length=4096, default={}, blank=True, null=True,
verbose_name=_('Parameters')
)
created_by = models.CharField(
max_length=32, null=True, blank=True, verbose_name=_('Created by')
)
date_created = models.DateTimeField(
auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')
)
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
class Meta:
verbose_name = _("RemoteApp")
unique_together = [('org_id', 'name')]
ordering = ('name', )
def __str__(self):
return self.name
@property
def parameters(self):
"""
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
"""
_parameters = list()
_parameters.append(self.type)
path = '\"%s\"' % self.path
_parameters.append(path)
for field in const.REMOTE_APP_TYPE_FIELDS_MAP[self.type]:
value = self.params.get(field['name'])
if value is None:
continue
_parameters.append(value)
_parameters = ' '.join(_parameters)
return _parameters
@property
def asset_info(self):
return {
'id': self.asset.id,
'hostname': self.asset.hostname
}

View File

@@ -0,0 +1,9 @@
from rest_framework import permissions
__all__ = ['IsRemoteApp']
class IsRemoteApp(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.category_remote_app

View File

@@ -1,5 +1,2 @@
from .application import *
from .remote_app import *
from .database_app import *
from .k8s_app import *
from .common import *

View File

@@ -4,15 +4,46 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import MethodSerializer
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
from .. import models
__all__ = [
'ApplicationSerializer',
'ApplicationSerializer', 'ApplicationSerializerMixin',
]
class ApplicationSerializer(BulkOrgResourceModelSerializer):
class ApplicationSerializerMixin(serializers.Serializer):
attrs = MethodSerializer()
def get_attrs_serializer(self):
default_serializer = serializers.Serializer(read_only=True)
if isinstance(self.instance, models.Application):
_type = self.instance.type
_category = self.instance.category
else:
_type = self.context['request'].query_params.get('type')
_category = self.context['request'].query_params.get('category')
if _type:
serializer_class = type_serializer_classes_mapping.get(_type)
elif _category:
serializer_class = category_serializer_classes_mapping.get(_category)
else:
serializer_class = default_serializer
if not serializer_class:
serializer_class = default_serializer
if isinstance(serializer_class, type):
serializer = serializer_class()
else:
serializer = serializer_class
return serializer
class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
@@ -26,19 +57,8 @@ class ApplicationSerializer(BulkOrgResourceModelSerializer):
'created_by', 'date_created', 'date_updated', 'get_type_display',
]
def create(self, validated_data):
attrs = validated_data.pop('attrs', {})
instance = super().create(validated_data)
instance.attrs = attrs
instance.save()
return instance
def update(self, instance, validated_data):
new_attrs = validated_data.pop('attrs', {})
instance = super().update(instance, validated_data)
attrs = instance.attrs
attrs.update(new_attrs)
instance.attrs = attrs
instance.save()
return instance
def validate_attrs(self, attrs):
_attrs = self.instance.attrs if self.instance else {}
_attrs.update(attrs)
return _attrs

View File

@@ -0,0 +1 @@
from .attrs import *

View File

@@ -0,0 +1,3 @@
from .remote_app import *
from .db import *
from .cloud import *

View File

@@ -0,0 +1,9 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
__all__ = ['CloudSerializer']
class CloudSerializer(serializers.Serializer):
cluster = serializers.CharField(max_length=1024, label=_('Cluster'), allow_null=True)

View File

@@ -0,0 +1,15 @@
# coding: utf-8
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
__all__ = ['DBSerializer']
class DBSerializer(serializers.Serializer):
host = serializers.CharField(max_length=128, label=_('Host'), allow_null=True)
port = serializers.IntegerField(label=_('Port'), allow_null=True)
database = serializers.CharField(
max_length=128, required=True, allow_null=True, label=_('Database')
)

View File

@@ -0,0 +1,52 @@
# coding: utf-8
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from common.utils import get_logger, is_uuid
from assets.models import Asset
logger = get_logger(__file__)
__all__ = ['RemoteAppSerializer']
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
instance = super().to_internal_value(data)
return str(instance.id)
def to_representation(self, value):
# value is instance.id
if self.pk_field is not None:
return self.pk_field.to_representation(value)
return value
class RemoteAppSerializer(serializers.Serializer):
asset_info = serializers.SerializerMethodField()
asset = CharPrimaryKeyRelatedField(
queryset=Asset.objects, required=False, label=_("Asset"), allow_null=True
)
path = serializers.CharField(
max_length=128, label=_('Application path'), allow_null=True
)
@staticmethod
def get_asset_info(obj):
asset_id = obj.get('asset')
if not asset_id or is_uuid(asset_id):
return {}
try:
asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname')
except ObjectDoesNotExist as e:
logger.error(e)
return {}
if not asset:
return {}
asset_info = {'id': str(asset[0]), 'hostname': asset[1]}
return asset_info

View File

@@ -0,0 +1,12 @@
from .mysql import *
from .mariadb import *
from .oracle import *
from .pgsql import *
from .chrome import *
from .mysql_workbench import *
from .vmware_client import *
from .custom import *
from .k8s import *

View File

@@ -0,0 +1,26 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['ChromeSerializer']
class ChromeSerializer(RemoteAppSerializer):
CHROME_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
path = serializers.CharField(
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
)
chrome_target = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True,
)
chrome_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Username'), allow_null=True,
)
chrome_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True
)

View File

@@ -0,0 +1,27 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['CustomSerializer']
class CustomSerializer(RemoteAppSerializer):
custom_cmdline = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Operating parameter'),
allow_null=True,
)
custom_target = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Target url'),
allow_null=True,
)
custom_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Username'),
allow_null=True,
)
custom_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True,
)

View File

@@ -0,0 +1,8 @@
from ..application_category import CloudSerializer
__all__ = ['K8SSerializer']
class K8SSerializer(CloudSerializer):
pass

View File

@@ -0,0 +1,8 @@
from .mysql import MySQLSerializer
__all__ = ['MariaDBSerializer']
class MariaDBSerializer(MySQLSerializer):
pass

View File

@@ -0,0 +1,15 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['MySQLSerializer']
class MySQLSerializer(DBSerializer):
port = serializers.IntegerField(default=3306, label=_('Port'), allow_null=True)

View File

@@ -0,0 +1,36 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['MySQLWorkbenchSerializer']
class MySQLWorkbenchSerializer(RemoteAppSerializer):
MYSQL_WORKBENCH_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
path = serializers.CharField(
max_length=128, label=_('Application path'), default=MYSQL_WORKBENCH_PATH,
allow_null=True,
)
mysql_workbench_ip = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('IP'),
allow_null=True,
)
mysql_workbench_port = serializers.IntegerField(
required=False, label=_('Port'),
allow_null=True,
)
mysql_workbench_name = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Database'),
allow_null=True,
)
mysql_workbench_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Username'),
allow_null=True,
)
mysql_workbench_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True,
)

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['OracleSerializer']
class OracleSerializer(DBSerializer):
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['PostgreSerializer']
class PostgreSerializer(DBSerializer):
port = serializers.IntegerField(default=5432, label=_('Port'), allow_null=True)

View File

@@ -0,0 +1,32 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..application_category import RemoteAppSerializer
__all__ = ['VMwareClientSerializer']
class VMwareClientSerializer(RemoteAppSerializer):
PATH = r'''
C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient
.exe
'''
VMWARE_CLIENT_PATH = ''.join(PATH.split())
path = serializers.CharField(
max_length=128, label=_('Application path'), default=VMWARE_CLIENT_PATH,
allow_null=True
)
vmware_target = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Target URL'),
allow_null=True
)
vmware_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Username'),
allow_null=True
)
vmware_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'),
allow_null=True
)

View File

@@ -0,0 +1,42 @@
from rest_framework import serializers
from applications import const
from . import application_category, application_type
__all__ = [
'category_serializer_classes_mapping',
'type_serializer_classes_mapping',
'get_serializer_class_by_application_type',
]
# define `attrs` field `category serializers mapping`
# ---------------------------------------------------
category_serializer_classes_mapping = {
const.ApplicationCategoryChoices.db.value: application_category.DBSerializer,
const.ApplicationCategoryChoices.remote_app.value: application_category.RemoteAppSerializer,
const.ApplicationCategoryChoices.cloud.value: application_category.CloudSerializer,
}
# define `attrs` field `type serializers mapping`
# -----------------------------------------------
type_serializer_classes_mapping = {
# db
const.ApplicationTypeChoices.mysql.value: application_type.MySQLSerializer,
const.ApplicationTypeChoices.mariadb.value: application_type.MariaDBSerializer,
const.ApplicationTypeChoices.oracle.value: application_type.OracleSerializer,
const.ApplicationTypeChoices.pgsql.value: application_type.PostgreSerializer,
# remote-app
const.ApplicationTypeChoices.chrome.value: application_type.ChromeSerializer,
const.ApplicationTypeChoices.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
const.ApplicationTypeChoices.vmware_client.value: application_type.VMwareClientSerializer,
const.ApplicationTypeChoices.custom.value: application_type.CustomSerializer,
# cloud
const.ApplicationTypeChoices.k8s.value: application_type.K8SSerializer
}
def get_serializer_class_by_application_type(_application_type):
return type_serializer_classes_mapping.get(_application_type)

View File

@@ -1,11 +0,0 @@
from rest_framework import serializers
class NoPasswordSerializer(serializers.JSONField):
def to_representation(self, value):
new_value = {}
for k, v in value.items():
if 'password' not in k:
new_value[k] = v
return new_value

View File

@@ -1,51 +0,0 @@
# coding: utf-8
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer
from .. import models
class DBAttrsSerializer(serializers.Serializer):
host = serializers.CharField(max_length=128, label=_('Host'))
port = serializers.IntegerField(label=_('Port'))
database = serializers.CharField(
max_length=128, required=False, allow_blank=True, allow_null=True, label=_('Database')
)
class MySQLAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=3306, label=_('Port'))
class PostgreAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=5432, label=_('Port'))
class OracleAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=1521, label=_('Port'))
class MariaDBAttrsSerializer(MySQLAttrsSerializer):
pass
class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = models.DatabaseApp
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'type', 'get_type_display', 'host', 'port',
'database', 'comment', 'created_by', 'date_created', 'date_updated',
]
read_only_fields = [
'created_by', 'date_created', 'date_updated'
'get_type_display',
]
extra_kwargs = {
'get_type_display': {'label': _('Type for display')},
}

View File

@@ -1,27 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .. import models
class CloudAttrsSerializer(serializers.Serializer):
cluster = serializers.CharField(max_length=1024, label=_('Cluster'))
class K8sAttrsSerializer(CloudAttrsSerializer):
pass
class K8sAppSerializer(BulkOrgResourceModelSerializer):
type_display = serializers.CharField(source='get_type_display', read_only=True, label=_('Type for display'))
class Meta:
model = models.K8sApp
fields = [
'id', 'name', 'type', 'type_display', 'comment', 'created_by',
'date_created', 'date_updated', 'cluster'
]
read_only_fields = [
'id', 'created_by', 'date_created', 'date_updated',
]

View File

@@ -1,89 +1,14 @@
# coding: utf-8
#
import copy
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from common.fields.serializer import CustomMetaDictField
from common.utils import get_logger
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Asset
from ..models import Application
from .. import const
from ..models import RemoteApp, Category, Application
logger = get_logger(__file__)
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
instance = super().to_internal_value(data)
return str(instance.id)
def to_representation(self, value):
# value is instance.id
if self.pk_field is not None:
return self.pk_field.to_representation(value)
return value
class RemoteAppAttrsSerializer(serializers.Serializer):
asset_info = serializers.SerializerMethodField()
asset = CharPrimaryKeyRelatedField(queryset=Asset.objects, required=False, label=_("Asset"))
path = serializers.CharField(max_length=128, label=_('Application path'))
@staticmethod
def get_asset_info(obj):
asset_info = {}
asset_id = obj.get('asset')
if not asset_id:
return asset_info
try:
asset = Asset.objects.get(id=asset_id)
asset_info.update({
'id': str(asset.id),
'hostname': asset.hostname
})
except ObjectDoesNotExist as e:
logger.error(e)
return asset_info
class ChromeAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
chrome_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
chrome_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
chrome_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class MySQLWorkbenchAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
mysql_workbench_ip = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('IP'))
mysql_workbench_port = serializers.IntegerField(required=False, label=_('Port'))
mysql_workbench_name = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Database'))
mysql_workbench_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
mysql_workbench_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class VMwareClientAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
vmware_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
vmware_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
vmware_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class CustomRemoteAppAttrsSeralizers(RemoteAppAttrsSerializer):
custom_cmdline = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Operating parameter'))
custom_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target url'))
custom_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
custom_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
__all__ = ['RemoteAppConnectionInfoSerializer']
class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
@@ -97,94 +22,10 @@ class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
]
read_only_fields = ['parameter_remote_app']
@staticmethod
def get_parameters(obj):
"""
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
"""
serializer_cls = Category.get_type_serializer_cls(obj.type)
fields = serializer_cls().get_fields()
fields.pop('asset', None)
fields_name = list(fields.keys())
attrs = obj.attrs
_parameters = list()
_parameters.append(obj.type)
for field_name in list(fields_name):
value = attrs.get(field_name, None)
if not value:
continue
if field_name == 'path':
value = '\"%s\"' % value
_parameters.append(str(value))
_parameters = ' '.join(_parameters)
return _parameters
def get_parameter_remote_app(self, obj):
parameters = self.get_parameters(obj)
parameter = {
'program': const.REMOTE_APP_BOOT_PROGRAM_NAME,
'working_directory': '',
'parameters': parameters,
}
return parameter
@staticmethod
def get_asset(obj):
return obj.attrs.get('asset')
# TODO: DELETE
class RemoteAppParamsDictField(CustomMetaDictField):
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
default_type = const.REMOTE_APP_TYPE_CHROME
convert_key_remove_type_prefix = False
convert_key_to_upper = False
# TODO: DELETE
class RemoteAppSerializer(BulkOrgResourceModelSerializer):
params = RemoteAppParamsDictField(label=_('Parameters'))
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
class Meta:
model = RemoteApp
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'asset', 'asset_info', 'type', 'get_type_display',
'path', 'params', 'date_created', 'created_by', 'comment',
]
read_only_fields = [
'created_by', 'date_created', 'asset_info',
'get_type_display'
]
extra_kwargs = {
'asset_info': {'label': _('Asset info')},
'get_type_display': {'label': _('Type for display')},
}
def process_params(self, instance, validated_data):
new_params = copy.deepcopy(validated_data.get('params', {}))
tp = validated_data.get('type', '')
if tp != instance.type:
return new_params
old_params = instance.params
fields = self.type_fields_map.get(instance.type, [])
for field in fields:
if not field.get('write_only', False):
continue
field_name = field['name']
new_value = new_params.get(field_name, '')
old_value = old_params.get(field_name, '')
field_value = new_value if new_value else old_value
new_params[field_name] = field_value
return new_params
def update(self, instance, validated_data):
params = self.process_params(instance, validated_data)
validated_data['params'] = params
return super().update(instance, validated_data)
@staticmethod
def get_parameter_remote_app(obj):
return obj.get_rdp_remote_app_setting()

View File

@@ -1,26 +1,20 @@
# coding:utf-8
#
from django.urls import path, re_path
from django.urls import path
from rest_framework_bulk.routes import BulkRouter
from common import api as capi
from .. import api
app_name = 'applications'
router = BulkRouter()
router.register(r'applications', api.ApplicationViewSet, 'application')
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
]
old_version_urlpatterns = [
re_path('(?P<resource>remote-app)/.*', capi.redirect_plural_name_api)
]
urlpatterns += router.urls + old_version_urlpatterns
urlpatterns += router.urls

View File

@@ -1,7 +0,0 @@
# coding:utf-8
from django.urls import path
app_name = 'applications'
urlpatterns = [
]

View File

@@ -29,10 +29,14 @@ class AdminUserViewSet(OrgBulkModelViewSet):
Admin user api set, for add,delete,update,list,retrieve resource
"""
model = AdminUser
filter_fields = ("name", "username")
search_fields = filter_fields
filterset_fields = ("name", "username")
search_fields = filterset_fields
serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,)
serializer_classes = {
'default': serializers.AdminUserSerializer,
'retrieve': serializers.AdminUserDetailSerializer,
}
def get_queryset(self):
queryset = super().get_queryset()
@@ -93,8 +97,8 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip")
search_fields = filter_fields
filterset_fields = ("hostname", "ip")
search_fields = filterset_fields
def get_object(self):
pk = self.kwargs.get('pk')

View File

@@ -12,7 +12,7 @@ from orgs.mixins import generics
from ..models import Asset, Node, Platform
from .. import serializers
from ..tasks import (
update_asset_hardware_info_manual, test_asset_connectivity_manual
update_assets_hardware_info_manual, test_assets_connectivity_manual
)
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
@@ -21,7 +21,7 @@ logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetPlatformRetrieveApi',
'AssetGatewayListApi', 'AssetPlatformViewSet',
'AssetTaskCreateApi',
'AssetTaskCreateApi', 'AssetsTaskCreateApi',
]
@@ -30,10 +30,15 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
API endpoint that allows Asset to be viewed or edited.
"""
model = Asset
filter_fields = (
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
"is_active"
)
filterset_fields = {
'hostname': ['exact'],
'ip': ['exact'],
'systemuser__id': ['exact'],
'admin_user__id': ['exact'],
'platform__base': ['exact'],
'is_active': ['exact'],
'protocols': ['exact', 'icontains']
}
search_fields = ("hostname", "ip")
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_classes = {
@@ -74,7 +79,7 @@ class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.PlatformSerializer
filter_fields = ['name', 'base']
filterset_fields = ['name', 'base']
search_fields = ['name']
def get_permissions(self):
@@ -90,32 +95,43 @@ class AssetPlatformViewSet(ModelViewSet):
return super().check_object_permissions(request, obj)
class AssetTaskCreateApi(generics.CreateAPIView):
class AssetsTaskMixin:
def perform_assets_task(self, serializer):
data = serializer.validated_data
assets = data['assets']
action = data['action']
if action == "refresh":
task = update_assets_hardware_info_manual.delay(assets)
else:
task = test_assets_connectivity_manual.delay(assets)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
def perform_create(self, serializer):
self.perform_assets_task(serializer)
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetTaskSerializer
permission_classes = (IsOrgAdmin,)
def get_object(self):
pk = self.kwargs.get("pk")
instance = get_object_or_404(Asset, pk=pk)
return instance
def create(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
request.data['assets'] = [pk]
return super().create(request, *args, **kwargs)
def perform_create(self, serializer):
asset = self.get_object()
action = serializer.validated_data["action"]
if action == "refresh":
task = update_asset_hardware_info_manual.delay(asset)
else:
task = test_asset_connectivity_manual.delay(asset)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetTaskSerializer
permission_classes = (IsOrgAdmin,)
class AssetGatewayListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer
model = Asset
def get_queryset(self):
asset_id = self.kwargs.get('pk')

View File

@@ -28,7 +28,7 @@ logger = get_logger(__name__)
class AssetUserFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
kwargs = {}
for field in view.filter_fields:
for field in view.filterset_fields:
value = request.GET.get(field)
if not value:
continue
@@ -78,7 +78,7 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
'retrieve': serializers.AssetUserReadSerializer,
}
permission_classes = [IsOrgAdminOrAppUser]
filter_fields = [
filterset_fields = [
"id", "ip", "hostname", "username",
"asset_id", "node_id",
"prefer", "prefer_id",
@@ -131,7 +131,7 @@ class AssetUserTaskCreateAPI(generics.CreateAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.AssetUserTaskSerializer
filter_backends = AssetUserViewSet.filter_backends
filter_fields = AssetUserViewSet.filter_fields
filterset_fields = AssetUserViewSet.filterset_fields
def get_asset_users(self):
manager = AssetUserManager()

View File

@@ -14,16 +14,16 @@ __all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
class CommandFilterViewSet(OrgBulkModelViewSet):
model = CommandFilter
filter_fields = ("name",)
search_fields = filter_fields
filterset_fields = ("name",)
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterSerializer
class CommandFilterRuleViewSet(OrgBulkModelViewSet):
model = CommandFilterRule
filter_fields = ("content",)
search_fields = filter_fields
filterset_fields = ("content",)
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer

View File

@@ -18,8 +18,8 @@ __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
class DomainViewSet(OrgBulkModelViewSet):
model = Domain
filter_fields = ("name", )
search_fields = filter_fields
filterset_fields = ("name", )
search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.DomainSerializer
@@ -31,7 +31,7 @@ class DomainViewSet(OrgBulkModelViewSet):
class GatewayViewSet(OrgBulkModelViewSet):
model = Gateway
filter_fields = ("domain__name", "name", "username", "ip", "domain")
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = ("domain__name", "name", "username", "ip")
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.GatewaySerializer

View File

@@ -13,7 +13,7 @@ __all__ = ['FavoriteAssetViewSet']
class FavoriteAssetViewSet(BulkModelViewSet):
serializer_class = FavoriteAssetSerializer
permission_classes = (IsValidUser,)
filter_fields = ['asset']
filterset_fields = ['asset']
def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org():

View File

@@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet):
permission_classes = [IsOrgAdmin]
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
filter_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']
search_fields = ['username', 'asset__ip', 'asset__hostname']

View File

@@ -28,8 +28,8 @@ __all__ = ['LabelViewSet']
class LabelViewSet(OrgBulkModelViewSet):
model = Label
filter_fields = ("name", "value")
search_fields = filter_fields
filterset_fields = ("name", "value")
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LabelSerializer

View File

@@ -1,14 +1,15 @@
from typing import List
from common.utils.common import timeit
from assets.models import Node, Asset
from assets.pagination import AssetLimitOffsetPagination
from common.utils import lazyproperty, dict_get_any, is_uuid, get_object_or_none
from assets.pagination import NodeAssetTreePagination
from common.utils import lazyproperty
from assets.utils import get_node, is_query_node_all_assets
class SerializeToTreeNodeMixin:
permission_classes = ()
@timeit
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
if with_asset_amount:
def _name(node: Node):
@@ -45,6 +46,7 @@ class SerializeToTreeNodeMixin:
return platform
return default
@timeit
def serialize_assets(self, assets, node_key=None):
if node_key is None:
get_pid = lambda asset: getattr(asset, 'parent_key', '')
@@ -69,6 +71,7 @@ class SerializeToTreeNodeMixin:
'ip': asset.ip,
'protocols': asset.protocols_as_list,
'platform': asset.platform_base,
'org_name': asset.org_name
},
}
}
@@ -78,7 +81,7 @@ class SerializeToTreeNodeMixin:
class FilterAssetByNodeMixin:
pagination_class = AssetLimitOffsetPagination
pagination_class = NodeAssetTreePagination
@lazyproperty
def is_query_node_all_assets(self):

View File

@@ -5,25 +5,26 @@ from collections import namedtuple, defaultdict
from rest_framework import status
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from rest_framework.decorators import action
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404, Http404
from django.utils.decorators import method_decorator
from django.db.models.signals import m2m_changed
from common.const.http import POST
from common.exceptions import SomeoneIsDoingThis
from common.const.signals import PRE_REMOVE, POST_REMOVE
from assets.models import Asset
from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY
from orgs.mixins.api import OrgModelViewSet
from orgs.mixins import generics
from orgs.lock import org_level_transaction_lock
from orgs.utils import current_org
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import (
update_node_assets_hardware_info_manual,
test_node_assets_connectivity_manual,
check_node_assets_amount_task
)
from .. import serializers
from .mixin import SerializeToTreeNodeMixin
@@ -41,7 +42,7 @@ __all__ = [
class NodeViewSet(OrgModelViewSet):
model = Node
filter_fields = ('value', 'key', 'id')
filterset_fields = ('value', 'key', 'id')
search_fields = ('value', )
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
@@ -52,6 +53,11 @@ class NodeViewSet(OrgModelViewSet):
serializer.validated_data["key"] = child_key
serializer.save()
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
def check_assets_amount_task(self, request):
task = check_node_assets_amount_task.delay(current_org.id)
return Response(data={'task': task.id})
def perform_update(self, serializer):
node = self.get_object()
if node.is_org_root() and node.value != serializer.validated_data['value']:
@@ -61,6 +67,9 @@ class NodeViewSet(OrgModelViewSet):
def destroy(self, request, *args, **kwargs):
node = self.get_object()
if node.is_org_root():
error = _("You can't delete the root node ({})".format(node.value))
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
if node.has_children_or_has_assets():
error = _("Deletion failed and the node contains children or assets")
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
@@ -118,9 +127,13 @@ class NodeChildrenApi(generics.ListCreateAPIView):
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
key = self.request.query_params.get("key")
if not pk and not key:
node = Node.org_root()
self.is_initial = True
if current_org.is_root():
node = None
else:
node = Node.org_root()
return node
if pk:
node = get_object_or_404(Node, pk=pk)
@@ -128,16 +141,26 @@ class NodeChildrenApi(generics.ListCreateAPIView):
node = get_object_or_404(Node, key=key)
return node
def get_org_root_queryset(self, query_all):
if query_all:
return Node.objects.all()
else:
return Node.org_root_nodes()
def get_queryset(self):
query_all = self.request.query_params.get("all", "0") == "all"
if not self.instance:
return Node.objects.none()
if self.is_initial and current_org.is_root():
return self.get_org_root_queryset(query_all)
if self.is_initial:
with_self = True
else:
with_self = False
if not self.instance:
return Node.objects.none()
if query_all:
queryset = self.instance.get_all_children(with_self=with_self)
else:
@@ -169,12 +192,12 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
def get_assets(self):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not include_assets:
if not self.instance or not include_assets:
return []
assets = self.instance.get_assets().only(
"id", "hostname", "ip", "os",
"org_id", "protocols",
)
"id", "hostname", "ip", "os", "platform_id",
"org_id", "protocols", "is_active",
).prefetch_related('platform')
return self.serialize_assets(assets, self.instance.key)
@@ -200,17 +223,13 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
def put(self, request, *args, **kwargs):
instance = self.get_object()
nodes_id = request.data.get("nodes")
children = [get_object_or_none(Node, id=pk) for pk in nodes_id]
node_ids = request.data.get("nodes")
children = Node.objects.filter(id__in=node_ids)
for node in children:
if not node:
continue
node.parent = instance
return Response("OK")
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class NodeAddAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
@@ -223,8 +242,6 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
instance.assets.add(*tuple(assets))
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class NodeRemoveAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
@@ -237,12 +254,13 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
node.assets.remove(*assets)
# 把孤儿资产添加到 root 节点
orphan_assets = Asset.objects.filter(id__in=[a.id for a in assets], nodes__isnull=True).distinct()
orphan_assets = Asset.objects.filter(
id__in=[a.id for a in assets],
nodes__isnull=True
).distinct()
Node.org_root().assets.add(*orphan_assets)
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class MoveAssetsToNodeApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer

View File

@@ -3,7 +3,8 @@ from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.drf.filters import CustomFilter
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.utils import tmp_to_org
@@ -12,7 +13,7 @@ from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer
from ..tasks import (
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
push_system_user_a_asset_manual,
push_system_user_to_assets
)
@@ -28,8 +29,12 @@ class SystemUserViewSet(OrgBulkModelViewSet):
System user api set, for add,delete,update,list,retrieve resource
"""
model = SystemUser
filter_fields = ("name", "username", "protocol")
search_fields = filter_fields
filterset_fields = {
'name': ['exact'],
'username': ['exact'],
'protocol': ['exact', 'in']
}
search_fields = filterset_fields
serializer_class = serializers.SystemUserSerializer
serializer_classes = {
'default': serializers.SystemUserSerializer,
@@ -82,18 +87,18 @@ class SystemUserTaskApi(generics.CreateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.SystemUserTaskSerializer
def do_push(self, system_user, asset=None):
if asset is None:
def do_push(self, system_user, asset_ids=None):
if asset_ids is None:
task = push_system_user_to_assets_manual.delay(system_user)
else:
username = self.request.query_params.get('username')
task = push_system_user_a_asset_manual.delay(
system_user, asset, username=username
task = push_system_user_to_assets.delay(
system_user.id, asset_ids, username=username
)
return task
@staticmethod
def do_test(system_user, asset=None):
def do_test(system_user):
task = test_system_user_connectivity_manual.delay(system_user)
return task
@@ -104,11 +109,16 @@ class SystemUserTaskApi(generics.CreateAPIView):
def perform_create(self, serializer):
action = serializer.validated_data["action"]
asset = serializer.validated_data.get('asset')
assets = serializer.validated_data.get('assets') or []
system_user = self.get_object()
if action == 'push':
task = self.do_push(system_user, asset)
assets = [asset] if asset else assets
asset_ids = [asset.id for asset in assets]
asset_ids = asset_ids if asset_ids else None
task = self.do_push(system_user, asset_ids)
else:
task = self.do_test(system_user, asset)
task = self.do_test(system_user)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
@@ -130,8 +140,8 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip")
search_fields = filter_fields
filterset_fields = ("hostname", "ip")
search_fields = filterset_fields
def get_object(self):
pk = self.kwargs.get('pk')

View File

@@ -19,9 +19,10 @@ __all__ = [
class RelationMixin:
def get_queryset(self):
queryset = self.model.objects.all()
org_id = current_org.org_id()
if org_id is not None:
if not current_org.is_root():
org_id = current_org.org_id()
queryset = queryset.filter(systemuser__org_id=org_id)
queryset = queryset.annotate(systemuser_display=Concat(
F('systemuser__name'), Value('('), F('systemuser__username'),
Value(')')
@@ -65,7 +66,7 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,)
filter_fields = [
filterset_fields = [
'id', 'asset', 'systemuser',
]
search_fields = [
@@ -91,7 +92,7 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,)
filter_fields = [
filterset_fields = [
'id', 'node', 'systemuser',
]
search_fields = [
@@ -112,7 +113,7 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserUserRelationSerializer
model = models.SystemUser.users.through
permission_classes = (IsOrgAdmin,)
filter_fields = [
filterset_fields = [
'id', 'user', 'systemuser',
]
search_fields = [

View File

@@ -1,16 +1,6 @@
from __future__ import unicode_literals
from django.apps import AppConfig
from django.db.models.signals import post_migrate
def initial_some_nodes():
from .models import Node
Node.initial_some_nodes()
def initial_some_nodes_callback(sender, **kwargs):
initial_some_nodes()
class AssetsConfig(AppConfig):
@@ -19,7 +9,3 @@ class AssetsConfig(AppConfig):
def ready(self):
super().ready()
from . import signals_handler
try:
initial_some_nodes()
except Exception:
post_migrate.connect(initial_some_nodes_callback, sender=self)

View File

@@ -40,7 +40,7 @@ class BaseBackend:
return values
@staticmethod
def make_assets_as_id(assets):
def make_assets_as_ids(assets):
if not assets:
return []
if isinstance(assets[0], Asset):

View File

@@ -69,9 +69,9 @@ class DBBackend(BaseBackend):
self.queryset = self.queryset.filter(union_id=union_id)
def _filter_assets(self, assets):
assets_id = self.make_assets_as_id(assets)
if assets_id:
self.queryset = self.queryset.filter(asset_id__in=assets_id)
asset_ids = self.make_assets_as_ids(assets)
if asset_ids:
self.queryset = self.queryset.filter(asset_id__in=asset_ids)
def _filter_node(self, node):
pass
@@ -165,7 +165,7 @@ class SystemUserBackend(DBBackend):
kwargs = self.get_annotate()
filters = self.get_filter()
qs = self.model.objects.all().annotate(**kwargs)
if current_org.org_id() is not None:
if not current_org.is_root():
filters['org_id'] = current_org.org_id()
qs = qs.filter(**filters)
qs = self.qs_to_values(qs)

20
apps/assets/locks.py Normal file
View File

@@ -0,0 +1,20 @@
from orgs.utils import current_org
from common.utils.lock import DistributedLock
class NodeTreeUpdateLock(DistributedLock):
name_template = 'assets.node.tree.update.<org_id:{org_id}>'
def get_name(self):
if current_org:
org_id = current_org.id
else:
org_id = 'current_org_is_null'
name = self.name_template.format(
org_id=org_id
)
return name
def __init__(self):
name = self.get_name()
super().__init__(name=name, release_on_transaction_commit=True, reentrant=True)

View File

@@ -0,0 +1,72 @@
# Generated by Jiangjie.Bai on 2020-12-01 10:47
from django.db import migrations
from django.db.models import Q
default_node_value = 'Default' # Always
old_default_node_key = '0' # Version <= 1.4.3
new_default_node_key = '1' # Version >= 1.4.4
def compute_parent_key(key):
try:
return key[:key.rindex(':')]
except ValueError:
return ''
def migrate_default_node_key(apps, schema_editor):
""" 将已经存在的Default节点的key从0修改为1 """
# 1.4.3版本中Default节点的key为0
print('')
Node = apps.get_model('assets', 'Node')
Asset = apps.get_model('assets', 'Asset')
# key为0的节点
old_default_node = Node.objects.filter(key=old_default_node_key, value=default_node_value).first()
if not old_default_node:
print(f'Check old default node `key={old_default_node_key} value={default_node_value}` not exists')
return
print(f'Check old default node `key={old_default_node_key} value={default_node_value}` exists')
# key为1的节点
new_default_node = Node.objects.filter(key=new_default_node_key, value=default_node_value).first()
if new_default_node:
print(f'Check new default node `key={new_default_node_key} value={default_node_value}` exists')
all_assets = Asset.objects.filter(
Q(nodes__key__startswith=f'{new_default_node_key}:') | Q(nodes__key=new_default_node_key)
).distinct()
if all_assets:
print(f'Check new default node has assets (count: {len(all_assets)})')
return
all_children = Node.objects.filter(key__startswith=f'{new_default_node_key}:')
if all_children:
print(f'Check new default node has children nodes (count: {len(all_children)})')
return
print(f'Check new default node not has assets and children nodes, delete it.')
new_default_node.delete()
# 执行修改
print(f'Modify old default node `key` from `{old_default_node_key}` to `{new_default_node_key}`')
nodes = Node.objects.filter(
Q(key__istartswith=f'{old_default_node_key}:') | Q(key=old_default_node_key)
)
for node in nodes:
old_key = node.key
key_list = old_key.split(':', maxsplit=1)
key_list[0] = new_default_node_key
new_key = ':'.join(key_list)
node.key = new_key
node.parent_key = compute_parent_key(node.key)
# 批量更新
print(f'Bulk update nodes `key` and `parent_key`, (count: {len(nodes)})')
Node.objects.bulk_update(nodes, ['key', 'parent_key'])
class Migration(migrations.Migration):
dependencies = [
('assets', '0062_auto_20201117_1938'),
]
operations = [
migrations.RunPython(migrate_default_node_key)
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1 on 2020-12-03 03:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0063_migrate_default_node_key'),
]
operations = [
migrations.AlterModelOptions(
name='node',
options={'ordering': ['parent_key', 'value'], 'verbose_name': 'Node'},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1 on 2021-01-21 07:49
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0064_auto_20201203_1100'),
]
operations = [
migrations.AlterModelOptions(
name='domain',
options={'ordering': ('name',), 'verbose_name': 'Domain'},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1 on 2021-02-08 10:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0065_auto_20210121_1549'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname'], 'verbose_name': 'Asset'},
),
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 3.1 on 2021-03-11 03:13
import django.core.validators
from django.db import migrations, models
def migrate_cmd_filter_priority(apps, schema_editor):
cmd_filter_rule_model = apps.get_model('assets', 'CommandFilterRule')
cmd_filter_rules = cmd_filter_rule_model.objects.all()
for cmd_filter_rule in cmd_filter_rules:
cmd_filter_rule.priority = 100 - cmd_filter_rule.priority + 1
cmd_filter_rule_model.objects.bulk_update(cmd_filter_rules, fields=['priority'])
def migrate_system_user_priority(apps, schema_editor):
system_user_model = apps.get_model('assets', 'SystemUser')
system_users = system_user_model.objects.all()
for system_user in system_users:
system_user.priority = 100 - system_user.priority + 1
system_user_model.objects.bulk_update(system_users, fields=['priority'])
class Migration(migrations.Migration):
dependencies = [
('assets', '0066_auto_20210208_1802'),
]
operations = [
migrations.RunPython(migrate_cmd_filter_priority),
migrations.RunPython(migrate_system_user_priority),
migrations.AlterModelOptions(
name='commandfilterrule',
options={'ordering': ('priority', 'action'), 'verbose_name': 'Command filter rule'},
),
migrations.AlterField(
model_name='commandfilterrule',
name='priority',
field=models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
),
migrations.AlterField(
model_name='systemuser',
name='priority',
field=models.IntegerField(default=20, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 3.1 on 2021-03-12 06:55
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0067_auto_20210311_1113'),
]
operations = [
migrations.AlterField(
model_name='systemuser',
name='priority',
field=models.IntegerField(default=81, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority'),
),
]

View File

@@ -17,7 +17,7 @@ from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import ConnectivityMixin
from .utils import Connectivity
__all__ = ['Asset', 'ProtocolsMixin', 'Platform']
__all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet']
logger = logging.getLogger(__name__)
@@ -41,13 +41,6 @@ def default_node():
class AssetManager(OrgManager):
def get_queryset(self):
return super().get_queryset().annotate(
platform_base=models.F('platform__base')
)
class AssetOrgManager(OrgManager):
pass
@@ -230,7 +223,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
objects = AssetManager.from_queryset(AssetQuerySet)()
org_objects = AssetOrgManager.from_queryset(AssetQuerySet)()
_connectivity = None
def __str__(self):
@@ -361,4 +353,4 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
class Meta:
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")
ordering = ["hostname", "ip"]
ordering = ["hostname", ]

View File

@@ -57,7 +57,7 @@ class AuthBook(BaseUser):
同时设置自己的 is_latest=True, version=max_version + 1
"""
username = kwargs['username']
asset = kwargs['asset']
asset = kwargs.get('asset') or kwargs.get('asset_id')
with transaction.atomic():
# 使用select_for_update限制并发创建相同的username、asset条目
instances = cls.objects.select_for_update().filter(username=username, asset=asset)

View File

@@ -11,9 +11,12 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.db.models import ChoiceSet
from common.utils import random_string
from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
)
from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric
from common import fields
from orgs.mixins.models import OrgModelMixin
@@ -105,6 +108,19 @@ class AuthMixin:
username = ''
_prefer = 'system_user'
@property
def ssh_key_fingerprint(self):
if self.public_key:
public_key = self.public_key
elif self.private_key:
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
else:
return ''
public_key_obj = sshpubkeys.SSHKey(public_key)
fingerprint = public_key_obj.hash_md5()
return fingerprint
@property
def private_key_obj(self):
if self.private_key:
@@ -204,8 +220,8 @@ class AuthMixin:
self.save()
@staticmethod
def gen_password():
return str(uuid.uuid4())
def gen_password(length=36):
return random_string(length, special_char=True)
@staticmethod
def gen_key(username):

View File

@@ -50,7 +50,7 @@ class CommandFilterRule(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"),
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)])
content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command"))
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
@@ -60,7 +60,7 @@ class CommandFilterRule(OrgModelMixin):
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
class Meta:
ordering = ('-priority', 'action')
ordering = ('priority', 'action')
verbose_name = _("Command filter rule")
@lazyproperty

View File

@@ -26,6 +26,7 @@ class Domain(OrgModelMixin):
class Meta:
verbose_name = _("Domain")
unique_together = [('org_id', 'name')]
ordering = ('name',)
def __str__(self):
return self.name

View File

@@ -16,17 +16,5 @@ class FavoriteAsset(CommonModelMixin):
unique_together = ('user', 'asset')
@classmethod
def get_user_favorite_assets_id(cls, user):
def get_user_favorite_asset_ids(cls, user):
return cls.objects.filter(user=user).values_list('asset', flat=True)
@classmethod
def get_user_favorite_assets(cls, user, asset_perms_id=None):
from assets.models import Asset
from perms.utils.asset.user_permission import get_user_granted_all_assets
asset_ids = get_user_granted_all_assets(
user,
via_mapping_node=False,
asset_perms_id=asset_perms_id
).values_list('id', flat=True)
query_name = cls.asset.field.related_query_name()
return Asset.org_objects.filter(**{f'{query_name}__user_id': user.id}, id__in=asset_ids).distinct()

View File

@@ -1,23 +1,32 @@
# -*- coding: utf-8 -*-
#
import uuid
import re
import time
import uuid
import threading
import os
import time
import uuid
from collections import defaultdict
from django.db import models, transaction
from django.db.models import Q
from django.db.models import Q, Manager
from django.db.utils import IntegrityError
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.db.transaction import atomic
from django.core.cache import cache
from common.utils.lock import DistributedLock
from common.utils.common import timeit
from common.db.models import output_as_string
from common.utils import get_logger
from common.utils.common import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
from orgs.models import Organization
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key']
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
logger = get_logger(__name__)
@@ -38,6 +47,7 @@ class FamilyMixin:
__children = None
__all_children = None
is_node = True
child_mark: int
@staticmethod
def clean_children_keys(nodes_keys):
@@ -103,7 +113,7 @@ class FamilyMixin:
if value is None:
value = child_key
child = self.__class__.objects.create(
id=_id, key=child_key, value=value, parent_key=self.key,
id=_id, key=child_key, value=value
)
return child
@@ -121,11 +131,22 @@ class FamilyMixin:
created = True
return child, created
def get_valid_child_mark(self):
key = "{}:{}".format(self.key, self.child_mark)
if not self.__class__.objects.filter(key=key).exists():
return self.child_mark
children_keys = self.get_children().values_list('key', flat=True)
children_keys_last = [key.split(':')[-1] for key in children_keys]
children_keys_last = [int(k) for k in children_keys_last if k.strip().isdigit()]
max_key_last = max(children_keys_last) if children_keys_last else 1
return max_key_last + 1
def get_next_child_key(self):
mark = self.child_mark
self.child_mark += 1
child_mark = self.get_valid_child_mark()
key = "{}:{}".format(self.key, child_mark)
self.child_mark = child_mark + 1
self.save()
return "{}:{}".format(self.key, mark)
return key
def get_next_child_preset_name(self):
name = ugettext("New node")
@@ -235,9 +256,147 @@ class FamilyMixin:
return [*tuple(ancestors), self, *tuple(children)]
class NodeAssetsMixin:
class NodeAllAssetsMappingMixin:
# Use a new plan
# { org_id: { node_key: [ asset1_id, asset2_id ] } }
orgid_nodekey_assetsid_mapping = defaultdict(dict)
locks_for_get_mapping_from_cache = defaultdict(threading.Lock)
@classmethod
def get_lock(cls, org_id):
lock = cls.locks_for_get_mapping_from_cache[str(org_id)]
return lock
@classmethod
def get_node_all_asset_ids_mapping(cls, org_id):
_mapping = cls.get_node_all_asset_ids_mapping_from_memory(org_id)
if _mapping:
return _mapping
logger.debug(f'Get node asset mapping from memory failed, acquire thread lock: '
f'thread={threading.get_ident()} '
f'org_id={org_id}')
with cls.get_lock(org_id):
logger.debug(f'Acquired thread lock ok. check if mapping is in memory now: '
f'thread={threading.get_ident()} '
f'org_id={org_id}')
_mapping = cls.get_node_all_asset_ids_mapping_from_memory(org_id)
if _mapping:
logger.debug(f'Mapping is already in memory now: '
f'thread={threading.get_ident()} '
f'org_id={org_id}')
return _mapping
_mapping = cls.get_node_all_asset_ids_mapping_from_cache_or_generate_to_cache(org_id)
cls.set_node_all_asset_ids_mapping_to_memory(org_id, mapping=_mapping)
return _mapping
# from memory
@classmethod
def get_node_all_asset_ids_mapping_from_memory(cls, org_id):
mapping = cls.orgid_nodekey_assetsid_mapping.get(org_id, {})
return mapping
@classmethod
def set_node_all_asset_ids_mapping_to_memory(cls, org_id, mapping):
cls.orgid_nodekey_assetsid_mapping[org_id] = mapping
@classmethod
def expire_node_all_asset_ids_mapping_from_memory(cls, org_id):
org_id = str(org_id)
cls.orgid_nodekey_assetsid_mapping.pop(org_id, None)
# get order: from memory -> (from cache -> to generate)
@classmethod
def get_node_all_asset_ids_mapping_from_cache_or_generate_to_cache(cls, org_id):
mapping = cls.get_node_all_asset_ids_mapping_from_cache(org_id)
if mapping:
return mapping
lock_key = f'KEY_LOCK_GENERATE_ORG_{org_id}_NODE_ALL_ASSET_ids_MAPPING'
with DistributedLock(lock_key):
# 这里使用无限期锁,原因是如果这里卡住了,就卡在数据库了,说明
# 数据库繁忙,所以不应该再有线程执行这个操作,使数据库忙上加忙
_mapping = cls.get_node_all_asset_ids_mapping_from_cache(org_id)
if _mapping:
return _mapping
_mapping = cls.generate_node_all_asset_ids_mapping(org_id)
cls.set_node_all_asset_ids_mapping_to_cache(org_id=org_id, mapping=_mapping)
return _mapping
@classmethod
def get_node_all_asset_ids_mapping_from_cache(cls, org_id):
cache_key = cls._get_cache_key_for_node_all_asset_ids_mapping(org_id)
mapping = cache.get(cache_key)
logger.info(f'Get node asset mapping from cache {bool(mapping)}: '
f'thread={threading.get_ident()} '
f'org_id={org_id}')
return mapping
@classmethod
def set_node_all_asset_ids_mapping_to_cache(cls, org_id, mapping):
cache_key = cls._get_cache_key_for_node_all_asset_ids_mapping(org_id)
cache.set(cache_key, mapping, timeout=None)
@classmethod
def expire_node_all_asset_ids_mapping_from_cache(cls, org_id):
cache_key = cls._get_cache_key_for_node_all_asset_ids_mapping(org_id)
cache.delete(cache_key)
@staticmethod
def _get_cache_key_for_node_all_asset_ids_mapping(org_id):
return 'ASSETS_ORG_NODE_ALL_ASSET_ids_MAPPING_{}'.format(org_id)
@classmethod
def generate_node_all_asset_ids_mapping(cls, org_id):
from .asset import Asset
logger.info(f'Generate node asset mapping: '
f'thread={threading.get_ident()} '
f'org_id={org_id}')
t1 = time.time()
with tmp_to_org(org_id):
node_ids_key = Node.objects.annotate(
char_id=output_as_string('id')
).values_list('char_id', 'key')
# * 直接取出全部. filter(node__org_id=org_id)(大规模下会更慢)
nodes_asset_ids = Asset.nodes.through.objects.all() \
.annotate(char_node_id=output_as_string('node_id')) \
.annotate(char_asset_id=output_as_string('asset_id')) \
.values_list('char_node_id', 'char_asset_id')
node_id_ancestor_keys_mapping = {
node_id: cls.get_node_ancestor_keys(node_key, with_self=True)
for node_id, node_key in node_ids_key
}
nodeid_assetsid_mapping = defaultdict(set)
for node_id, asset_id in nodes_asset_ids:
nodeid_assetsid_mapping[node_id].add(asset_id)
t2 = time.time()
mapping = defaultdict(set)
for node_id, node_key in node_ids_key:
asset_ids = nodeid_assetsid_mapping[node_id]
node_ancestor_keys = node_id_ancestor_keys_mapping[node_id]
for ancestor_key in node_ancestor_keys:
mapping[ancestor_key].update(asset_ids)
t3 = time.time()
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2))
return mapping
class NodeAssetsMixin(NodeAllAssetsMappingMixin):
org_id: str
key = ''
id = None
objects: Manager
def get_all_assets(self):
from .asset import Asset
@@ -251,8 +410,7 @@ class NodeAssetsMixin:
# 可是 startswith 会导致表关联时 Asset 索引失效
from .asset import Asset
node_ids = cls.objects.filter(
Q(key__startswith=f'{key}:') |
Q(key=key)
Q(key__startswith=f'{key}:') | Q(key=key)
).values_list('id', flat=True).distinct()
assets = Asset.objects.filter(
nodes__id__in=list(node_ids)
@@ -271,54 +429,42 @@ class NodeAssetsMixin:
return self.get_all_assets().valid()
@classmethod
def get_nodes_all_assets_ids(cls, nodes_keys):
assets_ids = cls.get_nodes_all_assets(nodes_keys).values_list('id', flat=True)
return assets_ids
def get_nodes_all_asset_ids_by_keys(cls, nodes_keys):
nodes = Node.objects.filter(key__in=nodes_keys)
asset_ids = cls.get_nodes_all_assets(*nodes).values_list('id', flat=True)
return asset_ids
@classmethod
def get_nodes_all_assets(cls, nodes_keys, extra_assets_ids=None):
def get_nodes_all_assets(cls, *nodes):
from .asset import Asset
nodes_keys = cls.clean_children_keys(nodes_keys)
q = Q()
node_ids = ()
for key in nodes_keys:
q |= Q(key__startswith=f'{key}:')
q |= Q(key=key)
if q:
node_ids = Node.objects.filter(q).distinct().values_list('id', flat=True)
node_ids = set()
descendant_node_query = Q()
for n in nodes:
node_ids.add(n.id)
descendant_node_query |= Q(key__istartswith=f'{n.key}:')
if descendant_node_query:
_ids = Node.objects.order_by().filter(descendant_node_query).values_list('id', flat=True)
node_ids.update(_ids)
return Asset.objects.order_by().filter(nodes__id__in=node_ids).distinct()
q = Q(nodes__id__in=list(node_ids))
if extra_assets_ids:
q |= Q(id__in=extra_assets_ids)
if q:
return Asset.org_objects.filter(q).distinct()
else:
return Asset.objects.none()
def get_all_asset_ids(self):
asset_ids = self.get_all_asset_ids_by_node_key(org_id=self.org_id, node_key=self.key)
return set(asset_ids)
@classmethod
def get_all_asset_ids_by_node_key(cls, org_id, node_key):
org_id = str(org_id)
nodekey_assetsid_mapping = cls.get_node_all_asset_ids_mapping(org_id)
asset_ids = nodekey_assetsid_mapping.get(node_key, [])
return set(asset_ids)
class SomeNodesMixin:
key = ''
default_key = '1'
default_value = 'Default'
empty_key = '-11'
empty_value = _("empty")
@classmethod
def default_node(cls):
with tmp_to_org(Organization.default()):
defaults = {'value': cls.default_value}
try:
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
except IntegrityError as e:
logger.error("Create default node failed: {}".format(e))
cls.modify_other_org_root_node_key()
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
return obj
def is_default_node(self):
return self.key == self.default_key
@@ -329,69 +475,61 @@ class SomeNodesMixin:
return False
@classmethod
def get_next_org_root_node_key(cls):
with tmp_to_org(Organization.root()):
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True)
if not org_nodes_roots_keys:
org_nodes_roots_keys = ['1']
max_key = max([int(k) for k in org_nodes_roots_keys])
key = str(max_key + 1) if max_key != 0 else '2'
return key
def org_root(cls):
# 如果使用current_org 在set_current_org时会死循环
ori_org = get_current_org()
if ori_org and ori_org.is_default():
return cls.default_node()
if ori_org and ori_org.is_root():
return None
org_roots = cls.org_root_nodes()
org_roots_length = len(org_roots)
if org_roots_length == 1:
root = org_roots[0]
return root
elif org_roots_length == 0:
root = cls.create_org_root_node()
return root
else:
error = 'Current org {} root node not 1, get {}'.format(ori_org, org_roots_length)
raise ValueError(error)
@classmethod
def default_node(cls):
default_org = Organization.default()
with tmp_to_org(default_org):
defaults = {'value': default_org.name}
obj, created = cls.objects.get_or_create(defaults=defaults, key=cls.default_key)
return obj
@classmethod
def create_org_root_node(cls):
# 如果使用current_org 在set_current_org时会死循环
ori_org = get_current_org()
with transaction.atomic():
if not ori_org.is_real():
return cls.default_node()
key = cls.get_next_org_root_node_key()
root = cls.objects.create(key=key, value=ori_org.name)
return root
@classmethod
def org_root(cls):
root = cls.objects.filter(parent_key='')\
.filter(key__regex=r'^[0-9]+$')\
.exclude(key__startswith='-')
if root:
return root[0]
else:
return cls.create_org_root_node()
def get_next_org_root_node_key(cls):
with tmp_to_root_org():
org_nodes_roots = cls.org_root_nodes()
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True)
if not org_nodes_roots_keys:
org_nodes_roots_keys = ['1']
max_key = max([int(k) for k in org_nodes_roots_keys])
key = str(max_key + 1) if max_key > 0 else '2'
return key
@classmethod
def initial_some_nodes(cls):
cls.default_node()
@classmethod
def modify_other_org_root_node_key(cls):
"""
解决创建 default 节点失败的问题,
因为在其他组织下存在 default 节点,故在 DEFAULT 组织下 get 不到 create 失败
"""
logger.info("Modify other org root node key")
with tmp_to_org(Organization.root()):
node_key1 = cls.objects.filter(key='1').first()
if not node_key1:
logger.info("Not found node that `key` = 1")
return
if not node_key1.org.is_real():
logger.info("Org is not real for node that `key` = 1")
return
with transaction.atomic():
with tmp_to_org(node_key1.org):
org_root_node_new_key = cls.get_next_org_root_node_key()
for n in cls.objects.all():
old_key = n.key
key_list = n.key.split(':')
key_list[0] = org_root_node_new_key
new_key = ':'.join(key_list)
n.key = new_key
n.save()
logger.info('Modify key ( {} > {} )'.format(old_key, new_key))
def org_root_nodes(cls):
root_nodes = cls.objects.filter(parent_key='', key__regex=r'^[0-9]+$') \
.exclude(key__startswith='-').order_by('key')
return root_nodes
class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
@@ -411,7 +549,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
class Meta:
verbose_name = _("Node")
ordering = ['value']
ordering = ['parent_key', 'value']
def __str__(self):
return self.full_value
@@ -490,10 +628,15 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
sort_key_func = lambda n: [int(i) for i in n.key.split(':')]
nodes_sorted = sorted(list(nodes), key=sort_key_func)
nodes_mapper = {n.key: n for n in nodes_sorted}
if not self.is_org_root():
# 如果是org_root那么parent_key为'', parent为自己所以这种情况不处理
# 更新自己时自己的parent_key获取不到
nodes_mapper.update({self.parent_key: self.parent})
for node in nodes_sorted:
parent = nodes_mapper.get(node.parent_key)
if not parent:
logger.error(f'Node parent node in mapper: {node.parent_key} {node.value}')
if node.parent_key:
logger.error(f'Node parent node in mapper: {node.parent_key} {node.value}')
continue
node.full_value = parent.full_value + '/' + node.value
self.__class__.objects.bulk_update(nodes, ['full_value'])

View File

@@ -88,6 +88,26 @@ class SystemUser(BaseUser):
(PROTOCOL_K8S, 'k8s'),
)
SUPPORT_PUSH_PROTOCOLS = [PROTOCOL_SSH, PROTOCOL_RDP]
ASSET_CATEGORY_PROTOCOLS = [
PROTOCOL_SSH, PROTOCOL_RDP, PROTOCOL_TELNET, PROTOCOL_VNC
]
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
PROTOCOL_RDP
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
PROTOCOL_MYSQL, PROTOCOL_ORACLE, PROTOCOL_MARIADB, PROTOCOL_POSTGRESQL
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
PROTOCOL_K8S
]
APPLICATION_CATEGORY_PROTOCOLS = [
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
*APPLICATION_CATEGORY_DB_PROTOCOLS,
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
]
LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = (
@@ -99,7 +119,7 @@ class SystemUser(BaseUser):
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
@@ -133,37 +153,23 @@ class SystemUser(BaseUser):
def login_mode_display(self):
return self.get_login_mode_display()
@property
def db_application_protocols(self):
return [
self.PROTOCOL_MYSQL, self.PROTOCOL_ORACLE, self.PROTOCOL_MARIADB,
self.PROTOCOL_POSTGRESQL
]
@property
def cloud_application_protocols(self):
return [self.PROTOCOL_K8S]
@property
def application_category_protocols(self):
protocols = []
protocols.extend(self.db_application_protocols)
protocols.extend(self.cloud_application_protocols)
return protocols
def is_need_push(self):
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
if self.auto_push and self.is_protocol_support_push:
return True
else:
return False
@property
def is_protocol_support_push(self):
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
@property
def is_need_cmd_filter(self):
return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_VNC]
@property
def is_need_test_asset_connective(self):
return self.protocol not in self.application_category_protocols
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
def has_special_auth(self, asset=None, username=None):
if username is None and self.username_same_with_user:
@@ -172,7 +178,7 @@ class SystemUser(BaseUser):
@property
def can_perm_to_asset(self):
return self.protocol not in self.application_category_protocols
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
def _merge_auth(self, other):
super()._merge_auth(other)
@@ -199,12 +205,23 @@ class SystemUser(BaseUser):
def get_all_assets(self):
from assets.models import Node
nodes_keys = self.nodes.all().values_list('key', flat=True)
assets_ids = set(self.assets.all().values_list('id', flat=True))
nodes_assets_ids = Node.get_nodes_all_assets_ids(nodes_keys)
assets_ids.update(nodes_assets_ids)
assets = Asset.objects.filter(id__in=assets_ids)
asset_ids = set(self.assets.all().values_list('id', flat=True))
nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys)
asset_ids.update(nodes_asset_ids)
assets = Asset.objects.filter(id__in=asset_ids)
return assets
@classmethod
def get_protocol_by_application_type(cls, app_type):
from applications.const import ApplicationTypeChoices
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
protocol = app_type
elif app_type in ApplicationTypeChoices.remote_app_types():
protocol = cls.PROTOCOL_RDP
else:
protocol = None
return protocol
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]

View File

@@ -1,39 +1,52 @@
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.request import Request
from common.utils import get_logger
from assets.models import Node
logger = get_logger(__name__)
class AssetPaginationBase(LimitOffsetPagination):
def init_attrs(self, queryset, request: Request, view=None):
self._request = request
self._view = view
self._user = request.user
def paginate_queryset(self, queryset, request: Request, view=None):
self.init_attrs(queryset, request, view)
return super().paginate_queryset(queryset, request, view=None)
class AssetLimitOffsetPagination(LimitOffsetPagination):
"""
需要与 `assets.api.mixin.FilterAssetByNodeMixin` 配合使用
"""
def get_count(self, queryset):
"""
1. 如果查询节点下的所有资产,那 count 使用 Node.assets_amount
2. 如果有其他过滤条件使用 super
3. 如果只查询该节点下的资产使用 super
"""
exclude_query_params = {
self.limit_query_param,
self.offset_query_param,
'node', 'all', 'show_current_asset',
'node_id', 'display', 'draw', 'fields_size',
'key', 'all', 'show_current_asset',
'cache_policy', 'display', 'draw',
'order', 'node', 'node_id', 'fields_size',
}
for k, v in self._request.query_params.items():
if k not in exclude_query_params and v is not None:
logger.warn(f'Not hit node.assets_amount because find a unknow query_param `{k}` -> {self._request.get_full_path()}')
return super().get_count(queryset)
node_assets_count = self.get_count_from_nodes(queryset)
if node_assets_count is None:
return super().get_count(queryset)
return node_assets_count
def get_count_from_nodes(self, queryset):
raise NotImplementedError
class NodeAssetTreePagination(AssetPaginationBase):
def get_count_from_nodes(self, queryset):
is_query_all = self._view.is_query_node_all_assets
if is_query_all:
node = self._view.node
if not node:
node = Node.org_root()
return node.assets_amount
return super().get_count(queryset)
def paginate_queryset(self, queryset, request: Request, view=None):
self._request = request
self._view = view
return super().paginate_queryset(queryset, request, view=None)
if node:
logger.debug(f'Hit node.assets_amount[{node.assets_amount}] -> {self._request.get_full_path()}')
return node.assets_amount
return None

View File

@@ -3,8 +3,6 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from ..models import Node, AdminUser
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -17,7 +15,6 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
"""
class Meta:
list_serializer_class = AdaptedBulkListSerializer
model = AdminUser
fields = [
'id', 'name', 'username', 'password', 'private_key', 'public_key',
@@ -26,6 +23,7 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
extra_kwargs = {
'username': {"required": True},
'password': {"write_only": True},
'private_key': {"write_only": True},
'public_key': {"write_only": True},
@@ -33,6 +31,11 @@ class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
}
class AdminUserDetailSerializer(AdminUserSerializer):
class Meta(AdminUserSerializer.Meta):
fields = AdminUserSerializer.Meta.fields + ['ssh_key_fingerprint']
class AdminUserAuthSerializer(AuthSerializer):
class Meta:

View File

@@ -2,7 +2,7 @@
#
from rest_framework import serializers
from django.db.models import F
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -98,9 +98,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
fields_as = list(annotates_fields.keys())
fields = fields_small + fields_fk + fields_m2m + fields_as
read_only_fields = [
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw',
'created_by', 'date_created',
] + fields_as
@@ -114,7 +111,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.select_related('admin_user', 'domain', 'platform')
queryset = queryset.prefetch_related('admin_user', 'domain', 'platform')
queryset = queryset.prefetch_related('nodes', 'labels')
return queryset
@@ -169,16 +166,17 @@ class AssetDisplaySerializer(AssetSerializer):
'connectivity',
]
@classmethod
def setup_eager_loading(cls, queryset):
queryset = super().setup_eager_loading(queryset)
queryset = queryset\
.annotate(admin_user_username=F('admin_user__username'))
return queryset
class PlatformSerializer(serializers.ModelSerializer):
meta = serializers.DictField(required=False, allow_null=True)
meta = serializers.DictField(required=False, allow_null=True, label=_('Meta'))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# TODO 修复 drf SlugField RegexValidator bug之后记得删除
validators = self.fields['name'].validators
if isinstance(validators[-1], RegexValidator):
validators.pop()
class Meta:
model = Platform
@@ -207,3 +205,6 @@ class AssetTaskSerializer(serializers.Serializer):
)
task = serializers.CharField(read_only=True)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
assets = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, required=False, allow_empty=True, many=True
)

View File

@@ -4,7 +4,7 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import AuthBook, Asset
from ..backends import AssetUserManager

View File

@@ -41,10 +41,6 @@ class AuthSerializerMixin:
def validate_private_key(self, private_key):
if not private_key:
return
if 'OPENSSH' in private_key:
msg = _("Not support openssh format key, using "
"ssh-keygen -t rsa -m pem to generate")
raise serializers.ValidationError(msg)
password = self.initial_data.get("password")
valid = validate_ssh_private_key(private_key, password)
if not valid:

View File

@@ -3,8 +3,7 @@
import re
from rest_framework import serializers
from common.fields import ChoiceDisplayField
from common.serializers import AdaptedBulkListSerializer
from common.drf.serializers import AdaptedBulkListSerializer
from ..models import CommandFilter, CommandFilterRule, SystemUser
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -26,7 +25,6 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
# serializer_choice_field = ChoiceDisplayField
invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
type_display = serializers.ReadOnlyField(source='get_type_display')
action_display = serializers.ReadOnlyField(source='get_action_display')

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