Compare commits

...

424 Commits

Author SHA1 Message Date
ibuler
669f659843 fix: 修复ws引起的redis连接增加 2021-08-20 15:23:45 +08:00
ibuler
a718862b37 perf: 修改健康监测 2021-08-20 14:34:39 +08:00
Bai
52898e951e fix: 资产mini接口返回platform、protocols 2021-08-20 14:12:35 +08:00
xinwen
0f03fd1ee1 fix: 用户创建 500 2021-08-20 10:06:54 +08:00
Jiangjie.Bai
10e06a4533 Merge pull request #6692 from jumpserver/dev
v2.13.0
2021-08-19 18:52:25 +08:00
ibuler
98e38ebfd8 perf: 修复系统用户翻译 2021-08-19 17:26:38 +08:00
Bai
9660e20176 fix: 修复远程应用资产必填项 2021-08-19 16:28:51 +08:00
xinwen
21a7ec9fec fix: 升级时清空缓存 2021-08-19 15:45:59 +08:00
xinwen
7d123ff8c5 fix: 登录时验证码卡住 2021-08-19 15:19:01 +08:00
ibuler
2af6ac504d perf: 优化版本号 2021-08-19 11:15:10 +08:00
Jiangjie.Bai
6c8d1c4e77 Merge pull request #6680 from jumpserver/dev
v2.13.0 rc4
2021-08-18 19:50:17 +08:00
Bai
52d3e1b34b fix: 修改协议错误信息翻译 2021-08-18 19:33:50 +08:00
Bai
bf6fcc9020 fix: 修改翻译信息;添加水印配置项到页面 2021-08-18 19:14:49 +08:00
Bai
a0b756ebaa feat: 添加同步任务执行历史保留天数配置项 2021-08-18 18:30:10 +08:00
xinwen
5e8a55f949 fix: AuthBook 变动审计 2021-08-18 16:47:28 +08:00
xinwen
f9218584f4 fix: 组织统计里系统用户与特权用户数量不对 2021-08-18 16:46:01 +08:00
Bai
228446979f fix: 修改过期节点资产映射缓存清除 2021-08-18 15:37:40 +08:00
Bai
aa37d86959 fix: 将flower放到web服务中;修复账号列表过滤节点的逻辑,获取所有子节点。 2021-08-18 15:36:13 +08:00
feng626
0e9079fa2e Merge pull request #6673 from jumpserver/pr@dev@user_role_search
fix: 用户角色搜索
2021-08-18 15:26:56 +08:00
feng626
58c058c1a5 fix: 修复user筛选用户bug 2021-08-18 15:25:35 +08:00
ibuler
f390556a87 perf: 优化spm请求的问题 2021-08-18 14:39:55 +08:00
Bai
b7378da46e fix: 修复创建/更新远程应用资产可以为空的问题 2021-08-18 11:28:28 +08:00
Bai
0c8c926aac fix: 修复校验用户登录规则的API权限 2021-08-18 10:31:42 +08:00
xinwen
81d8592ee1 fix: 系统用户的账号列表里修改密码后不能登录 2021-08-18 10:22:02 +08:00
Jiangjie.Bai
af827f3626 Merge pull request #6664 from jumpserver/dev
v2.13.0 rc3
2021-08-17 20:37:03 +08:00
ibuler
91b269fc36 fix: 修复应用账号 2021-08-17 20:31:19 +08:00
ibuler
1605a57df6 perf: 修复应用账号选择部分导出问题 2021-08-17 17:33:55 +08:00
ibuler
5cd23b843a fix: 修复授权树api数据结构 2021-08-17 17:31:45 +08:00
Bai
d46f1080f8 fix: 修复校验用户密码规则 2021-08-17 16:37:38 +08:00
feng626
9a541ebf05 fix: 资产批量推送和测试连接性 bug 2021-08-17 14:05:03 +08:00
ibuler
dba416f5eb fix: 修复remote app没有返回asset的bug 2021-08-17 11:29:02 +08:00
xinwen
7d7da9bf98 fix: 添加新的 es 时创建索引 2021-08-17 11:27:57 +08:00
老广
4425efd3c2 Merge pull request #6649 from jumpserver/dev
Dev
2021-08-17 10:17:49 +08:00
feng626
c6bb9e97fb fix: 工单已关闭 再审批bug 2021-08-16 19:24:42 +08:00
xinwen
9c7adb7a14 fix: 用户列表导出部分字段没翻译 2021-08-16 17:41:56 +08:00
xinwen
7b4faccf05 fix: 飞书没有翻译 2021-08-16 16:46:20 +08:00
xinwen
0cd3419e09 fix: 修复应用授权列表报错 2021-08-16 16:45:45 +08:00
Eric_Lee
e49dedf6b1 Merge pull request #6645 from jumpserver/dev
Dev
2021-08-16 11:25:24 +08:00
ibuler
bee4e05b5f fix: 修复应用账号中没有应用却有账号的问题 2021-08-16 11:23:15 +08:00
ibuler
a5419b49ee perf: 添加监测 celery 2021-08-16 09:51:48 +08:00
ibuler
84e60283b8 fix(account): 修复应用账号前端不对的问题 2021-08-16 09:51:22 +08:00
Jiangjie.Bai
96206384c0 Merge pull request #6634 from jumpserver/dev
v2.13.0 rc1
2021-08-12 19:51:48 +08:00
Bai
78c61d5afa feat: 升级依赖 jms-storage==0.0.39 2021-08-12 18:38:10 +08:00
Bai
ee712d9a9d feat: 升级依赖 s3transfer==0.5.0 2021-08-12 18:38:10 +08:00
ibuler
a1e8c2849a perf: 修改entrypoint 2021-08-12 17:55:03 +08:00
fit2bot
54751a715c feat: 添加 飞书 (#6602)
* feat: 添加 飞书

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: wenyann <64353056+wenyann@users.noreply.github.com>
2021-08-12 16:44:06 +08:00
fit2bot
a2907a6e6d fix: 将 es 的 doc_type 默认值改为 _doc (#6627)
* fix: 无效的 es 报 500

* fix: 修复索引不存在时报错

* fix: 将 es 的 doc_type 默认值改为 _doc

Co-authored-by: xinwen <coderWen@126.com>
2021-08-12 15:37:22 +08:00
Bai
33236aaa47 fix: 修复启动脚本beat进程偶尔不会结束的问题 2021-08-12 15:35:22 +08:00
Bai
cd6c7ce7fa perf: jms脚本添加collectstatic命令 2021-08-11 14:28:39 +08:00
Bai
363baece4f fix: 修复删除远程应用关联资产后,更新页面显示资产ID的问题 2021-08-11 14:23:47 +08:00
feng626
1db0e28346 feat: 用户管理增加角色搜素 2021-08-11 10:36:43 +08:00
xinwen
7366bbb197 fix: 修复 es 命令存储过滤不准确 2021-08-10 17:25:41 +08:00
Bai
7959f84bba fix: 修改启动脚本 2021-08-10 14:45:58 +08:00
ibuler
0c96bf61ef chore: 添加注释 2021-08-09 10:22:16 +08:00
fit2bot
39ce60c93a feat: 系统监控添加 Core/Celery Terminal; 修改检测终端状态逻辑; (#6570)
* feat: 系统监控添加 Core Terminal; 修改检测终端状态逻辑;

* feat: 添加management包

* feat: 添加management包

* feat: 添加 start 模块

* feat: 修改 start 模块

* feat: 修改启动命令目录结构

* feat: 修改启动命令目录结构

* feat: 修改启动命令目录结构

* feat: 修改启动命令目录结构

* feat: 修改启动命令目录结构

* feat: 修改启动命令目录结构

* feat: 修改启动命令目录结构

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

* feat: 修改启动脚本

Co-authored-by: Bai <bugatti_it@163.com>
2021-08-06 19:16:18 +08:00
Bai
8ad78ffef8 fix: 修改SECURITY_PASSWORD_MIN_LENGTH 2021-08-05 10:38:22 +08:00
xinwen
66b499b8e3 fix: 修复多对多审计内容太长报错&全局组织没有审计 2021-08-04 17:13:25 +08:00
xinwen
22406f47f7 fix: Luna 页面搜索资产,结果按资产名称排序 2021-08-04 10:32:20 +08:00
feng626
72f782b589 Merge pull request #6576 from jumpserver/pr@dev@edit_xpack_internationalization
feat: 修改xpack 国际化
2021-08-03 14:31:20 +08:00
xinwen
cf3df951a9 fix: xrdp 连接可以指定是否全屏 2021-08-03 11:28:49 +08:00
xinwen
4085df913b feat: 记录网关可连接性 2021-08-02 18:42:43 +08:00
ibuler
d93f3aca51 perf: 修改支持客户端拉起
perf: remove print
2021-08-02 18:15:11 +08:00
老广
b180a162cd Merge pull request #6580 from jumpserver/pr@dev@update_pip_version
perf: 修改依赖包版本
2021-08-02 17:59:15 +08:00
ibuler
1bf3ff5e1b perf: 修改依赖包版本 2021-08-02 17:55:22 +08:00
Bai
0def477b63 fix: 修复收集windows资产用户时未收集到全部用户的问题 2021-08-02 16:34:54 +08:00
feng626
337e1ba206 feat: 修改xpack 国际化 2021-08-02 14:19:54 +08:00
Bai
fe2d80046c fix: 修改用户密码更新API取消Retrieve权限 2021-08-02 12:00:14 +08:00
Bai
f16a9ddb86 fix: 修改 settings.SECURITY_MFA_AUTH 开启判断条件 2021-08-02 10:52:41 +08:00
老广
5f6c207721 Merge pull request #6572 from jumpserver/pr@dev@perf_account_filter
perf: 修改账号搜索
2021-07-30 19:52:08 +08:00
ibuler
988d686418 perf: 修改账号搜索 2021-07-30 19:48:23 +08:00
老广
89e654af80 Merge pull request #6571 from jumpserver/pr@dev@perf_i18n
perf: 优化i18n
2021-07-30 19:21:31 +08:00
ibuler
2ab1bbaa2c chore: merge with dev 2021-07-30 19:18:36 +08:00
ibuler
b43626b5a2 perf: 优化i18n 2021-07-30 19:13:47 +08:00
ibuler
5e4b3e924f perf: 优化树 2021-07-30 17:29:50 +08:00
fit2bot
66b0173e20 feat: 服务性能告警指标包含:Core服务和各组件状态;指标包括:cpu/disk/memory/is_alive (#6564)
* feat: 服务性能告警指标包含:Core服务和各组件状态;指标包括:cpu/disk/memory/is_alive

* feat: 服务性能告警指标包含:Core服务和各组件状态;指标包括:cpu/disk/memory/is_alive 2

Co-authored-by: Bai <bugatti_it@163.com>
2021-07-30 15:42:06 +08:00
fit2bot
67f6b1080e feat: 管理员和普通用户支持单独设置MFA和密码长度 (#6562)
* feat: 支持配置系统管理员强制MFA和独立密码长度限制

* feat: 支持配置系统管理员强制MFA和独立密码长度限制

* feat: 支持配置系统管理员强制MFA和独立密码长度限制, 翻译文件

* fix: 设置界面可设置管理员用户开启MFA,当在设置开启全局的时候,不改变用户的mfa字段状态

* fix: 修改管理员最小密码长度变量名称

* perf: 优化不同的配置

* perf: 修改check password rule

* perf: 添加配置文件

* perf: 修改profile

* perf: 优化代码

* fix: 修复bug

Co-authored-by: fit2cloud-jiangweidong <weidong.jiang@fit2cloud.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-07-30 15:19:00 +08:00
ibuler
b56b897260 perf: 优化计算数量 2021-07-30 11:08:12 +08:00
ibuler
f031f4d560 perf: 修复授权应用树的问题 2021-07-29 19:45:36 +08:00
Bai
d0e119fb50 feat: session.Task model add kwargs field (记录kill_session的用户名称) 2021-07-29 15:06:54 +08:00
Bai
7892e50aa2 feat: session.Task model add kwargs field (记录kill_session的用户名称) 2021-07-29 15:06:54 +08:00
ibuler
bff3582136 perf: 修改时间日志格式,兼容firefox 2021-07-28 17:56:23 +08:00
Bai
bdf95903ce feat: 支持终断DB会话 2021-07-28 17:48:53 +08:00
feng626
c1e6bc5d60 Merge pull request #6555 from jumpserver/pr@dev@user_collection_node_fix
fix: 用户管理-用户-授权的资产-收藏夹下拉菜单报404bug修复
2021-07-28 14:39:53 +08:00
feng626
da588ce0ae perf: 代码优化 2021-07-28 14:33:02 +08:00
feng626
d0680c3753 fix: 用户管理-用户-授权的资产-收藏夹下拉菜单报404bug修复 2021-07-28 14:21:53 +08:00
fit2bot
905d0d5131 perf: 统一应用树 (#6535)
* perf: 添加应用树api

* perf: perms tree

* perf: 统一应用树

* perf: 修改icon

* perf: stash it

* perf: 优化应用账号

* perf: 基本完成应用账号重构

* perf: 修改翻译

Co-authored-by: ibuler <ibuler@qq.com>
2021-07-27 16:06:00 +08:00
feng626
d347ed9862 perf: 优化代码 2021-07-27 15:54:44 +08:00
feng626
8611f765a3 feat: 增加rdp 云端唤醒 api 2021-07-27 15:54:44 +08:00
ibuler
962f1c0310 perf: 资产账号支持,几点过滤 2021-07-27 15:54:08 +08:00
feng626
473a66719b 终端 批量更新 数据必填 2021-07-27 15:52:59 +08:00
Bai
aeb43a04f6 feat: 添加翻译: trigger 触发模式、手动触发、自动触发 2021-07-27 10:45:28 +08:00
xinwen
49a35985a1 feat: 多对多关系添加审计 2021-07-26 18:10:41 +08:00
Bai
21b789e08c perf: 优化测试网域可连接性的错误提示信息 2021-07-26 15:53:08 +08:00
Bai
51387ad97e fix: 解决访问api-docs失败的问题 2021-07-26 14:32:53 +08:00
Bai
290d584ac9 perf: 校验系统用户/账号密码不能包含 {{ 字符;升级依赖包ansible==2.9.24 2021-07-23 19:00:43 +08:00
Bai
160b238058 fix: 修复ssh-private-key错误导致系统用户列表加载出现500的问题 2021-07-23 18:57:53 +08:00
Bai
938255df6f perf: 添加LDAPServerURL ldaps:// ldap:// 协议检测 2021-07-23 14:50:44 +08:00
Bai
4230da0fd9 perf: 添加LDAPServerURL ldaps:// ldap:// 协议检测 2021-07-23 11:24:05 +08:00
xinwen
fee3715d30 fix: 应用授权按type 过滤报错 2021-07-22 11:06:14 +08:00
Bai
689bd093be 云管中心同步任务支持设置同步IP网段和协议组(修改特权用户文案) 2021-07-21 16:50:04 +08:00
feng626
77461d7834 网域网管取消密码不为空校验 2021-07-21 16:33:09 +08:00
ibuler
ee5894c296 perf: 优化工单推荐资产的数量 2021-07-21 14:39:29 +08:00
Bai
07898004b0 feat: 丰富资产任务API创建;支持针对多个系统用户一个资产的推送和测试 2021-07-20 16:59:51 +08:00
Eric_Lee
630164cd51 Merge pull request #6494 from jumpserver/pr@dev@add_watermark
perf: 添加配置文件,控制luna水印
2021-07-19 18:28:24 +08:00
feng626
981319e553 关闭 网域网关 密码特殊字符校验 2021-07-19 18:27:11 +08:00
ibuler
fedd32ea7a merge: 合并dev 2021-07-19 18:26:04 +08:00
ibuler
e57574f10a perf: 添加配置文件,控制luna水印
perf: 添加配置文件样例
2021-07-19 18:20:51 +08:00
xinwen
3f0a0b33b5 feat: 应用按类型筛选可以指定多个类型 2021-07-19 10:11:07 +08:00
Bai
c21217d50c perf: 添加配置项 LOGIN_REDIRECT_TO_BACKEND 2021-07-16 14:34:23 +08:00
Bai
e44c8ae940 perf: 优化登录跳转flash时间间隔可配置;0表示直接跳转 2021-07-16 14:34:23 +08:00
wojiushixiaobai
1da187c373 perf: 优化 MFA 绑定提示 2021-07-16 14:33:08 +08:00
xinwen
36ad42beb2 fix: xrdp 设置分辨率不生效 2021-07-16 14:32:18 +08:00
Jiangjie.Bai
c0560ad3cc Merge pull request #6464 from jumpserver/dev
v2.12.0 rc5
2021-07-15 19:19:18 +08:00
ibuler
c318762f82 perf: 修改account密码加载 2021-07-15 19:12:58 +08:00
ibuler
5d373c0137 fix: 修复错误格式 2021-07-15 19:12:58 +08:00
Jiangjie.Bai
3aea998bd2 Merge pull request #6462 from jumpserver/dev
v2.12.0 rc5
2021-07-15 18:23:38 +08:00
ibuler
c1ca48a32a perf: 修改i18n 2021-07-15 18:22:53 +08:00
Jiangjie.Bai
2f0fcddc29 Merge pull request #6458 from jumpserver/dev
v2.12.0 rc5
2021-07-15 18:00:27 +08:00
ibuler
329565251a perf: 修改prefetch 2021-07-15 17:58:45 +08:00
ibuler
06a223376c perf: 基本完成 2021-07-15 17:58:45 +08:00
Bai
47e8ad3aac fix: 修复创建资产关联所在节点的系统用户时没有设置组织ID的问题 2021-07-15 17:12:56 +08:00
Jiangjie.Bai
c4fb3a8c04 Merge pull request #6455 from jumpserver/dev
v2.12.0 rc5
2021-07-15 17:04:22 +08:00
ibuler
9d4121c3b7 perf: 优化代码 2021-07-15 11:02:45 +08:00
xinwen
2eb1fe8547 fix: 系统用户与资产关系变化时 AuthBook 表的 org_id 可能是 root 组织 2021-07-15 11:02:45 +08:00
ibuler
e933774e6c fix: 修复创建 authbook 可能没有组织id的问题 2021-07-15 11:01:14 +08:00
Jiangjie.Bai
0b994d1c46 Merge pull request #6450 from jumpserver/dev
v2.12.0 rc4
2021-07-14 21:38:38 +08:00
xinwen
381b150c2b fix: 探测 authbook 在 root 组织下保存的情况 2021-07-14 21:37:48 +08:00
xinwen
53ebac9363 fix: 探测 authbook 在 root 组织下保存的情况 2021-07-14 21:36:53 +08:00
Jiangjie.Bai
a0638dd5c4 Merge pull request #6447 from jumpserver/dev
v2.12.0 rc4
2021-07-14 19:02:45 +08:00
Bai
5b741de896 fix: 修复系统用户资产导出包含组织名称 2021-07-14 18:56:40 +08:00
Bai
d7f587216d fix: 修复测试系统用户可连接性问题 2021-07-14 17:07:55 +08:00
ibuler
019f00a34a perf: 优化特权账号创建和导出
perf: 优化搜索

perf: Huany

perf: 还原

perf: 又改

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

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

perf: 修改i18n

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

* feat: Update README

* feat: Update README

* Update README.md

* feat: update README

* Update README.md

* docs: 修改英文版本

* Update README.md

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

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

* stash

stash

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

* perf: 添加信号

* perf: 添加算法

* perf: 去掉 asset user backends

* perf: 整理系统用户api

* perfF: 暂存一下

* stash

* perf: 暂存一下

* perf: 暂存

* xxx

* perf: ...

* stash it

* xxx

* xxx

* xxx

* xxx

* xxx

* stash it

* 修改Protocols

* perf: 修改创建authbook信号

* perf: 添加auth info

* .stash

* perf: 基本完成

* perf: 修复完成

* perf: 修复更改的id

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

* perf: 修改systemuser

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

* fix: 修复authbook加载的问题

* xxx

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

* fix: 修改翻译信息

* feat: 支持设置默认存储

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

* feat: 支持设置默认存储(3)
2021-06-28 10:32:59 +08:00
xinwen
aa6e550ba2 fix: 系统消息通知升级错误 2021-06-25 23:47:13 +08:00
Jiangjie.Bai
2ffaf59238 Merge pull request #6328 from jumpserver/ibuler-patch-1
Update README.md
2021-06-25 14:47:36 +08:00
ibuler
6c13fdbc46 perf: 优化图片大小
perf: ...
2021-06-25 10:16:26 +08:00
fghbng@qq.com
35941ddf7f feat: 优化缓存,将会话的缓存拿出来 2021-06-25 10:13:51 +08:00
fghbng@qq.com
3ae976c183 优化缓存,将会话的缓存拿出来 2021-06-25 10:13:51 +08:00
ibuler
999666f0eb docs: 修改英文版本 2021-06-23 17:16:01 +08:00
老广
1812074231 Update README.md 2021-06-23 11:31:51 +08:00
Bai
53eb32e620 fix: 修改翻译信息 2021-06-22 19:17:44 +08:00
Bai
50bd0b796d fix: 修改LDAP用户导入的组织为当前组织 2021-06-22 19:17:44 +08:00
wojiushixiaobai
a02d80a2ae feat: arm64 支持 2021-06-22 14:44:47 +08:00
ibuler
71a7eea8ad perf: 修复next为空可能会导致的bug 2021-06-22 11:13:44 +08:00
ibuler
2b927caa60 fix: 修复oidc登录的问题
..
2021-06-22 11:04:12 +08:00
ibuler
053d958f9a fix: 修复app无法下载xrdp文件 2021-06-22 10:20:37 +08:00
ibuler
8d25d0a653 fix: 修复登录页面的 i18n 问题 2021-06-22 10:18:48 +08:00
Bai
62eb131f59 fix: 修改创建用户时如果没有在任何组织内默认添加到default组织 2021-06-21 18:59:07 +08:00
jiangweidong
40eb7c79bb feat: 添加青云SDK 2021-06-21 15:57:21 +08:00
Bai
dabc9eb09b fix: 修改获取系统用户认证信息时username的选择逻辑;(单独设置过的系统用户认证信息登录资产失败) 2021-06-18 18:15:58 +08:00
Jiangjie.Bai
502657bad4 Merge pull request #6294 from jumpserver/dev
v2.11.0 rc5
2021-06-17 12:23:20 +08:00
ibuler
b5120e72c8 perf(notification): 发送html msg 2021-06-17 12:20:51 +08:00
Bai
2ca659414e fix: 修改应用账号序列类添加token字段 2021-06-17 12:13:06 +08:00
Jiangjie.Bai
64f772e747 Merge pull request #6291 from jumpserver/dev
v2.11.0. rc5
2021-06-17 11:49:04 +08:00
Bai
67a897f9c3 fix: 修改ldap导入 2021-06-17 11:48:00 +08:00
Jiangjie.Bai
d0a9ccbdfe Merge pull request #6286 from jumpserver/dev
v2.11.0 rc5 (2)
2021-06-16 19:47:51 +08:00
xinwen
1a30675a86 fix: 去掉命令告警开关 2021-06-16 18:03:17 +08:00
ibuler
f6273450bb perf: 优化批量危险命令告警 2021-06-16 18:02:17 +08:00
ibuler
8f35fcd6f9 perf: 优化通知迁移 2021-06-16 16:58:07 +08:00
xinwen
1999cfdfeb perf: 优化钉钉命令告警 2021-06-16 16:57:28 +08:00
Bai
c4af78c9f0 fix: 修改AuthBook删除raise异常类 2021-06-16 14:42:03 +08:00
Bai
a3d02decd6 fix: 修改翻译 2021-06-16 14:28:28 +08:00
ibuler
e623f63fcf perf: 修改i18n
perf: 优化命令告警,优化翻译
2021-06-16 14:24:57 +08:00
Jiangjie.Bai
4f1b2aceda Merge pull request #6277 from jumpserver/dev
v2.11.0 rc5
2021-06-16 13:03:17 +08:00
健健
94fc1fb53b fix: 导入数据解析 title 时,没有过滤 read only 字段 (#6269)
* feat: Update README (#6182)

* feat: Update README

* feat: Update README

* Update README.md

* feat: update README

* fix: 导入数据解析 title 时,没有过滤 read only 字段

type,type_display 翻译都是一样的,导出时使用的是 type,导入时识别成 type_display

Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2021-06-16 13:00:55 +08:00
xinwen
937acbd0b5 fix: 资产授权不能打开或关闭 2021-06-16 12:54:03 +08:00
xinwen
067a70463e fix: 高危命令邮件收不到 2021-06-16 12:41:14 +08:00
Bai
b115ed3b79 fix: 修改LDAP用户导入默认添加到Default组织 2021-06-16 11:13:21 +08:00
Jiangjie.Bai
057fbdf0b1 Merge pull request #6268 from jumpserver/dev
v2.11.0 rc3
2021-06-15 14:50:02 +08:00
xinwen
5263a146e2 fix: 修复站内信迁移脚本问题 2021-06-15 14:47:16 +08:00
Jiangjie.Bai
84070a558e Merge pull request #6265 from jumpserver/dev
v2.11.0 rc2
2021-06-15 10:49:46 +08:00
Bai
e0604a3211 feat: 修改翻译 2021-06-15 10:42:06 +08:00
Bai
00e4c3cd07 feat: 添加应用用户API 2021-06-15 10:42:06 +08:00
ibuler
97a0e27307 perf: 优化消息中心未读数量 2021-06-11 18:02:53 +08:00
ibuler
8d3c1bd783 perf: 优化获取token secret, 重新校验权限 2021-06-10 19:51:11 +08:00
ibuler
db99ab80db perf(auth): 授权token形式登录,支持记录登录日志 2021-06-10 18:07:24 +08:00
Jiangjie.Bai
1e8d9ba2ec Merge pull request #6256 from jumpserver/dev
v2.11.0 rc1
2021-06-10 14:03:45 +08:00
xinwen
7dddf0c3c2 fix: 站内信未读信息计数不准 2021-06-10 10:24:54 +08:00
fit2bot
891a5157a7 perf: 优化token时间 (#6252)
* perf: 修复上次引起的小bug

* perf: 优化token时间

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-09 20:10:34 +08:00
ibuler
34b2a5fe0b perf: 修复上次引起的小bug 2021-06-09 15:47:26 +08:00
ibuler
de6908e5a6 perf: rdp file添加domain
fix: 禁用的用户不返回信息

perf: 优化token,禁用的资产无法链接
2021-06-09 14:18:15 +08:00
fit2bot
d6527e3b02 perf: 优化支持记录密码 (#6247)
* perf: 优化 xrdp setting

* perf: 优化支持记录密码

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-08 20:50:15 +08:00
ibuler
33a29ae788 perf: 优化 xrdp setting 2021-06-08 15:25:32 +08:00
ibuler
a2eb431015 perf: 优化自动分辨率 2021-06-08 12:48:48 +08:00
Bai
8fbea2f702 fix: 修改资产账号name为非必填 2021-06-08 11:38:37 +08:00
fit2bot
af92271a52 feat: 调整站内信接口 (#6228)
* feat: 调整站内信接口

* 添加 websockt

* 添加信息类型字段

* 添加 has_read 过滤参数

* feat: 调整站内信接口

* 添加 websockt

* 添加信息类型字段

* 添加 has_read 过滤参数

* 去掉type websocket

* perf: 去掉type

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-06-08 11:11:27 +08:00
ibuler
391a5cb7d0 perf: 修复手动设置密码的问题 2021-06-07 10:46:58 +08:00
xinwen
daf7d98f0e fix: 其他组织中创建的用户不要添加到默认组织了 2021-06-04 04:30:40 -05:00
Jiangjie.Bai
ed297fd1bd Merge pull request #6226 from jumpserver/pr@dev@add_missing_migrations
chore: 添加删除的文件
2021-06-04 14:40:32 +08:00
Bai
f91bef4105 feat: 修改依赖包版本号: jms-storage-sdk==0.0.37 2021-06-04 14:37:02 +08:00
Bai
a8d84fc6e1 feat: 修改迁移文件 2021-06-04 11:39:17 +08:00
Bai
0c7838d0e3 feat: 修改迁移文件 2021-06-04 11:39:17 +08:00
Jiangjie.Bai
f26483c9cd Merge pull request #6224 from jumpserver/feat_account_manager
feat: 添加账号管理相关API
2021-06-04 11:15:53 +08:00
Bai
5daca6592b feat: 修改文案 后端 -> 来源 2021-06-04 11:14:53 +08:00
Bai
0bced39f08 fix: 修复redis服务异常时(如: 主从切换), 用户session立即过期的问题 2021-06-03 22:04:10 -05:00
ibuler
6d83dd0e3a chore: 添加删除的文件 2021-06-03 14:54:41 +08:00
ibuler
46e99d10cb Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-06-03 14:28:39 +08:00
liubo
95eb11422a feat: 支持添加 OBS 存储 2021-06-03 01:28:23 -05:00
ibuler
e8b3ee4565 perf: 优化系统用户,支持用户设置临时密码
perf: 优化rdp file下载

perf: 修改密码途观选项

perf: 优化api获取
2021-06-03 01:24:28 -05:00
Bai
1e99be1775 feat: 修改获取应用用户API 2021-06-03 13:59:44 +08:00
Bai
adae509bc0 fix: 修复组织批量删除的问题 2021-06-03 11:36:24 +08:00
ibuler
7868e91844 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-06-03 11:19:02 +08:00
xinwen
a9bdbcf7c6 fix: metadata api view 报错 2021-06-02 22:02:14 -05:00
xinwen
a809eac2b8 fix: 修复获取 Metadata 时,获取的总是 action 为 metadata 2021-06-02 04:41:01 -05:00
Bai
bdab93260f feat: 资产用户API返回 BackendDisplay 和 Name 字段 2021-06-02 17:00:31 +08:00
fit2bot
4ef3b2630a feat: 站内信 (#6183)
* 添加站内信

* s

* s

* 添加接口

* fix

* fix

* 重构了一些

* 完成

* 完善

* s

* s

* s

* s

* s

* s

* 测试ok

* 替换业务中发送消息的方式

* 修改

* s

* 去掉 update 兼容 create

* 添加 unread total 接口

* 调整json字段

Co-authored-by: xinwen <coderWen@126.com>
2021-05-31 17:20:38 +08:00
Bai
4eef25982d feat: 更新 ApplicationUserList API 2021-05-27 18:42:43 +08:00
xinwen
b82e9f860b fix: users 遗漏一个 migration 2021-05-26 15:26:56 +08:00
Bai
6b46f5b48e feat: 添加ApplicationUserList API 2021-05-24 19:11:47 +08:00
Jiangjie.Bai
fe717f0244 feat: Update README (#6182)
* feat: Update README

* feat: Update README

* Update README.md

* feat: update README
2021-05-24 16:04:40 +08:00
ibuler
33fb063f78 perf: 暂时禁用xrdp实时监控 2021-05-24 15:37:21 +08:00
老广
7edc9c37f8 Update README.md 2021-05-24 11:02:18 +08:00
Michael Bai
f8b4259a8c fix: 修复创建/更新用户时密码策略相关的问题 2021-05-23 21:56:37 -05:00
Michael Bai
572d0e3f27 fix: 修复parser没有处理int类型数据的问题 2021-05-23 21:54:14 -05:00
ibuler
b334f3c2d9 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-05-24 10:46:43 +08:00
Jiangjie.Bai
6b4b9f4b02 Merge pull request #6169 from jumpserver/dev
v2.10.1
2021-05-21 15:20:25 +08:00
ibuler
d765e61991 fix(assets): 修复网关信息没有密码的bug 2021-05-21 15:17:29 +08:00
Bai
9ccde03656 fix: 修改cloud翻译 2021-05-20 22:27:29 -05:00
xinwen
c66f366446 fix: 修复 default 组织用户数量统计错误 2021-05-21 10:36:27 +08:00
ibuler
34d46897f8 fix: 修复周期监测任务配置的bug 2021-05-21 10:35:39 +08:00
ibuler
2d9ce16601 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2021-05-20 15:53:16 +08:00
Jiangjie.Bai
0380be51dd Merge pull request #6155 from jumpserver/dev
Merge dev to master
2021-05-20 15:02:28 +08:00
Bai
47df0cfaab fix: 修改翻译 2021-05-20 15:00:10 +08:00
Bai
a2fb4a701e fix: 修复命令过滤器规则Action Choices显示 2021-05-20 01:07:33 -05:00
fit2bot
6e4381ac04 perf: 修改readme (#6152)
* perf: 修改readme

* perf: 修改readme

Co-authored-by: ibuler <ibuler@qq.com>
2021-05-20 13:14:34 +08:00
fghbng@qq.com
8ae03e4374 修复资产导出字段名显示 2021-05-20 00:11:44 -05:00
fghbng@qq.com
73f2022ff6 修复全局组织仪表盘用户总数统计 2021-05-20 00:07:49 -05:00
ibuler
bc4258256a perf: 修改readme 2021-05-20 13:06:37 +08:00
Jiangjie.Bai
58dfe58ae0 Merge pull request #6147 from jumpserver/dev
v2.10.0 rc4
2021-05-19 19:28:10 +08:00
fghbng@qq.com
53e3fa2590 修复全局组织仪表盘用户总数统计 2021-05-19 19:27:30 +08:00
xinwen
23dbdaf6c0 fix: 系统用户里测试资产可连接性不能指定资产 2021-05-19 18:07:29 +08:00
xinwen
3eba92548b fix: 修改企业微信&钉钉一些小问题和翻译 2021-05-19 18:02:34 +08:00
fghbng@qq.com
ac5f2c560d 修复网关更新获取到了明文密码 2021-05-19 17:58:26 +08:00
Bai
f7f9331c48 fix: 修复Dashboard活跃用户数据不准确问题 2021-05-19 17:49:03 +08:00
xinwen
77b4847bd9 fix: 有在线会话的终端不能删除 2021-05-19 16:17:47 +08:00
Jiangjie.Bai
0de9b29fa9 Merge pull request #6136 from jumpserver/dev
v2.10.0 rc3
2021-05-18 19:16:25 +08:00
xinwen
f9ca46dd67 fix: 修复用户历史密码在创建时不起作用 2021-05-18 19:15:58 +08:00
xinwen
ba28f3263d fix: 企业微信&钉钉解绑报错 2021-05-18 14:03:16 +08:00
xinwen
2e118665f5 fix: 过期用户退出登录 2021-05-17 21:08:01 -05:00
fit2bot
bf53df46dc fix: 修复包含组织管理员时可以删除组织的问题 (#6130)
Co-authored-by: Bai <bugatti_it@163.com>
2021-05-17 19:11:55 +08:00
fit2bot
6449f36c7e perf: 修改文案 (#6129)
* perf: 修改i18n

* perf: 修改文案

Co-authored-by: ibuler <ibuler@qq.com>
2021-05-17 19:11:28 +08:00
Bai
ba35f5906b fix: 修复收集用户interval等字段的校验 2021-05-17 18:31:30 +08:00
fghbng@qq.com
c8d7d42f66 仪表盘全局组织统计 2021-05-17 17:35:13 +08:00
fghbng@qq.com
20dacea260 仪表盘全局组织报500错误 2021-05-17 17:35:13 +08:00
ibuler
d2dc2ab02c perf: 修改i18n 2021-05-17 17:30:56 +08:00
xinwen
ba3b5a4027 fix: 创建 Es 时失败的提示翻译 2021-05-17 17:29:53 +08:00
xinwen
3743761024 fix: 修复绑定企业微信&钉钉的一些问题 2021-05-17 17:20:48 +08:00
Bai
70055b8af2 fix: 修复remoteapp获取asset_info失败的问题 2021-05-17 16:15:28 +08:00
ibuler
726fd94f65 fix: 修复 xslx 提交数字类型报错 2021-05-17 01:54:52 -05:00
Bai
8b951ce12c perf: 添加迁移文件(lion) 2021-05-17 14:50:35 +08:00
Bai
189bc9d74a perf: 添加lion终端类型; 修改加入会话校验逻辑(vnc/rdp) 2021-05-17 14:50:35 +08:00
Jiangjie.Bai
dd6c063478 Merge pull request #6119 from jumpserver/dev
v2.10.0 rc2 fix-dashboard
2021-05-17 10:11:06 +08:00
fghbng@qq.com
5e9006d0c2 修复仪表盘数据统计错误 2021-05-17 10:09:38 +08:00
Jiangjie.Bai
c42f69d1ba Merge pull request #6117 from jumpserver/dev
v2.10.0 rc2
2021-05-14 19:20:40 +08:00
fghbng@qq.com
c7dfd0edce 修复授权导入系统用户为空报错 2021-05-14 19:20:24 +08:00
fghbng@qq.com
4382921c57 修复授权导入优化资产、用户、用户组、节点、系统用户id为空报错的情况 2021-05-14 19:20:24 +08:00
Bai
45feb468be perf: 优化工单邮件信息 2021-05-14 16:19:25 +08:00
xinwen
c9b6b9a37a fix: 修复企业微信,钉钉登录 BACKEND 没有注册 2021-05-14 15:55:28 +08:00
Michael Bai
8010bdecea fix: 修复创建动态系统用户时设置了home目录,使得所有推送的用户共用同一个home目录,导致目录权限只限制在第一个推送的用户,其他用户进行可连接性测试时失败的问题 2021-05-14 15:43:50 +08:00
Bai
fc1c9c564a fix: 修改翻译文件 2021-05-14 11:01:36 +08:00
Jiangjie.Bai
7c13b72739 Merge pull request #6107 from jumpserver/dev
v2.10.0 rc1
2021-05-13 19:51:43 +08:00
Bai
6a4bc1f8b3 perf: 修改翻译 2021-05-13 19:50:52 +08:00
Jiangjie.Bai
7d51d8c570 Merge pull request #6105 from jumpserver/dev
v2.10 rc1
2021-05-13 19:19:42 +08:00
Bai
0ecd9fa32a fix: 修复自动生成公钥优先使用dss格式的问题(默认优先使用rsa) 2021-05-13 19:12:03 +08:00
xinwen
b37c8b09bf refactor: 添加一些翻译&修正字段WECOM_SECRET 2021-05-13 16:29:41 +08:00
fghbng@qq.com
23f22e92b8 首页的统计数据,可以从 org resource cache 中获取
首页的统计数据,可以从 org resource cache 中获取
2021-05-13 16:05:53 +08:00
xinwen
c16319ec48 feat: 添加企业微信,钉钉扫码登录 2021-05-13 14:15:07 +08:00
jym503558564
340547c889 perf(README): 白皮书下载 2021-05-12 02:12:34 -05:00
xinwen
54f5e65d36 feat: 检查资产授权过期接口添加过期时间 2021-05-11 10:40:33 +08:00
ibuler
4d6d4cbc22 perf: 优化登录,cas, openid 自动登录 2021-05-07 05:58:56 -05:00
xinwen
7294f6e5e0 refactor: command es storage IGNORE_VERIFY_CERTS 2021-05-07 03:48:59 -05:00
ibuler
8ca2522c71 fix: 修改tokent中信息中没有返回 Protocols 的问题
fix: 优化protocols

fix: session bpp

token 时间加长
2021-04-30 01:29:52 -05:00
fghbng@qq.com
72f9d0d371 serializer优化&&资产授权导入优化 2021-04-30 14:05:46 +08:00
fghbng@qq.com
9a92e24e50 serializer优化&&授权导入优化 2021-04-30 14:05:46 +08:00
Bai
fea0170c5e perf: 可以删除包含子孙节点但不包含子孙资产的节点 2021-04-29 00:42:58 -05:00
ibuler
5e5cd80bc2 perf: 优化登录前修改密码 2021-04-29 00:42:07 -05:00
fit2cloud-jiangweidong
e3511df4f8 feat: 管理员可以设置用户是否下次登录需修改密码 (#6006)
* feat: 管理员可以设置用户是否下次登录需修改密码

* feat: 管理员可以设置用户下次是否需要更改密码,本次修改:字段命名规范化

* feat: 管理员可以设置用户下次是否需要更改密码,本次修改:字段命名规范化

* fix: 用户下次登录是否需要改密,函数名及变量名规范化

* fix: 管理员设置用户下次是否改密功能的国际化翻译文件

* fixs: 管理员设置用户下次登录是否需改密功能,逻辑修改

* fix: 管理员可设置用户下次登录是否需要改密,字段名称更改
2021-04-28 19:25:30 +08:00
fit2cloud-jiangweidong
11e5a97f14 feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 (#6010)
* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数

* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 判断是否为历史密码逻辑修改

* feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 提示内容更人性化

* fixs: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数, 最新国际化翻译文件
2021-04-28 17:03:20 +08:00
fit2bot
4519ccfe1a 授权导入优化 (#6057)
* 授权导入优化,支持使用 用户名,资产名,ip,节点路径,系统用户名称导入

* Update permission.py

* 授权导入优化

* 授权导入优化

* 授权导入优化

* 授权导入优化

Co-authored-by: fghbng@qq.com <fghbng@qq.com>
2021-04-28 16:42:54 +08:00
xinwen
657a2ac7e7 fix: 命令记录导出选择项 2021-04-28 03:35:36 -05:00
Bai
f5d8e125cb fix: 修复创建资产不传nodes时报错的问题 & 修复Option资产API时报JSON序列化失败的问题 2021-04-28 03:19:00 -05:00
xinwen
fd203c67c3 fix: 添加无效的 es 命令记录存储时,抛出错误提示 2021-04-27 05:39:50 -05:00
jing guo
9fe5496ce9 通过 api 添加资产,不写 protocols 时,默认值应该是列表 2021-04-27 05:36:46 -05:00
老广
c0875f6a87 Merge pull request #6037 from jumpserver/pr@dev@perf_public_key_setting
perf: 优化公钥设置,并删掉一部分不用的 html
2021-04-27 05:07:53 -05:00
ibuler
d1a005f750 perf: 优化MFA verify requierd 2021-04-27 05:05:22 -05:00
ibuler
c52431b5ce chore(merge): 合并ddev 2021-04-27 18:01:15 +08:00
Bai
4a9e83ba15 feat: 添加命令复核逻辑; 添加命令复核工单; 5 2021-04-27 17:53:06 +08:00
Bai
7712c1659e feat: 添加命令复核逻辑; 添加命令复核工单; 4 2021-04-27 16:36:42 +08:00
Bai
74c7b18dc4 feat: 添加命令复核逻辑; 添加命令复核工单; 3 2021-04-27 16:36:42 +08:00
Bai
5a3c67989b feat: 添加命令复核逻辑; 添加命令复核工单; 2 2021-04-27 16:36:42 +08:00
Bai
50918a3dd2 feat: 添加命令复核逻辑; 添加命令复核工单; 2021-04-27 16:36:42 +08:00
Bai
e9b174f342 feat: 修改命令过滤规则Model: 添加Action-reconfirm; 添加field-reviewers 2021-04-27 16:36:42 +08:00
老广
63efbfe62e Merge pull request #6049 from jumpserver/pr@dev@fix_expire_caches
fix: 添加启动失效缓存
2021-04-27 03:12:53 -05:00
xinwen
99cce185dd fix: 添加启动失效缓存 2021-04-27 16:09:07 +08:00
ibuler
ab0fda93f6 perf: 优化公钥设置,并删掉一部分不用的 html 2021-04-26 10:21:22 +08:00
ibuler
d9552c0038 perf: 优化公钥设置,让用户可以选择是否开启 2021-04-25 18:13:41 +08:00
老广
f0f493081a Merge pull request #6032 from jumpserver/pr@dev@fix_panelboard
【仪表盘】在线用户数不对,(连上windows资产之后,在线用户数就不对了)
2021-04-25 02:06:34 -05:00
fghbng@qq.com
c4727e1eba 【仪表盘】在线用户数不对,(连上windows资产之后,在线用户数就不对了) 2021-04-25 14:58:06 +08:00
Bai
ce8143c2ec fix: 修改ACL提示支持的协议为: ssh、telnet 2021-04-23 16:35:50 +08:00
Bai
65ad63272c fix: 修复操作应用/应用授权/acl等未记录日志的问题2 2021-04-20 16:47:31 +08:00
老广
4a4d5f3243 Merge pull request #5999 from jumpserver/pr@dev@fix_rdp_file_addr
fix: 修复下载rdp文件失败的问题
2021-04-20 03:28:34 -05:00
ibuler
4563743f00 fix: 修复下载rdp文件失败的问题 2021-04-20 16:17:18 +08:00
ibuler
7b679f3e82 fix(task): 修复推送过期的问题
fix(rdp): 修复下载rdp文件失败的问题
2021-04-20 15:20:49 +08:00
Bai
3d6aa15ece fix: 修复操作应用/应用授权/acl等未记录日志的问题 2021-04-20 00:08:23 -05:00
ibuler
94a798eb01 fix(task): 修复推送过期的问题 2021-04-20 12:58:48 +08:00
ibuler
ec393c1440 fix(task): 修复推送过期的问题 2021-04-20 11:27:02 +08:00
ibuler
6571209864 fix: 修复创建的系统用户很快过期的问题 2021-04-19 17:01:48 +08:00
Jiangjie.Bai
d042de7b09 Merge pull request #5972 from jumpserver/dev
v2.9.0 发版
2021-04-15 21:02:28 +08:00
ibuler
5e6e97c822 perf: 优化推送系统用户,设置有效期 2021-04-15 19:25:58 +08:00
xinwen
f146873501 fix: key=0 修改到 key=1 时 parent_key 没有更新 2021-04-15 01:33:00 -05:00
Jiangjie.Bai
35dfdf831a Merge pull request #5965 from jumpserver/dev
v2.9.0 rc3
2021-04-14 18:43:45 +08:00
xinwen
2b31cb2806 fix: 命令记录导出适配 ES 2021-04-14 05:02:50 -05:00
xinwen
e43ffa7994 fix: 远程应用显示名称 2021-04-14 04:56:59 -05:00
ibuler
b0a9a83231 fix(terminal): 修复终端列表看到的在线会话数量不对的bug 2021-04-14 16:41:57 +08:00
xinwen
7da14571ac fix: 请求 token 接口,登录类型没内容 2021-04-14 03:14:10 -05:00
xinwen
73b67da4c0 fix: 修复 acl 一些翻译 2021-04-14 03:13:18 -05:00
Jiangjie.Bai
4bf2371cf0 Merge pull request #5952 from jumpserver/dev
v2.9.0 rc2
2021-04-13 19:19:43 +08:00
Jiangjie.Bai
075cbc497b Merge pull request #5953 from jumpserver/pr@dev@dev_merge
chore(merge): 合并
2021-04-13 19:16:54 +08:00
ibuler
1a0d9a20f9 chore(merge): 合并 2021-04-13 18:54:08 +08:00
ibuler
fdb8416cac fix: 修复组件在线会话数量不对的问题 2021-04-13 05:49:03 -05:00
ibuler
e2d5b69510 perf: 优化健康监测,并添加 health check 的 key 2021-04-13 05:48:35 -05:00
xinwen
9944474ba0 fix: settings 订阅不稳定 2021-04-13 05:40:04 -05:00
xinwen
ce6b9de07c fix: ES 自动创建索引 2021-04-13 04:44:44 -05:00
xinwen
b97759687d fix: 邀请用没有触发信号 2021-04-13 04:23:29 -05:00
xinwen
68b6236de2 fix: SSO 登录日志 2021-04-12 04:48:17 -05:00
xinwen
6616374c30 fix: subscribe_settings_change 2021-04-12 04:45:30 -05:00
xinwen
682f6b2fb9 fix: 资产节点关系变化时也要清空 root 组织的 node_assets_mapping 2021-04-12 04:44:13 -05:00
xinwen
a2e3979916 fix: org_mapping 保护订阅线程 2021-04-12 04:42:48 -05:00
xinwen
f11d3c1cf2 fix: 过期用户登录提示无效 2021-04-09 02:03:35 -05:00
xinwen
f0bad5f107 fix: 登录页面测试 cookie 失败 2021-04-09 01:46:21 -05:00
ibuler
ad3bc72dfb fix(terminal): 修复session id 长度误写为 35 的bug 2021-04-08 19:23:36 +08:00
xinwen
de9c69843d fix: 登录日志 user_agent 过长 2021-04-08 19:23:36 +08:00
xinwen
d2678e2a43 refactor: 移动 PermissionsMixin 位置 2021-04-08 19:23:36 +08:00
xinwen
632ea87f07 feat: MFA 登录次数限制 2021-04-08 19:23:36 +08:00
fit2bot
4e7e1d5e15 style: 优化全局组织设置相关代码 (#5921)
* feat:支持配置全局组织的显示名称

* style: 优化全局组织设置相关代码

Co-authored-by: liubo <liubo@fit2cloud.com>
2021-04-08 19:23:36 +08:00
liuboF2c
1ac8537a34 feat:支持配置全局组织的显示名称 (#5919)
Co-authored-by: liubo <liubo@fit2cloud.com>
2021-04-08 19:23:36 +08:00
fit2bot
dcaa798c2e perf: csv upload (#5894)
perf: 修改翻译

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-08 19:23:36 +08:00
xinwen
8da4027e32 fix: 授权资产列表 platform 应该显示名称 2021-04-08 19:23:36 +08:00
xinwen
32e2d19553 fix: 改密计划关掉周期执行再打开,任务不再执行 2021-04-08 19:23:36 +08:00
xinwen
48d1eecc08 fix: 修正 key 为 0 的节点 2021-04-08 19:23:36 +08:00
xinwen
0ab88ce754 fix: 访问 tokens 接口更新用户最后登录时间 2021-04-08 19:23:36 +08:00
xinwen
bee5500425 fix: 创建节点的时候加锁,可以并发调用 2021-04-08 19:23:36 +08:00
xinwen
7c03af7668 feat: 资产授权支持按名称模糊搜索 2021-04-08 19:23:36 +08:00
xinwen
7a61a671a2 fix: 管理用户输入带密码的秘钥报错 2021-04-08 19:23:36 +08:00
Bai
4a1fc0e2ac fix: 修复NodeChildrenAddAPI不支持patch方法的问题 2021-04-08 19:23:36 +08:00
ibuler
1e5e87e62a perf: 优化acl提示 2021-04-08 19:23:36 +08:00
ibuler
96c3b81383 perf: upgrade requirements version 2021-04-08 19:23:36 +08:00
xinwen
297fedeffa fix: Default 组织下出现 app user 2021-04-08 19:23:36 +08:00
ibuler
9cd5675209 perf: 修改terminal statuts
perf: 优化status api

perf: 优化 status api

perf: 修改sesion参数

perf: 修改migrations

perf: 优化数据结构

perf: 修改保留日志

perf: 优化之前的一个写法
2021-04-08 19:23:36 +08:00
xinwen
a5179d1596 feat: 增加 es 忽略 https 证书验证 2021-04-08 19:23:36 +08:00
Bai
c2463fe573 perf: Session Login from 添加 RDP Terminal 类型 2021-04-08 19:23:36 +08:00
xinwen
2f8042141c fix: 授权树节点排序 2021-04-08 19:23:36 +08:00
ibuler
06a4e0d395 perf: 修改表结构迁移,增加rdp terminal 2021-04-08 19:23:36 +08:00
xinwen
bb9d92fd7e perf: delete_test_cookie 2021-04-08 19:23:36 +08:00
ibuler
749f9d3f81 fix(terminal): 修复session id 长度误写为 35 的bug 2021-04-08 17:39:29 +08:00
xinwen
03ad7777d0 fix: 登录日志 user_agent 过长 2021-04-08 04:39:12 -05:00
xinwen
7e4f20f443 refactor: 移动 PermissionsMixin 位置 2021-04-08 02:15:02 -05:00
xinwen
607b7fd29f feat: MFA 登录次数限制 2021-04-08 01:46:36 -05:00
fit2bot
8895763ab4 style: 优化全局组织设置相关代码 (#5921)
* feat:支持配置全局组织的显示名称

* style: 优化全局组织设置相关代码

Co-authored-by: liubo <liubo@fit2cloud.com>
2021-04-08 14:18:53 +08:00
liuboF2c
8b1e202e68 feat:支持配置全局组织的显示名称 (#5919)
Co-authored-by: liubo <liubo@fit2cloud.com>
2021-04-08 13:55:58 +08:00
fit2bot
32fe8f674c perf: csv upload (#5894)
perf: 修改翻译

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-08 10:11:46 +08:00
xinwen
b4ef7bef55 fix: 授权资产列表 platform 应该显示名称 2021-04-08 10:10:32 +08:00
xinwen
31982c6547 fix: 改密计划关掉周期执行再打开,任务不再执行 2021-04-07 18:38:45 +08:00
xinwen
67d3b63c6d fix: 修正 key 为 0 的节点 2021-04-07 11:11:23 +08:00
xinwen
f34fb5d9d5 fix: 访问 tokens 接口更新用户最后登录时间 2021-04-07 10:45:34 +08:00
xinwen
3ec78ff9be fix: 创建节点的时候加锁,可以并发调用 2021-04-07 10:29:19 +08:00
xinwen
f361621ab5 feat: 资产授权支持按名称模糊搜索 2021-04-07 10:28:14 +08:00
xinwen
cd9587f68e fix: 管理用户输入带密码的秘钥报错 2021-04-06 19:46:16 +08:00
Bai
2ff01a4bb3 fix: 修复NodeChildrenAddAPI不支持patch方法的问题 2021-04-01 10:55:21 +08:00
ibuler
06ed358fbc perf: 优化acl提示 2021-03-30 10:36:41 +08:00
ibuler
3e11249e8c perf: upgrade requirements version 2021-03-30 10:25:30 +08:00
xinwen
6b5435b768 fix: Default 组织下出现 app user 2021-03-30 10:24:25 +08:00
ibuler
7d5a13de38 perf: 修改terminal statuts
perf: 优化status api

perf: 优化 status api

perf: 修改sesion参数

perf: 修改migrations

perf: 优化数据结构

perf: 修改保留日志

perf: 优化之前的一个写法
2021-03-29 19:21:32 +08:00
xinwen
07bd44990b feat: 增加 es 忽略 https 证书验证 2021-03-29 15:23:33 +08:00
noon
e4938ffc85 Update README_EN.md (#5856)
* Update README_EN.md

Translate parts of the README.md

* Update README_EN.md

* Update README_EN.md

change the word PAM to Bastion host

* Update README_EN.md

* Update README_EN.md

Clip the bug part to JumpServer 远程执行漏洞 2021-01-15
2021-03-27 20:14:45 +08:00
Bai
85d226eb07 perf: Session Login from 添加 RDP Terminal 类型 2021-03-26 10:38:48 +08:00
xinwen
c9a9ca7923 fix: 授权树节点排序 2021-03-24 10:23:45 +08:00
ibuler
306f7a08d1 perf: 修改表结构迁移,增加rdp terminal 2021-03-24 10:14:59 +08:00
老广
b86f9ac871 Update README.md 2021-03-23 15:47:19 +08:00
xinwen
2562386fe0 perf: delete_test_cookie 2021-03-23 15:27:06 +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
382 changed files with 12786 additions and 8523 deletions

2
.gitignore vendored
View File

@@ -15,6 +15,7 @@ dump.rdb
.tox
.cache/
.idea/
.vscode/
db.sqlite3
config.py
config.yml
@@ -38,3 +39,4 @@ logs/*
.vagrant/
release/*
releashe
/apps/script.py

277
README.md
View File

@@ -1,27 +1,28 @@
# JumpServer 多云环境下更好用的堡垒机
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
<h3 align="center">多云环境下更好用的堡垒机</h3>
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](https://hub.docker.com/u/jumpserver)
- [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)安全通知|
|------------------|
|2021年1月15日 JumpServer 发现远程执行漏洞,请速度修复 [详见](https://github.com/jumpserver/jumpserver/issues/5533) 非常感谢 **reactivity of Alibaba Hackerone bug bounty program**(瑞典) 向我们报告了此 BUG|
<p align="center">
<a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0"><img src="https://shields.io/github/license/jumpserver/jumpserver" alt="License: GPL v2"></a>
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
</p>
--------------------------
- [ENGLISH](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
改变世界,从一点点开始
改变世界,从一点点开始 ...
## 特色优势
### 特色优势
- 开源: 零门槛,线上快速获取和安装;
- 分布式: 轻松支持大规模并发访问;
@@ -32,226 +33,28 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- 多应用支持: 数据库Windows远程应用Kubernetes。
## 功能列表
<table>
<tr>
<td rowspan="8">身份认证<br>Authentication</td>
<td rowspan="5">登录认证</td>
<td>资源统一登录与认证</td>
</tr>
<tr>
<td>LDAP/AD 认证</td>
</tr>
<tr>
<td>RADIUS 认证</td>
</tr>
<tr>
<td>OpenID 认证(实现单点登录)</td>
</tr>
<tr>
<td>CAS 认证 (实现单点登录)</td>
</tr>
<tr>
<td rowspan="2">MFA认证</td>
<td>MFA 二次认证Google Authenticator</td>
</tr>
<tr>
<td>RADIUS 二次认证</td>
</tr>
<tr>
<td>登录复核</td>
<td>用户登录行为受管理员的监管与控制:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="11">账号管理<br>Account</td>
<td rowspan="2">集中账号</td>
<td>管理用户管理</td>
</tr>
<tr>
<td>系统用户管理</td>
</tr>
<tr>
<td rowspan="4">统一密码</td>
<td>资产密码托管</td>
</tr>
<tr>
<td>自动生成密码</td>
</tr>
<tr>
<td>自动推送密码</td>
</tr>
<tr>
<td>密码过期设置</td>
</tr>
<tr>
<td rowspan="2">批量改密</td>
<td>定期批量改密:small_orange_diamond:</td>
</tr>
<tr>
<td>多种密码策略:small_orange_diamond:</td>
</tr>
<tr>
<td>多云纳管 </td>
<td>对私有云、公有云资产自动统一纳管:small_orange_diamond:</td>
</tr>
<tr>
<td>收集用户 </td>
<td>自定义任务定期收集主机用户:small_orange_diamond:</td>
</tr>
<tr>
<td>密码匣子 </td>
<td>统一对资产主机的用户密码进行查看、更新、测试操作:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="15">授权控制<br>Authorization</td>
<td>多维授权</td>
<td>对用户、用户组、资产、资产节点、应用以及系统用户进行授权</td>
</tr>
<tr>
<td rowspan="4">资产授权</td>
<td>资产以树状结构进行展示</td>
</tr>
<tr>
<td>资产和节点均可灵活授权</td>
</tr>
<tr>
<td>节点内资产自动继承授权</td>
</tr>
<tr>
<td>子节点自动继承父节点授权</td>
</tr>
<tr>
<td rowspan="2">应用授权</td>
<td>实现更细粒度的应用级授权</td>
</tr>
<tr>
<td>MySQL 数据库应用、RemoteApp 远程应用:small_orange_diamond: </td>
</tr>
<tr>
<td>动作授权</td>
<td>实现对授权资产的文件上传、下载以及连接动作的控制</td>
</tr>
<tr>
<td>时间授权</td>
<td>实现对授权资源使用时间段的限制</td>
</tr>
<tr>
<td>特权指令</td>
<td>实现对特权指令的使用(支持黑白名单)</td>
</tr>
<tr>
<td>命令过滤</td>
<td>实现对授权系统用户所执行的命令进行控制</td>
</tr>
<tr>
<td>文件传输</td>
<td>SFTP 文件上传/下载</td>
</tr>
<tr>
<td>文件管理</td>
<td>实现 Web SFTP 文件管理</td>
</tr>
<tr>
<td>工单管理</td>
<td>支持对用户登录请求行为进行控制:small_orange_diamond:</td>
</tr>
<tr>
<td>组织管理</td>
<td>实现多租户管理与权限隔离:small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="7">安全审计<br>Audit</td>
<td>操作审计</td>
<td>用户操作行为审计</td>
</tr>
<tr>
<td rowspan="2">会话审计</td>
<td>在线会话内容审计</td>
</tr>
<tr>
<td>历史会话内容审计</td>
</tr>
<tr>
<td rowspan="2">录像审计</td>
<td>支持对 Linux、Windows 等资产操作的录像进行回放审计</td>
</tr>
<tr>
<td>支持对 RemoteApp:small_orange_diamond:、MySQL 等应用操作的录像进行回放审计</td>
</tr>
<tr>
<td>指令审计</td>
<td>支持对资产和应用等操作的命令进行审计</td>
</tr>
<tr>
<td>文件传输</td>
<td>可对文件的上传、下载记录进行审计</td>
</tr>
<tr>
<td rowspan="20">数据库审计<br>Database</td>
<td rowspan="2">连接方式</td>
<td>命令方式</td>
</tr>
<tr>
<td>Web UI方式 :small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="4">支持的数据库</td>
<td>MySQL</td>
</tr>
<tr>
<td>Oracle :small_orange_diamond:</td>
</tr>
<tr>
<td>MariaDB :small_orange_diamond:</td>
</tr>
<tr>
<td>PostgreSQL :small_orange_diamond:</td>
</tr>
<tr>
<td rowspan="6">功能亮点</td>
<td>语法高亮</td>
</tr>
<tr>
<td>SQL格式化</td>
</tr>
<tr>
<td>支持快捷键</td>
</tr>
<tr>
<td>支持选中执行</td>
</tr>
<tr>
<td>SQL历史查询</td>
</tr>
<tr>
<td>支持页面创建 DB, TABLE</td>
</tr>
<tr>
<td rowspan="2">会话审计</td>
<td>命令记录</td>
</tr>
<tr>
<td>录像回放</td>
</tr>
</table>
**说明**: 带 :small_orange_diamond: 后缀的是 X-PACK 插件有的功能
## 快速开始
### 快速开始
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
- [完整文档](https://docs.jumpserver.org)
- [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
- [手动安装](https://github.com/jumpserver/installer)
## 组件项目
### 组件项目
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
- [Guacamole](https://github.com/jumpserver/docker-guacamole) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
## 贡献
### 社区
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose) 或加入到我们的社区当中进行进一步交流沟通。
#### 微信交流群
<img src="https://download.jumpserver.org/images/weixin-group.jpeg" alt="微信群二维码" width="200"/>
### 贡献
如果有你好的想法创意,或者帮助我们修复了 Bug, 欢迎提交 Pull Request
感谢以下贡献者,让 JumpServer 更加完善
@@ -260,16 +63,29 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
<a href="https://github.com/jumpserver/koko/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
</a>
## 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化连接依赖
<a href="https://github.com/jumpserver/lina/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
</a>
<a href="https://github.com/jumpserver/luna/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
</a>
### 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化组件 Lion 依赖
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库JumpServer Web数据库依赖
## JumpServer 企业版
### JumpServer 企业版
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
## 案例研究
### 案例研究
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
@@ -280,7 +96,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
## 安全说明
### 安全说明
JumpServer是一款安全产品请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装.
@@ -290,7 +106,7 @@ JumpServer是一款安全产品请参考 [基本安全建议](https://docs.ju
- support@fit2cloud.com
- 400-052-0755
## License & Copyright
### License & Copyright
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
@@ -299,3 +115,4 @@ Licensed under The GNU General Public License version 2 (GPLv2) (the "License")
https://www.gnu.org/licenses/gpl-2.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -1,167 +1,88 @@
## Jumpserver
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
<h3 align="center">Open Source Bastion Host</h3>
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](https://hub.docker.com/u/jumpserver)
<p align="center">
<a href="https://www.gnu.org/licenses/old-licenses/gpl-2.0"><img src="https://shields.io/github/license/jumpserver/jumpserver" alt="License: GPL v2"></a>
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
</p>
----
## CRITICAL BUG WARNING
JumpServer is the world's first open-source Bastion Host and is licensed under the GNU GPL v2.0. It is a 4A-compliant professional operation and maintenance security audit system.
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.
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
Thanks for **reactivity from Alibaba Hackerone bug bounty program** report us this bug
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.
**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)
```
--------------------------
Change the world by taking every little step
----
### Advantages
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README.md)
- Open Source: huge transparency and free to access with quick installation process.
- Distributed: support large-scale concurrent access with ease.
- No Plugin required: all you need is a browser, the ultimate Web Terminal experience.
- Multi-Cloud supported: a unified system to manage assets on different clouds at the same time
- Cloud storage: audit records are stored in the cloud. Data lost no more!
- Multi-Tenant system: multiple subsidiary companies or departments access the same system simultaneously.
- Many applications supported: link to databases, windows remote applications, and Kubernetes cluster, etc.
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 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 Component Projects
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal
- [KoKo](https://github.com/jumpserver/koko) JumpServer Character protocaol Connector, replace original Python Version [Coco](https://github.com/jumpserver/coco)
- [Lion](https://github.com/jumpserver/lion-release) JumpServer Graphics protocol Connectorrely on [Apache Guacamole](https://guacamole.apache.org/)
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.
### Contribution
If you have any good ideas or helping us to fix bugs, please submit a Pull Request and accept our thanks :)
Change the world, starting from little things.
Thanks to the following contributors for making JumpServer better everyday!
----
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
### Features
<a href="https://github.com/jumpserver/koko/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
</a>
![Jumpserver 功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver148.jpeg "Jumpserver 功能")
<a href="https://github.com/jumpserver/lina/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
</a>
### Start
<a href="https://github.com/jumpserver/luna/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
</a>
Quick start [Docker Install](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
### Thanks to
- [Apache Guacamole](https://guacamole.apache.org/) Web page connection RDP, SSH, VNC protocol equipment. JumpServer graphical connection dependent.
- [OmniDB](https://omnidb.org/) Web page connection to databases. JumpServer Web database dependent.
Step by Step deployment. [Docs](http://docs.jumpserver.org/zh/docs/step_by_step.html)
Full documentation [Docs](http://docs.jumpserver.org)
### JumpServer Enterprise Version
- [Apply for it](https://jinshuju.net/f/kyOYpi)
### Demo、Video 和 Snapshot
### Case Study
We provide online demo, demo video and screenshots to get you started quickly.
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
[Demo](https://demo.jumpserver.org/auth/login/?next=/)
[Video](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
[Snapshot](http://docs.jumpserver.org/zh/docs/snapshot.html)
### For safety instructions
### SDK
JumpServer is a security product. Please refer to [Basic Security Recommendations](https://docs.jumpserver.org/zh/master/install/install_security/) for deployment and installation.
We provide the SDK for your other systems to quickly interact with the Jumpserver API.
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) Thanks to 恺珺 for providing his Java SDK vesrion.
If you find a security problem, please contact us directly
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755
### License & Copyright
Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.

View File

@@ -5,7 +5,7 @@ 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 tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..models import LoginAssetACL
from .. import serializers
@@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView):
org_id=self.serializer.org.id
)
confirm_status_url = reverse(
view_name='acls:login-asset-confirm-status',
view_name='api-acls:login-asset-confirm-status',
kwargs={'pk': str(ticket.id)}
)
ticket_detail_url = reverse(
@@ -72,34 +72,6 @@ class LoginAssetCheckAPI(CreateAPIView):
return serializer
class LoginAssetConfirmStatusAPI(RetrieveDestroyAPIView):
permission_classes = (IsAppUser, )
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass
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'])

View File

@@ -1,9 +0,0 @@
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

@@ -2,7 +2,7 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .base import BaseACL, BaseACLQuerySet
from ..utils import contains_ip
from common.utils.ip import contains_ip
class ACLManager(models.Manager):
@@ -33,6 +33,9 @@ class LoginACL(BaseACL):
class Meta:
ordering = ('priority', '-date_updated', 'name')
def __str__(self):
return self.name
@property
def action_reject(self):
return self.action == self.ActionChoices.reject

View File

@@ -3,7 +3,7 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import BaseACL, BaseACLQuerySet
from ..utils import contains_ip
from common.utils.ip import contains_ip
class ACLManager(OrgManager):
@@ -38,6 +38,9 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
unique_together = ('name', 'org_id')
ordering = ('priority', '-date_updated', 'name')
def __str__(self):
return self.name
@classmethod
def filter(cls, user, asset, system_user, action):
queryset = cls.objects.filter(action=action)

View File

@@ -3,8 +3,7 @@ from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from orgs.utils import current_org
from ..models import LoginACL
from ..utils import is_ip_address, is_ip_network, is_ip_segment
from .. import const
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
__all__ = ['LoginACLSerializer', ]
@@ -21,8 +20,14 @@ def ip_group_child_validator(ip_group_child):
class LoginACLSerializer(BulkModelSerializer):
ip_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. '
'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 '
)
ip_group = serializers.ListField(
default=['*'], label=_('IP'), help_text=const.ip_group_help_text,
default=['*'], label=_('IP'), help_text=ip_group_help_text,
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
)
user_display = serializers.ReadOnlyField(source='user.name', label=_('User'))
@@ -30,10 +35,15 @@ class LoginACLSerializer(BulkModelSerializer):
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'
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'priority', 'ip_group', 'action', 'action_display',
'is_active',
'date_created', 'date_updated',
'comment', 'created_by',
]
fields_fk = ['user', 'user_display',]
fields = fields_small + fields_fk
extra_kwargs = {
'priority': {'default': 50},
'is_active': {'default': True},

View File

@@ -1,46 +1,60 @@
from rest_framework import serializers
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy 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']
common_help_text = _('Format for comma-delimited string, with * indicating a match all. ')
class LoginAssetACLUsersSerializer(serializers.Serializer):
username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
help_text=const.common_help_text
help_text=common_help_text
)
class LoginAssetACLAssestsSerializer(serializers.Serializer):
ip_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. '
'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 '
'(Domain name support)'
)
ip_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=1024), label=_('IP'),
help_text=const.ip_group_help_text + _('(Domain name support)')
help_text=ip_group_help_text
)
hostname_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Hostname'),
help_text=const.common_help_text
help_text=common_help_text
)
class LoginAssetACLSystemUsersSerializer(serializers.Serializer):
protocol_group_help_text = _(
'Format for comma-delimited string, with * indicating a match all. '
'Protocol options: {}'
)
name_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Name'),
help_text=const.common_help_text
help_text=common_help_text
)
username_group = serializers.ListField(
default=['*'], child=serializers.CharField(max_length=128), label=_('Username'),
help_text=const.common_help_text
help_text=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)
help_text=protocol_group_help_text.format(
', '.join([SystemUser.Protocol.ssh, SystemUser.Protocol.telnet])
)
)
@@ -62,11 +76,15 @@ class LoginAssetACLSerializer(BulkOrgResourceModelSerializer):
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'
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'users', 'system_users', 'assets',
'is_active',
'date_created', 'date_updated',
'priority', 'action', 'action_display', 'comment', 'created_by', 'org_id'
]
fields_m2m = ['reviewers', 'reviewers_amount']
fields = fields_small + fields_m2m
extra_kwargs = {
"reviewers": {'allow_null': False, 'required': True},
'priority': {'default': 50},

View File

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

View File

@@ -1,3 +1,4 @@
from .application import *
from .account import *
from .mixin import *
from .remote_app import *

View File

@@ -0,0 +1,75 @@
# coding: utf-8
#
from django_filters import rest_framework as filters
from django.db.models import F, Value, CharField
from django.db.models.functions import Concat
from django.http import Http404
from common.drf.filters import BaseFilterSet
from common.drf.api import JMSModelViewSet
from common.utils import unique
from perms.models import ApplicationPermission
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
from .. import serializers
class AccountFilterSet(BaseFilterSet):
username = filters.CharFilter(field_name='username')
app = filters.CharFilter(field_name='applications', lookup_expr='exact')
app_name = filters.CharFilter(field_name='app_name', lookup_expr='exact')
class Meta:
model = ApplicationPermission
fields = ['type', 'category']
class ApplicationAccountViewSet(JMSModelViewSet):
permission_classes = (IsOrgAdmin, )
search_fields = ['username', 'app_name']
filterset_class = AccountFilterSet
filterset_fields = ['username', 'app_name', 'type', 'category']
serializer_class = serializers.ApplicationAccountSerializer
http_method_names = ['get', 'put', 'patch', 'options']
def get_queryset(self):
queryset = ApplicationPermission.objects\
.exclude(system_users__isnull=True) \
.exclude(applications__isnull=True) \
.annotate(uid=Concat(
'applications', Value('_'), 'system_users', output_field=CharField()
)) \
.annotate(systemuser=F('system_users')) \
.annotate(systemuser_display=F('system_users__name')) \
.annotate(username=F('system_users__username')) \
.annotate(password=F('system_users__password')) \
.annotate(app=F('applications')) \
.annotate(app_name=F("applications__name")) \
.values('username', 'password', 'systemuser', 'systemuser_display',
'app', 'app_name', 'category', 'type', 'uid', 'org_id')
return queryset
def get_object(self):
obj = self.get_queryset().filter(
uid=self.kwargs['pk']
).first()
if not obj:
raise Http404()
return obj
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset_list = unique(queryset, key=lambda x: (x['app'], x['systemuser']))
return queryset_list
@staticmethod
def filter_spm_queryset(resource_ids, queryset):
queryset = queryset.filter(uid__in=resource_ids)
return queryset
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
serializer_class = serializers.ApplicationAccountSecretSerializer
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
http_method_names = ['get', 'options']

View File

@@ -2,18 +2,36 @@
#
from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from common.tree import TreeNodeSerializer
from ..hands import IsOrgAdminOrAppUser
from .. import models, serializers
from .. import serializers
from ..models import Application
__all__ = ['ApplicationViewSet']
class ApplicationViewSet(OrgBulkModelViewSet):
model = models.Application
filterset_fields = ('name', 'type', 'category')
search_fields = filterset_fields
model = Application
filterset_fields = {
'name': ['exact'],
'category': ['exact'],
'type': ['exact', 'in'],
}
search_fields = ('name', 'type', 'category')
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationSerializer
serializer_classes = {
'default': serializers.ApplicationSerializer,
'get_tree': TreeNodeSerializer
}
@action(methods=['GET'], detail=False, url_path='tree')
def get_tree(self, request, *args, **kwargs):
show_count = request.query_params.get('show_count', '1') == '1'
queryset = self.filter_queryset(self.get_queryset())
tree_nodes = Application.create_tree_nodes(queryset, show_count=show_count)
serializer = self.get_serializer(tree_nodes, many=True)
return Response(serializer.data)

View File

@@ -1,89 +1,53 @@
from orgs.models import Organization
from django.utils.translation import ugettext as _
from common.tree import TreeNode
from orgs.models import Organization
from ..models import Application
__all__ = ['SerializeApplicationToTreeNodeMixin']
class SerializeApplicationToTreeNodeMixin:
@staticmethod
def _serialize_db(db):
return {
'id': db.id,
'name': db.name,
'title': db.name,
'pId': '',
'open': False,
'iconSkin': 'database',
'meta': {'type': 'database_app'}
}
@staticmethod
def _serialize_remote_app(remote_app):
return {
'id': remote_app.id,
'name': remote_app.name,
'title': remote_app.name,
'pId': '',
'open': False,
'isParent': False,
'iconSkin': 'chrome',
'meta': {'type': 'remote_app'}
}
@staticmethod
def _serialize_cloud(cloud):
return {
'id': cloud.id,
'name': cloud.name,
'title': cloud.name,
'pId': '',
'open': False,
'isParent': False,
'iconSkin': 'k8s',
'meta': {'type': 'k8s_app'}
}
def _serialize_application(self, application):
method_name = f'_serialize_{application.category}'
data = getattr(self, method_name)(application)
data.update({
'pId': application.org.id,
'org_name': application.org_name
})
return data
def serialize_applications(self, applications):
data = [self._serialize_application(application) for application in applications]
return data
@staticmethod
def _serialize_organization(org):
return {
'id': org.id,
'name': org.name,
'title': org.name,
'pId': '',
'open': True,
'isParent': True,
'meta': {
'type': 'node'
}
}
def serialize_organizations(self, organizations):
data = [self._serialize_organization(org) for org in organizations]
return data
@staticmethod
def filter_organizations(applications):
organization_ids = set(applications.values_list('org_id', flat=True))
organizations = [Organization.get_instance(org_id) for org_id in organization_ids]
organizations.sort(key=lambda x: x.name)
return organizations
@staticmethod
def create_root_node():
name = _('My applications')
node = TreeNode(**{
'id': 'applications',
'name': name,
'title': name,
'pId': '',
'open': True,
'isParent': True,
'meta': {
'type': 'root'
}
})
return node
def serialize_applications_with_org(self, applications):
root_node = self.create_root_node()
tree_nodes = [root_node]
organizations = self.filter_organizations(applications)
data_organizations = self.serialize_organizations(organizations)
data_applications = self.serialize_applications(applications)
data = data_organizations + data_applications
return data
for i, org in enumerate(organizations):
# 组织节点
org_node = org.as_tree_node(pid=root_node.id)
tree_nodes.append(org_node)
org_applications = applications.filter(org_id=org.id)
count = org_applications.count()
org_node.name += '({})'.format(count)
# 各应用节点
apps_nodes = Application.create_tree_nodes(
queryset=org_applications, root_node=org_node,
show_empty=False
)
tree_nodes += apps_nodes
return tree_nodes

View File

@@ -1,11 +1,10 @@
# coding: utf-8
#
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
class ApplicationCategoryChoices(TextChoices):
class AppCategory(TextChoices):
db = 'db', _('Database')
remote_app = 'remote_app', _('Remote app')
cloud = 'cloud', 'Cloud'
@@ -15,7 +14,7 @@ class ApplicationCategoryChoices(TextChoices):
return dict(cls.choices).get(category, '')
class ApplicationTypeChoices(TextChoices):
class AppType(TextChoices):
# db category
mysql = 'mysql', 'MySQL'
oracle = 'oracle', 'Oracle'
@@ -31,19 +30,38 @@ class ApplicationTypeChoices(TextChoices):
# cloud category
k8s = 'k8s', 'Kubernetes'
@classmethod
def category_types_mapper(cls):
return {
AppCategory.db: [cls.mysql, cls.oracle, cls.pgsql, cls.mariadb],
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
AppCategory.cloud: [cls.k8s]
}
@classmethod
def type_category_mapper(cls):
mapper = {}
for category, tps in cls.category_types_mapper().items():
for tp in tps:
mapper[tp] = category
return mapper
@classmethod
def get_label(cls, tp):
return dict(cls.choices).get(tp, '')
@classmethod
def db_types(cls):
return [cls.mysql.value, cls.oracle.value, cls.pgsql.value, cls.mariadb.value]
return [tp.value for tp in cls.category_types_mapper()[AppCategory.db]]
@classmethod
def remote_app_types(cls):
return [cls.chrome.value, cls.mysql_workbench.value, cls.vmware_client.value, cls.custom.value]
return [tp.value for tp in cls.category_types_mapper()[AppCategory.remote_app]]
@classmethod
def cloud_types(cls):
return [cls.k8s.value]
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]

View File

@@ -11,5 +11,5 @@
"""
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
from users.models import User, UserGroup

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.6 on 2021-06-23 09:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0070_auto_20210426_1515'),
('applications', '0008_auto_20210104_0435'),
]
operations = [
migrations.CreateModel(
name='ApplicationUser',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('assets.systemuser',),
),
]

View File

View File

@@ -1,19 +1,174 @@
from collections import defaultdict
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from assets.models import Asset
from common.tree import TreeNode
from assets.models import Asset, SystemUser
from .. import const
class Application(CommonModelMixin, OrgModelMixin):
class ApplicationTreeNodeMixin:
id: str
name: str
type: str
category: str
@classmethod
def create_choice_node(cls, c, id_, pid, tp, opened=False, counts=None,
show_empty=True, show_count=True):
count = counts.get(c.value, 0)
if count == 0 and not show_empty:
return None
label = c.label
if count is not None and show_count:
label = '{} ({})'.format(label, count)
data = {
'id': id_,
'name': label,
'title': label,
'pId': pid,
'isParent': bool(count),
'open': opened,
'iconSkin': '',
'meta': {
'type': tp,
'data': {
'name': c.name,
'value': c.value
}
}
}
return TreeNode(**data)
@classmethod
def create_root_tree_node(cls, queryset, show_count=True):
count = queryset.count() if show_count else None
root_id = 'applications'
root_name = _('Applications')
if count is not None and show_count:
root_name = '{} ({})'.format(root_name, count)
node = TreeNode(**{
'id': root_id,
'name': root_name,
'title': root_name,
'pId': '',
'isParent': True,
'open': True,
'iconSkin': '',
'meta': {
'type': 'applications_root',
}
})
return node
@classmethod
def create_category_tree_nodes(cls, root_node, counts=None, show_empty=True, show_count=True):
nodes = []
categories = const.AppType.category_types_mapper().keys()
for category in categories:
i = root_node.id + '_' + category.value
node = cls.create_choice_node(
category, i, pid=root_node.id, tp='category',
counts=counts, opened=False, show_empty=show_empty,
show_count=show_count
)
if not node:
continue
nodes.append(node)
return nodes
@classmethod
def create_types_tree_nodes(cls, root_node, counts, show_empty=True, show_count=True):
nodes = []
type_category_mapper = const.AppType.type_category_mapper()
for tp in const.AppType.type_category_mapper().keys():
category = type_category_mapper.get(tp)
pid = root_node.id + '_' + category.value
i = root_node.id + '_' + tp.value
node = cls.create_choice_node(
tp, i, pid, tp='type', counts=counts, opened=False,
show_empty=show_empty, show_count=show_count
)
if not node:
continue
nodes.append(node)
return nodes
@staticmethod
def get_tree_node_counts(queryset):
counts = defaultdict(int)
values = queryset.values_list('type', 'category')
for i in values:
tp = i[0]
category = i[1]
counts[tp] += 1
counts[category] += 1
return counts
@classmethod
def create_tree_nodes(cls, queryset, root_node=None, show_empty=True, show_count=True):
counts = cls.get_tree_node_counts(queryset)
tree_nodes = []
# 根节点有可能是组织名称
if root_node is None:
root_node = cls.create_root_tree_node(queryset, show_count=show_count)
tree_nodes.append(root_node)
# 类别的节点
tree_nodes += cls.create_category_tree_nodes(
root_node, counts, show_empty=show_empty,
show_count=show_count
)
# 类型的节点
tree_nodes += cls.create_types_tree_nodes(
root_node, counts, show_empty=show_empty,
show_count=show_count
)
# 应用的节点
for app in queryset:
pid = root_node.id + '_' + app.type
tree_nodes.append(app.as_tree_node(pid))
return tree_nodes
def as_tree_node(self, pid):
icon_skin_category_mapper = {
'remote_app': 'chrome',
'db': 'database',
'cloud': 'cloud'
}
icon_skin = icon_skin_category_mapper.get(self.category, 'file')
node = TreeNode(**{
'id': str(self.id),
'name': self.name,
'title': self.name,
'pId': pid,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'type': 'application',
'data': {
'category': self.category,
'type': self.type,
}
}
})
return node
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
name = models.CharField(max_length=128, verbose_name=_('Name'))
category = models.CharField(
max_length=16, choices=const.ApplicationCategoryChoices.choices, verbose_name=_('Category')
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
)
type = models.CharField(
max_length=16, choices=const.ApplicationTypeChoices.choices, verbose_name=_('Type')
max_length=16, choices=const.AppType.choices, verbose_name=_('Type')
)
domain = models.ForeignKey(
'assets.Domain', null=True, blank=True, related_name='applications',
@@ -35,7 +190,7 @@ class Application(CommonModelMixin, OrgModelMixin):
@property
def category_remote_app(self):
return self.category == const.ApplicationCategoryChoices.remote_app.value
return self.category == const.AppCategory.remote_app.value
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
@@ -68,3 +223,8 @@ class Application(CommonModelMixin, OrgModelMixin):
raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first()
return asset
class ApplicationUser(SystemUser):
class Meta:
proxy = True

View File

@@ -3,14 +3,17 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.models import Organization
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import MethodSerializer
from .attrs import category_serializer_classes_mapping, type_serializer_classes_mapping
from .. import models
from .. import const
__all__ = [
'ApplicationSerializer', 'ApplicationSerializerMixin',
'ApplicationAccountSerializer', 'ApplicationAccountSecretSerializer'
]
@@ -44,15 +47,18 @@ class ApplicationSerializerMixin(serializers.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'))
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category display'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
class Meta:
model = models.Application
fields = [
'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs',
'domain', 'created_by', 'date_created', 'date_updated', 'comment'
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'category', 'category_display', 'type', 'type_display',
'attrs', 'date_created', 'date_updated', 'created_by', 'comment'
]
fields_fk = ['domain']
fields = fields_small + fields_fk
read_only_fields = [
'created_by', 'date_created', 'date_updated', 'get_type_display',
]
@@ -62,3 +68,43 @@ class ApplicationSerializer(ApplicationSerializerMixin, BulkOrgResourceModelSeri
_attrs.update(attrs)
return _attrs
class ApplicationAccountSerializer(serializers.Serializer):
id = serializers.ReadOnlyField(label=_("Id"), source='uid')
username = serializers.ReadOnlyField(label=_("Username"))
password = serializers.CharField(write_only=True, label=_("Password"))
systemuser = serializers.ReadOnlyField(label=_('System user'))
systemuser_display = serializers.ReadOnlyField(label=_("System user display"))
app = serializers.ReadOnlyField(label=_('App'))
app_name = serializers.ReadOnlyField(label=_("Application name"), read_only=True)
category = serializers.ChoiceField(label=_('Category'), choices=const.AppCategory.choices, read_only=True)
category_display = serializers.SerializerMethodField(label=_('Category display'))
type = serializers.ChoiceField(label=_('Type'), choices=const.AppType.choices, read_only=True)
type_display = serializers.SerializerMethodField(label=_('Type display'))
uid = serializers.ReadOnlyField(label=_("Union id"))
org_id = serializers.ReadOnlyField(label=_("Organization"))
org_name = serializers.SerializerMethodField(label=_("Org name"))
category_mapper = dict(const.AppCategory.choices)
type_mapper = dict(const.AppType.choices)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
def get_category_display(self, obj):
return self.category_mapper.get(obj['category'])
def get_type_display(self, obj):
return self.type_mapper.get(obj['type'])
@staticmethod
def get_org_name(obj):
org = Organization.get_instance(obj['org_id'])
return org.name
class ApplicationAccountSecretSerializer(ApplicationAccountSerializer):
password = serializers.CharField(write_only=False, label=_("Password"))

View File

@@ -5,7 +5,7 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from common.utils import get_logger, is_uuid
from common.utils import get_logger, is_uuid, get_object_or_none
from assets.models import Asset
logger = get_logger(__file__)
@@ -14,39 +14,48 @@ logger = get_logger(__file__)
__all__ = ['RemoteAppSerializer']
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
instance = super().to_internal_value(data)
return str(instance.id)
def to_representation(self, value):
# value is instance.id
def to_representation(self, _id):
# _id 是 instance.id
if self.pk_field is not None:
return self.pk_field.to_representation(value)
return value
return self.pk_field.to_representation(_id)
# 解决删除资产后远程应用更新页面会显示资产ID的问题
asset = get_object_or_none(Asset, id=_id)
if not asset:
return None
return _id
class RemoteAppSerializer(serializers.Serializer):
asset_info = serializers.SerializerMethodField()
asset = CharPrimaryKeyRelatedField(
queryset=Asset.objects, required=False, label=_("Asset"), allow_null=True
asset = ExistAssetPrimaryKeyRelatedField(
queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
)
path = serializers.CharField(
max_length=128, label=_('Application path'), allow_null=True
)
def validate_asset(self, asset):
if not asset:
raise serializers.ValidationError(_('This field is required.'))
return asset
@staticmethod
def get_asset_info(obj):
asset_id = obj.get('asset')
if not asset_id or is_uuid(asset_id):
if not asset_id or not is_uuid(asset_id):
return {}
try:
asset = Asset.objects.filter(id=str(asset_id)).values_list('id', 'hostname')
asset = Asset.objects.get(id=str(asset_id))
except ObjectDoesNotExist as e:
logger.error(e)
return {}
if not asset:
return {}
asset_info = {'id': str(asset[0]), 'hostname': asset[1]}
asset_info = {'id': str(asset.id), 'hostname': asset.hostname}
return asset_info

View File

@@ -14,9 +14,9 @@ __all__ = [
# ---------------------------------------------------
category_serializer_classes_mapping = {
const.ApplicationCategoryChoices.db.value: application_category.DBSerializer,
const.ApplicationCategoryChoices.remote_app.value: application_category.RemoteAppSerializer,
const.ApplicationCategoryChoices.cloud.value: application_category.CloudSerializer,
const.AppCategory.db.value: application_category.DBSerializer,
const.AppCategory.remote_app.value: application_category.RemoteAppSerializer,
const.AppCategory.cloud.value: application_category.CloudSerializer,
}
# define `attrs` field `type serializers mapping`
@@ -24,17 +24,17 @@ category_serializer_classes_mapping = {
type_serializer_classes_mapping = {
# db
const.ApplicationTypeChoices.mysql.value: application_type.MySQLSerializer,
const.ApplicationTypeChoices.mariadb.value: application_type.MariaDBSerializer,
const.ApplicationTypeChoices.oracle.value: application_type.OracleSerializer,
const.ApplicationTypeChoices.pgsql.value: application_type.PostgreSerializer,
const.AppType.mysql.value: application_type.MySQLSerializer,
const.AppType.mariadb.value: application_type.MariaDBSerializer,
const.AppType.oracle.value: application_type.OracleSerializer,
const.AppType.pgsql.value: application_type.PostgreSerializer,
# remote-app
const.ApplicationTypeChoices.chrome.value: application_type.ChromeSerializer,
const.ApplicationTypeChoices.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
const.ApplicationTypeChoices.vmware_client.value: application_type.VMwareClientSerializer,
const.ApplicationTypeChoices.custom.value: application_type.CustomSerializer,
const.AppType.chrome.value: application_type.ChromeSerializer,
const.AppType.mysql_workbench.value: application_type.MySQLWorkbenchSerializer,
const.AppType.vmware_client.value: application_type.VMwareClientSerializer,
const.AppType.custom.value: application_type.CustomSerializer,
# cloud
const.ApplicationTypeChoices.k8s.value: application_type.K8SSerializer
const.AppType.k8s.value: application_type.K8SSerializer
}

View File

@@ -10,10 +10,14 @@ app_name = 'applications'
router = BulkRouter()
router.register(r'applications', api.ApplicationViewSet, 'application')
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')
urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
# path('accounts/', api.ApplicationAccountViewSet.as_view(), name='application-account'),
# path('account-secrets/', api.ApplicationAccountSecretViewSet.as_view(), name='application-account-secret')
]

View File

@@ -4,9 +4,9 @@ from .asset import *
from .label import *
from .system_user import *
from .system_user_relation import *
from .accounts import *
from .node import *
from .domain import *
from .cmd_filter import *
from .asset_user import *
from .gathered_user import *
from .favorite_asset import *

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

@@ -0,0 +1,113 @@
from django.db.models import F, Q
from rest_framework.decorators import action
from django_filters import rest_framework as filters
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from rest_framework.generics import CreateAPIView
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
from common.drf.filters import BaseFilterSet
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node
from .. import serializers
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
class AccountFilterSet(BaseFilterSet):
username = filters.CharFilter(method='do_nothing')
ip = filters.CharFilter(field_name='ip', lookup_expr='exact')
hostname = filters.CharFilter(field_name='hostname', lookup_expr='exact')
node = filters.CharFilter(method='do_nothing')
@property
def qs(self):
qs = super().qs
qs = self.filter_username(qs)
qs = self.filter_node(qs)
return qs
def filter_username(self, qs):
username = self.get_query_param('username')
if not username:
return qs
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
return qs
def filter_node(self, qs):
node_id = self.get_query_param('node')
if not node_id:
return qs
node = get_object_or_404(Node, pk=node_id)
node_ids = node.get_all_children(with_self=True).values_list('id', flat=True)
node_ids = list(node_ids)
qs = qs.filter(asset__nodes__in=node_ids)
return qs
class Meta:
model = AuthBook
fields = [
'asset', 'systemuser', 'id',
]
class AccountViewSet(OrgBulkModelViewSet):
model = AuthBook
filterset_fields = ("username", "asset", "systemuser", 'ip', 'hostname')
search_fields = ('username', 'ip', 'hostname', 'systemuser__username')
filterset_class = AccountFilterSet
serializer_classes = {
'default': serializers.AccountSerializer,
'verify_account': serializers.AssetTaskSerializer
}
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = super().get_queryset()\
.annotate(ip=F('asset__ip'))\
.annotate(hostname=F('asset__hostname'))
return queryset
@action(methods=['post'], detail=True, url_path='verify')
def verify_account(self, request, *args, **kwargs):
account = super().get_object()
task = test_accounts_connectivity_manual.delay([account])
return Response(data={'task': task.id})
class AccountSecretsViewSet(AccountViewSet):
"""
因为可能要导出所有账号,所以单独建立了一个 viewset
"""
serializer_classes = {
'default': serializers.AccountSecretSerializer
}
permission_classes = (IsOrgAdmin, NeedMFAVerify)
http_method_names = ['get']
class AccountTaskCreateAPI(CreateAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.AccountTaskSerializer
filterset_fields = AccountViewSet.filterset_fields
search_fields = AccountViewSet.search_fields
filterset_class = AccountViewSet.filterset_class
def get_accounts(self):
queryset = AuthBook.objects.all()
queryset = self.filter_queryset(queryset)
return queryset
def perform_create(self, serializer):
accounts = self.get_accounts()
task = test_accounts_connectivity_manual.delay(accounts)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
return task
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler

View File

@@ -1,109 +1,28 @@
from django.db import transaction
from django.db.models import Count
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import status
from rest_framework.response import Response
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.mixins.api import OrgBulkModelViewSet
from common.utils import get_logger
from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset
from ..models import SystemUser
from .. import serializers
from ..tasks import test_admin_user_connectivity_manual
logger = get_logger(__file__)
__all__ = [
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
'AdminUserAssetsListView',
]
__all__ = ['AdminUserViewSet']
# 兼容一下老的 api
class AdminUserViewSet(OrgBulkModelViewSet):
"""
Admin user api set, for add,delete,update,list,retrieve resource
"""
model = AdminUser
model = SystemUser
filterset_fields = ("name", "username")
search_fields = filterset_fields
serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,)
serializer_classes = {
'default': serializers.AdminUserSerializer,
'retrieve': serializers.AdminUserDetailSerializer,
}
def get_queryset(self):
queryset = super().get_queryset()
queryset = super().get_queryset().filter(type=SystemUser.Type.admin)
queryset = queryset.annotate(assets_amount=Count('assets'))
return queryset
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
has_related_asset = instance.assets.exists()
if has_related_asset:
data = {'msg': _('Deleted failed, There are related assets')}
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
return super().destroy(request, *args, **kwargs)
class AdminUserAuthApi(generics.UpdateAPIView):
model = AdminUser
serializer_class = serializers.AdminUserAuthSerializer
permission_classes = (IsOrgAdmin,)
class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
model = AdminUser
serializer_class = serializers.ReplaceNodeAdminUserSerializer
permission_classes = (IsOrgAdmin,)
def update(self, request, *args, **kwargs):
admin_user = self.get_object()
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
nodes = serializer.validated_data['nodes']
assets = []
for node in nodes:
assets.extend([asset.id for asset in node.get_all_assets()])
with transaction.atomic():
Asset.objects.filter(id__in=assets).update(admin_user=admin_user)
return Response({"msg": "ok"})
else:
return Response({'error': serializer.errors}, status=400)
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Test asset admin user assets_connectivity
"""
model = AdminUser
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object()
task = test_admin_user_connectivity_manual.delay(admin_user)
return Response({"task": task.id})
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filterset_fields = ("hostname", "ip")
search_fields = filterset_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(AdminUser, pk=pk)
def get_queryset(self):
admin_user = self.get_object()
return admin_user.get_related_assets()

View File

@@ -9,10 +9,11 @@ from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from ..models import Asset, Node, Platform
from ..models import Asset, Node, Platform, SystemUser
from .. import serializers
from ..tasks import (
update_assets_hardware_info_manual, test_assets_connectivity_manual
update_assets_hardware_info_manual, test_assets_connectivity_manual,
test_system_users_connectivity_a_asset, push_system_users_a_asset
)
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
@@ -33,8 +34,7 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
filterset_fields = {
'hostname': ['exact'],
'ip': ['exact'],
'systemuser__id': ['exact'],
'admin_user__id': ['exact'],
'system_users__id': ['exact'],
'platform__base': ['exact'],
'is_active': ['exact'],
'protocols': ['exact', 'icontains']
@@ -43,7 +43,6 @@ class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_classes = {
'default': serializers.AssetSerializer,
'display': serializers.AssetDisplaySerializer,
}
permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
@@ -96,21 +95,27 @@ class AssetPlatformViewSet(ModelViewSet):
class AssetsTaskMixin:
def perform_assets_task(self, serializer):
data = serializer.validated_data
assets = data['assets']
action = data['action']
assets = data.get('assets', [])
if action == "refresh":
task = update_assets_hardware_info_manual.delay(assets)
else:
# action == 'test':
task = test_assets_connectivity_manual.delay(assets)
return task
def perform_create(self, serializer):
task = self.perform_assets_task(serializer)
self.set_task_to_serializer_data(serializer, task)
def set_task_to_serializer_data(self, serializer, task):
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
def perform_create(self, serializer):
self.perform_assets_task(serializer)
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
@@ -119,13 +124,37 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
def create(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
request.data['asset'] = pk
request.data['assets'] = [pk]
return super().create(request, *args, **kwargs)
def perform_asset_task(self, serializer):
data = serializer.validated_data
action = data['action']
if action not in ['push_system_user', 'test_system_user']:
return
asset = data['asset']
system_users = data.get('system_users')
if not system_users:
system_users = asset.get_all_systemusers()
if action == 'push_system_user':
task = push_system_users_a_asset.delay(system_users, asset=asset)
elif action == 'test_system_user':
task = test_system_users_connectivity_a_asset.delay(system_users, asset=asset)
else:
task = None
return task
def perform_create(self, serializer):
task = self.perform_asset_task(serializer)
if not task:
task = self.perform_assets_task(serializer)
self.set_task_to_serializer_data(serializer, task)
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetTaskSerializer
serializer_class = serializers.AssetsTaskSerializer
permission_classes = (IsOrgAdmin,)

View File

@@ -1,157 +0,0 @@
# -*- coding: utf-8 -*-
#
import coreapi
from django.conf import settings
from rest_framework.response import Response
from rest_framework import generics, filters
from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
from common.utils import get_object_or_none, get_logger
from common.mixins import CommonApiMixin
from ..backends import AssetUserManager
from ..models import Asset, Node, SystemUser
from .. import serializers
from ..tasks import (
test_asset_users_connectivity_manual, push_system_user_a_asset_manual
)
__all__ = [
'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI',
]
logger = get_logger(__name__)
class AssetUserFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
kwargs = {}
for field in view.filterset_fields:
value = request.GET.get(field)
if not value:
continue
if field == "node_id":
value = get_object_or_none(Node, pk=value)
kwargs["node"] = value
continue
elif field == "asset_id":
field = "asset"
kwargs[field] = value
if kwargs:
queryset = queryset.filter(**kwargs)
logger.debug("Filter {}".format(kwargs))
return queryset
class AssetUserSearchBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
value = request.GET.get('search')
if not value:
return queryset
queryset = queryset.search(value)
return queryset
class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
def get_schema_fields(self, view):
return [
coreapi.Field(
name='latest', location='query', required=False,
type='string', example='1',
description='Only the latest version'
)
]
def filter_queryset(self, request, queryset, view):
latest = request.GET.get('latest') == '1'
if latest:
queryset = queryset.distinct()
return queryset
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
serializer_classes = {
'default': serializers.AssetUserWriteSerializer,
'display': serializers.AssetUserReadSerializer,
'retrieve': serializers.AssetUserReadSerializer,
}
permission_classes = [IsOrgAdminOrAppUser]
filterset_fields = [
"id", "ip", "hostname", "username",
"asset_id", "node_id",
"prefer", "prefer_id",
]
search_fields = ["ip", "hostname", "username"]
filter_backends = [
AssetUserFilterBackend, AssetUserSearchBackend,
AssetUserLatestFilterBackend,
]
def allow_bulk_destroy(self, qs, filtered):
return False
def get_object(self):
pk = self.kwargs.get("pk")
if pk is None:
return
queryset = self.get_queryset()
obj = queryset.get(id=pk)
return obj
def get_exception_handler(self):
def handler(e, context):
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=400)
return handler
def perform_destroy(self, instance):
manager = AssetUserManager()
manager.delete(instance)
def get_queryset(self):
manager = AssetUserManager()
queryset = manager.all()
return queryset
class AssetUserAuthInfoViewSet(AssetUserViewSet):
serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
http_method_names = ['get', 'post']
permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self):
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
return super().get_permissions()
class AssetUserTaskCreateAPI(generics.CreateAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.AssetUserTaskSerializer
filter_backends = AssetUserViewSet.filter_backends
filterset_fields = AssetUserViewSet.filterset_fields
def get_asset_users(self):
manager = AssetUserManager()
queryset = manager.all()
for cls in self.filter_backends:
queryset = cls().filter_queryset(self.request, queryset, self)
return list(queryset)
def perform_create(self, serializer):
asset_users = self.get_asset_users()
# action = serializer.validated_data["action"]
# only this
# if action == "test":
task = test_asset_users_connectivity_manual.delay(asset_users)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
return task
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler

View File

@@ -1,15 +1,23 @@
# -*- coding: utf-8 -*-
#
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from django.shortcuts import get_object_or_404
from common.utils import reverse
from common.utils import lazyproperty
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdmin
from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..hands import IsOrgAdmin, IsAppUser
from ..models import CommandFilter, CommandFilterRule
from .. import serializers
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
__all__ = [
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
'CommandConfirmStatusAPI'
]
class CommandFilterViewSet(OrgBulkModelViewSet):
@@ -35,3 +43,50 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
return cmd_filter.rules.all()
class CommandConfirmAPI(CreateAPIView):
permission_classes = (IsAppUser, )
serializer_class = serializers.CommandConfirmSerializer
def create(self, request, *args, **kwargs):
ticket = self.create_command_confirm_ticket()
response_data = self.get_response_data(ticket)
return Response(data=response_data, status=200)
def create_command_confirm_ticket(self):
ticket = self.serializer.cmd_filter_rule.create_command_confirm_ticket(
run_command=self.serializer.data.get('run_command'),
session=self.serializer.session,
cmd_filter_rule=self.serializer.cmd_filter_rule,
org_id=self.serializer.org.id
)
return ticket
@staticmethod
def get_response_data(ticket):
confirm_status_url = reverse(
view_name='api-assets:command-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)
return {
'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()]
}
@lazyproperty
def serializer(self):
serializer = self.get_serializer(data=self.request.data)
serializer.is_valid(raise_exception=True)
return serializer
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass

View File

@@ -33,7 +33,7 @@ class GatewayViewSet(OrgBulkModelViewSet):
model = Gateway
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = ("domain__name", "name", "username", "ip")
permission_classes = (IsOrgAdmin,)
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewaySerializer

View File

@@ -26,7 +26,7 @@ class SerializeToTreeNodeMixin:
'isParent': True,
'open': node.is_org_root(),
'meta': {
'node': {
'data': {
"id": node.id,
"key": node.key,
"value": node.value,
@@ -65,7 +65,7 @@ class SerializeToTreeNodeMixin:
'chkDisabled': not asset.is_active,
'meta': {
'type': 'asset',
'asset': {
'data': {
'id': asset.id,
'hostname': asset.hostname,
'ip': asset.ip,

View File

@@ -28,6 +28,7 @@ from ..tasks import (
)
from .. import serializers
from .mixin import SerializeToTreeNodeMixin
from assets.locks import NodeAddChildrenLock
logger = get_logger(__file__)
@@ -70,8 +71,8 @@ class NodeViewSet(OrgModelViewSet):
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")
if node.has_offspring_assets():
error = _("Deletion failed and the node contains assets")
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs)
@@ -114,15 +115,16 @@ class NodeChildrenApi(generics.ListCreateAPIView):
return super().initial(request, *args, **kwargs)
def perform_create(self, serializer):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if not value:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
serializer.instance = node
with NodeAddChildrenLock(self.instance):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if not value:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value
node._full_value = node.value
serializer.instance = node
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
@@ -221,7 +223,8 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAddChildrenSerializer
instance = None
def put(self, request, *args, **kwargs):
def update(self, request, *args, **kwargs):
""" 同时支持 put 和 patch 方法"""
instance = self.get_object()
node_ids = request.data.get("nodes")
children = Node.objects.filter(id__in=node_ids)

View File

@@ -3,14 +3,13 @@ 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
from common.drf.filters import CustomFilter
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.utils import tmp_to_org
from orgs.utils import tmp_to_root_org
from ..models import SystemUser, Asset
from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
from ..tasks import (
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
push_system_user_to_assets
@@ -21,6 +20,7 @@ logger = get_logger(__file__)
__all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi', 'SystemUserAssetsListView',
'SystemUserTempAuthInfoApi', 'SystemUserAppAuthInfoApi',
]
@@ -32,13 +32,13 @@ class SystemUserViewSet(OrgBulkModelViewSet):
filterset_fields = {
'name': ['exact'],
'username': ['exact'],
'protocol': ['exact', 'in']
'protocol': ['exact', 'in'],
'type': ['exact', 'in'],
}
search_fields = filterset_fields
serializer_class = serializers.SystemUserSerializer
serializer_classes = {
'default': serializers.SystemUserSerializer,
'list': serializers.SystemUserListSerializer,
}
permission_classes = (IsOrgAdminOrAppUser,)
@@ -57,6 +57,25 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
return Response(status=204)
class SystemUserTempAuthInfoApi(generics.CreateAPIView):
model = SystemUser
permission_classes = (IsValidUser,)
serializer_class = SystemUserTempAuthSerializer
def create(self, request, *args, **kwargs):
serializer = super().get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
pk = kwargs.get('pk')
user = self.request.user
data = serializer.validated_data
instance_id = data.get('instance_id')
with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(instance_id, user, data)
return Response(serializer.data, status=201)
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
"""
Get system user with asset auth info
@@ -65,22 +84,30 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler
def get_object(self):
instance = super().get_object()
asset_id = self.kwargs.get('asset_id')
user_id = self.request.query_params.get("user_id")
username = self.request.query_params.get("username")
instance.load_asset_more_auth(asset_id=asset_id, user_id=user_id, username=username)
return instance
class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
"""
Get system user with asset auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
def get_object(self):
instance = super().get_object()
username = instance.username
if instance.username_same_with_user:
username = self.request.query_params.get("username")
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, pk=asset_id)
with tmp_to_org(asset.org_id):
instance.load_asset_special_auth(asset=asset, username=username)
return instance
app_id = self.kwargs.get('app_id')
user_id = self.request.query_params.get("user_id")
if user_id:
instance.load_app_more_auth(app_id, user_id)
return instance
class SystemUserTaskApi(generics.CreateAPIView):
@@ -98,8 +125,8 @@ class SystemUserTaskApi(generics.CreateAPIView):
return task
@staticmethod
def do_test(system_user):
task = test_system_user_connectivity_manual.delay(system_user)
def do_test(system_user, asset_ids):
task = test_system_user_connectivity_manual.delay(system_user, asset_ids)
return task
def get_object(self):
@@ -109,16 +136,20 @@ 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 []
if asset:
assets = [asset]
else:
assets = serializer.validated_data.get('assets') or []
asset_ids = [asset.id for asset in assets]
asset_ids = asset_ids if asset_ids else None
system_user = self.get_object()
if action == 'push':
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)
task = self.do_test(system_user, asset_ids)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)

View File

@@ -6,6 +6,7 @@ from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat
from common.permissions import IsOrgAdmin
from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org
from .. import models, serializers
@@ -15,6 +16,8 @@ __all__ = [
'SystemUserUserRelationViewSet',
]
logger = get_logger(__name__)
class RelationMixin:
def get_queryset(self):
@@ -24,8 +27,8 @@ class RelationMixin:
queryset = queryset.filter(systemuser__org_id=org_id)
queryset = queryset.annotate(systemuser_display=Concat(
F('systemuser__name'), Value('('), F('systemuser__username'),
Value(')')
F('systemuser__name'), Value('('),
F('systemuser__username'), Value(')')
))
return queryset
@@ -41,10 +44,11 @@ class RelationMixin:
system_users_objects_map[i.systemuser].append(_id)
sender = self.get_sender()
for system_user, objects in system_users_objects_map.items():
for system_user, object_ids in system_users_objects_map.items():
logger.debug('System user relation changed, send m2m_changed signals')
m2m_changed.send(
sender=sender, instance=system_user, action='post_add',
reverse=False, model=model, pk_set=objects
reverse=False, model=model, pk_set=set(object_ids)
)
def get_sender(self):
@@ -71,7 +75,7 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
]
search_fields = [
"id", "asset__hostname", "asset__ip",
"systemuser__name", "systemuser__username"
"systemuser__name", "systemuser__username",
]
def get_objects_attr(self):

View File

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

View File

@@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
#
from abc import abstractmethod
from ..models import Asset
class BaseBackend:
@abstractmethod
def all(self):
pass
@abstractmethod
def filter(self, username=None, hostname=None, ip=None, assets=None,
node=None, prefer_id=None, **kwargs):
pass
@abstractmethod
def search(self, item):
pass
@abstractmethod
def get_queryset(self):
pass
@abstractmethod
def delete(self, union_id):
pass
@staticmethod
def qs_to_values(qs):
values = qs.values(
'hostname', 'ip', "asset_id",
'username', 'password', 'private_key', 'public_key',
'score', 'version',
"asset_username", "union_id",
'date_created', 'date_updated',
'org_id', 'backend',
)
return values
@staticmethod
def make_assets_as_ids(assets):
if not assets:
return []
if isinstance(assets[0], Asset):
assets = [a.id for a in assets]
return assets

View File

@@ -1,318 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from functools import reduce
from django.db.models import F, CharField, Value, IntegerField, Q, Count
from django.db.models.functions import Concat
from common.utils import get_object_or_none
from orgs.utils import current_org
from ..models import AuthBook, SystemUser, Asset, AdminUser
from .base import BaseBackend
class DBBackend(BaseBackend):
union_id_length = 2
def __init__(self, queryset=None):
if queryset is None:
queryset = self.all()
self.queryset = queryset
def _clone(self):
return self.__class__(self.queryset)
def all(self):
return AuthBook.objects.none()
def count(self):
return self.queryset.count()
def get_queryset(self):
return self.queryset
def delete(self, union_id):
cleaned_union_id = union_id.split('_')
# 如果union_id通不过本检查代表可能不是本backend, 应该返回空
if not self._check_union_id(union_id, cleaned_union_id):
return
return self._perform_delete_by_union_id(cleaned_union_id)
def _perform_delete_by_union_id(self, union_id_cleaned):
pass
def filter(self, assets=None, node=None, prefer=None, prefer_id=None,
union_id=None, id__in=None, **kwargs):
clone = self._clone()
clone._filter_union_id(union_id)
clone._filter_prefer(prefer, prefer_id)
clone._filter_node(node)
clone._filter_assets(assets)
clone._filter_other(kwargs)
clone._filter_id_in(id__in)
return clone
def _filter_union_id(self, union_id):
if not union_id:
return
cleaned_union_id = union_id.split('_')
# 如果union_id通不过本检查代表可能不是本backend, 应该返回空
if not self._check_union_id(union_id, cleaned_union_id):
self.queryset = self.queryset.none()
return
return self._perform_filter_union_id(union_id, cleaned_union_id)
def _check_union_id(self, union_id, cleaned_union_id):
return union_id and len(cleaned_union_id) == self.union_id_length
def _perform_filter_union_id(self, union_id, union_id_cleaned):
self.queryset = self.queryset.filter(union_id=union_id)
def _filter_assets(self, assets):
asset_ids = self.make_assets_as_ids(assets)
if asset_ids:
self.queryset = self.queryset.filter(asset_id__in=asset_ids)
def _filter_node(self, node):
pass
def _filter_id_in(self, ids):
if ids and isinstance(ids, list):
self.queryset = self.queryset.filter(union_id__in=ids)
@staticmethod
def clean_kwargs(kwargs):
return {k: v for k, v in kwargs.items() if v}
def _filter_other(self, kwargs):
kwargs = self.clean_kwargs(kwargs)
if kwargs:
self.queryset = self.queryset.filter(**kwargs)
def _filter_prefer(self, prefer, prefer_id):
pass
def search(self, item):
qs = []
for i in ['hostname', 'ip', 'username']:
kwargs = {i + '__startswith': item}
qs.append(Q(**kwargs))
q = reduce(lambda x, y: x | y, qs)
clone = self._clone()
clone.queryset = clone.queryset.filter(q).distinct()
return clone
class SystemUserBackend(DBBackend):
model = SystemUser.assets.through
backend = 'system_user'
prefer = backend
base_score = 0
union_id_length = 2
def _filter_prefer(self, prefer, prefer_id):
if prefer and prefer != self.prefer:
self.queryset = self.queryset.none()
if prefer_id:
self.queryset = self.queryset.filter(systemuser__id=prefer_id)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
system_user_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
asset_id=asset_id, systemuser__id=system_user_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
system_user_id, asset_id = union_id_cleaned
system_user = get_object_or_none(SystemUser, pk=system_user_id)
asset = get_object_or_none(Asset, pk=asset_id)
if all((system_user, asset)):
system_user.assets.remove(asset)
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
def get_annotate(self):
kwargs = dict(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
username=F("systemuser__username"),
password=F("systemuser__password"),
private_key=F("systemuser__private_key"),
public_key=F("systemuser__public_key"),
score=F("systemuser__priority") + self.base_score,
version=Value(0, IntegerField()),
date_created=F("systemuser__date_created"),
date_updated=F("systemuser__date_updated"),
asset_username=Concat(F("asset__id"), Value("_"),
F("systemuser__username"),
output_field=CharField()),
union_id=Concat(F("systemuser_id"), Value("_"), F("asset_id"),
output_field=CharField()),
org_id=F("asset__org_id"),
backend=Value(self.backend, CharField())
)
return kwargs
def get_filter(self):
return dict(
systemuser__username_same_with_user=False,
)
def all(self):
kwargs = self.get_annotate()
filters = self.get_filter()
qs = self.model.objects.all().annotate(**kwargs)
if not current_org.is_root():
filters['org_id'] = current_org.org_id()
qs = qs.filter(**filters)
qs = self.qs_to_values(qs)
return qs
class DynamicSystemUserBackend(SystemUserBackend):
backend = 'system_user_dynamic'
prefer = 'system_user'
union_id_length = 3
def get_annotate(self):
kwargs = super().get_annotate()
kwargs.update(dict(
username=F("systemuser__users__username"),
asset_username=Concat(
F("asset__id"), Value("_"),
F("systemuser__users__username"),
output_field=CharField()
),
union_id=Concat(
F("systemuser_id"), Value("_"), F("asset_id"),
Value("_"), F("systemuser__users__id"),
output_field=CharField()
),
users_count=Count('systemuser__users'),
))
return kwargs
def _perform_filter_union_id(self, union_id, union_id_cleaned):
system_user_id, asset_id, user_id = union_id_cleaned
self.queryset = self.queryset.filter(
asset_id=asset_id, systemuser_id=system_user_id,
union_id=union_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
system_user_id, asset_id, user_id = union_id_cleaned
system_user = get_object_or_none(SystemUser, pk=system_user_id)
if not system_user:
return
system_user.users.remove(user_id)
if system_user.users.count() == 0:
system_user.assets.remove(asset_id)
def get_filter(self):
return dict(
users_count__gt=0,
systemuser__username_same_with_user=True
)
class AdminUserBackend(DBBackend):
model = Asset
backend = 'admin_user'
prefer = backend
base_score = 200
def _filter_prefer(self, prefer, prefer_id):
if prefer and prefer != self.backend:
self.queryset = self.queryset.none()
if prefer_id:
self.queryset = self.queryset.filter(admin_user__id=prefer_id)
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(nodes__id=node.id)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
admin_user_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
id=asset_id, admin_user_id=admin_user_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
raise PermissionError(_("Could not remove asset admin user"))
def all(self):
qs = self.model.objects.all().annotate(
asset_id=F("id"),
username=F("admin_user__username"),
password=F("admin_user__password"),
private_key=F("admin_user__private_key"),
public_key=F("admin_user__public_key"),
score=Value(self.base_score, IntegerField()),
version=Value(0, IntegerField()),
date_updated=F("admin_user__date_updated"),
asset_username=Concat(F("id"), Value("_"), F("admin_user__username"), output_field=CharField()),
union_id=Concat(F("admin_user_id"), Value("_"), F("id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
)
qs = self.qs_to_values(qs)
return qs
class AuthbookBackend(DBBackend):
model = AuthBook
backend = 'db'
prefer = backend
base_score = 400
def _filter_node(self, node):
if node:
self.queryset = self.queryset.filter(asset__nodes__id=node.id)
def _filter_prefer(self, prefer, prefer_id):
if not prefer or not prefer_id:
return
if prefer.lower() == "admin_user":
model = AdminUser
elif prefer.lower() == "system_user":
model = SystemUser
else:
self.queryset = self.queryset.none()
return
obj = get_object_or_none(model, pk=prefer_id)
if obj is None:
self.queryset = self.queryset.none()
return
username = obj.get_username()
if isinstance(username, str):
self.queryset = self.queryset.filter(username=username)
# dynamic system user return more username
else:
self.queryset = self.queryset.filter(username__in=username)
def _perform_filter_union_id(self, union_id, union_id_cleaned):
authbook_id, asset_id = union_id_cleaned
self.queryset = self.queryset.filter(
id=authbook_id, asset_id=asset_id,
)
def _perform_delete_by_union_id(self, union_id_cleaned):
authbook_id, asset_id = union_id_cleaned
authbook = get_object_or_none(AuthBook, pk=authbook_id)
if authbook.is_latest:
raise PermissionError(_("Latest version could not be delete"))
AuthBook.objects.filter(id=authbook_id).delete()
def all(self):
qs = self.model.objects.all().annotate(
hostname=F("asset__hostname"),
ip=F("asset__ip"),
score=F('version') + self.base_score,
asset_username=Concat(F("asset__id"), Value("_"), F("username"), output_field=CharField()),
union_id=Concat(F("id"), Value("_"), F("asset_id"), output_field=CharField()),
backend=Value(self.backend, CharField()),
)
qs = self.qs_to_values(qs)
return qs

View File

@@ -1,162 +0,0 @@
# -*- coding: utf-8 -*-
#
from itertools import chain, groupby
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from orgs.utils import current_org
from common.utils import get_logger, lazyproperty
from common.struct import QuerySetChain
from ..models import AssetUser, AuthBook
from .db import (
AuthbookBackend, SystemUserBackend, AdminUserBackend,
DynamicSystemUserBackend
)
logger = get_logger(__name__)
class NotSupportError(Exception):
pass
class AssetUserQueryset:
ObjectDoesNotExist = ObjectDoesNotExist
MultipleObjectsReturned = MultipleObjectsReturned
def __init__(self, backends=()):
self.backends = backends
self._distinct_queryset = None
def backends_queryset(self):
return [b.get_queryset() for b in self.backends]
@lazyproperty
def backends_counts(self):
return [b.count() for b in self.backends]
def filter(self, hostname=None, ip=None, username=None,
assets=None, asset=None, node=None,
id=None, prefer_id=None, prefer=None, id__in=None):
if not assets and asset:
assets = [asset]
kwargs = dict(
hostname=hostname, ip=ip, username=username,
assets=assets, node=node, prefer=prefer, prefer_id=prefer_id,
id__in=id__in, union_id=id,
)
logger.debug("Filter: {}".format(kwargs))
backends = []
for backend in self.backends:
clone = backend.filter(**kwargs)
backends.append(clone)
return self._clone(backends)
def _clone(self, backends=None):
if backends is None:
backends = self.backends
return self.__class__(backends)
def search(self, item):
backends = []
for backend in self.backends:
new = backend.search(item)
backends.append(new)
return self._clone(backends)
def distinct(self):
logger.debug("Distinct asset user queryset")
queryset_chain = chain(*(backend.get_queryset() for backend in self.backends))
queryset_sorted = sorted(
queryset_chain,
key=lambda item: (item["asset_username"], item["score"]),
reverse=True,
)
results = groupby(queryset_sorted, key=lambda item: item["asset_username"])
final = [next(result[1]) for result in results]
self._distinct_queryset = final
return self
def get(self, latest=False, **kwargs):
queryset = self.filter(**kwargs)
if latest:
queryset = queryset.distinct()
queryset = list(queryset)
count = len(queryset)
if count == 1:
data = queryset[0]
return data
elif count > 1:
msg = 'Should return 1 record, but get {}'.format(count)
raise MultipleObjectsReturned(msg)
else:
msg = 'No record found(org is {})'.format(current_org.name)
raise ObjectDoesNotExist(msg)
def get_latest(self, **kwargs):
return self.get(latest=True, **kwargs)
@staticmethod
def to_asset_user(data):
obj = AssetUser()
for k, v in data.items():
setattr(obj, k, v)
return obj
@property
def queryset(self):
if self._distinct_queryset is not None:
return self._distinct_queryset
return QuerySetChain(self.backends_queryset())
def count(self):
if self._distinct_queryset is not None:
return len(self._distinct_queryset)
else:
return sum(self.backends_counts)
def __getitem__(self, ndx):
return self.queryset.__getitem__(ndx)
def __iter__(self):
self._data = iter(self.queryset)
return self
def __next__(self):
return self.to_asset_user(next(self._data))
class AssetUserManager:
support_backends = (
('db', AuthbookBackend),
('system_user', SystemUserBackend),
('admin_user', AdminUserBackend),
('system_user_dynamic', DynamicSystemUserBackend),
)
def __init__(self):
self.backends = [backend() for name, backend in self.support_backends]
self._queryset = AssetUserQueryset(self.backends)
def all(self):
return self._queryset
def delete(self, obj):
name_backends_map = dict(self.support_backends)
backend_name = obj.backend
backend_cls = name_backends_map.get(backend_name)
union_id = obj.union_id
if backend_cls:
backend_cls().delete(union_id)
else:
raise ObjectDoesNotExist("Not backend found")
@staticmethod
def create(**kwargs):
# 使用create方法创建AuthBook对象解决并发创建问题添加锁机制
authbook = AuthBook.create(**kwargs)
return authbook
def __getattr__(self, item):
return getattr(self._queryset, item)

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
from orgs.utils import current_org
from common.utils.lock import DistributedLock
from assets.models import Node
class NodeTreeUpdateLock(DistributedLock):
@@ -18,3 +19,11 @@ class NodeTreeUpdateLock(DistributedLock):
def __init__(self):
name = self.get_name()
super().__init__(name=name, release_on_transaction_commit=True, reentrant=True)
class NodeAddChildrenLock(DistributedLock):
name_template = 'assets.node.add_children.<org_id:{org_id}>'
def __init__(self, node: Node):
name = self.name_template.format(org_id=node.org_id)
super().__init__(name=name, release_on_transaction_commit=True)

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-05 10:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='adminuser',
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
),
migrations.AlterModelOptions(
name='asset',
options={'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='assetgroup',
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
),
migrations.AlterModelOptions(
name='cluster',
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'verbose_name': 'System user'},
),
]

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-09 15:31
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0002_auto_20180105_1807'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='cluster',
field=models.ForeignKey(default=assets.models.asset.default_cluster, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='assets', to='assets.Cluster', verbose_name='Cluster'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-25 04:18
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0003_auto_20180109_2331'),
]
operations = [
migrations.AlterField(
model_name='assetgroup',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-26 08:37
from __future__ import unicode_literals
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0004_auto_20180125_1218'),
]
operations = [
migrations.CreateModel(
name='Label',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
options={
'db_table': 'assets_label',
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together=set([('name', 'value')]),
),
migrations.AddField(
model_name='asset',
name='labels',
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
),
]

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-30 07:02
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_auto_20180126_1637'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='cabinet_no',
),
migrations.RemoveField(
model_name='asset',
name='cabinet_pos',
),
migrations.RemoveField(
model_name='asset',
name='env',
),
migrations.RemoveField(
model_name='asset',
name='remote_card_ip',
),
migrations.RemoveField(
model_name='asset',
name='status',
),
migrations.RemoveField(
model_name='asset',
name='type',
),
]

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-02-25 10:15
from __future__ import unicode_literals
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0006_auto_20180130_1502'),
]
operations = [
migrations.CreateModel(
name='Node',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
('value', models.CharField(max_length=128, unique=True, verbose_name='Value')),
('child_mark', models.IntegerField(default=0)),
('date_create', models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name='asset',
name='cluster',
),
migrations.RemoveField(
model_name='asset',
name='groups',
),
migrations.RemoveField(
model_name='systemuser',
name='cluster',
),
migrations.AlterField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='asset',
name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
),
migrations.AddField(
model_name='systemuser',
name='nodes',
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-06 10:04
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0007_auto_20180225_1815'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 04:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0008_auto_20180306_1804'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-07 09:49
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0009_auto_20180307_1212'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
),
]

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-03-26 01:57
from __future__ import unicode_literals
import assets.models.utils
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0010_auto_20180307_1749'),
]
operations = [
migrations.CreateModel(
name='Domain',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
),
migrations.CreateModel(
name='Gateway',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
('port', models.IntegerField(default=22, verbose_name='Port')),
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]

View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-04 05:02
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0011_auto_20180326_0957'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
]

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-11 03:35
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0012_auto_20180404_1302'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
),
migrations.AlterField(
model_name='systemuser',
name='sudo',
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
),
]

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-04-27 04:45
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0013_auto_20180411_1135'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
]

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-10 04:35
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0014_auto_20180427_1245'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-05-11 04:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0015_auto_20180510_1235'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
]

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-07-02 06:15
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
def migrate_win_to_ssh_protocol(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
class Migration(migrations.Migration):
dependencies = [
('assets', '0016_auto_20180511_1203'),
]
operations = [
migrations.AddField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.RunPython(migrate_win_to_ssh_protocol),
]

View File

@@ -0,0 +1,84 @@
# Generated by Django 2.0.7 on 2018-08-07 03:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0017_auto_20180702_1415'),
]
operations = [
migrations.AddField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='adminuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='asset',
name='hostname',
field=models.CharField(max_length=128, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='gateway',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='systemuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='adminuser',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='asset',
unique_together={('org_id', 'hostname')},
),
migrations.AlterUniqueTogether(
name='gateway',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='systemuser',
unique_together={('name', 'org_id')},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.0.7 on 2018-08-16 05:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0018_auto_20180807_1116'),
]
operations = [
migrations.AddField(
model_name='asset',
name='cpu_vcpus',
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value', 'org_id')},
),
]

View File

@@ -0,0 +1,61 @@
from django.db import migrations
from django.db.transaction import atomic
default_id = '00000000-0000-0000-0000-000000000002'
def change_key0_to_key1(apps, schema_editor):
from orgs.utils import set_current_org
# https://stackoverflow.com/questions/28777338/django-migrations-runpython-not-able-to-call-model-methods
Organization = apps.get_model('orgs', 'Organization')
Node = apps.get_model('assets', 'Node')
print()
org = Organization.objects.get(id=default_id)
set_current_org(org)
exists_0 = Node.objects.filter(key__startswith='0').exists()
if not exists_0:
print(f'--> Not exist key=0 nodes, do nothing.')
return
key_1_count = Node.objects.filter(key__startswith='1').count()
if key_1_count > 1:
print(f'--> Node key=1 have children, can`t just delete it. Please contact JumpServer team')
return
root_node = Node.objects.filter(key='1').first()
if root_node and root_node.assets.exists():
print(f'--> Node key=1 has assets, do nothing.')
return
with atomic():
if root_node:
print(f'--> Delete node key=1')
root_node.delete()
nodes_0 = Node.objects.filter(key__startswith='0')
for n in nodes_0:
old_key = n.key
key_list = n.key.split(':')
key_list[0] = '1'
new_key = ':'.join(key_list)
new_parent_key = ':'.join(key_list[:-1])
n.key = new_key
n.parent_key = new_parent_key
n.save()
print('--> Modify key ( {} > {} )'.format(old_key, new_key))
class Migration(migrations.Migration):
dependencies = [
('orgs', '0010_auto_20210219_1241'),
('assets', '0068_auto_20210312_1455'),
]
operations = [
migrations.RunPython(change_key0_to_key1)
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1 on 2021-04-26 07:15
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0069_change_node_key0_to_key1'),
]
operations = [
migrations.AddField(
model_name='commandfilterrule',
name='reviewers',
field=models.ManyToManyField(blank=True, related_name='review_cmd_filter_rules', to=settings.AUTH_USER_MODEL, verbose_name='Reviewers'),
),
migrations.AlterField(
model_name='commandfilterrule',
name='action',
field=models.IntegerField(choices=[(0, 'Deny'), (1, 'Allow'), (2, 'Reconfirm')], default=0, verbose_name='Action'),
),
]

View File

@@ -0,0 +1,90 @@
# Generated by Django 3.1.6 on 2021-06-04 16:46
from django.db import migrations, models, transaction
import django.db.models.deletion
from django.db.models import F
def migrate_admin_user_to_system_user(apps, schema_editor):
admin_user_model = apps.get_model("assets", "AdminUser")
system_user_model = apps.get_model("assets", "SystemUser")
db_alias = schema_editor.connection.alias
admin_users = admin_user_model.objects.using(db_alias).all()
print()
for admin_user in admin_users:
kwargs = {}
for attr in [
'id', 'org_id', 'username', 'password', 'private_key', 'public_key',
'comment', 'date_created', 'date_updated', 'created_by',
]:
value = getattr(admin_user, attr)
kwargs[attr] = value
name = admin_user.name
exist = system_user_model.objects.using(db_alias).filter(
name=admin_user.name, org_id=admin_user.org_id
).exists()
if exist:
name = admin_user.name + '_' + str(admin_user.id)[:5]
kwargs.update({
'name': name,
'type': 'admin',
'protocol': 'ssh',
'auto_push': False,
})
with transaction.atomic():
s = system_user_model(**kwargs)
s.save()
print(" Migrate admin user to system user: {} => {}".format(admin_user.name, s.name))
assets = admin_user.assets.all()
s.assets.set(assets)
def migrate_assets_admin_user(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
assets = asset_model.objects.using(db_alias).all()
assets.update(admin_user=F('_admin_user'))
class Migration(migrations.Migration):
dependencies = [
('assets', '0070_auto_20210426_1515'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='type',
field=models.CharField(choices=[('common', 'Common user'), ('admin', 'Admin user')], default='common', max_length=16, verbose_name='Type'),
),
migrations.AlterField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic managed'), ('manual', 'Manually input')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.RunPython(migrate_admin_user_to_system_user),
migrations.RenameField(
model_name='asset',
old_name='admin_user',
new_name='_admin_user',
),
migrations.AddField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='admin_assets', to='assets.systemuser', verbose_name='Admin user'),
),
migrations.RunPython(migrate_assets_admin_user),
migrations.RemoveField(
model_name='asset',
name='_admin_user',
),
]

View File

@@ -0,0 +1,85 @@
# Generated by Django 3.1.6 on 2021-06-05 16:10
import common.fields.model
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
import uuid
from django.utils import timezone
from django.db import migrations, transaction
def migrate_old_authbook_to_history(apps, schema_editor):
authbook_model = apps.get_model("assets", "AuthBook")
history_model = apps.get_model("assets", "HistoricalAuthBook")
db_alias = schema_editor.connection.alias
print()
while True:
authbooks = authbook_model.objects.using(db_alias).filter(is_latest=False)[:20]
if not authbooks:
break
historys = []
authbook_ids = []
# Todo: 或许能优化成更新那样
for authbook in authbooks:
authbook_ids.append(authbook.id)
history = history_model()
for attr in [
'id', 'username', 'password', 'private_key', 'public_key', 'version',
'comment', 'created_by', 'asset', 'date_created', 'date_updated'
]:
setattr(history, attr, getattr(authbook, attr))
history.history_type = '-'
history.history_date = timezone.now()
historys.append(history)
with transaction.atomic():
print(" Migrate old auth book to history table: {} items".format(len(authbook_ids)))
history_model.objects.bulk_create(historys, ignore_conflicts=True)
authbook_model.objects.filter(id__in=authbook_ids).delete()
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0071_systemuser_type'),
]
operations = [
migrations.CreateModel(
name='HistoricalAuthBook',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('is_latest', models.BooleanField(default=False, verbose_name='Latest version')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField()),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('asset', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.asset', verbose_name='Asset')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical AuthBook',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.RunPython(migrate_old_authbook_to_history)
]

View File

@@ -0,0 +1,105 @@
# Generated by Django 3.1.6 on 2021-06-06 03:42
from django.utils import timezone
from django.db import migrations, models, transaction
import django.db.models.deletion
def migrate_system_assets_to_authbook(apps, schema_editor):
system_user_model = apps.get_model("assets", "SystemUser")
system_user_asset_model = system_user_model.assets.through
authbook_model = apps.get_model('assets', 'AuthBook')
history_model = apps.get_model("assets", "HistoricalAuthBook")
print()
system_users = system_user_model.objects.all()
for s in system_users:
while True:
systemuser_asset_relations = system_user_asset_model.objects.filter(systemuser=s)[:20]
if not systemuser_asset_relations:
break
authbooks = []
relations_ids = []
historys = []
for i in systemuser_asset_relations:
authbook = authbook_model(asset=i.asset, systemuser=i.systemuser, org_id=s.org_id)
authbooks.append(authbook)
relations_ids.append(i.id)
history = history_model(
asset=i.asset, systemuser=i.systemuser,
date_created=timezone.now(), date_updated=timezone.now(),
)
history.history_type = '-'
history.history_date = timezone.now()
historys.append(history)
with transaction.atomic():
print(" Migrate system user assets relations: {} items".format(len(relations_ids)))
authbook_model.objects.bulk_create(authbooks, ignore_conflicts=True)
history_model.objects.bulk_create(historys)
system_user_asset_model.objects.filter(id__in=relations_ids).delete()
def migrate_authbook_secret_to_system_user(apps, schema_editor):
authbook_model = apps.get_model('assets', 'AuthBook')
history_model = apps.get_model('assets', 'HistoricalAuthBook')
print()
authbooks_without_systemuser = authbook_model.objects.filter(systemuser__isnull=True)
for authbook in authbooks_without_systemuser:
matched = authbook_model.objects.filter(
asset=authbook.asset, systemuser__username=authbook.username
)
if not matched:
continue
historys = []
for i in matched:
history = history_model(
asset=i.asset, systemuser=i.systemuser,
date_created=timezone.now(), date_updated=timezone.now(),
version=authbook.version
)
history.history_type = '-'
history.history_date = timezone.now()
historys.append(history)
with transaction.atomic():
print(" Migrate secret to system user assets account: {} items".format(len(historys)))
matched.update(password=authbook.password, private_key=authbook.private_key,
public_key=authbook.public_key, version=authbook.version)
history_model.objects.bulk_create(historys)
class Migration(migrations.Migration):
dependencies = [
('assets', '0072_historicalauthbook'),
]
operations = [
migrations.AddField(
model_name='authbook',
name='systemuser',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.systemuser', verbose_name='System user'),
),
migrations.AddField(
model_name='historicalauthbook',
name='systemuser',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='assets.systemuser', verbose_name='System user'),
),
migrations.AlterUniqueTogether(
name='authbook',
unique_together={('username', 'asset', 'systemuser')},
),
migrations.RunPython(migrate_system_assets_to_authbook),
migrations.RunPython(migrate_authbook_secret_to_system_user),
migrations.RemoveField(
model_name='authbook',
name='is_latest',
),
migrations.RemoveField(
model_name='historicalauthbook',
name='is_latest',
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.1.6 on 2021-06-06 03:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0073_auto_20210606_1142'),
]
operations = [
migrations.RemoveField(
model_name='systemuser',
name='assets',
),
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, related_name='system_users', through='assets.AuthBook', to='assets.Asset', verbose_name='Assets'),
),
]

View File

@@ -0,0 +1,53 @@
# Generated by Django 3.1 on 2021-07-05 09:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0074_remove_systemuser_assets'),
]
operations = [
migrations.AddField(
model_name='asset',
name='connectivity',
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
),
migrations.AddField(
model_name='asset',
name='date_verified',
field=models.DateTimeField(null=True, verbose_name='Date verified'),
),
migrations.AddField(
model_name='authbook',
name='connectivity',
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
),
migrations.AddField(
model_name='authbook',
name='date_verified',
field=models.DateTimeField(null=True, verbose_name='Date verified'),
),
migrations.AddField(
model_name='historicalauthbook',
name='connectivity',
field=models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], default='unknown', max_length=16, verbose_name='Connectivity'),
),
migrations.AddField(
model_name='historicalauthbook',
name='date_verified',
field=models.DateTimeField(null=True, verbose_name='Date verified'),
),
migrations.AlterField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='gateway',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 3.1.6 on 2021-07-12 02:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0075_auto_20210705_1759'),
]
operations = [
migrations.DeleteModel(
name='AssetUser',
),
]

View File

@@ -2,7 +2,6 @@ from .base import *
from .asset import *
from .label import Label
from .user import *
from .asset_user import *
from .cluster import *
from .group import *
from .domain import *

View File

@@ -4,18 +4,19 @@
import uuid
import logging
import random
from functools import reduce
from collections import OrderedDict
from django.db import models
from common.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
from common.fields.model import JsonDictTextField
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import ConnectivityMixin
from .utils import Connectivity
from .base import AbsConnectivity
__all__ = ['Asset', 'ProtocolsMixin', 'Platform', 'AssetQuerySet']
logger = logging.getLogger(__name__)
@@ -35,7 +36,7 @@ def default_node():
try:
from .node import Node
root = Node.org_root()
return root
return Node.objects.filter(id=root.id)
except:
return None
@@ -57,16 +58,12 @@ class AssetQuerySet(models.QuerySet):
class ProtocolsMixin:
protocols = ''
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'),
)
class Protocol(TextChoices):
ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
@property
def protocols_as_list(self):
@@ -167,7 +164,7 @@ class Platform(models.Model):
# ordering = ('name',)
class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
class Asset(AbsConnectivity, ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
# Important
PLATFORM_CHOICES = (
('Linux', 'Linux'),
@@ -182,8 +179,8 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
protocol = models.CharField(max_length=128, default=ProtocolsMixin.PROTOCOL_SSH,
choices=ProtocolsMixin.PROTOCOL_CHOICES,
protocol = models.CharField(max_length=128, default=ProtocolsMixin.Protocol.ssh,
choices=ProtocolsMixin.Protocol.choices,
verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port'))
protocols = models.CharField(max_length=128, default='ssh/22', blank=True, verbose_name=_("Protocols"))
@@ -193,7 +190,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Auth
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"), related_name='assets')
admin_user = models.ForeignKey('assets.SystemUser', on_delete=models.SET_NULL, null=True, verbose_name=_("Admin user"), related_name='admin_assets')
# Some information
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
@@ -223,11 +220,26 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
objects = AssetManager.from_queryset(AssetQuerySet)()
_connectivity = None
def __str__(self):
return '{0.hostname}({0.ip})'.format(self)
def set_admin_user_relation(self):
from .authbook import AuthBook
if not self.admin_user:
return
if self.admin_user.type != 'admin':
raise ValidationError('System user should be type admin')
defaults = {'asset': self, 'systemuser': self.admin_user, 'org_id': self.org_id}
AuthBook.objects.get_or_create(defaults=defaults, asset=self, systemuser=self.admin_user)
@property
def admin_user_display(self):
if not self.admin_user:
return ''
return str(self.admin_user)
@property
def is_valid(self):
warning = ''
@@ -276,23 +288,6 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
else:
return ''
@property
def connectivity(self):
if self._connectivity:
return self._connectivity
if not self.admin_user_username:
return Connectivity.unknown()
connectivity = ConnectivityMixin.get_asset_username_connectivity(
self, self.admin_user_username
)
return connectivity
@connectivity.setter
def connectivity(self, value):
if not self.admin_user:
return
self.admin_user.set_asset_connectivity(self, value)
def get_auth_info(self):
if not self.admin_user:
return {}
@@ -338,7 +333,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
'iconSkin': icon_skin,
'meta': {
'type': 'asset',
'asset': {
'data': {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
@@ -350,6 +345,13 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
tree_node = TreeNode(**data)
return tree_node
def get_all_systemusers(self):
from .user import SystemUser
system_user_ids = SystemUser.assets.through.objects.filter(asset=self)\
.values_list('systemuser_id', flat=True)
system_users = SystemUser.objects.filter(id__in=system_user_ids)
return system_users
class Meta:
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")

View File

@@ -1,14 +0,0 @@
# -*- coding: utf-8 -*-
#
from .authbook import AuthBook
class AssetUser(AuthBook):
hostname = ""
ip = ""
backend = ""
union_id = ""
asset_username = ""
class Meta:
proxy = True

View File

@@ -1,91 +1,101 @@
# -*- coding: utf-8 -*-
#
from django.db import models, transaction
from django.db.models import Max
from django.db import models
from django.utils.translation import ugettext_lazy as _
from simple_history.models import HistoricalRecords
from orgs.mixins.models import OrgManager
from .base import BaseUser
from common.utils import lazyproperty
from .base import BaseUser, AbsConnectivity
__all__ = ['AuthBook']
class AuthBookQuerySet(models.QuerySet):
def delete(self):
if self.count() > 1:
raise PermissionError(_("Bulk delete deny"))
return super().delete()
class AuthBookManager(OrgManager):
pass
class AuthBook(BaseUser):
class AuthBook(BaseUser, AbsConnectivity):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
systemuser = models.ForeignKey('assets.SystemUser', on_delete=models.CASCADE, null=True, verbose_name=_("System user"))
version = models.IntegerField(default=1, verbose_name=_('Version'))
history = HistoricalRecords()
_systemuser_display = ''
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
backend = "db"
# 用于system user和admin_user的动态设置
_connectivity = None
CONN_CACHE_KEY = "ASSET_USER_CONN_{}"
auth_attrs = ['username', 'password', 'private_key', 'public_key']
class Meta:
verbose_name = _('AuthBook')
unique_together = [('username', 'asset', 'systemuser')]
def get_related_assets(self):
return [self.asset]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.auth_snapshot = {}
def generate_id_with_asset(self, asset):
return self.id
def get_or_systemuser_attr(self, attr):
val = getattr(self, attr, None)
if val:
return val
if self.systemuser:
return getattr(self.systemuser, attr, '')
return ''
@classmethod
def get_max_version(cls, username, asset):
version_max = cls.objects.filter(username=username, asset=asset) \
.aggregate(Max('version'))
version_max = version_max['version__max'] or 0
return version_max
def load_auth(self):
for attr in self.auth_attrs:
value = self.get_or_systemuser_attr(attr)
self.auth_snapshot[attr] = [getattr(self, attr), value]
setattr(self, attr, value)
@classmethod
def create(cls, **kwargs):
"""
使用并发锁机制创建AuthBook对象, (主要针对并发创建 username, asset 相同的对象时)
并更新其他对象的 is_latest=False (其他对象: 与当前对象的 username, asset 相同)
同时设置自己的 is_latest=True, version=max_version + 1
"""
username = kwargs['username']
asset = kwargs.get('asset') or kwargs.get('asset_id')
with transaction.atomic():
# 使用select_for_update限制并发创建相同的username、asset条目
instances = cls.objects.select_for_update().filter(username=username, asset=asset)
instances.filter(is_latest=True).update(is_latest=False)
max_version = cls.get_max_version(username, asset)
kwargs.update({
'version': max_version + 1,
'is_latest': True
})
obj = cls.objects.create(**kwargs)
return obj
def unload_auth(self):
if not self.systemuser:
return
for attr, values in self.auth_snapshot.items():
origin_value, loaded_value = values
current_value = getattr(self, attr, '')
if current_value == loaded_value:
setattr(self, attr, origin_value)
def save(self, *args, **kwargs):
self.unload_auth()
instance = super().save(*args, **kwargs)
self.load_auth()
return instance
@property
def connectivity(self):
return self.get_asset_connectivity(self.asset)
def username_display(self):
return self.get_or_systemuser_attr('username') or '*'
@lazyproperty
def systemuser_display(self):
if self._systemuser_display:
return self._systemuser_display
if not self.systemuser:
return ''
return str(self.systemuser)
@property
def keyword(self):
return '{}_#_{}'.format(self.username, str(self.asset.id))
def smart_name(self):
username = self.username_display
@property
def hostname(self):
return self.asset.hostname
if self.asset:
asset = str(self.asset)
else:
asset = '*'
return '{}@{}'.format(username, asset)
@property
def ip(self):
return self.asset.ip
def sync_to_system_user_account(self):
if self.systemuser:
return
matched = AuthBook.objects.filter(
asset=self.asset, systemuser__username=self.username
)
if not matched:
return
for i in matched:
i.password = self.password
i.private_key = self.private_key
i.public_key = self.public_key
i.comment = 'Update triggered by account {}'.format(self.id)
i.save(update_fields=['password', 'private_key', 'public_key'])
def __str__(self):
return '{}@{}'.format(self.username, self.asset)
return self.smart_name

View File

@@ -8,11 +8,12 @@ from hashlib import md5
import sshpubkeys
from django.core.cache import cache
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.db.models import QuerySet
from common.db.models import ChoiceSet
from common.utils import random_string
from common.utils import random_string, signer
from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
)
@@ -20,85 +21,39 @@ from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric
from common import fields
from orgs.mixins.models import OrgModelMixin
from .utils import Connectivity
logger = get_logger(__file__)
class ConnectivityMixin:
CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY"
CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT"
ASSET_USER_CACHE_TIME = 3600 * 24
id = ''
username = ''
class Connectivity(models.TextChoices):
unknown = 'unknown', _('Unknown')
ok = 'ok', _('Ok')
failed = 'failed', _('Failed')
@property
def part_id(self):
i = '-'.join(str(self.id).split('-')[:3])
return i
def set_connectivity(self, summary):
unreachable = summary.get('dark', {}).keys()
reachable = summary.get('contacted', {}).keys()
class AbsConnectivity(models.Model):
connectivity = models.CharField(
choices=Connectivity.choices, default=Connectivity.unknown,
max_length=16, verbose_name=_('Connectivity')
)
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
assets = self.get_related_assets()
if not isinstance(assets, list):
assets = assets.only('id', 'hostname', 'admin_user__id')
for asset in assets:
if asset.hostname in unreachable:
self.set_asset_connectivity(asset, Connectivity.unreachable())
elif asset.hostname in reachable:
self.set_asset_connectivity(asset, Connectivity.reachable())
else:
self.set_asset_connectivity(asset, Connectivity.unknown())
cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id)
cache.delete(cache_key)
@property
def connectivity(self):
assets = self.get_related_assets()
if not isinstance(assets, list):
assets = assets.only('id', 'hostname', 'admin_user__id')
data = {
'unreachable': [],
'reachable': [],
'unknown': [],
}
for asset in assets:
connectivity = self.get_asset_connectivity(asset)
if connectivity.is_reachable():
data["reachable"].append(asset.hostname)
elif connectivity.is_unreachable():
data["unreachable"].append(asset.hostname)
else:
data["unknown"].append(asset.hostname)
return data
@property
def connectivity_amount(self):
cache_key = self.CONNECTIVITY_AMOUNT_CACHE_KEY.format(self.username, self.part_id)
amount = cache.get(cache_key)
if not amount:
amount = {k: len(v) for k, v in self.connectivity.items()}
cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME)
return amount
def set_connectivity(self, val):
self.connectivity = val
self.date_verified = timezone.now()
self.save(update_fields=['connectivity', 'date_verified'])
@classmethod
def get_asset_username_connectivity(cls, asset, username):
key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id)
return Connectivity.get(key)
def bulk_set_connectivity(cls, queryset_or_id, connectivity):
if not isinstance(queryset_or_id, QuerySet):
queryset = cls.objects.filter(id__in=queryset_or_id)
else:
queryset = queryset_or_id
queryset.update(connectivity=connectivity, date_verified=timezone.now())
def get_asset_connectivity(self, asset):
key = self.get_asset_connectivity_key(asset)
return Connectivity.get(key)
def get_asset_connectivity_key(self, asset):
return self.CONNECTIVITY_ASSET_CACHE_KEY.format(self.username, asset.id)
def set_asset_connectivity(self, asset, c):
key = self.get_asset_connectivity_key(asset)
Connectivity.set(key, c)
class Meta:
abstract = True
class AuthMixin:
@@ -106,14 +61,16 @@ class AuthMixin:
password = ''
public_key = ''
username = ''
_prefer = 'system_user'
@property
def ssh_key_fingerprint(self):
if self.public_key:
public_key = self.public_key
elif self.private_key:
public_key = ssh_pubkey_gen(self.private_key, self.password)
try:
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
except IOError as e:
return str(e)
else:
return ''
@@ -174,38 +131,6 @@ class AuthMixin:
if update_fields:
self.save(update_fields=update_fields)
def has_special_auth(self, asset=None, username=None):
from .authbook import AuthBook
if username is None:
username = self.username
queryset = AuthBook.objects.filter(username=username)
if asset:
queryset = queryset.filter(asset=asset)
return queryset.exists()
def get_asset_user(self, asset, username=None):
from ..backends import AssetUserManager
if username is None:
username = self.username
try:
manager = AssetUserManager()
other = manager.get_latest(
username=username, asset=asset,
prefer_id=self.id, prefer=self._prefer,
)
return other
except Exception as e:
logger.error(e, exc_info=True)
return None
def load_asset_special_auth(self, asset=None, username=None):
if not asset:
return self
instance = self.get_asset_user(asset, username=username)
if instance:
self._merge_auth(instance)
def _merge_auth(self, other):
if other.password:
self.password = other.password
@@ -245,7 +170,7 @@ class AuthMixin:
)
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
class BaseUser(OrgModelMixin, AuthMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
@@ -260,8 +185,6 @@ class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
ASSET_USER_CACHE_TIME = 600
_prefer = "system_user"
def get_related_assets(self):
assets = self.assets.filter(org_id=self.org_id)
return assets

View File

@@ -41,11 +41,12 @@ class CommandFilterRule(OrgModelMixin):
(TYPE_COMMAND, _('Command')),
)
ACTION_DENY, ACTION_ALLOW, ACTION_UNKNOWN = range(3)
ACTION_CHOICES = (
(ACTION_DENY, _('Deny')),
(ACTION_ALLOW, _('Allow')),
)
ACTION_UNKNOWN = 10
class ActionChoices(models.IntegerChoices):
deny = 0, _('Deny')
allow = 1, _('Allow')
confirm = 2, _('Reconfirm')
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
@@ -53,7 +54,13 @@ class CommandFilterRule(OrgModelMixin):
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"))
action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action"))
# 动作: 附加字段
# - confirm: 命令复核人
reviewers = models.ManyToManyField(
'users.User', related_name='review_cmd_filter_rules', blank=True,
verbose_name=_("Reviewers")
)
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
@@ -89,10 +96,32 @@ class CommandFilterRule(OrgModelMixin):
if not found:
return self.ACTION_UNKNOWN, ''
if self.action == self.ACTION_ALLOW:
return self.ACTION_ALLOW, found.group()
if self.action == self.ActionChoices.allow:
return self.ActionChoices.allow, found.group()
else:
return self.ACTION_DENY, found.group()
return self.ActionChoices.deny, found.group()
def __str__(self):
return '{} % {}'.format(self.type, self.content)
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketTypeChoices
from tickets.models import Ticket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketTypeChoices.command_confirm,
'meta': {
'apply_run_user': session.user,
'apply_run_asset': session.asset,
'apply_run_system_user': session.system_user,
'apply_run_command': run_command,
'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
},
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
ticket.assignees.set(self.reviewers.all())
ticket.open(applicant=session.user_obj)
return ticket

View File

@@ -1,18 +1,21 @@
# -*- coding: utf-8 -*-
#
import socket
import uuid
import random
import re
from django.core.cache import cache
import paramiko
from django.db import models
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from common.utils.strings import no_special_chars
from common.utils import get_logger
from orgs.mixins.models import OrgModelMixin
from .base import BaseUser
logger = get_logger(__file__)
__all__ = ['Domain', 'Gateway']
@@ -39,19 +42,25 @@ class Domain(OrgModelMixin):
return self.gateway_set.filter(is_active=True)
def random_gateway(self):
return random.choice(self.gateways)
gateways = [gw for gw in self.gateways if gw.is_connective]
if gateways:
return random.choice(gateways)
else:
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
return random.choice(self.gateways)
class Gateway(BaseUser):
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
)
UNCONNECTIVE_KEY_TMPL = 'asset_unconnective_gateway_{}'
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
class Protocol(TextChoices):
ssh = 'ssh', 'SSH'
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port'))
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol"))
protocol = models.CharField(choices=Protocol.choices, max_length=16, default=Protocol.ssh, verbose_name=_("Protocol"))
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
@@ -63,11 +72,40 @@ class Gateway(BaseUser):
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")
def set_unconnective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
unconnective_silence_period = cache.get(unconnective_silence_period_key,
self.UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE)
cache.set(unconnective_silence_period_key, unconnective_silence_period * 2)
cache.set(unconnective_key, unconnective_silence_period, unconnective_silence_period)
def set_connective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
unconnective_silence_period_key = self.UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL.format(self.id)
cache.delete(unconnective_key)
cache.delete(unconnective_silence_period_key)
def get_is_unconnective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)
return cache.get(unconnective_key, False)
@property
def is_connective(self):
return not self.get_is_unconnective()
@is_connective.setter
def is_connective(self, value):
if value:
self.set_connective()
else:
self.set_unconnective()
def test_connective(self, local_port=None):
if local_port is None:
local_port = self.port
if self.password and not no_special_chars(self.password):
return False, _("Password should not contains special characters")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -82,8 +120,16 @@ class Gateway(BaseUser):
except(paramiko.AuthenticationException,
paramiko.BadAuthenticationType,
paramiko.SSHException,
paramiko.ssh_exception.NoValidConnectionsError) as e:
return False, str(e)
paramiko.ssh_exception.NoValidConnectionsError,
socket.gaierror) as e:
err = str(e)
if err.startswith('[Errno None] Unable to connect to port'):
err = _('Unable to connect to port {port} on {ip}')
err = err.format(port=self.port, ip=self.ip)
elif err == 'Authentication failed.':
err = _('Authentication failed')
self.is_connective = False
return False, err
try:
sock = proxy.get_transport().open_channel(
@@ -97,7 +143,9 @@ class Gateway(BaseUser):
timeout=5)
except (paramiko.SSHException, paramiko.ssh_exception.SSHException,
paramiko.AuthenticationException, TimeoutError) as e:
self.is_connective = False
return False, str(e)
finally:
client.close()
self.is_connective = True
return True, None

View File

@@ -38,8 +38,7 @@ def compute_parent_key(key):
class NodeQuerySet(models.QuerySet):
def delete(self):
raise NotImplementedError
pass
class FamilyMixin:
@@ -307,6 +306,15 @@ class NodeAllAssetsMappingMixin:
org_id = str(org_id)
cls.orgid_nodekey_assetsid_mapping.pop(org_id, None)
@classmethod
def expire_all_orgs_node_all_asset_ids_mapping_from_memory(cls):
orgs = Organization.objects.all()
org_ids = [str(org.id) for org in orgs]
org_ids.append(Organization.ROOT_ID)
for id in org_ids:
cls.expire_node_all_asset_ids_mapping_from_memory(id)
# 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):
@@ -600,7 +608,7 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
'isParent': True,
'open': self.is_org_root(),
'meta': {
'node': {
'data': {
"id": self.id,
"name": self.name,
"value": self.value,
@@ -613,14 +621,14 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
tree_node = TreeNode(**data)
return tree_node
def has_children_or_has_assets(self):
if self.children or self.get_assets().exists():
return True
return False
def has_offspring_assets(self):
# 拥有后代资产
return self.get_all_assets().exists()
def delete(self, using=None, keep_parents=False):
if self.has_children_or_has_assets():
if self.has_offspring_assets():
return
self.all_children.delete()
return super().delete(using=using, keep_parents=keep_parents)
def update_child_full_value(self):

View File

@@ -7,17 +7,277 @@ import logging
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.cache import cache
from common.utils import signer
from common.fields.model import JsonListCharField
from common.utils import signer, get_object_or_none
from common.db.models import TextChoices
from .base import BaseUser
from .asset import Asset
from .authbook import AuthBook
__all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__)
class ProtocolMixin:
protocol: str
class Protocol(TextChoices):
ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
mysql = 'mysql', 'MySQL'
oracle = 'oracle', 'Oracle'
mariadb = 'mariadb', 'MariaDB'
postgresql = 'postgresql', 'PostgreSQL'
k8s = 'k8s', 'K8S'
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
ASSET_CATEGORY_PROTOCOLS = [
Protocol.ssh, Protocol.rdp, Protocol.telnet, Protocol.vnc
]
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
Protocol.rdp
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
Protocol.mysql, Protocol.oracle, Protocol.mariadb, Protocol.postgresql
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
Protocol.k8s
]
APPLICATION_CATEGORY_PROTOCOLS = [
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
*APPLICATION_CATEGORY_DB_PROTOCOLS,
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
]
@property
def is_protocol_support_push(self):
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
@classmethod
def get_protocol_by_application_type(cls, app_type):
from applications.const import AppType
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
protocol = app_type
elif app_type in AppType.remote_app_types():
protocol = cls.Protocol.rdp
else:
protocol = None
return protocol
@property
def can_perm_to_asset(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
class AuthMixin:
username_same_with_user: bool
protocol: str
ASSET_CATEGORY_PROTOCOLS: list
login_mode: str
LOGIN_MANUAL: str
id: str
username: str
password: str
private_key: str
public_key: str
def set_temp_auth(self, asset_or_app_id, user_id, auth, ttl=300):
if not auth:
raise ValueError('Auth not set')
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Set system user temp auth: {key}')
cache.set(key, auth, ttl)
def get_temp_auth(self, asset_or_app_id, user_id):
key = 'TEMP_PASSWORD_{}_{}_{}'.format(self.id, asset_or_app_id, user_id)
logger.debug(f'Get system user temp auth: {key}')
password = cache.get(key)
return password
def load_tmp_auth_if_has(self, asset_or_app_id, user):
if not asset_or_app_id or not user:
return
if self.login_mode != self.LOGIN_MANUAL:
return
auth = self.get_temp_auth(asset_or_app_id, user)
if not auth:
return
username = auth.get('username')
password = auth.get('password')
if username:
self.username = username
if password:
self.password = password
def load_app_more_auth(self, app_id=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
if not user_id:
return
user = get_object_or_none(User, pk=user_id)
if not user:
return
self.load_tmp_auth_if_has(app_id, user)
def load_asset_special_auth(self, asset, username=''):
"""
"""
authbooks = list(AuthBook.objects.filter(asset=asset, systemuser=self))
if len(authbooks) == 0:
return None
elif len(authbooks) == 1:
authbook = authbooks[0]
else:
authbooks.sort(key=lambda x: 1 if x.username == username else 0, reverse=True)
authbook = authbooks[0]
authbook.load_auth()
self.password = authbook.password
self.private_key = authbook.private_key
self.public_key = authbook.public_key
def load_asset_more_auth(self, asset_id=None, username=None, user_id=None):
from users.models import User
if self.login_mode == self.LOGIN_MANUAL:
self.password = ''
self.private_key = ''
asset = None
if asset_id:
asset = get_object_or_none(Asset, pk=asset_id)
# 没有资产就没有必要继续了
if not asset:
logger.debug('Asset not found, pass')
return
user = None
if user_id:
user = get_object_or_none(User, pk=user_id)
_username = self.username
if self.username_same_with_user:
if user and not username:
_username = user.username
else:
_username = username
self.username = _username
# 加载某个资产的特殊配置认证信息
self.load_asset_special_auth(asset, _username)
self.load_tmp_auth_if_has(asset_id, user)
class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = (
(LOGIN_AUTO, _('Automatic managed')),
(LOGIN_MANUAL, _('Manually input'))
)
class Type(TextChoices):
common = 'common', _('Common user')
admin = 'admin', _('Admin user')
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField(
'assets.Asset', blank=True, verbose_name=_("Assets"),
through='assets.AuthBook', through_fields=['systemuser', 'asset'],
related_name='system_users'
)
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
type = models.CharField(max_length=16, choices=Type.choices, default=Type.common, verbose_name=_('Type'))
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=ProtocolMixin.Protocol.choices, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256)
def __str__(self):
username = self.username
if self.username_same_with_user:
username = '*'
return '{0.name}({1})'.format(self, username)
@property
def nodes_amount(self):
return self.nodes.all().count()
@property
def login_mode_display(self):
return self.get_login_mode_display()
def is_need_push(self):
if self.auto_push and self.is_protocol_support_push:
return True
else:
return False
@property
def is_admin_user(self):
return self.type == self.Type.admin
@property
def is_need_cmd_filter(self):
return self.protocol not in [self.Protocol.rdp, self.Protocol.vnc]
@property
def is_need_test_asset_connective(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule
rules = CommandFilterRule.objects.filter(
filter__in=self.cmd_filters.all()
).distinct()
return rules
def is_command_can_run(self, command):
for rule in self.cmd_filter_rules:
action, matched_cmd = rule.match(command)
if action == rule.ActionChoices.allow:
return True, None
elif action == rule.ActionChoices.deny:
return False, matched_cmd
return True, None
def get_all_assets(self):
from assets.models import Node
nodes_keys = self.nodes.all().values_list('key', flat=True)
asset_ids = set(self.assets.all().values_list('id', flat=True))
nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys)
asset_ids.update(nodes_asset_ids)
assets = Asset.objects.filter(id__in=asset_ids)
return assets
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("System user")
# Todo: 准备废弃
class AdminUser(BaseUser):
"""
A privileged user that ansible can use it to push system user and so on
@@ -64,165 +324,3 @@ class AdminUser(BaseUser):
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("Admin user")
class SystemUser(BaseUser):
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_MYSQL = 'mysql'
PROTOCOL_ORACLE = 'oracle'
PROTOCOL_MARIADB = 'mariadb'
PROTOCOL_POSTGRESQL = 'postgresql'
PROTOCOL_K8S = 'k8s'
PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'),
(PROTOCOL_MYSQL, 'mysql'),
(PROTOCOL_ORACLE, 'oracle'),
(PROTOCOL_MARIADB, 'mariadb'),
(PROTOCOL_POSTGRESQL, 'postgresql'),
(PROTOCOL_K8S, 'k8s'),
)
SUPPORT_PUSH_PROTOCOLS = [PROTOCOL_SSH, PROTOCOL_RDP]
ASSET_CATEGORY_PROTOCOLS = [
PROTOCOL_SSH, PROTOCOL_RDP, PROTOCOL_TELNET, PROTOCOL_VNC
]
APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS = [
PROTOCOL_RDP
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
PROTOCOL_MYSQL, PROTOCOL_ORACLE, PROTOCOL_MARIADB, PROTOCOL_POSTGRESQL
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
PROTOCOL_K8S
]
APPLICATION_CATEGORY_PROTOCOLS = [
*APPLICATION_CATEGORY_REMOTE_APP_PROTOCOLS,
*APPLICATION_CATEGORY_DB_PROTOCOLS,
*APPLICATION_CATEGORY_CLOUD_PROTOCOLS
]
LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = (
(LOGIN_AUTO, _('Automatic login')),
(LOGIN_MANUAL, _('Manually login'))
)
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
priority = models.IntegerField(default=81, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256)
_prefer = 'system_user'
def __str__(self):
username = self.username
if self.username_same_with_user:
username = 'dynamic'
return '{0.name}({1})'.format(self, username)
def get_username(self):
if self.username_same_with_user:
return list(self.users.values_list('username', flat=True))
else:
return self.username
@property
def nodes_amount(self):
return self.nodes.all().count()
@property
def login_mode_display(self):
return self.get_login_mode_display()
def is_need_push(self):
if self.auto_push and self.is_protocol_support_push:
return True
else:
return False
@property
def is_protocol_support_push(self):
return self.protocol in self.SUPPORT_PUSH_PROTOCOLS
@property
def is_need_cmd_filter(self):
return self.protocol not in [self.PROTOCOL_RDP, self.PROTOCOL_VNC]
@property
def is_need_test_asset_connective(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
def has_special_auth(self, asset=None, username=None):
if username is None and self.username_same_with_user:
raise TypeError('System user is dynamic, username should be pass')
return super().has_special_auth(asset=asset, username=username)
@property
def can_perm_to_asset(self):
return self.protocol in self.ASSET_CATEGORY_PROTOCOLS
def _merge_auth(self, other):
super()._merge_auth(other)
if self.username_same_with_user:
self.username = other.username
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule
rules = CommandFilterRule.objects.filter(
filter__in=self.cmd_filters.all()
).distinct()
return rules
def is_command_can_run(self, command):
for rule in self.cmd_filter_rules:
action, matched_cmd = rule.match(command)
if action == rule.ACTION_ALLOW:
return True, None
elif action == rule.ACTION_DENY:
return False, matched_cmd
return True, None
def get_all_assets(self):
from assets.models import Node
nodes_keys = self.nodes.all().values_list('key', flat=True)
asset_ids = set(self.assets.all().values_list('id', flat=True))
nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys)
asset_ids.update(nodes_asset_ids)
assets = Asset.objects.filter(id__in=asset_ids)
return assets
@classmethod
def get_protocol_by_application_type(cls, app_type):
from applications.const import ApplicationTypeChoices
if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
protocol = app_type
elif app_type in ApplicationTypeChoices.remote_app_types():
protocol = cls.PROTOCOL_RDP
else:
protocol = None
return protocol
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("System user")

View File

@@ -11,7 +11,7 @@ from common.utils import validate_ssh_private_key
__all__ = [
'init_model', 'generate_fake', 'private_key_validator', 'Connectivity',
'init_model', 'generate_fake', 'private_key_validator',
]
@@ -35,74 +35,3 @@ def private_key_validator(value):
_('%(value)s is not an even number'),
params={'value': value},
)
class Connectivity:
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
status = UNKNOWN
datetime = timezone.now()
def __init__(self, status, datetime):
self.status = status
self.datetime = datetime
def display(self):
return dict(self.__class__.CONNECTIVITY_CHOICES).get(self.status)
def is_reachable(self):
return self.status == self.REACHABLE
def is_unreachable(self):
return self.status == self.UNREACHABLE
def is_unknown(self):
return self.status == self.UNKNOWN
@classmethod
def unreachable(cls):
return cls(cls.UNREACHABLE, timezone.now())
@classmethod
def reachable(cls):
return cls(cls.REACHABLE, timezone.now())
@classmethod
def unknown(cls):
return cls(cls.UNKNOWN, timezone.now())
@classmethod
def set(cls, key, value, ttl=None):
cache.set(key, value, ttl)
@classmethod
def get(cls, key):
value = cache.get(key, cls.unknown())
if not isinstance(value, cls):
value = cls.unknown()
return value
@classmethod
def set_unreachable(cls, key, ttl=0):
cls.set(key, cls.unreachable(), ttl)
@classmethod
def set_reachable(cls, key, ttl=0):
cls.set(key, cls.reachable(), ttl)
def __eq__(self, other):
return self.status == other.status
def __gt__(self, other):
return self.status > other.status
def __lt__(self, other):
return not self.__gt__(other)
def __str__(self):
return self.display()

View File

@@ -8,6 +8,6 @@ from .system_user import *
from .node import *
from .domain import *
from .cmd_filter import *
from .asset_user import *
from .gathered_user import *
from .favorite_asset import *
from .account import *

View File

@@ -0,0 +1,61 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from assets.models import AuthBook
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin
from .utils import validate_password_contains_left_double_curly_bracket
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
ip = serializers.ReadOnlyField(label=_("IP"))
hostname = serializers.ReadOnlyField(label=_("Hostname"))
class Meta:
model = AuthBook
fields_mini = ['id', 'username', 'ip', 'hostname', 'version']
fields_write_only = ['password', 'private_key', "public_key"]
fields_other = ['date_created', 'date_updated', 'connectivity', 'date_verified', 'comment']
fields_small = fields_mini + fields_write_only + fields_other
fields_fk = ['asset', 'systemuser', 'systemuser_display']
fields = fields_small + fields_fk
extra_kwargs = {
'username': {'required': True},
'password': {
'write_only': True,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
'systemuser_display': {'label': _('System user display')}
}
ref_name = 'AssetAccountSerializer'
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('systemuser', 'asset')
return queryset
def to_representation(self, instance):
instance.load_auth()
return super().to_representation(instance)
class AccountSecretSerializer(AccountSerializer):
class Meta(AccountSerializer.Meta):
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
'public_key': {'write_only': False},
'systemuser_display': {'label': _('System user display')}
}
class AccountTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
task = serializers.CharField(read_only=True)

View File

@@ -1,68 +1,25 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..models import Node, AdminUser
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializer, AuthSerializerMixin
from ..models import SystemUser
from .system_user import SystemUserSerializer as SuS
class AdminUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class AdminUserSerializer(SuS):
"""
管理用户
"""
class Meta:
model = AdminUser
fields = [
'id', 'name', 'username', 'password', 'private_key', 'public_key',
'comment', 'assets_amount', 'date_created', 'date_updated', 'created_by',
]
read_only_fields = ['date_created', 'date_updated', 'created_by', 'assets_amount']
class Meta(SuS.Meta):
fields = SuS.Meta.fields_mini + \
SuS.Meta.fields_write_only + \
SuS.Meta.fields_m2m + \
[
'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint',
'date_created', 'date_updated', 'comment', 'created_by',
]
extra_kwargs = {
'username': {"required": True},
'password': {"write_only": True},
'private_key': {"write_only": True},
'public_key': {"write_only": True},
'assets_amount': {'label': _('Asset')},
}
def validate_type(self, val):
return SystemUser.Type.admin
class AdminUserDetailSerializer(AdminUserSerializer):
class Meta(AdminUserSerializer.Meta):
fields = AdminUserSerializer.Meta.fields + ['ssh_key_fingerprint']
class AdminUserAuthSerializer(AuthSerializer):
class Meta:
model = AdminUser
fields = ['password', 'private_key']
class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
"""
管理用户更新关联到的集群
"""
nodes = serializers.PrimaryKeyRelatedField(
many=True, queryset=Node.objects
)
class Meta:
model = AdminUser
fields = ['id', 'nodes']
class TaskIDSerializer(serializers.Serializer):
task = serializers.CharField(read_only=True)
class AssetUserTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
task = serializers.CharField(read_only=True)
def validate_protocol(self, val):
return 'ssh'

View File

@@ -1,26 +1,23 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.db.models import F
from django.core.validators import RegexValidator
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node, Platform
from .base import ConnectivitySerializer
from ..models import Asset, Node, Platform, SystemUser
__all__ = [
'AssetSerializer', 'AssetSimpleSerializer',
'AssetDisplaySerializer',
'ProtocolsField', 'PlatformSerializer',
'AssetDetailSerializer', 'AssetTaskSerializer',
'AssetTaskSerializer', 'AssetsTaskSerializer', 'ProtocolsField'
]
class ProtocolField(serializers.RegexField):
protocols = '|'.join(dict(Asset.PROTOCOL_CHOICES).keys())
protocols = '|'.join(dict(Asset.Protocol.choices).keys())
default_error_messages = {
'invalid': _('Protocol format should {}/{}'.format(protocols, '1-65535'))
'invalid': _('Protocol format should {}/{}').format(protocols, '1-65535')
}
regex = r'^(%s)/(\d{1,5})$' % protocols
@@ -65,9 +62,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
platform = serializers.SlugRelatedField(
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
)
protocols = ProtocolsField(label=_('Protocols'), required=False)
protocols = ProtocolsField(label=_('Protocols'), required=False, default=['ssh/22'])
domain_display = serializers.ReadOnlyField(source='domain.name', label=_('Domain name'))
admin_user_display = serializers.ReadOnlyField(source='admin_user.name', label=_('Admin user name'))
nodes_display = serializers.ListField(child=serializers.CharField(), label=_('Nodes name'), required=False)
"""
@@ -75,43 +71,46 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
"""
class Meta:
model = Asset
fields_mini = ['id', 'hostname', 'ip']
fields_mini = ['id', 'hostname', 'ip', 'platform', 'protocols']
fields_small = fields_mini + [
'protocol', 'port', 'protocols', 'is_active', 'public_ip',
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw', 'comment',
'created_by', 'date_created', 'hardware_info',
'hardware_info', 'connectivity', 'date_verified'
]
fields_fk = [
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
'domain', 'domain_display', 'platform', 'admin_user', 'admin_user_display'
]
fk_only_fields = {
'platform': ['name']
}
fields_m2m = [
'nodes', 'nodes_display', 'labels',
]
annotates_fields = {
# 'admin_user_display': 'admin_user__name'
}
fields_as = list(annotates_fields.keys())
fields = fields_small + fields_fk + fields_m2m + fields_as
read_only_fields = [
'created_by', 'date_created',
] + fields_as
]
fields = fields_small + fields_fk + fields_m2m + read_only_fields
extra_kwargs = {
'protocol': {'write_only': True},
'port': {'write_only': True},
'hardware_info': {'label': _('Hardware info')},
'org_name': {'label': _('Org name')}
'org_name': {'label': _('Org name')},
'admin_user_display': {'label': _('Admin user display')}
}
def get_fields(self):
fields = super().get_fields()
admin_user_field = fields.get('admin_user')
# 因为 mixin 中对 fields 有处理,可能不需要返回 admin_user
if admin_user_field:
admin_user_field.queryset = SystemUser.objects.filter(type=SystemUser.Type.admin)
return fields
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('admin_user', 'domain', 'platform')
queryset = queryset.prefetch_related('domain', 'platform', 'admin_user')
queryset = queryset.prefetch_related('nodes', 'labels')
return queryset
@@ -158,15 +157,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
return instance
class AssetDisplaySerializer(AssetSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
class Meta(AssetSerializer.Meta):
fields = AssetSerializer.Meta.fields + [
'connectivity',
]
class PlatformSerializer(serializers.ModelSerializer):
meta = serializers.DictField(required=False, allow_null=True, label=_('Meta'))
@@ -186,19 +176,14 @@ class PlatformSerializer(serializers.ModelSerializer):
]
class AssetDetailSerializer(AssetSerializer):
platform = PlatformSerializer(read_only=True)
class AssetSimpleSerializer(serializers.ModelSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'connectivity', 'port']
fields = ['id', 'hostname', 'ip', 'port', 'connectivity', 'date_verified']
class AssetTaskSerializer(serializers.Serializer):
class AssetsTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('refresh', 'refresh'),
('test', 'test'),
@@ -208,3 +193,17 @@ class AssetTaskSerializer(serializers.Serializer):
assets = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, required=False, allow_empty=True, many=True
)
class AssetTaskSerializer(AssetsTaskSerializer):
ACTION_CHOICES = tuple(list(AssetsTaskSerializer.ACTION_CHOICES) + [
('push_system_user', 'push_system_user'),
('test_system_user', 'test_system_user')
])
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
asset = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, required=False, allow_empty=True, many=False
)
system_users = serializers.PrimaryKeyRelatedField(
queryset=SystemUser.objects, required=False, allow_empty=True, many=True
)

View File

@@ -1,91 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import AuthBook, Asset
from ..backends import AssetUserManager
from .base import ConnectivitySerializer, AuthSerializerMixin
__all__ = [
'AssetUserWriteSerializer', 'AssetUserReadSerializer',
'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer',
]
class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = AuthBook
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'username', 'password', 'private_key', "public_key",
'asset', 'comment',
]
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
def create(self, validated_data):
if not validated_data.get("name") and validated_data.get("username"):
validated_data["name"] = validated_data["username"]
instance = AssetUserManager.create(**validated_data)
return instance
class AssetUserReadSerializer(AssetUserWriteSerializer):
id = serializers.CharField(read_only=True, source='union_id', label=_("ID"))
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
ip = serializers.CharField(read_only=True, label=_("IP"))
asset = serializers.CharField(source='asset_id', label=_('Asset'))
backend = serializers.CharField(read_only=True, label=_("Backend"))
class Meta(AssetUserWriteSerializer.Meta):
read_only_fields = (
'date_created', 'date_updated',
'created_by', 'version',
)
fields = [
'id', 'username', 'password', 'private_key', "public_key",
'asset', 'hostname', 'ip', 'backend', 'version',
'date_created', "date_updated", 'comment',
]
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
class AssetUserAuthInfoSerializer(AssetUserReadSerializer):
password = serializers.CharField(
max_length=256, allow_blank=True, allow_null=True,
required=False, label=_('Password')
)
public_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True,
required=False, label=_('Public key')
)
private_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True,
required=False, label=_('Private key')
)
class AssetUserPushSerializer(serializers.Serializer):
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset"))
username = serializers.CharField(max_length=1024)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass

View File

@@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.utils import ssh_pubkey_gen, validate_ssh_private_key
from ..models import AssetUser
class AuthSerializer(serializers.ModelSerializer):
@@ -29,11 +28,6 @@ class AuthSerializer(serializers.ModelSerializer):
return self.instance
class ConnectivitySerializer(serializers.Serializer):
status = serializers.IntegerField()
datetime = serializers.DateTimeField()
class AuthSerializerMixin:
def validate_password(self, password):
return password
@@ -64,15 +58,3 @@ class AuthSerializerMixin:
def update(self, instance, validated_data):
self.clean_auth_fields(validated_data)
return super().update(instance, validated_data)
class AuthInfoSerializer(serializers.ModelSerializer):
private_key = serializers.ReadOnlyField(source='get_private_key')
class Meta:
model = AssetUser
fields = [
'username', 'password',
'private_key', 'public_key',
'date_updated',
]

View File

@@ -3,21 +3,27 @@
import re
from rest_framework import serializers
from common.drf.serializers import AdaptedBulkListSerializer
from ..models import CommandFilter, CommandFilterRule, SystemUser
from ..models import CommandFilter, CommandFilterRule
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from orgs.utils import tmp_to_root_org
from common.utils import get_object_or_none, lazyproperty
from terminal.models import Session
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = CommandFilter
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'org_id', 'org_name', 'is_active', 'comment',
'created_by', 'date_created', 'date_updated', 'rules', 'system_users'
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'org_id', 'org_name',
'is_active',
'date_created', 'date_updated',
'comment', 'created_by',
]
fields_fk = ['rules']
fields_m2m = ['system_users']
fields = fields_small + fields_fk + fields_m2m
extra_kwargs = {
'rules': {'read_only': True},
'system_users': {'required': False},
@@ -34,12 +40,26 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
fields_mini = ['id']
fields_small = fields_mini + [
'type', 'type_display', 'content', 'priority',
'action', 'action_display',
'comment', 'created_by', 'date_created', 'date_updated'
'action', 'action_display', 'reviewers',
'date_created', 'date_updated',
'comment', 'created_by',
]
fields_fk = ['filter']
fields = '__all__'
list_serializer_class = AdaptedBulkListSerializer
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_action_choices()
def set_action_choices(self):
from django.conf import settings
action = self.fields.get('action')
if not action:
return
choices = action._choices
if not settings.XPACK_ENABLED:
choices.pop(CommandFilterRule.ActionChoices.confirm, None)
action._choices = choices
# def validate_content(self, content):
# tp = self.initial_data.get("type")
@@ -50,3 +70,35 @@ class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
# msg = _("Content should not be contain: {}").format(invalid_char)
# raise serializers.ValidationError(msg)
# return content
class CommandConfirmSerializer(serializers.Serializer):
session_id = serializers.UUIDField(required=True, allow_null=False)
cmd_filter_rule_id = serializers.UUIDField(required=True, allow_null=False)
run_command = serializers.CharField(required=True, allow_null=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.session = None
self.cmd_filter_rule = None
def validate_session_id(self, session_id):
self.session = self.validate_object_exist(Session, session_id)
return session_id
def validate_cmd_filter_rule_id(self, cmd_filter_rule_id):
self.cmd_filter_rule = self.validate_object_exist(CommandFilterRule, cmd_filter_rule_id)
return cmd_filter_rule_id
@staticmethod
def validate_object_exist(model, field_id):
with tmp_to_root_org():
obj = get_object_or_none(model, id=field_id)
if not obj:
error = '{} Model object does not exist'.format(model.__name__)
raise serializers.ValidationError(error)
return obj
@lazyproperty
def org(self):
return self.session.org

View File

@@ -3,16 +3,14 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.validators import NoSpecialChars
from ..models import Domain, Gateway
from .base import AuthSerializerMixin
class DomainSerializer(BulkOrgResourceModelSerializer):
asset_count = serializers.SerializerMethodField(label=_('Assets count'))
application_count = serializers.SerializerMethodField(label=_('Applications count'))
asset_count = serializers.SerializerMethodField(label=_('Assets amount'))
application_count = serializers.SerializerMethodField(label=_('Applications amount'))
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
class Meta:
@@ -29,7 +27,6 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
extra_kwargs = {
'assets': {'required': False, 'label': _('Assets')},
}
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
def get_asset_count(obj):
@@ -45,36 +42,36 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
is_connective = serializers.BooleanField(required=False)
class Meta:
model = Gateway
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'ip', 'port', 'protocol', 'username', 'password',
'private_key', 'public_key', 'domain', 'is_active', 'date_created',
'date_updated', 'created_by', 'comment',
fields_mini = ['id', 'name']
fields_write_only = [
'password', 'private_key', 'public_key',
]
fields_small = fields_mini + fields_write_only + [
'username', 'ip', 'port', 'protocol',
'is_active', 'is_connective',
'date_created', 'date_updated',
'created_by', 'comment',
]
fields_fk = ['domain']
fields = fields_small + fields_fk
extra_kwargs = {
'password': {'validators': [NoSpecialChars()]}
'password': {'write_only': True},
'private_key': {"write_only": True},
'public_key': {"write_only": True},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_limit_to_ssh()
def protocol_limit_to_ssh(self):
protocol_field = self.fields['protocol']
choices = protocol_field.choices
choices.pop('rdp')
protocol_field._choices = choices
class GatewayWithAuthSerializer(GatewaySerializer):
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(
['password', 'private_key']
)
return fields
class Meta(GatewaySerializer.Meta):
extra_kwargs = {
'password': {'write_only': False},
'private_key': {"write_only": False},
'public_key': {"write_only": False},
}
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):

View File

@@ -4,7 +4,6 @@
from rest_framework import serializers
from orgs.utils import tmp_to_root_org
from common.drf.serializers import AdaptedBulkListSerializer
from common.mixins import BulkSerializerMixin
from ..models import FavoriteAsset
@@ -18,6 +17,5 @@ class FavoriteAssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
)
class Meta:
list_serializer_class = AdaptedBulkListSerializer
model = FavoriteAsset
fields = ['user', 'asset']

View File

@@ -10,11 +10,14 @@ from ..models import GatheredUser
class GatheredUserSerializer(OrgResourceModelSerializerMixin):
class Meta:
model = GatheredUser
fields = [
'id', 'asset', 'hostname', 'ip', 'username',
'date_last_login', 'ip_last_login',
'present', 'date_created', 'date_updated'
fields_mini = ['id']
fields_small = fields_mini + [
'username', 'ip_last_login',
'present',
'date_last_login', 'date_created', 'date_updated'
]
fields_fk = ['asset', 'hostname', 'ip']
fields = fields_small + fields_fk
read_only_fields = fields
extra_kwargs = {
'hostname': {'label': _("Hostname")},

View File

@@ -3,7 +3,6 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.drf.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Label
@@ -15,17 +14,21 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = Label
fields = [
'id', 'name', 'value', 'category', 'is_active', 'comment',
'date_created', 'asset_count', 'assets', 'category_display'
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'value', 'category', 'category_display',
'is_active',
'date_created',
'comment',
]
fields_m2m = ['asset_count', 'assets']
fields = fields_small + fields_m2m
read_only_fields = (
'category', 'date_created', 'asset_count',
)
extra_kwargs = {
'assets': {'required': False}
}
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
def get_asset_count(obj):

View File

@@ -2,18 +2,19 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from common.drf.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import SystemUser, Asset
from .utils import validate_password_contains_left_double_curly_bracket
from .base import AuthSerializerMixin
__all__ = [
'SystemUserSerializer', 'SystemUserListSerializer',
'SystemUserSerializer',
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
'SystemUserTempAuthSerializer',
]
@@ -22,22 +23,28 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
系统用户
"""
auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True)
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint'))
class Meta:
model = SystemUser
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'username', 'protocol',
'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display',
'priority', 'username_same_with_user',
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root', 'token',
'assets_amount', 'date_created', 'date_updated', 'created_by',
'home', 'system_groups', 'ad_domain'
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key']
fields_small = fields_mini + fields_write_only + [
'type', 'type_display', 'protocol', 'login_mode', 'login_mode_display',
'priority', 'sudo', 'shell', 'sftp_root', 'token', 'ssh_key_fingerprint',
'home', 'system_groups', 'ad_domain',
'username_same_with_user', 'auto_push', 'auto_generate_key',
'date_created', 'date_updated',
'comment', 'created_by',
]
fields_m2m = ['cmd_filters', 'assets_amount']
fields = fields_small + fields_m2m
extra_kwargs = {
'password': {"write_only": True},
'password': {
"write_only": True,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'public_key': {"write_only": True},
'private_key': {"write_only": True},
'token': {"write_only": True},
@@ -49,18 +56,18 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
}
def validate_auto_push(self, value):
login_mode = self.initial_data.get("login_mode")
protocol = self.initial_data.get("protocol")
login_mode = self.get_initial_value("login_mode")
protocol = self.get_initial_value("protocol")
if login_mode == SystemUser.LOGIN_MANUAL or \
protocol in [SystemUser.PROTOCOL_TELNET,
SystemUser.PROTOCOL_VNC]:
if login_mode == SystemUser.LOGIN_MANUAL:
value = False
elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
value = False
return value
def validate_auto_generate_key(self, value):
login_mode = self.initial_data.get("login_mode")
protocol = self.initial_data.get("protocol")
login_mode = self.get_initial_value("login_mode")
protocol = self.get_initial_value("protocol")
if self.context["request"].method.lower() != "post":
value = False
@@ -68,16 +75,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
value = False
elif login_mode == SystemUser.LOGIN_MANUAL:
value = False
elif protocol in [SystemUser.PROTOCOL_TELNET, SystemUser.PROTOCOL_VNC]:
elif protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
value = False
return value
def validate_username_same_with_user(self, username_same_with_user):
if not username_same_with_user:
return username_same_with_user
protocol = self.initial_data.get("protocol", "ssh")
protocol = self.get_initial_value("protocol", "ssh")
queryset = SystemUser.objects.filter(
protocol=protocol, username_same_with_user=True
protocol=protocol,
username_same_with_user=True
)
if self.instance:
queryset = queryset.exclude(id=self.instance.id)
@@ -90,17 +98,24 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
def validate_username(self, username):
if username:
return username
login_mode = self.initial_data.get("login_mode")
protocol = self.initial_data.get("protocol")
username_same_with_user = self.initial_data.get("username_same_with_user")
login_mode = self.get_initial_value("login_mode")
protocol = self.get_initial_value("protocol")
username_same_with_user = self.get_initial_value("username_same_with_user")
if username_same_with_user:
return ''
if login_mode == SystemUser.LOGIN_AUTO and \
protocol != SystemUser.PROTOCOL_VNC:
if login_mode == SystemUser.LOGIN_AUTO and protocol != SystemUser.Protocol.vnc:
msg = _('* Automatic login mode must fill in the username.')
raise serializers.ValidationError(msg)
return username
def validate_home(self, home):
username_same_with_user = self.get_initial_value("username_same_with_user")
if username_same_with_user:
return ''
return home
def validate_sftp_root(self, value):
if value in ['home', 'tmp']:
return value
@@ -109,65 +124,58 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
raise serializers.ValidationError(error)
return value
def validate_admin_user(self, attrs):
if self.instance:
tp = self.instance.type
else:
tp = attrs.get('type')
if tp != SystemUser.Type.admin:
return attrs
attrs['protocol'] = SystemUser.Protocol.ssh
attrs['login_mode'] = SystemUser.LOGIN_AUTO
attrs['username_same_with_user'] = False
attrs['auto_push'] = False
return attrs
def validate_password(self, password):
super().validate_password(password)
auto_gen_key = self.initial_data.get("auto_generate_key", False)
private_key = self.initial_data.get("private_key")
login_mode = self.initial_data.get("login_mode")
auto_gen_key = self.get_initial_value("auto_generate_key", False)
private_key = self.get_initial_value("private_key")
login_mode = self.get_initial_value("login_mode")
if not self.instance and not auto_gen_key and not password and \
not private_key and login_mode == SystemUser.LOGIN_AUTO:
raise serializers.ValidationError(_("Password or private key required"))
return password
def validate(self, attrs):
def validate_gen_key(self, attrs):
username = attrs.get("username", "manual")
auto_gen_key = attrs.pop("auto_generate_key", False)
protocol = attrs.get("protocol")
if protocol not in [SystemUser.PROTOCOL_RDP, SystemUser.PROTOCOL_SSH]:
if protocol not in SystemUser.SUPPORT_PUSH_PROTOCOLS:
return attrs
if auto_gen_key:
# 自动生成
if auto_gen_key and not self.instance:
password = SystemUser.gen_password()
attrs["password"] = password
if protocol == SystemUser.PROTOCOL_SSH:
if protocol == SystemUser.Protocol.ssh:
private_key, public_key = SystemUser.gen_key(username)
attrs["private_key"] = private_key
attrs["public_key"] = public_key
# 如果设置了private key没有设置public key则生成
# 如果设置了private key没有设置public key则生成
elif attrs.get("private_key", None):
private_key = attrs["private_key"]
password = attrs.get("password")
public_key = ssh_pubkey_gen(private_key, password=password,
username=username)
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
attrs["public_key"] = public_key
return attrs
class SystemUserListSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
fields = [
'id', 'name', 'username', 'protocol',
'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display',
'priority', "username_same_with_user",
'auto_push', 'sudo', 'shell', 'comment',
"assets_amount", 'home', 'system_groups',
'auto_generate_key', 'ad_domain',
'sftp_root', 'created_by', 'date_created',
'date_updated',
]
extra_kwargs = {
'password': {"write_only": True},
'public_key': {"write_only": True},
'private_key': {"write_only": True},
'nodes_amount': {'label': _('Nodes amount')},
'assets_amount': {'label': _('Assets amount')},
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
'ad_domain': {'label': _('Ad domain')},
}
def validate(self, attrs):
attrs = self.validate_admin_user(attrs)
attrs = self.validate_gen_key(attrs)
return attrs
@classmethod
def setup_eager_loading(cls, queryset):
@@ -178,15 +186,15 @@ class SystemUserListSerializer(SystemUserSerializer):
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
fields = [
'id', 'name', 'username', 'protocol',
'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display',
'priority', 'username_same_with_user',
'auto_push', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root', 'token',
'ad_domain',
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key']
fields_small = fields_mini + fields_write_only + [
'protocol', 'login_mode', 'login_mode_display', 'priority',
'sudo', 'shell', 'ad_domain', 'sftp_root', 'token',
"username_same_with_user", 'auto_push', 'auto_generate_key',
'comment',
]
fields = fields_small
extra_kwargs = {
'nodes_amount': {'label': _('Node')},
'assets_amount': {'label': _('Asset')},
@@ -205,31 +213,34 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
class RelationMixin(BulkSerializerMixin, serializers.Serializer):
systemuser_display = serializers.ReadOnlyField()
systemuser_display = serializers.ReadOnlyField(label=_("System user name"))
org_name = serializers.ReadOnlyField(label=_("Org name"))
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend(['systemuser', "systemuser_display"])
fields.extend(['systemuser', "systemuser_display", "org_name"])
return fields
class Meta:
list_serializer_class = AdaptedBulkListSerializer
class SystemUserAssetRelationSerializer(RelationMixin, serializers.ModelSerializer):
asset_display = serializers.ReadOnlyField()
asset_display = serializers.ReadOnlyField(label=_('Asset hostname'))
class Meta(RelationMixin.Meta):
class Meta:
model = SystemUser.assets.through
fields = [
'id', "asset", "asset_display",
"id", "asset", "asset_display", 'systemuser', 'systemuser_display',
"connectivity", 'date_verified', 'org_id'
]
use_model_bulk_create = True
model_bulk_create_kwargs = {
'ignore_conflicts': True
}
class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerializer):
node_display = serializers.SerializerMethodField()
class Meta(RelationMixin.Meta):
class Meta:
model = SystemUser.nodes.through
fields = [
'id', 'node', "node_display",
@@ -242,7 +253,7 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
class Meta:
model = SystemUser.users.through
fields = [
'id', "user", "user_display",
@@ -263,3 +274,10 @@ class SystemUserTaskSerializer(serializers.Serializer):
many=True
)
task = serializers.CharField(read_only=True)
class SystemUserTempAuthSerializer(SystemUserSerializer):
instance_id = serializers.CharField()
class Meta(SystemUserSerializer.Meta):
fields = ['instance_id', 'username', 'password']

View File

@@ -0,0 +1,9 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
def validate_password_contains_left_double_curly_bracket(password):
# validate password contains left double curly bracket
# check password not contains `{{`
if '{{' in password:
raise serializers.ValidationError(_('Password can not contains `{{` '))

View File

@@ -1,3 +1,5 @@
from .common import *
from .asset import *
from .system_user import *
from .authbook import *
from .node_assets_amount import *
from .node_assets_mapping import *

View File

@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import (
post_save, m2m_changed, pre_delete, post_delete, pre_save
)
from django.dispatch import receiver
from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE
from common.utils import get_logger
from common.decorator import on_transaction_commit
from assets.models import Asset, SystemUser, Node
from assets.tasks import (
update_assets_hardware_info_util,
test_asset_connectivity_util,
push_system_user_to_assets,
)
logger = get_logger(__file__)
def update_asset_hardware_info_on_created(asset):
logger.debug("Update asset `{}` hardware info".format(asset))
update_assets_hardware_info_util.delay([asset])
def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectivity_util.delay([asset])
@receiver(pre_save, sender=Node)
def on_node_pre_save(sender, instance: Node, **kwargs):
instance.parent_key = instance.compute_parent_key()
@receiver(post_save, sender=Asset)
@on_transaction_commit
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
"""
当资产创建时,更新硬件信息,更新可连接性
确保资产必须属于一个节点
"""
if created:
logger.info("Asset create signal recv: {}".format(instance))
# 获取资产硬件信息
update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance)
# 确保资产存在一个节点
has_node = instance.nodes.all().exists()
if not has_node:
instance.nodes.add(Node.org_root())
instance.set_admin_user_relation()
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
"""
本操作共访问 4 次数据库
当资产的节点发生变化时,或者 当节点的资产关系发生变化时,
节点下新增的资产,添加到节点关联的系统用户中
"""
if action != POST_ADD:
return
logger.debug("Assets node add signal recv: {}".format(action))
if reverse:
nodes = [instance.key]
asset_ids = pk_set
else:
nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True)
asset_ids = [instance.id]
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
nodes_ancestors_keys = set()
for node in nodes:
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
system_user_ids = SystemUser.objects.filter(
nodes__key__in=nodes_ancestors_keys
).distinct().values_list('id', flat=True)
# 查询所有已存在的关系
m2m_model = SystemUser.assets.through
exist = set(m2m_model.objects.filter(
systemuser_id__in=system_user_ids, asset_id__in=asset_ids
).values_list('systemuser_id', 'asset_id'))
# TODO 优化
to_create = []
for system_user_id in system_user_ids:
asset_ids_to_push = []
for asset_id in asset_ids:
if (system_user_id, asset_id) in exist:
continue
asset_ids_to_push.append(asset_id)
to_create.append(m2m_model(
systemuser_id=system_user_id,
asset_id=asset_id,
org_id=instance.org_id
))
if asset_ids_to_push:
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
m2m_model.objects.bulk_create(to_create)
RELATED_NODE_IDS = '_related_node_ids'
@receiver(pre_delete, sender=Asset)
def on_asset_delete(instance: Asset, using, **kwargs):
node_ids = set(Node.objects.filter(
assets=instance
).distinct().values_list('id', flat=True))
setattr(instance, RELATED_NODE_IDS, node_ids)
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
)
@receiver(post_delete, sender=Asset)
def on_asset_post_delete(instance: Asset, using, **kwargs):
node_ids = getattr(instance, RELATED_NODE_IDS, None)
if node_ids:
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
)

View File

@@ -0,0 +1,42 @@
from django.dispatch import receiver
from django.apps import apps
from simple_history.signals import pre_create_historical_record
from django.db.models.signals import post_save, pre_save
from common.utils import get_logger
from ..models import AuthBook, SystemUser
AuthBookHistory = apps.get_model('assets', 'HistoricalAuthBook')
logger = get_logger(__name__)
@receiver(pre_create_historical_record, sender=AuthBookHistory)
def pre_create_historical_record_callback(sender, history_instance=None, **kwargs):
attrs_to_copy = ['username', 'password', 'private_key']
for attr in attrs_to_copy:
if getattr(history_instance, attr):
continue
try:
system_user = history_instance.systemuser
except SystemUser.DoesNotExist:
continue
if not system_user:
continue
system_user_attr_value = getattr(history_instance.systemuser, attr)
if system_user_attr_value:
setattr(history_instance, attr, system_user_attr_value)
@receiver(post_save, sender=AuthBook)
def on_authbook_post_create(sender, instance, **kwargs):
if not instance.systemuser:
instance.sync_to_system_user_account()
@receiver(pre_save, sender=AuthBook)
def on_authbook_pre_create(sender, instance, **kwargs):
# 升级版本号
instance.version += 1
# 即使在 root 组织也不怕
instance.org_id = instance.asset.org_id

View File

@@ -1,223 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import (
post_save, m2m_changed, pre_delete, post_delete, pre_save
)
from django.dispatch import receiver
from common.exceptions import M2MReverseNotAllowed
from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE
from common.utils import get_logger
from common.decorator import on_transaction_commit
from assets.models import Asset, SystemUser, Node
from users.models import User
from assets.tasks import (
update_assets_hardware_info_util,
test_asset_connectivity_util,
push_system_user_to_assets_manual,
push_system_user_to_assets,
add_nodes_assets_to_system_users
)
logger = get_logger(__file__)
def update_asset_hardware_info_on_created(asset):
logger.debug("Update asset `{}` hardware info".format(asset))
update_assets_hardware_info_util.delay([asset])
def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectivity_util.delay([asset])
@receiver(pre_save, sender=Node)
def on_node_pre_save(sender, instance: Node, **kwargs):
instance.parent_key = instance.compute_parent_key()
@receiver(post_save, sender=Asset)
@on_transaction_commit
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
"""
当资产创建时,更新硬件信息,更新可连接性
确保资产必须属于一个节点
"""
if created:
logger.info("Asset create signal recv: {}".format(instance))
# 获取资产硬件信息
update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance)
# 确保资产存在一个节点
has_node = instance.nodes.all().exists()
if not has_node:
instance.nodes.add(Node.org_root())
@receiver(post_save, sender=SystemUser, dispatch_uid="jms")
@on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
"""
当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上,
其实应该当 用户名,密码,秘钥 sudo等更新时再推送这里偷个懒,
这里直接取了 instance.assets 因为nodes和系统用户发生变化时会自动将nodes下的资产
关联到上面
"""
if instance and not created:
logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])
@receiver(m2m_changed, sender=SystemUser.assets.through)
@on_transaction_commit
def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
"""
当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中
"""
if action != POST_ADD:
return
logger.debug("System user assets change signal recv: {}".format(instance))
if model == Asset:
system_user_ids = [instance.id]
asset_ids = pk_set
else:
system_user_ids = pk_set
asset_ids = [instance.id]
for system_user_id in system_user_ids:
push_system_user_to_assets.delay(system_user_id, asset_ids)
@receiver(m2m_changed, sender=SystemUser.users.through)
@on_transaction_commit
def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs):
"""
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.username_same_with_user:
return
logger.debug("System user users change signal recv: {}".format(instance))
usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
for username in usernames:
push_system_user_to_assets_manual.delay(instance, username)
@receiver(m2m_changed, sender=SystemUser.nodes.through)
@on_transaction_commit
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
"""
当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上
"""
if action != POST_ADD:
return
logger.info("System user nodes update signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == Node:
nodes_keys = queryset.values_list('key', flat=True)
system_users = [instance]
else:
nodes_keys = [instance.key]
system_users = queryset
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
@receiver(m2m_changed, sender=SystemUser.groups.through)
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
"""
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
logger.info("System user groups update signal recv: {}".format(instance))
users = User.objects.filter(groups__id__in=pk_set).distinct()
instance.users.add(*users)
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
"""
本操作共访问 4 次数据库
当资产的节点发生变化时,或者 当节点的资产关系发生变化时,
节点下新增的资产,添加到节点关联的系统用户中
"""
if action != POST_ADD:
return
logger.debug("Assets node add signal recv: {}".format(action))
if reverse:
nodes = [instance.key]
asset_ids = pk_set
else:
nodes = Node.objects.filter(pk__in=pk_set).values_list('key', flat=True)
asset_ids = [instance.id]
# 节点资产发生变化时,将资产关联到节点及祖先节点关联的系统用户, 只关注新增的
nodes_ancestors_keys = set()
for node in nodes:
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
system_user_ids = SystemUser.objects.filter(
nodes__key__in=nodes_ancestors_keys
).distinct().values_list('id', flat=True)
# 查询所有已存在的关系
m2m_model = SystemUser.assets.through
exist = set(m2m_model.objects.filter(
systemuser_id__in=system_user_ids, asset_id__in=asset_ids
).values_list('systemuser_id', 'asset_id'))
# TODO 优化
to_create = []
for system_user_id in system_user_ids:
asset_ids_to_push = []
for asset_id in asset_ids:
if (system_user_id, asset_id) in exist:
continue
asset_ids_to_push.append(asset_id)
to_create.append(m2m_model(
systemuser_id=system_user_id,
asset_id=asset_id
))
if asset_ids_to_push:
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
m2m_model.objects.bulk_create(to_create)
RELATED_NODE_IDS = '_related_node_ids'
@receiver(pre_delete, sender=Asset)
def on_asset_delete(instance: Asset, using, **kwargs):
node_ids = set(Node.objects.filter(
assets=instance
).distinct().values_list('id', flat=True))
setattr(instance, RELATED_NODE_IDS, node_ids)
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
)
@receiver(post_delete, sender=Asset)
def on_asset_post_delete(instance: Asset, using, **kwargs):
node_ids = getattr(instance, RELATED_NODE_IDS, None)
if node_ids:
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
)

View File

@@ -13,6 +13,7 @@ from common.signals import django_ready
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from assets.models import Asset, Node
from orgs.models import Organization
logger = get_logger(__file__)
@@ -36,13 +37,17 @@ node_assets_mapping_for_memory_pub_sub = NodeAssetsMappingForMemoryPubSub()
def expire_node_assets_mapping_for_memory(org_id):
# 所有进程清除(自己的 memory 数据)
org_id = str(org_id)
node_assets_mapping_for_memory_pub_sub.publish(org_id)
root_org_id = Organization.ROOT_ID
# 当前进程清除(cache 数据)
logger.debug(
"Expire node assets id mapping from cache of org={}, pid={}"
"".format(org_id, os.getpid())
)
Node.expire_node_all_asset_ids_mapping_from_cache(org_id)
Node.expire_node_all_asset_ids_mapping_from_cache(root_org_id)
node_assets_mapping_for_memory_pub_sub.publish(org_id)
@receiver(post_save, sender=Node)
@@ -73,16 +78,24 @@ def subscribe_node_assets_mapping_expire(sender, **kwargs):
logger.debug("Start subscribe for expire node assets id mapping from memory")
def keep_subscribe():
subscribe = node_assets_mapping_for_memory_pub_sub.subscribe()
for message in subscribe.listen():
if message["type"] != "message":
continue
org_id = message['data'].decode()
Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
logger.debug(
"Expire node assets id mapping from memory of org={}, pid={}"
"".format(str(org_id), os.getpid())
)
while True:
try:
subscribe = node_assets_mapping_for_memory_pub_sub.subscribe()
for message in subscribe.listen():
if message["type"] != "message":
continue
org_id = message['data'].decode()
root_org_id = Organization.ROOT_ID
Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
Node.expire_node_all_asset_ids_mapping_from_memory(root_org_id)
logger.debug(
"Expire node assets id mapping from memory of org={}, pid={}"
"".format(str(org_id), os.getpid())
)
except Exception as e:
logger.exception(f'subscribe_node_assets_mapping_expire: {e}')
Node.expire_all_orgs_node_all_asset_ids_mapping_from_memory()
t = threading.Thread(target=keep_subscribe)
t.daemon = True
t.start()

View File

@@ -0,0 +1,142 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import (
post_save, m2m_changed, pre_save, pre_delete, post_delete
)
from django.dispatch import receiver
from common.exceptions import M2MReverseNotAllowed
from common.const.signals import POST_ADD
from common.utils import get_logger
from common.decorator import on_transaction_commit
from assets.models import Asset, SystemUser, Node, AuthBook
from users.models import User
from orgs.utils import tmp_to_root_org
from assets.tasks import (
push_system_user_to_assets_manual,
push_system_user_to_assets,
add_nodes_assets_to_system_users
)
logger = get_logger(__file__)
@receiver(m2m_changed, sender=SystemUser.assets.through)
@on_transaction_commit
def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
"""
当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中
"""
logger.debug("System user assets change signal recv: {}".format(instance))
if not instance:
logger.debug('No system user found')
return
if model == Asset:
system_user_ids = [instance.id]
asset_ids = pk_set
else:
system_user_ids = pk_set
asset_ids = [instance.id]
org_id = instance.org_id
# 关联创建的 authbook 没有系统用户id
with tmp_to_root_org():
authbooks = AuthBook.objects.filter(
asset_id__in=asset_ids,
systemuser_id__in=system_user_ids
)
if action == POST_ADD:
authbooks.update(org_id=org_id)
save_action_mapper = {
'pre_add': pre_save,
'post_add': post_save,
'pre_remove': pre_delete,
'post_remove': post_delete
}
for ab in authbooks:
ab.org_id = org_id
save_action = save_action_mapper[action]
logger.debug('Send AuthBook post save signal: {} -> {}'.format(action, ab.id))
save_action.send(sender=AuthBook, instance=ab, created=True)
if action == POST_ADD:
for system_user_id in system_user_ids:
push_system_user_to_assets.delay(system_user_id, asset_ids)
@receiver(m2m_changed, sender=SystemUser.users.through)
@on_transaction_commit
def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs):
"""
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
if not instance.username_same_with_user:
return
logger.debug("System user users change signal recv: {}".format(instance))
usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
for username in usernames:
push_system_user_to_assets_manual.delay(instance, username)
@receiver(m2m_changed, sender=SystemUser.nodes.through)
@on_transaction_commit
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
"""
当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上
"""
if action != POST_ADD:
return
logger.info("System user nodes update signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == Node:
nodes_keys = queryset.values_list('key', flat=True)
system_users = [instance]
else:
nodes_keys = [instance.key]
system_users = queryset
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
@receiver(m2m_changed, sender=SystemUser.groups.through)
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
"""
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
"""
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
logger.info("System user groups update signal recv: {}".format(instance))
users = User.objects.filter(groups__id__in=pk_set).distinct()
instance.users.add(*users)
@receiver(post_save, sender=SystemUser, dispatch_uid="jms")
@on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
"""
当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上,
其实应该当 用户名,密码,秘钥 sudo等更新时再推送这里偷个懒,
这里直接取了 instance.assets 因为nodes和系统用户发生变化时会自动将nodes下的资产
关联到上面
"""
if instance and not created:
logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])

View File

@@ -2,9 +2,8 @@
#
from .utils import *
from .common import *
from .admin_user_connectivity import *
from .asset_connectivity import *
from .asset_user_connectivity import *
from .account_connectivity import *
from .gather_asset_users import *
from .gather_asset_hardware_info import *
from .push_system_user import *

View File

@@ -3,9 +3,9 @@
from celery import shared_task
from django.utils.translation import ugettext as _
from common.utils import get_logger, get_object_or_none
from common.utils import get_logger
from orgs.utils import org_aware_func
from ..models import Asset
from ..models import Connectivity
from . import const
from .utils import check_asset_can_run_ansible
@@ -14,13 +14,13 @@ logger = get_logger(__file__)
__all__ = [
'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual',
'get_test_asset_user_connectivity_tasks', 'test_user_connectivity',
'test_account_connectivity_util', 'test_accounts_connectivity_manual',
'get_test_account_connectivity_tasks', 'test_user_connectivity',
'run_adhoc',
]
def get_test_asset_user_connectivity_tasks(asset):
def get_test_account_connectivity_tasks(asset):
if asset.is_unixlike():
tasks = const.PING_UNIXLIKE_TASKS
elif asset.is_windows():
@@ -57,7 +57,7 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke
"""
from ops.inventory import JMSCustomInventory
tasks = get_test_asset_user_connectivity_tasks(asset)
tasks = get_test_account_connectivity_tasks(asset)
if not tasks:
logger.debug("No tasks ")
return {}, {}
@@ -71,62 +71,39 @@ def test_user_connectivity(task_name, asset, username, password=None, private_ke
return raw, summary
@org_aware_func("asset_user")
def test_asset_user_connectivity_util(asset_user, task_name):
@org_aware_func("account")
def test_account_connectivity_util(account, task_name):
"""
:param asset_user: <AuthBook>对象
:param account: <AuthBook>对象
:param task_name:
:return:
"""
if not check_asset_can_run_ansible(asset_user.asset):
if not check_asset_can_run_ansible(account.asset):
return
account.load_auth()
try:
raw, summary = test_user_connectivity(
task_name=task_name, asset=asset_user.asset,
username=asset_user.username, password=asset_user.password,
private_key=asset_user.private_key_file
task_name=task_name, asset=account.asset,
username=account.username, password=account.password,
private_key=account.private_key_file
)
except Exception as e:
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
return
asset_user.set_connectivity(summary)
if summary.get('success'):
account.set_connectivity(Connectivity.ok)
else:
account.set_connectivity(Connectivity.failed)
@shared_task(queue="ansible")
def test_asset_users_connectivity_manual(asset_users):
def test_accounts_connectivity_manual(accounts):
"""
:param asset_users: <AuthBook>对象
:param accounts: <AuthBook>对象
"""
for asset_user in asset_users:
task_name = _("Test asset user connectivity: {}").format(asset_user)
test_asset_user_connectivity_util(asset_user, task_name)
@shared_task(queue="ansible")
def push_asset_user_util(asset_user):
"""
:param asset_user: <Asset user>对象
"""
from .push_system_user import push_system_user_util
if not asset_user.backend.startswith('system_user'):
logger.error("Asset user is not from system user")
return
union_id = asset_user.union_id
union_id_list = union_id.split('_')
if len(union_id_list) < 2:
logger.error("Asset user union id length less than 2")
return
system_user_id = union_id_list[0]
asset_id = union_id_list[1]
asset = get_object_or_none(Asset, pk=asset_id)
system_user = None
if not asset:
return
hosts = check_asset_can_run_ansible([asset])
if asset.is_unixlike:
pass
for account in accounts:
task_name = _("Test account connectivity: {}").format(account)
test_account_connectivity_util(account, task_name)
print(".\n")

View File

@@ -1,69 +0,0 @@
# ~*~ coding: utf-8 ~*~
from celery import shared_task
from django.utils.translation import ugettext as _
from django.core.cache import cache
from orgs.utils import tmp_to_root_org, org_aware_func
from common.utils import get_logger
from ops.celery.decorator import register_as_period_task
from ..models import AdminUser
from .utils import clean_ansible_task_hosts
from .asset_connectivity import test_asset_connectivity_util
from . import const
logger = get_logger(__file__)
__all__ = [
'test_admin_user_connectivity_util', 'test_admin_user_connectivity_manual',
'test_admin_user_connectivity_period'
]
@org_aware_func("admin_user")
def test_admin_user_connectivity_util(admin_user, task_name):
"""
Test asset admin user can connect or not. Using ansible api do that
:param admin_user:
:param task_name:
:return:
"""
assets = admin_user.get_related_assets()
hosts = clean_ansible_task_hosts(assets)
if not hosts:
return {}
summary = test_asset_connectivity_util(hosts, task_name)
return summary
@shared_task(queue="ansible")
@register_as_period_task(interval=3600)
def test_admin_user_connectivity_period():
"""
A period task that update the ansible task period
"""
if not const.PERIOD_TASK_ENABLED:
logger.debug('Period task off, skip')
return
key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD'
prev_execute_time = cache.get(key)
if prev_execute_time:
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
return
cache.set(key, 1, 60*40)
with tmp_to_root_org():
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
task_name = _("Test admin user connectivity period: {}").format(
admin_user.name
)
test_admin_user_connectivity_util(admin_user, task_name)
cache.set(key, 1, 60*40)
@shared_task(queue="ansible")
def test_admin_user_connectivity_manual(admin_user):
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
test_admin_user_connectivity_util(admin_user, task_name)
return True

View File

@@ -6,7 +6,7 @@ from django.utils.translation import ugettext as _
from common.utils import get_logger
from orgs.utils import org_aware_func
from ..models.utils import Connectivity
from ..models import Asset, Connectivity, AuthBook
from . import const
from .utils import clean_ansible_task_hosts, group_asset_by_platform
@@ -18,6 +18,28 @@ __all__ = [
]
def set_assets_accounts_connectivity(assets, results_summary):
asset_ids_ok = set()
asset_ids_failed = set()
asset_hostnames_ok = results_summary.get('contacted', {}).keys()
for asset in assets:
if asset.hostname in asset_hostnames_ok:
asset_ids_ok.add(asset.id)
else:
asset_ids_failed.add(asset.id)
Asset.bulk_set_connectivity(asset_ids_ok, Connectivity.ok)
Asset.bulk_set_connectivity(asset_ids_failed, Connectivity.failed)
accounts_ok = AuthBook.objects.filter(asset_id__in=asset_ids_ok, systemuser__type='admin')
accounts_failed = AuthBook.objects.filter(asset_id__in=asset_ids_failed, systemuser__type='admin')
AuthBook.bulk_set_connectivity(accounts_ok, Connectivity.ok)
AuthBook.bulk_set_connectivity(accounts_failed, Connectivity.failed)
@shared_task(queue="ansible")
@org_aware_func("assets")
def test_asset_connectivity_util(assets, task_name=None):
@@ -60,14 +82,7 @@ def test_asset_connectivity_util(assets, task_name=None):
results_summary['contacted'].update(contacted)
results_summary['dark'].update(dark)
continue
for asset in assets:
if asset.hostname in results_summary.get('dark', {}).keys():
asset.connectivity = Connectivity.unreachable()
elif asset.hostname in results_summary.get('contacted', {}).keys():
asset.connectivity = Connectivity.reachable()
else:
asset.connectivity = Connectivity.unknown()
set_assets_accounts_connectivity(assets, results_summary)
return results_summary

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