Compare commits

...

723 Commits
v1.5 ... v2.6.2

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

* fix: 修改导入task问题

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

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

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

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

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

* feat: 修改组件status文案

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

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

* feat: 修改ComponentTypeChoices

* feat: 取消考虑CoreComponent类型

* feat: 修改Terminal status判断逻辑

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

* feat: 添加PrometheusMetricsAPI

* feat: 修改PrometheusMetricsAPI

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

* perf: 优化验证码的高度

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

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

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

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

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

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

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

* perf: 合并层数

* feat: 修改Dockerfile

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

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

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

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

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

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

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

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

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

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

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

* stash

* feat(system): 添加系统app

* stash

* fix: 修复一些bug

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

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

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

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

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

* perf: 添加excel依赖包

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

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

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

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

* feat: 修改drf/renderer的bug

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

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

* fix: 锁定pip版本

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

* fix(build): 引用pip缓存

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

* [fix]: 兼容django 3

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

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

Co-authored-by: ibuler <ibuler@qq.com>
2020-11-19 15:50:31 +08:00
xinwen
7fa94008c9 fix(old-api): 调整旧的组织与用户关联接口 2020-11-19 15:21:16 +08:00
Jiangjie.Bai
9685a25dc6 Merge pull request #5034 from jumpserver/dev
fix(perms): nodes-with-assets 接口添加刷新重构树
2020-11-18 11:53:12 +08:00
xinwen
1af4fcd381 fix(perms): nodes-with-assets 接口添加刷新重构树 2020-11-18 11:47:50 +08:00
Jiangjie.Bai
177055acdc Merge pull request #5032 from jumpserver/dev
fix(assets): 修复获取org_root的问题
2020-11-18 11:29:53 +08:00
ibuler
6ec0b3ad54 fix(assets): 修复获取org_root的问题 2020-11-18 11:23:39 +08:00
Orange
49dd611292 Merge pull request #5029 from jumpserver/dev
Dev
2020-11-17 19:46:36 +08:00
xinwen
f557c1dace fix(assets): model 添加 asset 默认排序 2020-11-17 19:40:49 +08:00
xinwen
6e87b94789 fix(command): 系统设置-安全设置-告警接收邮件字段如果为空,则更新不了 2020-11-17 19:19:08 +08:00
xinwen
b0dba35e5a fix(public-api): 缺少SECURITY_PASSWORD_EXPIRATION_TIME字段 2020-11-17 19:11:39 +08:00
ibuler
d0b19f20c3 perf(tickets): 优化授权申请工单 2020-11-17 19:10:23 +08:00
xinwen
3e78d627f8 fix(perms): 作业中心-批量命令-选择系统用户之后,左侧资产列表未筛选,还是全部资产 2020-11-17 18:45:45 +08:00
Jiangjie.Bai
0763404235 Merge pull request #5021 from jumpserver/dev
Dev
2020-11-17 16:48:07 +08:00
ibuler
31cd441a34 fix(assets): 修复资产导入时填写节点引起的空节点名称的问题 2020-11-17 16:41:17 +08:00
ibuler
40c0aac5a9 fix(ops): 修复run as 可能引起的返回多个asset user 2020-11-17 16:39:13 +08:00
ibuler
83099dcd16 fix(ops): 修复task run as的问题 2020-11-17 16:39:13 +08:00
ibuler
0db72bd00f fix(i18n): 修复js18n的问题 2020-11-17 16:29:09 +08:00
Bai
732b8cc0b8 fix(ops): 修复AdHocExecution字段task_display长度问题 2020-11-17 16:27:41 +08:00
Bai
a9f90b4e31 perf(terminal): 修改数据库字段长度(command_stroage/replay_storage name 128) 2020-11-17 15:59:42 +08:00
Bai
cf1fbabca1 fix(command): 修复批量命令执行可能获取到host为None的问题 2020-11-17 15:25:11 +08:00
ibuler
cbcefe8bd3 fix(ops): 修复因为更改controlmaster引起的连接不服用维内托 2020-11-16 15:22:25 +08:00
ibuler
133a2e4714 fix(assets): 修复动态系统用户推送的bug 2020-11-16 15:07:01 +08:00
fit2bot
b4b9149d5d chore(docs): 修改readme (#5005)
* chore(docs): 修改readme

* chore(docs): 修改文档拼写

Co-authored-by: ibuler <ibuler@qq.com>
2020-11-16 14:52:07 +08:00
fit2bot
9af4d5f76f fix(crypto): 有时解密失败 (#5003)
* fix(nodes): 节点默认按 value 排序

* fix(crypto): 有时解密失败

Co-authored-by: xinwen <coderWen@126.com>
2020-11-16 14:47:27 +08:00
Orange
96d26cc96f Merge pull request #4991 from jumpserver/dev
fix(system_user): 修复更新系统用户时ad_domain字段为空提交失败的问题
2020-11-13 14:57:23 +08:00
Bai
18d8f59bb5 fix(system_user): 修复更新系统用户时ad_domain字段为空提交失败的问题 2020-11-13 14:56:15 +08:00
老广
841f707b6d Merge pull request #4982 from jumpserver/dev
Dev
2020-11-12 14:12:35 +08:00
xinwen
448c5db3bb fix(orgs): 添加旧的 member 相关 api 2020-11-12 11:20:42 +08:00
Bai
75b886675e perf(i18n): 更新翻译 2020-11-12 11:14:23 +08:00
Bai
24e22115de perf(i18n): 更新翻译 2020-11-12 11:14:23 +08:00
Bai
b18ead0ffa perf(i18n): 更新翻译 2020-11-12 11:01:48 +08:00
xinwen
6e8922da1c fix(trans): 完善翻译 2020-11-12 10:12:25 +08:00
Bai
dcb38ef534 perf(session): 修改变量名terminate 2020-11-11 19:10:53 +08:00
Bai
8a693d9fa7 feat(session): session列表返回can_termination字段值;设置所有db协议类型会话不可被终断和监控 2020-11-11 17:49:54 +08:00
xinwen
487932590b fix(terminal): 扩展 Terminal name 长度 2020-11-11 15:01:30 +08:00
peijianbo
79b5aa68c8 feat(terminal):危险命令告警功能 2020-11-10 18:31:16 +08:00
Bai
50a4735b07 pref(cloud): 添加 Azure 依赖包 2020-11-10 11:13:00 +08:00
Bai
1183109354 perf(github): 更新github issue模版 2020-11-10 10:30:15 +08:00
xinwen
202e619c4b feat(assets): 添加系统用户资产列表 api 2020-11-10 10:23:51 +08:00
xinwen
179cb7531c feat(assets): 管理用户的资产列表增加 options 方法 2020-11-10 10:23:51 +08:00
ibuler
987f840431 fix(i18n): 修复重置密码那的i18n问题 2020-11-10 10:17:55 +08:00
fit2bot
f04544e8df feat(MFA): 修改文案Google Authenticator为手机验证器 (#4964)
* feat(MFA): 修改文案Google Authenticator为手机验证器

* feat(MFA): 修改文案手机验证器 为 MFA验证器

* feat(MFA): 修改文案手机验证器 为 MFA验证器2

Co-authored-by: Bai <bugatti_it@163.com>
2020-11-09 19:08:24 +08:00
fit2bot
cd6dc6a722 fix(perms): 由于组织不对,导致生成或显示授权树错误 (#4957)
* perf(perms): 优化授权树生成速度

* fix(perms): 由于组织不对,导致生成或显示授权树错误

Co-authored-by: xinwen <coderWen@126.com>
2020-11-09 17:30:50 +08:00
Bai
150552d734 perf(requirements): 升级依赖版本jms-storage==0.0.35 2020-11-09 17:29:10 +08:00
Bai
388314ca5a fix(applications): 修复应用列表会返回所有组织下数据的问题 2020-11-09 16:46:38 +08:00
Bai
26d00329e7 fix(assets): 修复计算node full value时,获取node value 有可能为 __proxy__ 的问题 2020-11-06 10:20:48 +08:00
xinwen
e93be8f828 feat(crypto): 支持国密算法 2020-11-05 19:04:50 +08:00
Bai
2690092faf perf(ldap): LDAP用户搜索,本地忽略大小写,远端支持模糊 2020-11-05 14:57:08 +08:00
Bai
eabaae81ac perf(application): 优化RemoteApp应用Chrome序列类的字符串主键关联字段 2020-11-05 14:13:45 +08:00
Bai
6df331cbed perf(perms): 用户/用户组授权的所有应用API返回attrs属性 2020-11-05 11:25:32 +08:00
xinwen
0390e37fd5 feat(orgs): relation 增加搜索功能 2020-11-05 10:40:00 +08:00
xinwen
44d9aff573 fix(orgs): 改正单词拼写 2020-11-05 09:58:58 +08:00
xinwen
2b4f8bd11c feat(ops): Task 支持批量操作 2020-11-05 09:54:51 +08:00
xinwen
231332585d fix(perms): 重建授权树冲突时,响应里加 code 2020-11-03 17:53:49 +08:00
ibuler
531de188d6 perf(systemuser): 优化系统用户家目录权限更改 2020-11-03 17:51:02 +08:00
ibuler
0c1f717fb2 feat(assets): 推送系统用户增加comment 2020-11-03 17:51:02 +08:00
xinwen
9d9177ed05 refactor(terminal): 去掉 Session 中 Terminal 的外键关系 2020-11-03 15:00:44 +08:00
xinwen
ab77d5db4b fix(orgs): org-member-relation url 拼写错误 2020-11-03 14:49:50 +08:00
ibuler
eadecb83ed fix: 修改系统用户节点关系的抖索 2020-11-03 14:33:15 +08:00
ibuler
5d6088abd3 fix(assets): 修复nodes display pop 引起的bug 2020-11-03 11:22:36 +08:00
xinwen
38f7c123e5 fix(audits): sso 登录日志没有 type 2020-11-03 11:18:56 +08:00
xinwen
d7daf7071a fix(ticket): 工单申请资产的时候审批人不能搜索 2020-11-03 10:53:31 +08:00
Bai
795245d7f4 perf(perms): 授权给用户应用列表API添加dispaly字段 2020-11-02 10:26:39 +08:00
Bai
7ea2a0d6a5 perf(perms): 应用授权规则序列类添加applications类型校验 2020-10-30 17:25:34 +08:00
xinwen
c90b9d70dc perf(perms): 优化根据资产获取授权的系统用户 2020-10-30 15:59:19 +08:00
Bai
f6c24f809c perf(application): 修改DoaminAPI返回application数量;修改Application数据库datbase字段required=False 2020-10-30 15:58:46 +08:00
Bai
e369a8d51f perf(readme): 修改Readme 2020-10-30 15:44:42 +08:00
Bai
c74c9f51f0 perf(readme): 修改Readme 2020-10-30 15:44:05 +08:00
ibuler
57bf9ca8b1 perf(assets): 优化更新子节点名称的算法 2020-10-30 14:17:09 +08:00
Bai
ddc2d1106b perf(perms): 修改授权授权应用API,添加category/type过滤字段 2020-10-30 12:54:33 +08:00
ibuler
15992c636a perf: 优化node full value 2020-10-30 10:50:45 +08:00
Bai
36cd18ab9a perf(perms): 修改变量名 2020-10-30 10:47:32 +08:00
Bai
676ee93837 perf(perms): 优化方法名称;授权查询语句; 2020-10-30 10:47:32 +08:00
fit2bot
c02f8e499b feat: 添加资产导入时可以直接写节点 (#4868)
* feat: 优化资产导入, 可以添加节点全称,并自动创建

* feat: 添加资产导入时可以直接写节点

* fix: 修改错误

* fix: 添加node value校验,不能包含/

* chore: merge migrations

* perf: 去掉full value replace

Co-authored-by: ibuler <ibuler@qq.com>
2020-10-30 10:16:49 +08:00
ibuler
4ebb4d1b6d chore: resolve conflict 2020-10-29 19:19:20 +08:00
Bai
5e7650d719 perf(application): RemoteApp应用序列类返回asset_info字段 2020-10-29 05:48:01 -05:00
Bai
bf302f47e5 perf(application): 修改RemoteA序列类asset required=False 2020-10-29 05:48:01 -05:00
Bai
1ddc228449 perf(application): RemoteApp序列类字段asset,设置为CharPrimaryKeyRelatedField,修改其他字段的required=Flase 2020-10-29 05:48:01 -05:00
Bai
c9065fd96e perf(application): 优化一些小细节 2020-10-29 05:48:01 -05:00
Bai
4a09dc6e3e feat(assets): 修改GatewayModel ip字段类型为CharField 2020-10-29 05:48:01 -05:00
Bai
55bfb942e2 perf(applications): 修改应用序列类字段label 2020-10-29 05:48:01 -05:00
Bai
9aed51ffe9 perf(applications): 修改应用序列类字段长度限制 2020-10-29 05:48:01 -05:00
Bai
a98816462f perf(applications): 添加DB序列类字段翻译 2020-10-29 05:48:01 -05:00
Bai
abe32e6c79 perf(perms): 添加应用/应用授权API的type_display/category_display字段 2020-10-29 05:48:01 -05:00
Bai
77c8ca5863 perf(perms): 应用授权表添加字段,type和category 2020-10-29 05:48:01 -05:00
Bai
8fa15b3378 perf(assets/terminal): 资产系统用户和Session会话添加协议选项: mysql/oracle/postgresql 2020-10-29 05:48:01 -05:00
Bai
a3507975fb perf(application): 修改获取远程应用连接参数的API(2) 2020-10-29 05:48:01 -05:00
Bai
76ca6d587d perf(application): 修改获取远程应用连接参数的API 2020-10-29 05:48:01 -05:00
Bai
038582a8c1 feat(applications): 修改应用/应用授权的迁移文件,解决多种应用/应用授权name字段重复的问题 2020-10-29 05:48:01 -05:00
Bai
ca2fc3cb5e feat(applications): 修改ApplicationAPI方法获取序列类的逻辑 2020-10-29 05:48:01 -05:00
Bai
cc30b766f8 feat(applications): 修改ApplicationAPI方法method判断->action 2020-10-29 05:48:01 -05:00
Michael Bai
b7bd88b8a0 feat(applications): 修改ApplicationAPI方法options 2020-10-29 05:48:01 -05:00
Bai
5518e1e00f perf(application): 优化Application获取序列类attrs字段适配 2020-10-29 05:48:01 -05:00
Bai
0632e88f5d feat(application): 修改Application Model的domain字段选项2 2020-10-29 05:48:01 -05:00
Bai
9dc2255894 feat(application): 修改Application Model的domain字段选项 2020-10-29 05:48:01 -05:00
Bai
1baf35004d refactor(perms): 添加应用授权规则迁移文件;迁移旧的应用授权(db/remoteapp/k8sapp)到新的应用授权 2020-10-29 05:48:01 -05:00
Bai
5acff310f7 perf(application): 修改迁移文件,迁移应用包含id字段 2020-10-29 05:48:01 -05:00
Bai
fdded8b90f refactor(perms): 修改授权规则的目录结构(asset、application) 2020-10-29 05:48:01 -05:00
Bai
1d550cbe64 feat(perms): 添加ApplicationPermission API(包含用户/用户组/授权/校验等API) 2020-10-29 05:48:01 -05:00
Bai
4847b7a680 feat(perms): 添加ApplicationPermission Model 和 API(包含ViewSet和RelationViewSet) 2020-10-29 05:48:01 -05:00
Bai
1c551b4fe8 feat(application): 迁移old_application到new_application 2020-10-29 05:48:01 -05:00
Bai
6ffba739f2 perf(requirements): 添加依赖django-mysql==3.9.0 2020-10-29 05:48:01 -05:00
ibuler
0282346945 perf: 修改创建 2020-10-29 05:48:01 -05:00
ibuler
f6d9af8beb perf(application): 优化type优先级 2020-10-29 05:48:01 -05:00
ibuler
ba4e6e9a9f refacter: 重构application 2020-10-29 05:48:01 -05:00
ibuler
874a3eeebf perf(sessions): 优化命令 2020-10-29 18:27:36 +08:00
ibuler
dd793a4eca perf: 优化日志保存策略 2020-10-29 18:27:36 +08:00
xinwen
f7e6c14bc5 fix(assets): 向资产推送系统用户bug 2020-10-28 21:40:40 -05:00
xinwen
f6031d6f5d fix(logger): 把 drf 异常放到单独的日志文件中 2020-10-28 21:26:35 -05:00
xinwen
5e779e6542 fix(systemuser): 系统用户添加 ad_domain 字段 2020-10-28 21:23:29 -05:00
xinwen
7031b7f28b fix(auth): 修复用户登录失败次数出现0次 2020-10-28 06:06:57 -05:00
xinwen
e2f540a1f4 fix(assets): 网关的密码不能包含特殊字符 2020-10-28 06:05:53 -05:00
ibuler
108a1da212 chore: 修改github 语言识别 2020-10-26 20:51:09 -05:00
xinwen
b4a8cb768b fix(assets): 系统用户与用户组发生变化时报错 2020-10-26 05:31:30 -05:00
xinwen
6b2f606430 fix(tickets): 工单申请资产授权通过人显示不对 2020-10-26 02:03:12 -05:00
xinwen
70a8db895d fix(migrations): 生成一下遗漏的 migrations 2020-10-23 17:00:37 +08:00
xinwen
0043dc6110 fix(assets): 资产列表添加默认 date_created 排序 2020-10-23 17:00:37 +08:00
xinwen
87d2798612 fix(assets): 资产列表不能用到assets_amount字段 2020-10-23 16:54:19 +08:00
ibuler
e2d8eee629 fix(i18n): 修改收藏夹的翻译 2020-10-23 16:52:06 +08:00
xinwen
8404db8cef fix(assets): 修改 AssetViewSet.filter_fields 2020-10-23 02:25:04 -05:00
Bai
fd7f379b10 perf(requirements): 升级依赖django-timezone-field==4.0 2020-10-21 13:01:28 +08:00
ibuler
111c63ee6a perf: 修改beat版本 2020-10-20 15:02:11 +08:00
ibuler
4eb5d51840 fix: domain端口可能随便填写的问题 2020-10-20 15:01:06 +08:00
ibuler
7f53a80855 fix: 修改textfield 不再限制长度 2020-10-20 15:00:22 +08:00
ibuler
90afabdcb2 fix(perms): 修复用户的资产不区分组织的问题 2020-10-20 14:56:23 +08:00
ibuler
de405be753 fix(perms): 修复asset permission导入的bug 2020-10-20 14:46:31 +08:00
Bai
f84b845385 perf(config): 升级依赖redis==3.5.3; 添加CACHES配置: health_check_interval=30; 解决因网络不稳定导致的redis连接失败异常 2020-10-19 04:45:34 -05:00
xinwen
b1ac3fa94f fix(orgs): 更新用户时org_roles参数为None时不更新组织角色 2020-10-15 04:59:25 -05:00
Jiangjie.Bai
32fab08ed3 Merge pull request #4802 from jumpserver/dev
chore: merge dev to master
2020-10-15 14:08:36 +08:00
fit2bot
8943850ca9 perf(Dockerfile): 去掉多余的代码 (#4801)
* perf(Dockerfile): 优化构建docker,经常变动的包不使用镜像

Co-authored-by: ibuler <ibuler@qq.com>
2020-10-15 14:05:11 +08:00
ibuler
8fff57813a perf(Dockerfile): 优化构建docker,经常变动的包不使用镜像 2020-10-15 00:59:58 -05:00
xinwen
9128210e87 Merge pull request #4798 from jumpserver/dev
Dev to master
2020-10-15 12:22:00 +08:00
xinwen
e3dd03f4c7 Dev (#4791)
* fix(xpack): 修复last login太长的问题 (#4786)

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

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

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

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

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

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

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

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

Co-authored-by: fit2bot <68588906+fit2bot@users.noreply.github.com>
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Bai <bugatti_it@163.com>
2020-10-15 12:00:38 +08:00
fit2bot
aabb2aff1f perf(requirements): 修改jms-storage==0.0.34 (#4797)
Co-authored-by: Bai <bugatti_it@163.com>
2020-10-15 11:51:45 +08:00
xinwen
f8bbca38e3 fix(orgs): 修复组织添加用户bug 2020-10-14 22:50:55 -05:00
ibuler
12b180ddea fix(terminal): 修复有的录像存储有问题的导致下载录像的bug 2020-10-15 11:48:52 +08:00
ibuler
4917769964 fix(terminal): 修复获取螺旋的异步api 2020-10-15 11:48:52 +08:00
fit2bot
5868d56e18 perf: 更新密码中也发送邮件 (#4789)
Co-authored-by: ibuler <ibuler@qq.com>
2020-10-14 19:59:05 +08:00
fit2bot
bab76f3cda fix(xpack): 修复last login太长的问题 (#4786)
Co-authored-by: ibuler <ibuler@qq.com>
2020-10-14 19:58:44 +08:00
xinwen
475c0e4187 Merge pull request #4784 from jumpserver/dev
Dev
2020-10-14 15:59:21 +08:00
老广
3d070231f4 Merge pull request #4783 from jumpserver/pr@dev@chore_merge
chore: merge with master
2020-10-14 02:55:28 -05:00
ibuler
c5b0cafabd chore: merge with master 2020-10-14 15:53:06 +08:00
ibuler
a69bba8702 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-10-14 15:48:32 +08:00
xinwen
cfd0098019 fix(perms): 修复一次性获取所有资产与节点sql泛滥问题 2020-10-14 02:43:22 -05:00
ibuler
52f1dcf662 fix(users): 修复邀请用户的bug 2020-10-14 15:31:24 +08:00
xinwen
373c6c77e0 fix(perms): 未激活资产不能使用 2020-10-13 19:34:19 +08:00
xinwen
f3d052554d fix(perms): 修复失效资产授权action 还在的问题 2020-10-13 19:34:19 +08:00
xinwen
a57ce482dd fix(assets): 资产树批量删除资产数量不对 2020-10-13 19:34:19 +08:00
xinwen
a449d97f67 fix(orgs): 组织添加成员bug 2020-10-13 19:34:19 +08:00
ibuler
84e4238848 fix(ops): 修复任务schedule属性的bug 2020-10-13 19:34:19 +08:00
clannon
82dd1c35ea Update config_example.yml
SECRET_KEY 和 BOOTSTRAP_TOKEN 后面默认留个空格,遇到好几个新手对yaml格式不注意了,虽然是个小问题……
2020-10-13 19:34:19 +08:00
ibuler
5ac974a44c fix(deps): 修复依赖版本 2020-10-13 19:34:19 +08:00
fit2bot
c4caeb92ee perf: 优化生成假数据 (#4759)
* perf: 优化生成假数据
2020-10-13 19:34:19 +08:00
ibuler
f4799d90c0 fix(assets): 修复点击节点更新硬件信息的bug 2020-10-13 19:34:19 +08:00
ibuler
f97685c788 perf(orgs): 优化组织用户添加 2020-10-13 19:34:19 +08:00
ibuler
0439376326 feat(users): 添加用户suggetion api 2020-10-13 19:34:19 +08:00
xinwen
dd2413edd8 fix(perms): 授权树与资产列表的一些 bug 2020-10-13 19:34:19 +08:00
xinwen
459c5c07c9 fix(perms): 未激活资产不能使用 2020-10-13 06:30:18 -05:00
xinwen
ef86a49c1e fix(perms): 修复失效资产授权action 还在的问题 2020-10-13 06:20:43 -05:00
xinwen
0ad389515b fix(assets): 资产树批量删除资产数量不对 2020-10-13 05:47:54 -05:00
xinwen
2432b9a553 fix(orgs): 组织添加成员bug 2020-10-13 03:38:25 -05:00
ibuler
12216a718a Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-10-13 10:52:18 +08:00
ibuler
2190db1bb5 fix(ops): 修复任务schedule属性的bug 2020-10-12 21:51:31 -05:00
ibuler
5d36537404 fix(ops): 修复任务schedule属性的bug 2020-10-12 19:08:13 +08:00
clannon
5a87634c26 Update config_example.yml
SECRET_KEY 和 BOOTSTRAP_TOKEN 后面默认留个空格,遇到好几个新手对yaml格式不注意了,虽然是个小问题……
2020-10-12 05:23:49 -05:00
老广
e5eb84999a Merge pull request #4768 from jumpserver/pr@dev@chore_merge_with_master
Merge branch 'master' into dev
2020-10-12 05:22:25 -05:00
ibuler
426a86b52d Merge branch 'master' into dev 2020-10-12 18:19:58 +08:00
ibuler
f84acfe282 fix(deps): 修复依赖版本 2020-10-12 05:13:52 -05:00
fit2bot
c73b49fe30 perf: 优化生成假数据 (#4759)
* perf: 优化生成假数据
2020-10-12 12:44:30 +08:00
ibuler
98238f71ae fix(assets): 修复点击节点更新硬件信息的bug 2020-10-11 22:31:08 -05:00
ibuler
66e45f1c80 perf(orgs): 优化组织用户添加 2020-10-12 11:25:43 +08:00
ibuler
93a400f6e6 feat(users): 添加用户suggetion api 2020-10-12 11:18:11 +08:00
xinwen
535d7d8373 fix(perms): 授权树与资产列表的一些 bug 2020-10-11 21:43:44 -05:00
Bai
db268280b4 perf(requirements): 修改依赖包Pillow==7.1.0 2020-10-08 22:06:43 -05:00
Bai
873789bdab perf(requirements): 修改依赖包Pillow==7.1.0 2020-10-08 22:04:23 -05:00
Jiangjie.Bai
6584890ab1 Merge pull request #4754 from jumpserver/dev
Dev
2020-10-09 10:37:47 +08:00
fit2bot
96d5c519ec perf(i18n): 添加翻译信息 (#4748)
* perf(i18n): 添加翻译信息

* perf(users): 重置密码成功邮件添加DEBUG信息

* perf(i18n): 修改翻译信息

* perf(i18n): 修改翻译信息

Co-authored-by: Bai <bugatti_it@163.com>
2020-09-30 16:11:03 +08:00
Bai
6e91217303 perf(authentication): 修改用户登录页面,使用其他方式认证时点击忘记密码提示联系管理员 2020-09-30 02:05:14 -05:00
xinwen
5dd1dfc59e fix(orgs): 用户修改组织角色报错 2020-09-30 11:40:52 +08:00
Bai
a53e930950 perf(tickets): 申请资产工单支持授权多个系统用户 2020-09-29 19:11:20 +08:00
xinwen
8f52f79d91 fix(migrations): 增加迁移脚本 2020-09-29 17:48:19 +08:00
xinwen
3af0e68c84 fix(perms): 用户授权树bug 2020-09-29 17:25:00 +08:00
Bai
3ccf32ed48 feat(authentication): 用户重置密码成功后,发送用户重置密码成功邮件 2020-09-29 16:28:14 +08:00
xinwen
d52ed2ffb9 fix(xpack): GatheredUser 点击资产树报错 2020-09-29 16:26:02 +08:00
ibuler
38588151d1 继续修改issue template 2020-09-29 14:28:27 +08:00
ibuler
2a95aca28f chore: 修改issue模板 2020-09-29 14:18:14 +08:00
Bai
1915224063 fix(terminal): 修复正在使用的命令/录像存储可以被删除的问题 2020-09-29 13:37:48 +08:00
fit2bot
da4f9efb42 fix(perms): 修改检查资产授权过期策略 (#4722)
* fix(perms): 修改检查资产授权过期策略

* perf: 优化一行代码

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: ibuler <ibuler@qq.com>
2020-09-29 13:34:55 +08:00
fit2bot
579c2c1d7a feat(celery): 保证同时只有一个beat在运行 (#4723)
* feat(celery): 保证同时只有一个beat在运行

* fix: 修复代码拼写错误

* fix: 修复拼写

* fix: remove import

Co-authored-by: ibuler <ibuler@qq.com>
2020-09-29 13:33:53 +08:00
xinwen
2a86c3a376 fix(perms): 完善检查资产授权过期的celery task 2020-09-29 11:44:20 +08:00
Bai
5558e854de perf(permms): 应用授权树返回授权应用的总数量 2020-09-29 11:43:31 +08:00
xinwen
31b6c3b679 feat(celery): 设置 node_tree celery work 为 2 2020-09-28 18:49:40 +08:00
fit2bot
2209a2c8d2 feat(orgs): 修改OrgMemberRelationAPI,支持通过查询参数控制是否忽略已存在的数据 (#4720)
* feat(orgs): 修改OrgMemberRelationAPI,支持通过查询参数控制是否忽略已存在的数据

* feat(orgs): 修改构建数据库查询参数的问题

Co-authored-by: Bai <bugatti_it@163.com>
2020-09-28 18:49:18 +08:00
xinwen
f596b65ed7 feat(auth): sso 生成的地址重复访问的时候,重定向到用户指定的 next 地址 2020-09-28 16:20:32 +08:00
fit2bot
2c9c64a13f fix(jms): 启动脚本 task 添加 celery_node_tree, check_asset_perm_expired 两个 worker (#4716)
* fix(jms): 启动脚本 task 添加 celery_node_tree, check_asset_perm_expired 两个 worker

* fix: 修改脚本

Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: ibuler <ibuler@qq.com>
2020-09-28 16:19:49 +08:00
xinwen
1d49b3deca fix(perms): 用户搜索全部授权资产报错 2020-09-28 13:17:45 +08:00
xinwen
6701a1b604 fix(perms): 用户添加到用户组报错 2020-09-27 20:53:34 +08:00
ibuler
b8ff3b38bf fix: 修复middleware引起的bug 2020-09-27 17:58:41 +08:00
fit2bot
d3be16ffe8 fix (#4680)
* perf(perms): 资产授权列表关联数据改为 `prefetch_related`

* perf(perms): 优化一波

* dispatch_mapping_node_tasks.delay

* perf: 在做一些优化

* perf: 再优化一波

* perf(perms): 授权更改节点慢的问题

* fix: 修改一处bug

* perf(perms): ungrouped 资产数量计算方式

* fix: 修复dispatch data中的bug

* fix(assets): add_nodes_assets_to_system_users celery task

* fix: 修复ungrouped的bug

* feat(nodes): 添加 favorite 节点

* feat(node): 添加 favorite api

* fix: 修复clean keys的bug


Co-authored-by: xinwen <coderWen@126.com>
Co-authored-by: ibuler <ibuler@qq.com>
2020-09-27 16:02:44 +08:00
老广
e3648d11b1 feat: 录像存储server类型,可以设置如何存储了 (#4699)
feat: Server 类型的录像存储可以上传到 oss等上面
2020-09-27 14:34:47 +08:00
fit2bot
d4037998c8 perf: 优化middleware的使用 (#4707)
perf: 优化middleware的使用
2020-09-27 14:34:05 +08:00
fit2bot
82de636b5c perf(common): 检查referer (#4697)
Co-authored-by: ibuler <ibuler@qq.com>
2020-09-27 11:48:21 +08:00
Bai
91f1280f97 perf(settings): public setting API返回LOGIN_TITLE字段 2020-09-27 11:37:38 +08:00
fit2bot
7c82f5aa2b chore: 修改issue template (#4703)
* chore: 修改issue template

* perf: 又修改了些

Co-authored-by: ibuler <ibuler@qq.com>
2020-09-25 16:09:56 +08:00
老广
6a801eaf33 Update issue templates 2020-09-25 15:47:43 +08:00
xinwen
28da819735 perf(assets): 优化节点树
修改树策略,做读优化,写的速度降低
2020-09-21 10:23:09 +08:00
Jiangjie.Bai
cdf3cf3e8f Merge pull request #4667 from jumpserver/dev
fix(command): 修复命令导出选中项问题
2020-09-16 19:55:18 +08:00
Bai
118564577e fix(command): 修复命令导出选中项问题 2020-09-16 19:53:58 +08:00
Jiangjie.Bai
47f2df0a5b Merge pull request #4665 from jumpserver/dev
fix(command): 修复命令导出选中项问题
2020-09-16 19:38:16 +08:00
Bai
e4aafc236d fix(command): 修复命令导出选中项问题 2020-09-16 19:36:21 +08:00
Jiangjie.Bai
b1c530bba8 Merge pull request #4661 from jumpserver/dev
Dev
2020-09-16 19:03:38 +08:00
Bai
95aa9781c3 fix(command): 修复命令导出选中项会导出全部的问题 2020-09-16 19:02:49 +08:00
xinwen
9f6540afe3 fix(tickets): 调整登录确认工单 title 2020-09-16 18:03:15 +08:00
ibuler
832bb832ce fix(authentication): 修复cas退出的bug 2020-09-16 17:53:01 +08:00
ibuler
501329a8db fix: 再次修复 2020-09-16 17:51:45 +08:00
ibuler
8913aacd1e fix(authentication): 修复同时开启radius, openid引起的问题 2020-09-16 17:51:45 +08:00
xinwen
e461fbdf50 fix(tickets): 修复已处理工单的 待处理人 字段 2020-09-16 17:47:26 +08:00
peijianbo
3941539408 fix(authentication):修复开启二次认证时,地址跳转出错问题 2020-09-16 16:46:28 +08:00
xinwen
605db2d905 fix(auth): 调整登录复核工单 title 2020-09-16 15:31:20 +08:00
Jiangjie.Bai
1ef3f24465 Merge pull request #4648 from jumpserver/dev
chore: merge dev to master
2020-09-15 17:23:48 +08:00
peijianbo
4090a0b123 feat(uathentication):登录表单回车可直接提交表单 2020-09-15 17:10:52 +08:00
ibuler
a55e28fc87 perf: 优化ldap超时时间 2020-09-15 15:26:18 +08:00
ibuler
82cf53181f perf(settings): 修改默认超时时间为10s 2020-09-15 15:26:18 +08:00
ibuler
78232aa900 perf(terminal): 优化命令提交 2020-09-14 19:25:50 +08:00
ibuler
d2c93aff66 feat: 可以关闭工单菜单 2020-09-14 18:25:47 +08:00
peijianbo
516e2309c0 bug(authentication): 登录表单仅提交时加密(xpack) 2020-09-14 17:28:49 +08:00
peijianbo
4688e46f97 feat(authentication):将cas认证通过的登录日志记录到系统 2020-09-14 12:46:12 +08:00
peijianbo
1299f3da75 feat(authentication):登录表单仅提交时加密 2020-09-14 12:45:44 +08:00
Bai
fe502cbe41 fix(assets): 修复系统用户导入模版没有密码字段的问题 2020-09-14 12:43:39 +08:00
xinwen
09bfac34f1 fix(orgs): 修复 org-memeber-relation POST 报错 2020-09-14 11:00:10 +08:00
Jiangjie.Bai
12a86d7244 Merge pull request #4611 from jumpserver/dev
Dev
2020-09-08 20:08:53 +08:00
Jiangjie.Bai
269eea8802 Merge branch 'master' into dev 2020-09-08 19:32:38 +08:00
老广
72aa265dd7 doc: 修改readme,添加子项目连接 (#4602)
* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Update README.md
2020-09-08 14:31:54 +08:00
老广
e26716e1e1 Update README.md
docs: 添加  developer wanted
2020-09-08 14:30:31 +08:00
Bai
80b9db417c feat(ldap): 获取ldap用户列表,采用线程方式 2020-09-08 11:57:45 +08:00
Bai
d944b5f4ff feat(tickets): 工单添加comment字段 2020-09-07 20:00:09 +08:00
peijianbo
1b84afee0c feat(audits):修改日志默认保存时间(90->9999) 2020-09-07 18:35:19 +08:00
fit2bot
172b6edd28 feat(user):同一个账号仅允许在一台终端设备登录 (#4590)
* feat(user):同一个账号仅允许在一台终端设备登录

* feat(user):同一个账号仅允许在一台终端设备登录

* feat(user):同一个账号仅允许在一台终端设备登录

* feat(user):同一个账号仅允许在一台终端设备登录

* feat(user):同一个账号仅允许在一台终端设备登录

Co-authored-by: peijianbo <peijainbo3006@163.com>
2020-09-07 17:42:59 +08:00
Bai
e6f248bfa0 feat(i18n): 添加云同步实例任务hostname_strategy字段翻译信息 2020-09-07 17:39:35 +08:00
ibuler
1f037b1933 feat(i18n): 添加新翻译 2020-09-02 15:21:48 +08:00
Bai
ae9bbd2683 fix(common) 修复管理员未设置Email主题前缀导致发送邮件失败的问题 2020-09-02 10:23:44 +08:00
xinwen
a0085c4eab feat(README): 添加企业版试用链接 2020-09-01 16:50:43 +08:00
ibuler
ddb71c43c4 fix(users): 修复用户在不同组织引起的问题 2020-09-01 16:47:13 +08:00
herealways
8227f44058 feat: 添加AES GCM模式为默认的加密方式 2020-09-01 14:48:40 +08:00
ibuler
e81762d692 ci(Dockerfile): 修改依赖的setuptools版本,导致的ldap无法安装问题 2020-09-01 13:39:08 +08:00
老广
5b8fa1809c Update README.md
docs: 添加  developer wanted
2020-08-24 10:04:03 +08:00
BaiJiangJie
90ba6442dd Merge pull request #4523 from jumpserver/dev
fix(orgs): 完善组织与用户变化时的信号
2020-08-20 16:40:07 +08:00
xinwen
a28334b6d8 fix(orgs): 完善组织与用户变化时的信号 2020-08-20 16:34:10 +08:00
BaiJiangJie
692dd6c8c4 Merge pull request #4519 from jumpserver/dev
Dev
2020-08-20 12:48:19 +08:00
xinwen
15992ad5b3 fix(tickets): 修复工单comment 2020-08-20 10:51:24 +08:00
Bai
072c3155ca style(orgs): 修改组织删除失败翻译信息 2020-08-19 21:56:03 +08:00
fit2bot
9cb5985947 fix(orgs): 创建组织用户不必填 (#4515)
* fix(role): 更改role的顺序

* fix(tickets): 修复工单邮件跳转地址

* fix(tickets): 修复工单复制链接地址不对

* fix(orgs): 创建组织用户不必填

Co-authored-by: xinwen <coderWen@126.com>
2020-08-19 21:44:20 +08:00
ibuler
0fd2f18240 fix(authentication): 修复登录时有时解密失败 2020-08-19 21:42:07 +08:00
xinwen
9ca8ab218c fix(authentication): 登录复核Not found 2020-08-19 07:42:05 -05:00
xinwen
fcd8356e90 fix(users): 组织管理员,移除组织成员报错500 #231 2020-08-19 07:42:05 -05:00
xinwen
64d093e677 fix(users): 用户接口添加org_roles字段 2020-08-19 07:42:05 -05:00
xinwen
11493b9f3d fix(tickets): 修复申请资产工单不能关闭 2020-08-19 07:42:05 -05:00
xinwen
5a9c91d9dd fix(authentication): 组织成员禁用再激活,登录报错 #239 2020-08-19 07:42:05 -05:00
xinwen
25dcb9510c fix(audits): 修复超级审计员登录-日志审计-批量命令-单击主机列链接报错误信息 403 2020-08-19 07:42:05 -05:00
Bai
a38a1868ca style(ticket): 修改工单状态翻译: Open:待处理;Closed:已完成 2 2020-08-19 07:21:31 -05:00
Bai
bec9b97092 style(ticket): 修改工单状态翻译: Open:待处理;Closed:已完成 2020-08-19 07:21:31 -05:00
ibuler
d5b9596e7d perf(config): 修改默认登录日志保存时间 2020-08-18 05:17:40 -05:00
xinwen
af85d551ad fix(users): 修改用户角色显示名称 2020-08-14 17:13:01 +08:00
BaiJiangJie
ab8c57894e Merge pull request #4488 from jumpserver/dev
Dev
2020-08-14 11:45:42 +08:00
xinwen
0e0c9275bd fix(application): 远程应用MySQL Workbench添加port字段 2020-08-13 16:50:54 +08:00
xinwen
21b4a8600c fix(terminal): Session can_join 添加 k8s 2020-08-13 14:14:20 +08:00
xinwen
4cf5573c36 fix(users): 修复用户与用户组关系变化时没触发信号 2020-08-12 18:28:18 +08:00
xinwen
962ea67b84 refactor(authentication): 密码解密抽取成方法 2020-08-12 18:27:41 +08:00
xinwen
31720c9dcc feat(assets): 系统用户添加 home system_groups 字段 2020-08-12 15:05:46 +08:00
xinwen
54fe4835f6 fix(authentication): SSO登录添加 next_url 2020-08-11 19:36:34 +08:00
xinwen
91649a3908 feat(applications): 添加 k8s 应用 2020-08-11 12:56:54 +08:00
xinwen
0a242c3e81 fix(audis): 生成操作日志时间字段索引迁移脚本 2020-08-11 11:27:19 +08:00
huamaolin
25d1b3334f fix(OperateLog): 修复操作日志按时间查询慢
增加表OperateLog datetime字段索引
2020-08-10 19:28:26 -07:00
xinwen
ffde306a04 fix(assets): 修复批量删除资产失败问题 2020-08-11 10:15:14 +08:00
xinwen
f1e29a91f7 fix(users): 用户接口添加汇总角色字段 2020-08-07 18:53:03 +08:00
xinwen
ec2b3b4cda fix(user): 调整User接口字段 2020-08-07 13:57:22 +08:00
xinwen
1a9d9e4145 feat(ticket): 申请资产工单添加actions字段 2020-08-06 15:21:54 +08:00
xinwen
a14f121fad fix(orgs): 组织成员关系接口添加role_display字段 2020-08-05 19:24:28 +08:00
xinwen
a25da8d479 feat(authentication): 超级管理员密码不能是admin 2020-08-05 17:23:58 +08:00
fit2bot
15fe7f810b perf(url): 优化 /api/docs/? 都可以访问文档 (#4446)
Co-authored-by: ibuler <ibuler@qq.com>
2020-08-05 14:09:23 +08:00
xinwen
f6a4253936 feat(ticket): 工单关闭生成 Comment 2020-08-05 11:13:23 +08:00
xinwen
c3c5801d2e refactor(orgs): 重构组织与用户关系接口 2020-08-04 11:33:15 +08:00
Orange
f0d564180c perf: 优化Issues Template 2020-08-04 10:39:13 +08:00
ibuler
8ee7230ead fix(auth): 修复radius decode error的问题 2020-08-03 10:31:44 +08:00
xinwen
90f03dda62 feat(authentication): 类似腾讯企业邮单点登录功能 2020-07-31 19:37:51 +08:00
ibuler
4e7a5d8d4f ci: 修改docker构建的问题 2020-07-29 19:50:41 +08:00
xinwen
2ed0927b18 fix(login): 用户登录堡垒机的时候偶尔会出现“密码解密失败”,导致无法正常登录。 #4408 2020-07-29 17:54:59 +08:00
xinwen
e98235ca27 refactor(serializer): 设置 BulkSerializerMixin 的默认 ListSerializerAdaptedBulkListSerializer 2020-07-29 17:09:48 +08:00
xinwen
1b052a8729 feat(terminal): 终端管理添加批量更新接口 2020-07-29 15:14:50 +08:00
xinwen
34b188bbe7 fix(csv): 修复JMSCSVParser调用serializer导致循环调用问题 2020-07-29 10:45:41 +08:00
ibuler
3e6cd1c1d3 ci(dockerfile): 修改dockerfile构建 2020-07-28 19:29:15 +08:00
xinwen
f8e248f0af feat(ticket): 调整申请资产工单 2020-07-28 19:19:37 +08:00
xinwen
b331730422 fix(users): 替换旧有角色常量 2020-07-28 18:24:53 +08:00
xinwen
de3865fa1d refactor(orgs): 重构组织表结构 2020-07-28 10:16:26 +08:00
xinwen
1bc913ab13 feat(perms): 资产授权添加GUI复制粘贴动作 2020-07-27 15:24:09 +08:00
OrangeM21
2f11a70341 fix(authentication): 调整登录页面样式 2020-07-24 18:39:17 +08:00
xinwen
c277aec561 feat(authenticaion): 添加登录页面验证码与MFA开关 2020-07-24 17:40:41 +08:00
ibuler
2a53a20808 perf(assets): 修改 系统用户 管理用户 等的用户名长度到128 2020-07-24 11:40:36 +08:00
Bai
674ad40f67 fix(perms): 修复perms api UserPermissionMixin 中 kwargs 参数传递(未发现引出其他问题) 2020-07-23 11:09:01 +08:00
github-actions
78089e01a3 fix(cas): 修复cas校验不同的问题 2020-07-22 17:20:39 +08:00
ibuler
1b71350199 fix(es): 修复es7数据结构引起的命令无法查询的问题
- 更新了 jms-storage版本依赖
2020-07-22 17:14:23 +08:00
老广
5d08438dad feat(ops): 项目启动时,清除指定的celery定时任务;添加获取celery定时任务的函数 (#4378)
* feat(ops): 添加获取celery定时任务的函数

* feat(ops): 项目启动时,清除指定的celery定时任务

* feat(ops): 项目启动时,清除指定的celery定时任务 2

Co-authored-by: Bai <bugatti_it@163.com>
2020-07-21 15:33:33 +08:00
github-actions
31ba0564e4 ci(github): 添加通用action 2020-07-21 14:57:52 +08:00
github-actions
ea5b7cd921 ci(github): 添加通用action 2020-07-21 14:32:35 +08:00
Bai
3e541162e3 fix(authentication): 修复用户认证Radius认证中参数传递错误问题 *kwargs -> **kwargs 2020-07-21 10:30:19 +08:00
BaiJiangJie
6e19384231 chore(readme): 更新README (#4359)
* Update README.md

* Update README.md
2020-07-17 16:31:54 +08:00
老广
19903c80c3 Merge pull request #4345 from jumpserver/dev
fix(radius): 修复radius认证失败问题 (#4342)
2020-07-16 18:20:15 +08:00
BaiJiangJie
070af8c491 fix(radius): 修复radius认证失败问题 (#4342) (#4343)
* fix(radius): 修复radius认证失败问题,添加get_django_user方法参数(django-radius==1.4.0 中添加了额外参数)

* fix(radius): 修复radius认证失败问题,重写authenticate方法(django-radius 不接受public_key参数)
2020-07-16 18:08:44 +08:00
BaiJiangJie
0fca33d874 fix(radius): 修复radius认证失败问题 (#4342)
* fix(radius): 修复radius认证失败问题,添加get_django_user方法参数(django-radius==1.4.0 中添加了额外参数)

* fix(radius): 修复radius认证失败问题,重写authenticate方法(django-radius 不接受public_key参数)
2020-07-16 17:07:40 +08:00
BaiJiangJie
08fdc57543 Merge pull request #4338 from jumpserver/dev
merge(master): Merge from dev to master
2020-07-16 10:50:26 +08:00
Bai
bb60d2a1d9 fix(users): 组织管理员创建用户时,角色只能选择: 用户 2020-07-15 20:14:14 +08:00
xinwen
0014bd0cb9 fix(audits): 操作日志中的动作搜索条件,删除文件字段改成删除 (#4334) 2020-07-15 20:13:21 +08:00
xinwen
9488c8bd97 fix(cmd_filter): 命令过滤器唯一应该为 name + org_id (#4325) 2020-07-15 20:04:46 +08:00
Bai
1f30d459ae fix(command): 修复命令记录没有根据sesion进行过滤的问题 2020-07-15 17:30:55 +08:00
Bai
4e933fc1ca feat(session + db): 会话搜索添加登录来源选项 2020-07-15 17:25:43 +08:00
Bai
c0f3a1f64a fix(all): 修复创建资源时,created_by字段长度限制导致创建失败的问题 2020-07-15 16:27:40 +08:00
Bai
0f70f5eccf fix(orgs): 删除组织失败时返回对应错误信息 2020-07-15 16:25:34 +08:00
Bai
eef942c155 fix(gather_asset_users): 修复收集资产用户日志中用户名显示不完整的问题 2020-07-15 16:16:11 +08:00
xinwen
061592fa6b fix(terminal): 移除CommandQueryMixin.get_filter_fields 2020-07-14 19:35:18 +08:00
Bai
c7a02586c1 chore(jms): 修改celery队列数量: 2 -> 4 2020-07-14 19:34:17 +08:00
Bai
ddcd4ebbfc fix(asset_user): 修改创建AuthBook对象锁机制,使用select_for_update替换redis_lock3 2020-07-14 19:06:06 +08:00
Bai
9550ea62fb fix(asset_user): 修改创建AuthBook对象锁机制,使用select_for_update替换redis_lock2 2020-07-14 19:06:06 +08:00
Bai
abcb589658 fix(asset_user): 修改创建AuthBook对象锁机制,使用select_for_update替换redis_lock 2020-07-14 19:06:06 +08:00
Bai
1bb366ad94 fix(authbook): 修改创建AuthBook对象锁机制,解决并发操作堵塞问题 2020-07-14 19:06:06 +08:00
xinwen
a5df7738f6 fix(audits): 日志审计模块 Serializer 添加 org_id 字段 2020-07-14 18:05:21 +08:00
xinwen
da858c8998 fix(tickets): 隐藏申请资产工单URL (#4307) 2020-07-13 17:49:00 +08:00
BaiJiangJie
724a8f6324 fix(assets): 修复用户name字段长度与资产created_by字段长度不一致导致创建资产失败的问题 (#4302)
* fix(assets): 修复用户name字段长度与资产created_by字段长度不一致导致创建资产失败的问题

* fix(assets): 修复用户name字段长度与资产created_by字段长度不一致导致创建资产失败的问题(修改迁移文件名称 0050_auto_20200702_1602.py -> 0051_auto_20200713_1143.py)
2020-07-13 12:00:44 +08:00
ibuler
437df9a533 fix(assets): node asset 关系发生变化是,关联系统用户引起的问题 2020-07-10 17:31:51 +08:00
ibuler
f2c70d0bba ci(fix): 修改构建脚本 2020-07-09 17:44:54 +08:00
ibuler
ea913a5b6e ci(build): 修改构建逻辑 2020-07-09 17:44:54 +08:00
ibuler
c0cd8878dc ci(fix): 修改构建脚本 2020-07-09 17:41:27 +08:00
ibuler
15e995ade6 ci(build): 修改构建逻辑 2020-07-09 17:41:27 +08:00
BaiJiangJie
cadf42f3fa Merge pull request #4280 from jumpserver/dev
merge: Merge to master from branch dev
2020-07-09 15:02:47 +08:00
BaiJiangJie
f588093cd3 Merge pull request #4282 from jumpserver/dev_master
merge: Merge to master from branch dev
2020-07-09 14:51:03 +08:00
Bai
7c12f8f462 merge: Merge to dev from branch master 2020-07-09 14:27:08 +08:00
xinwen
6f5a92c21f [Update] assets/gathered_user 添加过滤字段 2020-07-09 14:08:46 +08:00
xinwen
17a76994dc [Update] 系统用户添加过滤字段 2020-07-09 14:08:46 +08:00
jym503558564
39d793bc47 fix:修改 ftp 日志按开始日期排序 2020-07-09 14:08:46 +08:00
xinwen
c3eafbee8c [Fix] X-Pack/云管中心 i18n 2020-07-09 14:08:46 +08:00
ibuler
10f99be100 添加example api 2020-07-09 14:08:46 +08:00
xinwen
8eb6cfa9c9 fix(ticket): 修改工单获取系统用户的字段 (#4274)
fix(ticket): 申请资产工单修改bug
2020-07-09 14:06:06 +08:00
xinwen
f430c9e435 Merge pull request #4270 from jumpserver/request-asset-ticket-dev
feat(ticket): 添加申请资产工单
2020-07-08 15:42:04 +08:00
BaiJiangJie
10c428a432 Merge pull request #4269 from jumpserver/dev_user_group
fix(user_group): 用户组中添加用户,取消审计员的限制
2020-07-08 15:23:45 +08:00
Bai
a30c603bdc fix(user_group): 用户组中添加用户,取消审计员的限制 2020-07-08 15:17:07 +08:00
xinwen
39a75074af [Feature] 添加申请资产工单 2020-07-08 15:09:44 +08:00
BaiJiangJie
452ed2baf1 Merge pull request #4268 from jumpserver/dev_adminuser
fix(assets): 修复测试管理用户/系统用户资产可连接性问题
2020-07-08 14:47:09 +08:00
Bai
8c7240193a fix(system_user): 修复系统用户测试可连接性失败问题(所有资产)(不应该执行校验系统用户是否可以推送的逻辑) 2020-07-08 11:30:37 +08:00
Bai
b622aca9af fix(admin_user): 修复管理用户单独测试某台资产可连接性失败的情况(private_key_file) 2020-07-08 10:48:25 +08:00
ibuler
ce7edc1612 ci(release): 修改 release 使用的tag,而不是自动生成的 2020-07-08 10:36:43 +08:00
BaiJiangJie
ebf1a9d5e2 Merge pull request #4255 from jumpserver/dev_asset
feat(assets): 资产序列类修改字段名 _name 为 _display
2020-07-07 14:36:27 +08:00
ibuler
23ef185b7e fix(build): 修改调用action jumpserver/action-build-upload-asset的参数 2020-07-07 14:30:34 +08:00
Bai
69f49f7776 feat(i18n): 修改翻译 2020-07-07 14:13:20 +08:00
ibuler
6b16aa6bc0 ci(build): 修改 构建脚本
- sed 在不同系统下表现不同
2020-07-07 13:52:50 +08:00
ibuler
43741dc9b2 ci(build): 修改 workflow
- 修改使用action jumpserver/action-build-upload-assets
2020-07-07 13:38:26 +08:00
ibuler
18174e2867 fix(build): 修稿构建 2020-07-07 13:28:59 +08:00
ibuler
3077d11483 ci(release&build): 添加 github workflows, 自动构建 release
- 添加 utils/build.sh 脚本,构建后放到 release 目录中
- 当 push tags时,自动创建 Release Draft
- 自动生成 Release Change Log
- 自动构建包,上传到 Release Assets
2020-07-07 13:03:48 +08:00
Bai
fcd684e2db feat(assets): 资产序列类修改字段名 _name 为 _display 2020-07-07 11:09:43 +08:00
BaiJiangJie
afcb6bd77c Merge pull request #4242 from jumpserver/dev_cloud
feat(node + domain + domain_migrate): NodeModel添加get_or_create_child()方法,修改网域唯一字段 org_id+name
2020-07-06 15:18:13 +08:00
Bai
1c264399bb feat(domain + migrate): 修改网域唯一字段为:org_id + name 2020-07-02 18:51:16 +08:00
Bai
872e2546e9 feat(node): NodeModel添加方法get_or_create_child() 2020-07-02 18:49:17 +08:00
BaiJiangJie
8f347eee4d Merge pull request #4216 from jumpserver/dev_org
feat(Cloud): 组织管理ViewSet添加搜索字段
2020-07-01 14:57:50 +08:00
BaiJiangJie
fa886b90c2 Merge pull request #4211 from jumpserver/dev_command_execute
feat(Command execute): 批量命令执行配置添加默认值True
2020-07-01 14:57:37 +08:00
Bai
caf312c5be feat(Cloud): 组织管理ViewSet添加搜索字段 2020-07-01 14:38:03 +08:00
Bai
ac6168a06c feat(Command execute): 批量命令执行配置添加默认值True 2020-07-01 11:19:57 +08:00
BaiJiangJie
eba9f2325a Merge pull request #4204 from jumpserver/dev_login_password_encrypt
feat(Login password ecrypt): 登录密码加密传输
2020-06-30 18:57:51 +08:00
Bai
b46e772d09 feat(login password ecrypt): 登录密码加密传输 4 2020-06-30 18:35:01 +08:00
Bai
183df82a75 feat(login password ecrypt): 登录密码加密传输 3 2020-06-30 18:14:53 +08:00
Bai
98c91d0f18 feat(login password ecrypt): 登录密码加密传输(添加翻译) 2020-06-30 17:37:16 +08:00
Bai
e17d875206 feat(login password ecrypt): 登录密码加密传输2 2020-06-30 17:23:56 +08:00
Bai
4b1e84ed8a Merge branch 'dev' into dev_login_password_encrypt 2020-06-30 17:13:08 +08:00
Bai
71ee33e3be feat(login password ecrypt): 登录密码加密传输 2020-06-30 17:12:38 +08:00
xinwen
5dd24f5cf9 Merge pull request #4188 from jumpserver/limit-upload-csv
[Update] 限制上传CSV文件的大小
2020-06-28 19:13:53 +08:00
xinwen
2b6e818943 [Update] 限制上传CSV文件的大小 2020-06-28 19:02:20 +08:00
老广
8c4e9720d3 Merge pull request #4183 from jumpserver/fix_template
docs(github): 修改 github issue 模板
2020-06-28 15:22:41 +08:00
ibuler
d43709f584 docs(github): 修改 github issue 模板
更改版本号说明,1.4及之前不再提供支持
2020-06-28 15:17:15 +08:00
BaiJiangJie
89496baae5 Merge pull request #4148 from jumpserver/v2.0
V2.0
2020-06-28 10:47:56 +08:00
BaiJiangJie
ea6d995f55 Merge pull request #4147 from jumpserver/v2.0_bugfix_csv
[Update] 修改csv导出,最大限制条目数从100->10000条
2020-06-28 10:46:31 +08:00
Bai
cf6aba1f38 [Update] 修改csv导出,最大限制条目数从100->10000条 2020-06-28 10:43:47 +08:00
BaiJiangJie
fdcda83c93 Merge pull request #4142 from jumpserver/sftp-log-i18n
Sftp log i18n
2020-06-24 17:30:57 +08:00
xinwen
6e3369c944 [Update] sftp log页面操作翻译 2020-06-24 17:22:53 +08:00
BaiJiangJie
d7e432a851 Merge pull request #4139 from jumpserver/dev_session
[Update] UserProfileAPI 判断是否设置session过期时间,解决前端关闭浏览器session未失效的问题
2020-06-24 16:14:36 +08:00
Bai
c0a153d13a [Update] UserProfileAPI 判断是否设置session过期时间,解决前端关闭浏览器session未失效的问题 2020-06-24 10:52:05 +08:00
老广
2acc1dc875 Merge pull request #4134 from jumpserver/fix-mfa-1.5
Fix mfa 1.5
2020-06-22 19:05:15 +08:00
xinwen
32ed43ba7b Merge branch 'v2.0' into fix-mfa-1.5 2020-06-22 19:04:42 +08:00
老广
e04e31eb30 Merge pull request #4129 from jumpserver/readme
feat: readme 添加docker pull
2020-06-22 12:08:28 +08:00
ibuler
ff747f9e42 feat: readme 添加docker pull 2020-06-22 12:06:42 +08:00
BaiJiangJie
c4bd093fd7 Merge pull request #4126 from jumpserver/v2.0
V2.0
2020-06-20 19:26:12 +08:00
BaiJiangJie
408b2d6dbd Merge pull request #4125 from jumpserver/v2.0_bugfix
v2.0 添加改密计划安全模式配置项
2020-06-20 19:24:17 +08:00
Michael Bai
f1e5c7c2bb 添加改密计划安全模式配置项 2020-06-20 16:18:58 +08:00
xinwen
1d640eccf6 [Fix] /opt/jumpserver/apps/jumpserver/views/index.py redirect(assets:user-asset-list) (#4121) 2020-06-19 18:28:43 +08:00
BaiJiangJie
92fc0ceb16 Merge pull request #4118 from jumpserver/master
Update readme
2020-06-18 15:03:32 +08:00
BaiJiangJie
217ea03c18 Merge pull request #4117 from jumpserver/dev
Dev
2020-06-18 15:02:40 +08:00
BaiJiangJie
923f0ed477 Update README.md 2020-06-18 15:01:08 +08:00
BaiJiangJie
3c6cfaa6cf Update README.md 2020-06-18 14:59:13 +08:00
BaiJiangJie
0bfe255966 Update README.md 2020-06-18 14:58:00 +08:00
BaiJiangJie
af5d531131 Merge pull request #4116 from jumpserver/dev
Dev
2020-06-18 10:56:36 +08:00
Bai
10d58ef424 [Update] Merge from master to dev 2020-06-17 19:30:37 +08:00
Bai
64064cb526 [Update] 注释节点树print 2020-06-17 17:47:45 +08:00
Eric_Lee
46941037dd add SECURITY_COMMAND_EXECUTION (#4114) 2020-06-17 15:03:41 +08:00
xinwen
8ad71b6dd9 [Update] Move LOCAL_DYNAMIC_SETTINGS (#4113) 2020-06-16 18:11:23 +08:00
ibuler
220ccda04d fix: 修改domain创建的assets不是必填的 2020-06-16 17:55:18 +08:00
Eric_Lee
6d30fe797c fix ldap test bug (#4110) 2020-06-16 17:25:57 +08:00
xinwen
3318df1771 [Fix] 日志审计/FTP日志 (#4109) 2020-06-16 16:33:53 +08:00
Eric_Lee
0ccd806eca add system user perm api (#4108) 2020-06-16 16:12:59 +08:00
xinwen
7ebe1c2916 [Update] 优化 dynamic settings (#4107) 2020-06-16 15:54:19 +08:00
ibuler
08904c2a9f Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-16 15:30:39 +08:00
ibuler
19e34270d1 feat: gather user添加搜索字段 2020-06-16 15:30:25 +08:00
Bai
afa515d570 Merge branch 'dev' of https://github.com/jumpserver/jumpserver into dev 2020-06-16 15:04:38 +08:00
Bai
bcba408517 [Update] 修改用户source默认local 2020-06-16 15:04:01 +08:00
ibuler
80d94074e7 feat: 修改资产的标签搜索 2020-06-16 14:56:19 +08:00
ibuler
9347405f08 feat: terminal增加搜索 2020-06-16 13:50:21 +08:00
ibuler
da4ad11a69 fix: 修改session command 翻译 2020-06-16 13:39:49 +08:00
ibuler
b81e424e80 fix: 修改翻译 2020-06-16 12:20:16 +08:00
ibuler
1f15937139 feat: 修改翻译,添加settings 2020-06-16 11:04:59 +08:00
Bai
f4fa011714 [Update] 修改mfa校验code无效翻译 2020-06-15 20:31:50 +08:00
xinwen
c5a9a85818 [Update] 完善 作业中心/任务列表 (#4105) 2020-06-15 19:42:36 +08:00
ibuler
e23bfa0f69 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-15 17:55:13 +08:00
ibuler
451690fe8b fix: 修改重定向 2020-06-15 17:44:40 +08:00
Eric_Lee
5bea782b9f add assignee field for ticket (#4104) 2020-06-15 17:24:05 +08:00
Bai
f26f7ca1e7 [Update] 修改parsers,处理dict字段值;解决remote-app csv导入时异常 2020-06-15 16:34:51 +08:00
Bai
583d295fd1 Merge branch 'dev' of https://github.com/jumpserver/jumpserver into dev 2020-06-15 13:50:05 +08:00
Bai
b51af1f7d7 [Update] 修改MFA应用下载图片 2020-06-15 13:49:51 +08:00
xinwen
edcf9921fe [Update] apps/assets/serializers/system_user.py (#4102) 2020-06-15 13:24:52 +08:00
Eric_Lee
eef172d0e2 Merge pull request #4098 from jumpserver/fix-ops-adhoc
[Fix] ops.models.adhoc (#4096)
2020-06-12 12:23:05 +08:00
Eric_Lee
5b407fe8bc Merge pull request #4095 from jumpserver/update-user
[Update] 修改 用户相关 Serializer
2020-06-12 12:22:16 +08:00
Bai
1bb9048910 [Update] 标签序列类assets字段required为False 2020-06-12 11:45:03 +08:00
xinwen
787cdbcadf [Fix] ops.models.adhoc (#4096) 2020-06-12 11:44:29 +08:00
xinwen
b14ca14120 [Fix] ops.models.adhoc (#4096) 2020-06-11 20:30:59 +08:00
ibuler
4b2fd0d0da fix: 修复系统用户过滤权限规则的bug 2020-06-11 19:36:34 +08:00
xinwen
3393f18399 [Update] 修改 用户相关 Serializer 2020-06-11 18:24:56 +08:00
ibuler
7e1a379e47 feat: 修改org返回的数据结构 2020-06-11 18:05:18 +08:00
ibuler
213fdd461b fix: 修改翻译 2020-06-11 14:33:42 +08:00
ibuler
148c7ffb43 fix: 添加注释 2020-06-11 14:10:55 +08:00
ibuler
75be45ce43 feat: 使用新的对称加密方式: aes 2020-06-11 12:10:00 +08:00
ibuler
04eb670ada Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-11 11:29:28 +08:00
ibuler
66f3706142 feat: 添加aes fix: 修改时区的bug 2020-06-11 11:28:37 +08:00
xinwen
9ea98bf2b2 [Feature] 添加 会话管理/历史会话/下载 api (#4093) 2020-06-10 17:34:56 +08:00
ibuler
4695f80172 faet: 修改翻译 2020-06-10 17:17:16 +08:00
ibuler
0452d53c3f Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-10 16:00:24 +08:00
ibuler
ec30ef1f8b feat: readme添加版本说明 2020-06-10 15:57:41 +08:00
Eric_Lee
1c0ad08d80 fix ldap test i18n msg (#4092) 2020-06-10 15:10:43 +08:00
ibuler
e1ab453780 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-10 12:58:43 +08:00
ibuler
1a6597b572 feat: 修改command filter数据结构 2020-06-10 12:58:18 +08:00
xinwen
865522953a [Feature] 作业中心/任务列表/任务详情 最后运行成功或失败的主机 (#4090) 2020-06-09 20:26:23 +08:00
ibuler
4d4a107101 feat: 修改oid获取顺序,添加从cookie中获取 2020-06-09 16:43:07 +08:00
ibuler
82f70cb0dc feat: ticket添加翻译 2020-06-09 16:10:31 +08:00
ibuler
820186c6d0 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-09 15:16:01 +08:00
ibuler
4468e2d379 feat: 限制gateway 仅有ssh协议 2020-06-09 15:08:15 +08:00
Eric_Lee
bd802e6a50 add logo urls (#4088) 2020-06-09 14:06:11 +08:00
ibuler
9362c272cb Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-09 10:19:07 +08:00
ibuler
ee4534ac4b feat: public setting添加key 2020-06-09 10:17:16 +08:00
xinwen
7ef09a4ca1 [Update] UserSerializer 添加 login_confirm_settings (#4086) 2020-06-08 17:16:30 +08:00
ibuler
31daaed4cd feat: 添加auth confirm 到public setting 2020-06-08 16:48:10 +08:00
ibuler
71202e83f5 feat: 修改用户的api 2020-06-05 20:15:23 +08:00
ibuler
b1640e5592 fix: 修改common resource api的权限,否则auditor无法使用 2020-06-05 14:43:40 +08:00
ibuler
076b7babcb 修改批量命令的api 2020-06-05 14:20:34 +08:00
ibuler
8569910658 feat: command exexution audit log的搜索 2020-06-05 14:07:23 +08:00
xinwen
34c556d375 [Fix] spm (#4082) 2020-06-05 10:42:03 +08:00
ibuler
a43d6ad34d feat: 资产添加admin_user_display 2020-06-04 20:26:42 +08:00
ibuler
ca6825008b Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-06-04 20:00:48 +08:00
ibuler
9c6f118dbd feat: 添加批量命令的relation 2020-06-04 20:00:39 +08:00
Bai
5730e60089 [Update] 修改用户profile序列类 2020-06-04 17:24:31 +08:00
Bai
afc7f3bb9c [Update] 修改用户public_key生成密钥的view 2020-06-04 17:05:36 +08:00
Bai
c411b0a38e Merge branch 'dev' of https://github.com/jumpserver/jumpserver into dev 2020-06-04 17:02:52 +08:00
Bai
403b6fc563 [Update] 修改用户更新Public_key的条件判断 2020-06-04 17:02:43 +08:00
xinwen
55ae8bb5e6 [Fix] 系统设置/邮件设置/测试链接失败 (#4081) 2020-06-04 16:47:43 +08:00
Eric_Lee
dbcf785e42 add flower view (#4078) 2020-06-04 15:10:02 +08:00
ibuler
e6cd126045 feat: 修改api的权限 2020-06-04 14:55:33 +08:00
Bai
420f3c0c4c [Update] 添加celery task log view 2020-06-04 14:30:44 +08:00
Bai
9b2c5cb305 [Update] 修改用户profile序列类3 2020-06-03 21:00:37 +08:00
Bai
907f0068db [Update] 修改用户profile序列类2 2020-06-03 20:21:28 +08:00
Bai
a16b3260ba Merge branch 'dev' of https://github.com/jumpserver/jumpserver into dev 2020-06-03 18:20:43 +08:00
Bai
1845821f6c [Update] 修改用户profile序列类 2020-06-03 18:20:33 +08:00
ibuler
27d906a877 feat: 去掉js i18n catalog 2020-06-03 16:26:10 +08:00
xinwen
431ba36a26 [Fix] 会话管理/命令记录 时间过滤 bug (#4070) 2020-06-03 15:05:55 +08:00
Eric_Lee
229c782157 change PAGE_SIZE_CHOICE to string value (#4069) 2020-06-03 14:06:44 +08:00
ibuler
5bacab7475 fix: 重置密钥到auth中 2020-06-03 12:44:45 +08:00
ibuler
999286a089 fix: 修复用户公钥错误引起的profile bug 2020-06-03 12:32:52 +08:00
ibuler
68ccaf0cb3 feat: 去掉第一次登录的那个导航 2020-06-03 12:26:53 +08:00
ibuler
8d58d58519 feat: 修改profile view 2020-06-03 11:58:16 +08:00
ibuler
8efc0331de feat: 删掉所有view, templates, forms 2020-06-03 11:43:43 +08:00
ibuler
7c479c2479 feat: 修改docs的url 2020-06-03 11:17:00 +08:00
ibuler
96551856a2 [Update] 部分view放到auth中 2020-06-03 11:06:44 +08:00
ibuler
b1f5cc7728 [Update] 禁用view 2020-06-03 10:38:44 +08:00
ibuler
1a84661ca9 feat: 修改filterset_fields => filter_fields,option方法不支持filterset 2020-06-02 20:02:22 +08:00
ibuler
c87b9f203f feat: 修改依赖库版本 2020-06-02 15:51:24 +08:00
老广
9442acfb74 Celery version (#4064)
* [Update] 升级celery版本

* [Update] 修改redis-cache 版本
2020-06-02 15:44:45 +08:00
ibuler
50bea55732 merge: with dev 2020-06-02 15:43:42 +08:00
ibuler
a4ece2b271 feat: audits中添加id字段 2020-06-02 15:41:27 +08:00
xinwen
b460e4abaa [Fix] 日志审计 日期过滤与排序bug (#4063) 2020-06-02 15:40:07 +08:00
BaiJiangJie
087ba9ae95 Merge pull request #4059 from jumpserver/lina
Lina
2020-06-01 14:05:22 +08:00
ibuler
5f2345852d Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-06-01 11:41:55 +08:00
ibuler
ad1c17aa7b feat: 仅支持fields_size=mini,small 2020-06-01 10:10:28 +08:00
xinwen
3a79bfd5f6 [Update] 添加一些 i18n (#4052) 2020-05-29 15:18:25 +08:00
Bai
5ee8519274 [Update] 修改AccessKey序列类 2020-05-29 14:04:50 +08:00
Bai
ff546774e9 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-28 20:46:30 +08:00
Bai
1f4fc9b6f0 [Update] 修改API-Key序列类 2020-05-28 20:46:17 +08:00
xinwen
f8142e23cd [Fix] 权限管理-> 资产与数据库 Bug (#4049) 2020-05-28 19:08:48 +08:00
Bai
196f1654ab Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-28 17:47:27 +08:00
Bai
3b8a24eeb7 [Update] 添加用户profile public-key序列类 2020-05-28 17:47:18 +08:00
xinwen
7dde15cb04 [Feature] 权限管理-> 远程应用 添加 api (#4040)
* [Feature] 权限管理-> 远程应用 添加 api

* [Feature] 添加 RelationMixin
2020-05-28 17:00:42 +08:00
Bai
3e5d949610 [Update] 添加用户profile password序列类 2020-05-28 16:10:28 +08:00
Bai
25c3691f6b Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-28 15:03:07 +08:00
Bai
f3bc6c0b22 [Update] 修改用户profile序列类 2020-05-28 15:02:53 +08:00
ibuler
caf0d85939 [feat] 修改remote app, database app的过滤参数 2020-05-28 14:47:30 +08:00
ibuler
a463f632e8 [Update] 优化重定向 2020-05-27 20:33:09 +08:00
ibuler
a0e6d09770 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-27 19:51:45 +08:00
BaiJiangJie
54623a5b06 Merge pull request #4047 from jumpserver/lina_dev
Lina dev
2020-05-27 18:46:48 +08:00
Bai
7afff5e392 [Update] merge from dev to lina 2020-05-27 18:44:12 +08:00
ibuler
9a5fee5a4c Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-27 18:03:14 +08:00
ibuler
a840e611cd [Update] 修改core 的base url 2020-05-27 18:02:18 +08:00
Bai
566419cac4 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-27 17:27:38 +08:00
Bai
7b362bfc76 [Update] 用户序列类添加mfa相关字段 2020-05-27 17:27:28 +08:00
ibuler
f528dd4888 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-26 19:00:00 +08:00
ibuler
3c95c6fe11 [Feat] 添加失效用户权限的api 2020-05-26 18:56:03 +08:00
老广
71a72dd957 Merge pull request #4033 from jumpserver/update-i18n
[Update] i18n
2020-05-25 01:53:02 -05:00
老广
78ac1968dd Merge pull request #4039 from jumpserver/asset_permission-add-field
[Update] perms.serializers.asset_permission.AssetPermissionSerializer…
2020-05-25 01:52:35 -05:00
xinwen
93453cc8c3 [Update] perms.serializers.asset_permission.AssetPermissionSerializer 添加字段 2020-05-25 14:50:07 +08:00
xinwen
11527b9033 [Update] i18n 2020-05-22 18:18:40 +08:00
ibuler
2ff2266417 [Update] 修改 SerializerMixin 还原之前的更改 2020-05-21 20:35:44 +08:00
ibuler
3320e6105c [Update] 删掉没用的内容 2020-05-21 19:00:47 +08:00
老广
072e74ce49 Merge pull request #4027 from jumpserver/update-org-user
[Update] org retrieve api
2020-05-21 03:15:05 -05:00
xinwen
492b1c4311 [Update] org retrieve api 2020-05-21 16:09:42 +08:00
老广
0babada459 Merge pull request #4025 from jumpserver/update-org-user-add-id
[Update] orgs.serializers.OrgReadSerializer add `id`
2020-05-21 02:43:35 -05:00
xinwen
7b0993959e [Update] orgs.serializers.OrgReadSerializer add id 2020-05-21 15:40:41 +08:00
ibuler
701582fe38 [Update] 修改license的判断方法 2020-05-21 14:47:00 +08:00
ibuler
b371676813 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-20 19:35:48 +08:00
ibuler
0cac8d66b3 [Update] 修改index api 2020-05-20 19:35:33 +08:00
老广
3e0f5af848 Merge pull request #4019 from jumpserver/update-some-i18n
[Update] 一些本地化
2020-05-20 05:17:12 -05:00
xinwen
245d28b03d [Update] 一些本地化 2020-05-20 17:45:50 +08:00
Bai
75f4f6d0a2 [Update] 资产授权序列类is_expired, is_valid设置为read_only 2020-05-19 20:50:33 +08:00
Bai
f9e167cb0e Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-19 20:36:41 +08:00
Bai
f224e49de7 [Update] 去掉资产授权序列类中不存在的字段 2020-05-19 20:36:17 +08:00
ibuler
76ef9b292b [Update] 修改public api 2020-05-18 14:55:16 +08:00
ibuler
1540cbdcaa [Update] 修改csv render 2020-05-18 11:52:50 +08:00
ibuler
5d129fd0da Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-14 14:51:05 +08:00
ibuler
b529127461 [Update] 修改user role 2020-05-14 14:49:54 +08:00
Bai
d06ea2944e [Update] 修改Dashboard API 2020-05-14 10:53:30 +08:00
ibuler
154aad1e22 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-13 11:06:24 +08:00
ibuler
17163dd909 [Update] 用户Profile添加在当前组织的角色 2020-05-13 11:05:58 +08:00
BaiJiangJie
b789a8bb05 Merge pull request #3994 from jumpserver/lina_assetuser
[Update] 修复AssetUserViewSet 使用Option方法时error
2020-05-12 20:32:58 +08:00
Bai
9341ce9f84 [Update] 修复AssetUserViewSet 使用Option方法时error 2020-05-12 20:31:32 +08:00
ibuler
195cbbbe42 [Update] 修改profile serialzier 2020-05-12 17:57:35 +08:00
xinwen
6e5e340a25 Merge pull request #3993 from jumpserver/update-assets-filter
[Update] Asset filter 添加 platform__base 字段
2020-05-12 17:26:59 +08:00
xinwen
eb74d13059 [Update] Asset filter 添加 platform__base 字段 2020-05-12 17:24:51 +08:00
老广
16f916c40a Merge pull request #3992 from jumpserver/update-assets-filter
Update assets filter
2020-05-12 04:11:49 -05:00
xinwen
4dd6d4498b [Update] Asset filter 添加 platform__name 字段 2020-05-12 17:09:36 +08:00
ibuler
dd5bf546df [Update] 修改serialzier_class 2020-05-12 15:37:37 +08:00
ibuler
d6debde566 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-11 19:28:24 +08:00
ibuler
efc66cc7ee [Update] 默认关闭debug 2020-05-11 19:28:02 +08:00
BaiJiangJie
b310731ba7 Merge pull request #3990 from jumpserver/lina_dev
Lina dev
2020-05-11 17:45:51 +08:00
Bai
4bd2681bf0 [Update] Merge from dev to lina 2020-05-11 17:43:48 +08:00
BaiJiangJie
fdd55511a6 Merge pull request #3986 from jumpserver/lina_dev
Lina dev
2020-05-11 14:04:52 +08:00
Bai
0cff6ab29b [Update] Merge fromo dev to lina 2020-05-11 14:03:24 +08:00
ibuler
cda677a30f [update] 修改remote apps的serializer 2020-05-11 12:35:33 +08:00
ibuler
5571651c02 [Update] 升级requirements 2020-05-09 16:13:06 +08:00
ibuler
2680396680 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-09 16:08:32 +08:00
ibuler
227f97c2f5 [Update] 修改remote apps 字段 2020-05-09 16:08:09 +08:00
老广
ad3231c8a3 Merge pull request #3982 from jumpserver/update-ops-migrate
[Update] 重建 ops 0018 迁移脚本
2020-05-09 01:54:37 -05:00
ibuler
e39d8dce3c [Update] 修改fields 2020-05-09 14:52:44 +08:00
ibuler
0bdc425c55 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-09 14:51:47 +08:00
ibuler
0a7f63cc5e [Update] 修改remote app api 2020-05-09 14:51:19 +08:00
xinwen
c6e0e9a79a [Update] 重建 ops 0018 迁移脚本 2020-05-09 14:50:22 +08:00
老广
7fde392774 Merge pull request #3980 from jumpserver/update-audits-verbose
[Update] audits 模块为一些 models 字段添加 verbose 信息
2020-05-08 22:40:34 -05:00
xinwen
3ee051303a [Update] audits 模块为一些 models 字段添加 verbose 信息 2020-05-09 11:29:25 +08:00
老广
9fa31be4bf Merge pull request #3976 from jumpserver/add-audits-apis
[Add] audits apis
2020-05-08 08:04:14 -05:00
老广
ae2e4049db Merge pull request #3979 from jumpserver/lina_setting_dict_field
AUTH_LDAP_USER_ATTR_MAP to Dict field
2020-05-08 08:02:04 -05:00
Eric
48b71bb11b AUTH_LDAP_USER_ATTR_MAP to Dict field 2020-05-08 20:58:13 +08:00
xinwen
e339ed1fb3 [Add] audits apis 2020-05-08 17:59:58 +08:00
老广
4517a92b2b Merge pull request #3971 from jumpserver/add-ftp-log-api
[Add] ftp-logs api
2020-05-08 00:52:03 -05:00
xinwen
ac902501ec [Add] ftp-logs api 2020-05-08 12:46:18 +08:00
老广
363b5d04d9 Merge pull request #3969 from jumpserver/update-login-logs
[Update] login-logs api 添加 `mfa_display`
2020-05-07 22:23:37 -05:00
xinwen
dec89ae5ee [Update] login-logs api 添加 mfa_display 2020-05-08 10:08:57 +08:00
老广
5d37269a6c Merge pull request #3967 from jumpserver/lina_status_code
[Update] Reponse status code from 401 to 400
2020-05-07 04:03:34 -05:00
Eric
9a39ccd37d [Update] Reponse status code from 401 to 400 2020-05-07 16:57:57 +08:00
ibuler
8c0bf0b71b [Update] Login log 添加display field 2020-05-06 19:35:19 +08:00
老广
5812c50a33 Merge pull request #3960 from jumpserver/lina_setting
[Update] save settings
2020-05-06 04:05:51 -05:00
Eric
7b339df430 [Update] save settings 2020-05-06 16:36:36 +08:00
ibuler
6bb13a26f5 [Update] 修改用户serializer 2020-04-30 16:58:08 +08:00
ibuler
b92137afd9 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-04-30 16:53:21 +08:00
老广
962763dc7b Merge pull request #3954 from xuxinwen/feature-audits-apis
[Feature] 添加 login-logs API
2020-04-30 03:52:57 -05:00
xuxinwen
f4eca83a49 [feature] 添加 login-logs API 2020-04-30 16:40:55 +08:00
老广
02135ea04f Merge pull request #3948 from jumpserver/settings_api
[Update] Add settings api
2020-04-29 18:25:10 +08:00
Eric
79eb838250 [Update] setting fields automatically generated by serializer 2020-04-29 18:10:42 +08:00
Eric
82710294f4 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into settings_api 2020-04-29 17:09:50 +08:00
Eric
2d18acf6f7 [Update] settings api 2020-04-29 17:04:58 +08:00
ibuler
f8c323cf5c [Update] 优化user group serializer 2020-04-29 16:01:14 +08:00
ibuler
b6f5b335bd Merge branch 'api_perf' into lina 2020-04-29 15:47:46 +08:00
Eric
f451f8a979 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into settings_api 2020-04-29 14:33:22 +08:00
ibuler
5c4dfabc48 [Update] 修改settings 2020-04-29 14:32:51 +08:00
ibuler
0c0c0e6d6f Merge branch 'dev' into lina 2020-04-29 13:00:47 +08:00
Eric
20cf7c7c52 [Update] Add settings api 2020-04-29 11:09:35 +08:00
ibuler
230d6137f3 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-04-29 10:21:06 +08:00
ibuler
aa9533eb5b [Update] Groups users字段可以显示 2020-04-29 10:19:25 +08:00
ibuler
efb5d4135a [Update] 优化api字段显示 2020-04-23 11:14:02 +08:00
ibuler
5f2c9c3801 [Update] 添加 authentication backend header 2020-04-22 15:13:04 +08:00
BaiJiangJie
1b1a686b96 Merge pull request #3922 from jumpserver/dev
Dev
2020-04-18 23:19:11 +08:00
BaiJiangJie
e9fe5b3004 Merge pull request #3908 from jumpserver/dev
Dev
2020-04-16 18:30:54 +08:00
BaiJiangJie
5001f48982 Merge pull request #3899 from jumpserver/dev
Dev
2020-04-14 19:46:36 +08:00
BaiJiangJie
4eaaa2462b Merge pull request #3897 from jumpserver/dev
Dev
2020-04-14 17:53:52 +08:00
BaiJiangJie
093e3924a2 Merge pull request #3894 from jumpserver/dev
Dev
2020-04-14 16:00:08 +08:00
BaiJiangJie
8fd5e6521f Merge pull request #3892 from jumpserver/dev_bai
Dev bai
2020-04-14 13:20:50 +08:00
BaiJiangJie
6cdba2e8d2 Merge pull request #3890 from jumpserver/dev_bai
Dev bai
2020-04-14 12:56:20 +08:00
BaiJiangJie
4dcd4749c3 Merge pull request #3884 from jumpserver/dev
Dev
2020-04-13 18:24:54 +08:00
ibuler
0d2b4d7ca3 [Update] 添加ids filter 2020-04-13 10:40:13 +08:00
BaiJiangJie
42c5c02709 Merge pull request #3871 from jumpserver/lina_dev
Lina dev
2020-04-10 14:50:05 +08:00
Bai
d1d73da322 [Update] Merge from dev 2020-04-10 14:46:35 +08:00
BaiJiangJie
88fcf8dbd7 Merge pull request #3869 from jumpserver/lina_user
[Update] 修改用户Profile序列类返回的admin_orgs
2020-04-10 12:49:57 +08:00
Bai
396bc9b6ae [Update] 修改用户ViewSet序列类返回的admin_orgs(解决组织管理员登录的Bug) 2020-04-10 12:47:16 +08:00
ibuler
cd7946f3f0 [Update] 修改一些bug 2020-04-09 18:58:22 +08:00
ibuler
e7031d0ac1 [Update] 修改serailizer mixin 2020-04-09 10:33:20 +08:00
ibuler
f8dae2a3c9 [Update] 时区允许设置 2020-04-01 17:27:32 +08:00
628 changed files with 18483 additions and 27409 deletions

View File

@@ -1,17 +0,0 @@
[简述你的问题]
##### 使用版本
[请提供你使用的Jumpserver版本 1.x.x 注: 0.3.x不再提供支持]
##### 问题复现步骤
1. [步骤1]
2. [步骤2]
##### 具体表现[截图可能会更好些,最好能截全]
##### 其他
[注:] 完成后请关闭 issue

10
.github/ISSUE_TEMPLATE/----.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: 需求建议
about: 提出针对本项目的想法和建议
title: "[Feature] "
labels: 类型:需求
assignees: ibuler
---
**请描述您的需求或者改进建议.**

22
.github/ISSUE_TEMPLATE/bug---.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进
title: "[Bug] "
labels: 类型:bug
assignees: wojiushixiaobai
---
**JumpServer 版本(v1.5.9以下不再支持)**
**浏览器版本**
**Bug 描述**
**Bug 重现步骤(有截图更好)**
1.
2.
3.

10
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[Question] "
labels: 类型:提问
assignees: wojiushixiaobai
---
**请描述您的问题.**

44
.github/release-config.yml vendored Normal file
View File

@@ -0,0 +1,44 @@
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
- title: '🌱 新功能 Features'
labels:
- 'feature'
- 'enhancement'
- 'feat'
- '新功能'
- title: '🚀 性能优化 Optimization'
labels:
- 'perf'
- 'opt'
- 'refactor'
- 'Optimization'
- '优化'
- title: '🐛 Bug修复 Bug Fixes'
labels:
- 'fix'
- 'bugfix'
- 'bug'
- title: '🧰 其它 Maintenance'
labels:
- 'chore'
- 'docs'
exclude-labels:
- 'no'
- '无需处理'
- 'wontfix'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
version-resolver:
major:
labels:
- 'major'
minor:
labels:
- 'minor'
patch:
labels:
- 'patch'
default: patch
template: |
## 版本变化 Whats Changed
$CHANGES

View File

@@ -0,0 +1,12 @@
on: [push, pull_request, release]
name: JumpServer repos generic handler
jobs:
generic_handler:
name: Run generic handler
runs-on: ubuntu-latest
steps:
- uses: jumpserver/action-generic-handler@master
env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}

46
.github/workflows/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Create Release And Upload assets
jobs:
create-realese:
name: Create Release
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Get version
id: get_version
run: |
TAG=$(basename ${GITHUB_REF})
VERSION=${TAG/v/}
echo "::set-output name=TAG::$TAG"
echo "::set-output name=VERSION::$VERSION"
- name: Create Release
id: create_release
uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
config-name: release-config.yml
version: ${{ steps.get_version.outputs.TAG }}
tag: ${{ steps.get_version.outputs.TAG }}
build-and-release:
needs: create-realese
name: Build and Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build it and upload
uses: jumpserver/action-build-upload-assets@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create-realese.outputs.upload_url }}

4
.gitignore vendored
View File

@@ -35,4 +35,6 @@ docs/_build/
xpack xpack
logs/* logs/*
### Vagrant ### ### Vagrant ###
.vagrant/ .vagrant/
release/*
releashe

View File

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

View File

@@ -2,6 +2,11 @@
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/) [![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/) [![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](https://hub.docker.com/u/jumpserver)
|Developer Wanted|
|------------------|
|JumpServer 正在寻找开发者,一起为改变世界做些贡献吧,哪怕一点点,联系我 <ibuler@fit2cloud.com> |
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。 JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
@@ -20,7 +25,24 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验; - 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
- 多云支持: 一套系统,同时管理不同云上面的资产; - 多云支持: 一套系统,同时管理不同云上面的资产;
- 云端存储: 审计录像云端存储,永不丢失; - 云端存储: 审计录像云端存储,永不丢失;
- 多租户: 一套系统,多个子公司和部门同时使用 - 多租户: 一套系统,多个子公司和部门同时使用
- 多应用支持: 数据库Windows远程应用Kubernetes。
## 版本说明
自 v2.0.0 发布后, JumpServer 版本号命名将变更为v大版本.功能版本.Bug修复版本。比如
```
v2.0.1 是 v2.0.0 之后的Bug修复版本
v2.1.0 是 v2.0.0 之后的功能版本。
```
像其它优秀开源项目一样JumpServer 每个月会发布一个功能版本,并同时维护 3 个功能版本。比如:
```
在 v2.4 发布前,我们会同时维护 v2.1、v2.2、v2.3
在 v2.4 发布后,我们会同时维护 v2.2、v2.3、v2.4v2.1 会停止维护。
```
## 功能列表 ## 功能列表
@@ -177,13 +199,76 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
<td>文件传输</td> <td>文件传输</td>
<td>可对文件的上传、下载记录进行审计</td> <td>可对文件的上传、下载记录进行审计</td>
</tr> </tr>
<tr>
<td rowspan="20">数据库审计<br>Database</td>
<td rowspan="2">连接方式</td>
<td>命令方式</td>
</tr>
<tr>
<td>Web UI方式 (X-PACK)</td>
</tr>
<tr>
<td rowspan="4">支持的数据库</td>
<td>MySQL</td>
</tr>
<tr>
<td>Oracle (X-PACK)</td>
</tr>
<tr>
<td>MariaDB (X-PACK)</td>
</tr>
<tr>
<td>PostgreSQL (X-PACK)</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> </table>
## 快速开始 ## 快速开始
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/) - [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
- [完整文档](https://docs.jumpserver.org) - [完整文档](https://docs.jumpserver.org)
- [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4) - [演示视频](https://www.bilibili.com/video/BV1ZV41127GB)
## 组件项目
- [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/)
## 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化连接依赖
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库JumpServer Web数据库依赖
## JumpServer 企业版
- [申请企业版试用](https://jinshuju.net/f/kyOYpi)
> 注:企业版支持离线安装,申请通过后会提供高速下载链接。
## 案例研究 ## 案例研究

2
apps/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
*.js linguist-language=python
*.html linguist-language=python

View File

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

View File

@@ -0,0 +1,20 @@
# coding: utf-8
#
from orgs.mixins.api import OrgBulkModelViewSet
from .mixin import ApplicationAttrsSerializerViewMixin
from ..hands import IsOrgAdminOrAppUser
from .. import models, serializers
__all__ = [
'ApplicationViewSet',
]
class ApplicationViewSet(ApplicationAttrsSerializerViewMixin, OrgBulkModelViewSet):
model = models.Application
filter_fields = ('name', 'type', 'category')
search_fields = filter_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.ApplicationSerializer

View File

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

View File

@@ -0,0 +1,139 @@
import uuid
from common.exceptions import JMSException
from orgs.models import Organization
from .. import models
class ApplicationAttrsSerializerViewMixin:
def get_serializer_class(self):
serializer_class = super().get_serializer_class()
if getattr(self, 'swagger_fake_view', False):
return serializer_class
app_type = self.request.query_params.get('type')
app_category = self.request.query_params.get('category')
type_options = list(dict(models.Category.get_all_type_serializer_mapper()).keys())
category_options = list(dict(models.Category.get_category_serializer_mapper()).keys())
# ListAPIView 没有 action 属性
# 不使用method属性因为options请求时为method为post
action = getattr(self, 'action', 'list')
if app_type and app_type not in type_options:
raise JMSException(
'Invalid query parameter `type`, select from the following options: {}'
''.format(type_options)
)
if app_category and app_category not in category_options:
raise JMSException(
'Invalid query parameter `category`, select from the following options: {}'
''.format(category_options)
)
if action in [
'create', 'update', 'partial_update', 'bulk_update', 'partial_bulk_update'
] and not app_type:
# action: create / update
raise JMSException(
'The `{}` action must take the `type` query parameter'.format(action)
)
if app_type:
# action: create / update / list / retrieve / metadata
attrs_cls = models.Category.get_type_serializer_cls(app_type)
class_name = 'ApplicationDynamicSerializer{}'.format(app_type.title())
elif app_category:
# action: list / retrieve / metadata
attrs_cls = models.Category.get_category_serializer_cls(app_category)
class_name = 'ApplicationDynamicSerializer{}'.format(app_category.title())
else:
attrs_cls = models.Category.get_no_password_serializer_cls()
class_name = 'ApplicationDynamicSerializer'
cls = type(class_name, (serializer_class,), {'attrs': attrs_cls()})
return cls
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):
organizations_id = set(applications.values_list('org_id', flat=True))
organizations = [Organization.get_instance(org_id) for org_id in organizations_id]
return organizations
def serialize_applications_with_org(self, applications):
organizations = self.filter_organizations(applications)
data_organizations = self.serialize_organizations(organizations)
data_applications = self.serialize_applications(applications)
data = data_organizations + data_applications
return data

View File

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

View File

@@ -23,6 +23,7 @@ REMOTE_APP_TYPE_CHROME_FIELDS = [
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [ REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
{'name': 'mysql_workbench_ip'}, {'name': 'mysql_workbench_ip'},
{'name': 'mysql_workbench_name'}, {'name': 'mysql_workbench_name'},
{'name': 'mysql_workbench_port'},
{'name': 'mysql_workbench_username'}, {'name': 'mysql_workbench_username'},
{'name': 'mysql_workbench_password', 'write_only': True} {'name': 'mysql_workbench_password', 'write_only': True}
] ]

View File

@@ -1,2 +0,0 @@
from .remote_app import *
from .database_app import *

View File

@@ -1,26 +0,0 @@
# coding: utf-8
#
from django import forms
from django.utils.translation import ugettext_lazy as _
from .. import models
__all__ = ['DatabaseAppMySQLForm']
class BaseDatabaseAppForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['type'].widget.attrs['disabled'] = True
class Meta:
model = models.DatabaseApp
fields = [
'name', 'type', 'host', 'port', 'database', 'comment'
]
class DatabaseAppMySQLForm(BaseDatabaseAppForm):
pass

View File

@@ -1,120 +0,0 @@
# coding: utf-8
#
from django.utils.translation import ugettext as _
from django import forms
from orgs.mixins.forms import OrgModelForm
from ..models import RemoteApp
__all__ = [
'RemoteAppChromeForm', 'RemoteAppMySQLWorkbenchForm',
'RemoteAppVMwareForm', 'RemoteAppCustomForm'
]
class BaseRemoteAppForm(OrgModelForm):
default_initial_data = {}
def __init__(self, *args, **kwargs):
# 过滤RDP资产和系统用户
super().__init__(*args, **kwargs)
field_asset = self.fields['asset']
field_asset.queryset = field_asset.queryset.has_protocol('rdp')
self.fields['type'].widget.attrs['disabled'] = True
self.fields.move_to_end('comment')
self.initial_default()
def initial_default(self):
for name, value in self.default_initial_data.items():
field = self.fields.get(name)
if not field:
continue
field.initial = value
class Meta:
model = RemoteApp
fields = [
'name', 'asset', 'type', 'path', 'comment'
]
widgets = {
'asset': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Asset')
}),
}
class RemoteAppChromeForm(BaseRemoteAppForm):
default_initial_data = {
'path': r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
}
chrome_target = forms.CharField(
max_length=128, label=_('Target URL'), required=False
)
chrome_username = forms.CharField(
max_length=128, label=_('Login username'), required=False
)
chrome_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
max_length=128, label=_('Login password'), required=False
)
class RemoteAppMySQLWorkbenchForm(BaseRemoteAppForm):
default_initial_data = {
'path': r'C:\Program Files\MySQL\MySQL Workbench 8.0 CE'
r'\MySQLWorkbench.exe'
}
mysql_workbench_ip = forms.CharField(
max_length=128, label=_('Database IP'), required=False
)
mysql_workbench_name = forms.CharField(
max_length=128, label=_('Database name'), required=False
)
mysql_workbench_username = forms.CharField(
max_length=128, label=_('Database username'), required=False
)
mysql_workbench_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
max_length=128, label=_('Database password'), required=False
)
class RemoteAppVMwareForm(BaseRemoteAppForm):
default_initial_data = {
'path': r'C:\Program Files (x86)\VMware\Infrastructure'
r'\Virtual Infrastructure Client\Launcher\VpxClient.exe'
}
vmware_target = forms.CharField(
max_length=128, label=_('Target address'), required=False
)
vmware_username = forms.CharField(
max_length=128, label=_('Login username'), required=False
)
vmware_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
max_length=128, label=_('Login password'), required=False
)
class RemoteAppCustomForm(BaseRemoteAppForm):
custom_cmdline = forms.CharField(
max_length=128, label=_('Operating parameter'), required=False
)
custom_target = forms.CharField(
max_length=128, label=_('Target address'), required=False
)
custom_username = forms.CharField(
max_length=128, label=_('Login username'), required=False
)
custom_password = forms.CharField(
widget=forms.PasswordInput, strip=True,
max_length=128, label=_('Login password'), required=False
)

View File

@@ -0,0 +1,34 @@
# Generated by Django 2.2.13 on 2020-08-07 07:13
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('applications', '0004_auto_20191218_1705'),
]
operations = [
migrations.CreateModel(
name='K8sApp',
fields=[
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('type', models.CharField(choices=[('k8s', 'Kubernetes')], default='k8s', max_length=128, verbose_name='Type')),
('cluster', models.CharField(max_length=1024, verbose_name='Cluster')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
],
options={
'verbose_name': 'KubernetesApp',
'ordering': ('name',),
'unique_together': {('org_id', 'name')},
},
),
]

View File

@@ -0,0 +1,140 @@
# Generated by Django 2.2.13 on 2020-10-19 12:01
from django.db import migrations, models
import django.db.models.deletion
import django_mysql.models
import uuid
CATEGORY_DB_LIST = ['mysql', 'oracle', 'postgresql', 'mariadb']
CATEGORY_REMOTE_LIST = ['chrome', 'mysql_workbench', 'vmware_client', 'custom']
CATEGORY_CLOUD_LIST = ['k8s']
CATEGORY_DB = 'db'
CATEGORY_REMOTE = 'remote_app'
CATEGORY_CLOUD = 'cloud'
CATEGORY_LIST = [CATEGORY_DB, CATEGORY_REMOTE, CATEGORY_CLOUD]
def get_application_category(old_app):
_type = old_app.type
if _type in CATEGORY_DB_LIST:
category = CATEGORY_DB
elif _type in CATEGORY_REMOTE_LIST:
category = CATEGORY_REMOTE
elif _type in CATEGORY_CLOUD_LIST:
category = CATEGORY_CLOUD
else:
category = None
return category
def common_to_application_json(old_app):
category = get_application_category(old_app)
date_updated = old_app.date_updated if hasattr(old_app, 'date_updated') else old_app.date_created
return {
'id': old_app.id,
'name': old_app.name,
'type': old_app.type,
'category': category,
'comment': old_app.comment,
'created_by': old_app.created_by,
'date_created': old_app.date_created,
'date_updated': date_updated,
'org_id': old_app.org_id
}
def db_to_application_json(database):
app_json = common_to_application_json(database)
app_json.update({
'attrs': {
'host': database.host,
'port': database.port,
'database': database.database
}
})
return app_json
def remote_to_application_json(remote):
app_json = common_to_application_json(remote)
attrs = {
'asset': str(remote.asset.id),
'path': remote.path,
}
attrs.update(remote.params)
app_json.update({
'attrs': attrs
})
return app_json
def k8s_to_application_json(k8s):
app_json = common_to_application_json(k8s)
app_json.update({
'attrs': {
'cluster': k8s.cluster
}
})
return app_json
def migrate_and_integrate_applications(apps, schema_editor):
db_alias = schema_editor.connection.alias
database_app_model = apps.get_model("applications", "DatabaseApp")
remote_app_model = apps.get_model("applications", "RemoteApp")
k8s_app_model = apps.get_model("applications", "K8sApp")
database_apps = database_app_model.objects.using(db_alias).all()
remote_apps = remote_app_model.objects.using(db_alias).all()
k8s_apps = k8s_app_model.objects.using(db_alias).all()
database_applications = [db_to_application_json(db_app) for db_app in database_apps]
remote_applications = [remote_to_application_json(remote_app) for remote_app in remote_apps]
k8s_applications = [k8s_to_application_json(k8s_app) for k8s_app in k8s_apps]
applications_json = database_applications + remote_applications + k8s_applications
application_model = apps.get_model("applications", "Application")
applications = [
application_model(**application_json)
for application_json in applications_json
if application_json['category'] in CATEGORY_LIST
]
for application in applications:
if application_model.objects.using(db_alias).filter(name=application.name).exists():
application.name = '{}-{}'.format(application.name, application.type)
application.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0057_fill_node_value_assets_amount_and_parent_key'),
('applications', '0005_k8sapp'),
]
operations = [
migrations.CreateModel(
name='Application',
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
('attrs', django_mysql.models.JSONField(default=dict)),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
],
options={
'ordering': ('name',),
'unique_together': {('org_id', 'name')},
},
),
migrations.RunPython(migrate_and_integrate_applications),
]

View File

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

View File

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

View File

@@ -0,0 +1,140 @@
from itertools import chain
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from common.db.models import ChoiceSet
class DBType(ChoiceSet):
mysql = 'mysql', 'MySQL'
oracle = 'oracle', 'Oracle'
pgsql = 'postgresql', 'PostgreSQL'
mariadb = 'mariadb', 'MariaDB'
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import database_app
mapper = {
cls.mysql: database_app.MySQLAttrsSerializer,
cls.oracle: database_app.OracleAttrsSerializer,
cls.pgsql: database_app.PostgreAttrsSerializer,
cls.mariadb: database_app.MariaDBAttrsSerializer,
}
return mapper
class RemoteAppType(ChoiceSet):
chrome = 'chrome', 'Chrome'
mysql_workbench = 'mysql_workbench', 'MySQL Workbench'
vmware_client = 'vmware_client', 'vSphere Client'
custom = 'custom', _('Custom')
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import remote_app
mapper = {
cls.chrome: remote_app.ChromeAttrsSerializer,
cls.mysql_workbench: remote_app.MySQLWorkbenchAttrsSerializer,
cls.vmware_client: remote_app.VMwareClientAttrsSerializer,
cls.custom: remote_app.CustomRemoteAppAttrsSeralizers,
}
return mapper
class CloudType(ChoiceSet):
k8s = 'k8s', 'Kubernetes'
@classmethod
def get_type_serializer_cls_mapper(cls):
from ..serializers import k8s_app
mapper = {
cls.k8s: k8s_app.K8sAttrsSerializer,
}
return mapper
class Category(ChoiceSet):
db = 'db', _('Database')
remote_app = 'remote_app', _('Remote app')
cloud = 'cloud', 'Cloud'
@classmethod
def get_category_type_mapper(cls):
return {
cls.db: DBType,
cls.remote_app: RemoteAppType,
cls.cloud: CloudType
}
@classmethod
def get_category_type_choices_mapper(cls):
return {
name: tp.choices
for name, tp in cls.get_category_type_mapper().items()
}
@classmethod
def get_type_choices(cls, category):
return cls.get_category_type_choices_mapper().get(category, [])
@classmethod
def get_all_type_choices(cls):
all_grouped_choices = tuple(cls.get_category_type_choices_mapper().values())
return tuple(chain(*all_grouped_choices))
@classmethod
def get_all_type_serializer_mapper(cls):
mapper = {}
for tp in cls.get_category_type_mapper().values():
mapper.update(tp.get_type_serializer_cls_mapper())
return mapper
@classmethod
def get_type_serializer_cls(cls, tp):
mapper = cls.get_all_type_serializer_mapper()
return mapper.get(tp, None)
@classmethod
def get_category_serializer_mapper(cls):
from ..serializers import remote_app, database_app, k8s_app
return {
cls.db: database_app.DBAttrsSerializer,
cls.remote_app: remote_app.RemoteAppAttrsSerializer,
cls.cloud: k8s_app.CloudAttrsSerializer,
}
@classmethod
def get_category_serializer_cls(cls, cg):
mapper = cls.get_category_serializer_mapper()
return mapper.get(cg, None)
@classmethod
def get_no_password_serializer_cls(cls):
from ..serializers import common
return common.NoPasswordSerializer
class Application(CommonModelMixin, OrgModelMixin):
name = models.CharField(max_length=128, verbose_name=_('Name'))
domain = models.ForeignKey('assets.Domain', null=True, blank=True, related_name='applications', verbose_name=_("Domain"), on_delete=models.SET_NULL)
category = models.CharField(max_length=16, choices=Category.choices, verbose_name=_('Category'))
type = models.CharField(max_length=16, choices=Category.get_all_type_choices(), verbose_name=_('Type'))
attrs = models.JSONField()
comment = models.TextField(
max_length=128, default='', blank=True, verbose_name=_('Comment')
)
class Meta:
unique_together = [('org_id', 'name')]
ordering = ('name',)
def __str__(self):
category_display = self.get_category_display()
type_display = self.get_type_display()
return f'{self.name}({type_display})[{category_display}]'
def category_is_remote_app(self):
return self.category == Category.remote_app

View File

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

View File

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

View File

@@ -0,0 +1,42 @@
# coding: utf-8
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .. import models
__all__ = [
'ApplicationSerializer',
]
class ApplicationSerializer(BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
class Meta:
model = models.Application
fields = [
'id', 'name', 'category', 'category_display', 'type', 'type_display', 'attrs',
'domain', 'created_by', 'date_created', 'date_updated', 'comment'
]
read_only_fields = [
'created_by', 'date_created', 'date_updated', 'get_type_display',
]
def create(self, validated_data):
validated_data['attrs'] = validated_data.pop('attrs', {})
instance = super().create(validated_data)
return instance
def update(self, instance, validated_data):
new_attrs = validated_data.pop('attrs', {})
instance = super().update(instance, validated_data)
attrs = instance.attrs
attrs.update(new_attrs)
instance.attrs = attrs
instance.save()
return instance

View File

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

View File

@@ -1,14 +1,35 @@
# coding: utf-8 # coding: utf-8
# #
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from .. import models from .. import models
__all__ = [
'DatabaseAppSerializer', class DBAttrsSerializer(serializers.Serializer):
] host = serializers.CharField(max_length=128, label=_('Host'))
port = serializers.IntegerField(label=_('Port'))
# 添加allow_null=True兼容之前数据库中database字段为None的情况
database = serializers.CharField(max_length=128, required=True, allow_null=True, label=_('Database'))
class MySQLAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=3306, label=_('Port'))
class PostgreAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=5432, label=_('Port'))
class OracleAttrsSerializer(DBAttrsSerializer):
port = serializers.IntegerField(default=1521, label=_('Port'))
class MariaDBAttrsSerializer(MySQLAttrsSerializer):
pass
class DatabaseAppSerializer(BulkOrgResourceModelSerializer): class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
@@ -24,3 +45,6 @@ class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
'created_by', 'date_created', 'date_updated' 'created_by', 'date_created', 'date_updated'
'get_type_display', 'get_type_display',
] ]
extra_kwargs = {
'get_type_display': {'label': _('Type for display')},
}

View File

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

View File

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

View File

@@ -1,55 +0,0 @@
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="DatabaseAppForm" method="post" class="form-horizontal">
{% bootstrap_form form layout="horizontal" %}
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
{% endblock %}
{% block custom_foot_js %}
<script type="text/javascript">
var app_type_id = '#' + '{{ form.type.id_for_label }}';
function getFormDataType(){
return $(app_type_id+ " option:selected").val();
}
function getFormData(form){
var data = form.serializeObject();
data['type'] = getFormDataType();
return data
}
$(document).ready(function () {
})
.on("submit", "form", function (evt) {
evt.preventDefault();
var the_url = '{% url "api-applications:database-app-list" %}';
var redirect_to = '{% url "applications:database-app-list" %}';
var method = "POST";
{% if api_action == "update" %}
the_url = '{% url "api-applications:database-app-detail" object.id %}';
method = "PUT";
{% endif %}
var form = $("form");
var data = getFormData(form);
var props = {
url: the_url,
data: data,
method: method,
form: form,
redirect_to: redirect_to
};
formSubmit(props);
});
</script>
{% endblock %}

View File

@@ -1,103 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li class="active">
<a href="{% url 'applications:database-app-detail' pk=database_app.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'applications:database-app-update' pk=database_app.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-delete-application">
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label"><b>{{ database_app.name }}</b></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td>{% trans 'Name' %}:</td>
<td><b>{{ database_app.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Type' %}:</td>
<td><b>{{ database_app.get_type_display }}</b></td>
</tr>
<tr>
<td>{% trans 'Host' %}:</td>
<td><b>{{ database_app.host }}</b></td>
</tr>
<tr>
<td>{% trans 'Port' %}:</td>
<td><b>{{ database_app.port }}</b></td>
</tr>
<tr>
<td>{% trans 'Database' %}:</td>
<td><b>{{ database_app.database }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ database_app.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ database_app.created_by }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ database_app.comment }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
})
.on('click', '.btn-delete-application', function () {
var $this = $(this);
var name = "{{ database_app.name }}";
var rid = "{{ database_app.id }}";
var the_url = '{% url "api-applications:database-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
var redirect_url = "{% url 'applications:database-app-list' %}";
objectDelete($this, name, the_url, redirect_url);
})
</script>
{% endblock %}

View File

@@ -1,88 +0,0 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block help_message %}
{% endblock %}
{% block table_search %}{% endblock %}
{% block table_container %}
<div class="btn-group uc pull-left m-r-5">
<button data-toggle="dropdown" class="btn btn-primary btn-sm dropdown-toggle">
{% trans "Create DatabaseApp" %}
<span class="caret"></span></button>
<ul class="dropdown-menu">
{% for key, value in type_choices %}
<li><a class="" href="{% url 'applications:database-app-create' %}?type={{ key }}">{{ value }}</a></li>
{% endfor %}
</ul>
</div>
<table class="table table-striped table-bordered table-hover " id="database_app_list_table" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Type' %}</th>
<th class="text-center">{% trans 'Host' %}</th>
<th class="text-center">{% trans 'Port' %}</th>
<th class="text-center">{% trans 'Database' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#database_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'applications:database-app-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 2, createdCell: function (td, cellData, rowData) {
$(td).html(rowData.get_type_display)
}},
{targets: 7, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "applications:database-app-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-rid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-applications:database-app-list" %}',
columns: [
{data: "id"},
{data: "name" },
{data: "type"},
{data: "host"},
{data: "port"},
{data: "database"},
{data: "comment"},
{data: "id", orderable: false, width: "120px"}
],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#database_app_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var rid = $this.data('rid');
var the_url = '{% url "api-applications:database-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}

View File

@@ -1,71 +0,0 @@
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="RemoteAppForm" method="post" class="form-horizontal">
{% bootstrap_form form layout="horizontal" %}
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
{% endblock %}
{% block custom_foot_js %}
<script type="text/javascript">
var app_type_id = '#' + '{{ form.type.id_for_label }}';
function getFormDataType(){
return $(app_type_id+ " option:selected").val();
}
function constructFormDataParams(data){
var params = {};
var type =data.type;
for (var k in data){
if (k.startsWith(type)){
params[k] = data[k];
delete data[k]
}
}
return params
}
function getFormData(form){
var data = form.serializeObject();
data['type'] = getFormDataType();
data['params'] = constructFormDataParams(data);
return data
}
$(document).ready(function () {
$('.select2').select2({
closeOnSelect: true
});
}).on("submit", "form", function (evt) {
evt.preventDefault();
var the_url = '{% url "api-applications:remote-app-list" %}';
var redirect_to = '{% url "applications:remote-app-list" %}';
var method = "POST";
{% if api_action == "update" %}
the_url = '{% url "api-applications:remote-app-detail" object.id %}';
method = "PUT";
{% endif %}
var form = $("form");
var data = getFormData(form);
var props = {
url: the_url,
data: data,
method: method,
form: form,
redirect_to: redirect_to
};
formSubmit(props);
})
;
</script>
{% endblock %}

View File

@@ -1,100 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li class="active">
<a href="{% url 'applications:remote-app-detail' pk=remote_app.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'applications:remote-app-update' pk=remote_app.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-delete-application">
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label"><b>{{ remote_app.name }}</b></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td>{% trans 'Name' %}:</td>
<td><b>{{ remote_app.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Asset' %}:</td>
<td><b><a href="{% url 'assets:asset-detail' pk=remote_app.asset.id %}">{{ remote_app.asset.hostname }}</a></b></td>
</tr>
<tr>
<td>{% trans 'App type' %}:</td>
<td><b>{{ remote_app.get_type_display }}</b></td>
</tr>
<tr>
<td>{% trans 'App path' %}:</td>
<td><b>{{ remote_app.path }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ remote_app.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ remote_app.created_by }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ remote_app.comment }}</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
jumpserver.nodes_selected = {};
$(document).ready(function () {
})
.on('click', '.btn-delete-application', function () {
var $this = $(this);
var name = "{{ remote_app.name }}";
var rid = "{{ remote_app.id }}";
var the_url = '{% url "api-applications:remote-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
var redirect_url = "{% url 'applications:remote-app-list' %}";
objectDelete($this, name, the_url, redirect_url);
})
</script>
{% endblock %}

View File

@@ -1,92 +0,0 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block help_message %}
{% trans 'Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application' %}
<b><a href="https://github.com/jumpserver/Jmservisor/releases" target="view_window" >{% trans 'Download application loader' %}</a></b>
{% endblock %}
{% block table_search %}{% endblock %}
{% block table_container %}
<div class="btn-group uc pull-left m-r-5">
<button data-toggle="dropdown" class="btn btn-primary btn-sm dropdown-toggle">
{% trans "Create RemoteApp" %}
<span class="caret"></span></button>
<ul class="dropdown-menu">
{% for key, value in type_choices %}
<li><a class="" href="{% url 'applications:remote-app-create' %}?type={{ key }}">{{ value }}</a></li>
{% endfor %}
</ul>
</div>
<table class="table table-striped table-bordered table-hover " id="remote_app_list_table" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'App type' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#remote_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'applications:remote-app-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var hostname = htmlEscape(cellData.hostname);
var detail_btn = '<a href="{% url 'assets:asset-detail' pk=DEFAULT_PK %}">' + hostname + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', cellData.id));
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var comment = htmlEscape(cellData);
$(td).html(comment)
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "applications:remote-app-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-delete" data-rid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-applications:remote-app-list" %}',
columns: [
{data: "id"},
{data: "name" },
{data: "get_type_display", orderable: false},
{data: "asset_info", orderable: false},
{data: "comment"},
{data: "id", orderable: false, width: "120px"}
],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#remote_app_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var rid = $this.data('rid');
var the_url = '{% url "api-applications:remote-app-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', rid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}

View File

@@ -1,83 +0,0 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block custom_head_css_js %}
<script src="{% static 'js/jquery.form.min.js' %}"></script>
{% endblock %}
{% block content %}
<div class="mail-box-header">
<table class="table table-striped table-bordered table-hover " id="database_app_list_table" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Type' %}</th>
<th class="text-center">{% trans 'Host' %}</th>
<th class="text-center">{% trans 'Database' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var inited = false;
var database_app_table, url;
function initTable() {
if (inited){
return
} else {
inited = true;
}
url = '{% url "api-perms:my-database-apps" %}';
var options = {
ele: $('#database_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var name = htmlEscape(cellData);
$(td).html(name)
}},
{targets: 2, createdCell: function (td, cellData, rowData) {
var type = htmlEscape(rowData.get_type_display);
$(td).html(type);
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var host = htmlEscape(cellData);
$(td).html(host);
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var database = htmlEscape(cellData);
$(td).html(database);
}},
{targets: 6, createdCell: function (td, cellData, rowData) {
var conn_btn = '<a href="{% url "luna-view" %}?type=database_app&login_to=' + cellData +'" class="btn btn-xs btn-primary" target="_blank">{% trans "Connect" %}</a>';
$(td).html(conn_btn)
}}
],
ajax_url: url,
columns: [
{data: "id"},
{data: "name"},
{data: "type"},
{data: "host"},
{data: "database"},
{data: "comment", orderable: false},
{data: "id", orderable: false}
]
};
database_app_table = jumpserver.initServerSideDataTable(options);
return database_app_table
}
$(document).ready(function(){
initTable();
})
</script>
{% endblock %}

View File

@@ -1,73 +0,0 @@
{% extends 'base.html' %}
{% load i18n static %}
{% block custom_head_css_js %}
<script src="{% static 'js/jquery.form.min.js' %}"></script>
{% endblock %}
{% block content %}
<div class="mail-box-header">
<table class="table table-striped table-bordered table-hover " id="remote_app_list_table" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'App type' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var inited = false;
var remote_app_table, url;
function initTable() {
if (inited){
return
} else {
inited = true;
}
url = '{% url "api-perms:my-remote-apps" %}';
var options = {
ele: $('#remote_app_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var name = htmlEscape(cellData);
$(td).html(name)
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var hostname = htmlEscape(cellData.hostname);
$(td).html(hostname);
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var conn_btn = '<a href="{% url "luna-view" %}?type=remote_app&login_to=' + cellData +'" class="btn btn-xs btn-primary" target="_blank">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(conn_btn)
}}
],
ajax_url: url,
columns: [
{data: "id"},
{data: "name"},
{data: "get_type_display", orderable: false},
{data: "asset_info", orderable: false},
{data: "comment", orderable: false},
{data: "id", orderable: false}
]
};
remote_app_table = jumpserver.initServerSideDataTable(options);
return remote_app_table
}
$(document).ready(function(){
initTable();
})
</script>
{% endblock %}

View File

@@ -10,8 +10,10 @@ from .. import api
app_name = 'applications' app_name = 'applications'
router = BulkRouter() router = BulkRouter()
router.register(r'applications', api.ApplicationViewSet, 'application')
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app') router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app') router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
urlpatterns = [ urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'), path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),

View File

@@ -1,23 +1,7 @@
# coding:utf-8 # coding:utf-8
from django.urls import path from django.urls import path
from .. import views
app_name = 'applications' app_name = 'applications'
urlpatterns = [ urlpatterns = [
# RemoteApp
path('remote-app/', views.RemoteAppListView.as_view(), name='remote-app-list'),
path('remote-app/create/', views.RemoteAppCreateView.as_view(), name='remote-app-create'),
path('remote-app/<uuid:pk>/update/', views.RemoteAppUpdateView.as_view(), name='remote-app-update'),
path('remote-app/<uuid:pk>/', views.RemoteAppDetailView.as_view(), name='remote-app-detail'),
# User RemoteApp view
path('user-remote-app/', views.UserRemoteAppListView.as_view(), name='user-remote-app-list'),
path('database-app/', views.DatabaseAppListView.as_view(), name='database-app-list'),
path('database-app/create/', views.DatabaseAppCreateView.as_view(), name='database-app-create'),
path('database-app/<uuid:pk>/update/', views.DatabaseAppUpdateView.as_view(), name='database-app-update'),
path('database-app/<uuid:pk>/', views.DatabaseAppDetailView.as_view(), name='database-app-detail'),
# User DatabaseApp view
path('user-database-app/', views.UserDatabaseAppListView.as_view(), name='user-database-app-list'),
] ]

View File

@@ -1,2 +0,0 @@
from .remote_app import *
from .database_app import *

View File

@@ -1,115 +0,0 @@
# coding: utf-8
#
from django.http import Http404
from django.views.generic import TemplateView
from django.views.generic.edit import CreateView, UpdateView
from django.utils.translation import ugettext_lazy as _
from django.views.generic.detail import DetailView
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
from .. import models, const, forms
__all__ = [
'DatabaseAppListView', 'DatabaseAppCreateView', 'DatabaseAppUpdateView',
'DatabaseAppDetailView', 'UserDatabaseAppListView',
]
class DatabaseAppListView(PermissionsMixin, TemplateView):
template_name = 'applications/database_app_list.html'
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _("Application"),
'action': _('DatabaseApp list'),
'type_choices': const.DATABASE_APP_TYPE_CHOICES
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class BaseDatabaseAppCreateUpdateView:
template_name = 'applications/database_app_create_update.html'
model = models.DatabaseApp
permission_classes = [IsOrgAdmin]
default_type = const.DATABASE_APP_TYPE_MYSQL
form_class = forms.DatabaseAppMySQLForm
form_class_choices = {
const.DATABASE_APP_TYPE_MYSQL: forms.DatabaseAppMySQLForm,
}
def get_initial(self):
return {'type': self.get_type()}
def get_type(self):
return self.default_type
def get_form_class(self):
tp = self.get_type()
form_class = self.form_class_choices.get(tp)
if not form_class:
raise Http404()
return form_class
class DatabaseAppCreateView(BaseDatabaseAppCreateUpdateView, CreateView):
def get_type(self):
tp = self.request.GET.get("type")
if tp:
return tp.lower()
return super().get_type()
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('Create DatabaseApp'),
'api_action': 'create'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DatabaseAppUpdateView(BaseDatabaseAppCreateUpdateView, UpdateView):
def get_type(self):
return self.object.type
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('Create DatabaseApp'),
'api_action': 'update'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class DatabaseAppDetailView(PermissionsMixin, DetailView):
template_name = 'applications/database_app_detail.html'
model = models.DatabaseApp
context_object_name = 'database_app'
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('DatabaseApp detail'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserDatabaseAppListView(PermissionsMixin, TemplateView):
template_name = 'applications/user_database_app_list.html'
permission_classes = [IsValidUser]
def get_context_data(self, **kwargs):
context = {
'action': _('My DatabaseApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@@ -1,128 +0,0 @@
# coding: utf-8
#
from django.http import Http404
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from django.views.generic.edit import CreateView, UpdateView
from django.views.generic.detail import DetailView
from common.permissions import PermissionsMixin, IsOrgAdmin, IsValidUser
from ..models import RemoteApp
from .. import forms, const
__all__ = [
'RemoteAppListView', 'RemoteAppCreateView', 'RemoteAppUpdateView',
'RemoteAppDetailView', 'UserRemoteAppListView',
]
class RemoteAppListView(PermissionsMixin, TemplateView):
template_name = 'applications/remote_app_list.html'
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('RemoteApp list'),
'type_choices': const.REMOTE_APP_TYPE_CHOICES,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class BaseRemoteAppCreateUpdateView:
template_name = 'applications/remote_app_create_update.html'
model = RemoteApp
permission_classes = [IsOrgAdmin]
default_type = const.REMOTE_APP_TYPE_CHROME
form_class = forms.RemoteAppChromeForm
form_class_choices = {
const.REMOTE_APP_TYPE_CHROME: forms.RemoteAppChromeForm,
const.REMOTE_APP_TYPE_MYSQL_WORKBENCH: forms.RemoteAppMySQLWorkbenchForm,
const.REMOTE_APP_TYPE_VMWARE_CLIENT: forms.RemoteAppVMwareForm,
const.REMOTE_APP_TYPE_CUSTOM: forms.RemoteAppCustomForm
}
def get_initial(self):
return {'type': self.get_type()}
def get_type(self):
return self.default_type
def get_form_class(self):
tp = self.get_type()
form_class = self.form_class_choices.get(tp)
if not form_class:
raise Http404()
return form_class
class RemoteAppCreateView(BaseRemoteAppCreateUpdateView,
PermissionsMixin, CreateView):
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('Create RemoteApp'),
'api_action': 'create'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
def get_type(self):
tp = self.request.GET.get("type")
if tp:
return tp.lower()
return super().get_type()
class RemoteAppUpdateView(BaseRemoteAppCreateUpdateView,
PermissionsMixin, UpdateView):
def get_initial(self):
initial_data = super().get_initial()
params = {k: v for k, v in self.object.params.items()}
initial_data.update(params)
return initial_data
def get_type(self):
return self.object.type
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('Update RemoteApp'),
'api_action': 'update'
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class RemoteAppDetailView(PermissionsMixin, DetailView):
template_name = 'applications/remote_app_detail.html'
model = RemoteApp
context_object_name = 'remote_app'
permission_classes = [IsOrgAdmin]
def get_context_data(self, **kwargs):
context = {
'app': _('Applications'),
'action': _('RemoteApp detail'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserRemoteAppListView(PermissionsMixin, TemplateView):
template_name = 'applications/user_remote_app_list.html'
permission_classes = [IsValidUser]
def get_context_data(self, **kwargs):
context = {
'action': _('My RemoteApp'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@@ -1,3 +1,4 @@
from .mixin import *
from .admin_user import * from .admin_user import *
from .asset import * from .asset import *
from .label import * from .label import *

View File

@@ -1,17 +1,4 @@
# ~*~ coding: utf-8 ~*~
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
#
# Licensed under the GNU General Public License v2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://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.
from django.db import transaction from django.db import transaction
from django.db.models import Count from django.db.models import Count
@@ -49,7 +36,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets')) queryset = queryset.annotate(assets_amount=Count('assets'))
return queryset return queryset
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
@@ -107,7 +94,6 @@ class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip") filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields search_fields = filter_fields
def get_object(self): def get_object(self):

View File

@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from assets.api import FilterAssetByNodeMixin
import random
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView from rest_framework.generics import RetrieveAPIView
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -17,7 +14,7 @@ from .. import serializers
from ..tasks import ( from ..tasks import (
update_asset_hardware_info_manual, test_asset_connectivity_manual update_asset_hardware_info_manual, test_asset_connectivity_manual
) )
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -28,12 +25,15 @@ __all__ = [
] ]
class AssetViewSet(OrgBulkModelViewSet): class AssetViewSet(FilterAssetByNodeMixin, OrgBulkModelViewSet):
""" """
API endpoint that allows Asset to be viewed or edited. API endpoint that allows Asset to be viewed or edited.
""" """
model = Asset model = Asset
filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id") filter_fields = (
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
"is_active"
)
search_fields = ("hostname", "ip") search_fields = ("hostname", "ip")
ordering_fields = ("hostname", "ip", "port", "cpu_cores") ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_classes = { serializer_classes = {
@@ -41,7 +41,7 @@ class AssetViewSet(OrgBulkModelViewSet):
'display': serializers.AssetDisplaySerializer, 'display': serializers.AssetDisplaySerializer,
} }
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend] extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
def set_assets_node(self, assets): def set_assets_node(self, assets):
if not isinstance(assets, list): if not isinstance(assets, list):
@@ -74,12 +74,16 @@ class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all() queryset = Platform.objects.all()
permission_classes = (IsSuperUser,) permission_classes = (IsSuperUser,)
serializer_class = serializers.PlatformSerializer serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base'] filter_fields = ['name', 'base']
search_fields = ['name'] search_fields = ['name']
def get_permissions(self):
if self.request.method.lower() in ['get', 'options']:
self.permission_classes = (IsOrgAdmin,)
return super().get_permissions()
def check_object_permissions(self, request, obj): def check_object_permissions(self, request, obj):
if request.method.lower() in ['delete', 'put', 'patch'] and \ if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
obj.internal:
self.permission_denied( self.permission_denied(
request, message={"detail": "Internal platform"} request, message={"detail": "Internal platform"}
) )
@@ -111,7 +115,6 @@ class AssetTaskCreateApi(generics.CreateAPIView):
class AssetGatewayListApi(generics.ListAPIView): class AssetGatewayListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,) permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer serializer_class = serializers.GatewayWithAuthSerializer
model = Asset
def get_queryset(self): def get_queryset(self):
asset_id = self.kwargs.get('pk') asset_id = self.kwargs.get('pk')

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import coreapi
from django.conf import settings from django.conf import settings
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import generics, filters from rest_framework import generics, filters
@@ -54,6 +55,15 @@ class AssetUserSearchBackend(filters.BaseFilterBackend):
class AssetUserLatestFilterBackend(filters.BaseFilterBackend): 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): def filter_queryset(self, request, queryset, view):
latest = request.GET.get('latest') == '1' latest = request.GET.get('latest') == '1'
if latest: if latest:
@@ -64,7 +74,7 @@ class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet): class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
serializer_classes = { serializer_classes = {
'default': serializers.AssetUserWriteSerializer, 'default': serializers.AssetUserWriteSerializer,
'list': serializers.AssetUserReadSerializer, 'display': serializers.AssetUserReadSerializer,
'retrieve': serializers.AssetUserReadSerializer, 'retrieve': serializers.AssetUserReadSerializer,
} }
permission_classes = [IsOrgAdminOrAppUser] permission_classes = [IsOrgAdminOrAppUser]
@@ -84,12 +94,15 @@ class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
def get_object(self): def get_object(self):
pk = self.kwargs.get("pk") pk = self.kwargs.get("pk")
if pk is None:
return
queryset = self.get_queryset() queryset = self.get_queryset()
obj = queryset.get(id=pk) obj = queryset.get(id=pk)
return obj return obj
def get_exception_handler(self): def get_exception_handler(self):
def handler(e, context): def handler(e, context):
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=400) return Response({"error": str(e)}, status=400)
return handler return handler

View File

@@ -1,7 +1,9 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from rest_framework.views import APIView, Response
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext as _
from rest_framework.views import APIView, Response
from rest_framework.serializers import ValidationError
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
@@ -42,6 +44,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all()) self.object = self.get_object(Gateway.objects.all())
local_port = self.request.data.get('port') or self.object.port local_port = self.request.data.get('port') or self.object.port
try:
local_port = int(local_port)
except ValueError:
raise ValidationError({'port': _('Number required')})
ok, e = self.object.test_connective(local_port=local_port) ok, e = self.object.test_connective(local_port=local_port)
if ok: if ok:
return Response("ok") return Response("ok")

View File

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

90
apps/assets/api/mixin.py Normal file
View File

@@ -0,0 +1,90 @@
from typing import List
from assets.models import Node, Asset
from assets.pagination import AssetLimitOffsetPagination
from common.utils import lazyproperty, dict_get_any, is_uuid, get_object_or_none
from assets.utils import get_node, is_query_node_all_assets
class SerializeToTreeNodeMixin:
permission_classes = ()
def serialize_nodes(self, nodes: List[Node], with_asset_amount=False):
if with_asset_amount:
def _name(node: Node):
return '{} ({})'.format(node.value, node.assets_amount)
else:
def _name(node: Node):
return node.value
data = [
{
'id': node.key,
'name': _name(node),
'title': _name(node),
'pId': node.parent_key,
'isParent': True,
'open': node.is_org_root(),
'meta': {
'node': {
"id": node.id,
"key": node.key,
"value": node.value,
},
'type': 'node'
}
}
for node in nodes
]
return data
def get_platform(self, asset: Asset):
default = 'file'
icon = {'windows', 'linux'}
platform = asset.platform_base.lower()
if platform in icon:
return platform
return default
def serialize_assets(self, assets, node_key=None):
if node_key is None:
get_pid = lambda asset: getattr(asset, 'parent_key', '')
else:
get_pid = lambda asset: node_key
data = [
{
'id': str(asset.id),
'name': asset.hostname,
'title': asset.ip,
'pId': get_pid(asset),
'isParent': False,
'open': False,
'iconSkin': self.get_platform(asset),
'chkDisabled': not asset.is_active,
'meta': {
'type': 'asset',
'asset': {
'id': asset.id,
'hostname': asset.hostname,
'ip': asset.ip,
'protocols': asset.protocols_as_list,
'platform': asset.platform_base,
'org_name': asset.org_name
},
}
}
for asset in assets
]
return data
class FilterAssetByNodeMixin:
pagination_class = AssetLimitOffsetPagination
@lazyproperty
def is_query_node_all_assets(self):
return is_query_node_all_assets(self.request)
@lazyproperty
def node(self):
return get_node(self.request)

View File

@@ -1,16 +1,28 @@
# ~*~ coding: utf-8 ~*~ # ~*~ coding: utf-8 ~*~
from functools import partial
from collections import namedtuple, defaultdict
from collections import namedtuple
from rest_framework import status from rest_framework import status
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.decorators import action
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404, Http404 from django.shortcuts import get_object_or_404, Http404
from django.utils.decorators import method_decorator
from django.db.models.signals import m2m_changed
from common.const.http import POST
from common.exceptions import SomeoneIsDoingThis
from common.const.signals import PRE_REMOVE, POST_REMOVE
from assets.models import Asset
from common.utils import get_logger, get_object_or_none from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer from common.tree import TreeNodeSerializer
from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY
from orgs.mixins.api import OrgModelViewSet from orgs.mixins.api import OrgModelViewSet
from orgs.mixins import generics from orgs.mixins import generics
from orgs.lock import org_level_transaction_lock
from orgs.utils import current_org
from assets.tasks import check_node_assets_amount_task
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import Node from ..models import Node
from ..tasks import ( from ..tasks import (
@@ -18,12 +30,13 @@ from ..tasks import (
test_node_assets_connectivity_manual, test_node_assets_connectivity_manual,
) )
from .. import serializers from .. import serializers
from .mixin import SerializeToTreeNodeMixin
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi', 'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi', 'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi',
'NodeAddChildrenApi', 'NodeListAsTreeApi', 'NodeAddChildrenApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi', 'NodeChildrenAsTreeApi',
'NodeTaskCreateApi', 'NodeTaskCreateApi',
@@ -37,6 +50,11 @@ class NodeViewSet(OrgModelViewSet):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer serializer_class = serializers.NodeSerializer
@action(methods=[POST], detail=False, url_name='launch-check-assets-amount-task')
def launch_check_assets_amount_task(self, request):
task = check_node_assets_amount_task.delay(current_org.id)
return Response(data={'task': task.id})
# 仅支持根节点指直接创建子节点下的节点需要通过children接口创建 # 仅支持根节点指直接创建子节点下的节点需要通过children接口创建
def perform_create(self, serializer): def perform_create(self, serializer):
child_key = Node.org_root().get_next_child_key() child_key = Node.org_root().get_next_child_key()
@@ -52,6 +70,9 @@ class NodeViewSet(OrgModelViewSet):
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
node = self.get_object() node = self.get_object()
if node.is_org_root():
error = _("You can't delete the root node ({})".format(node.value))
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
if node.has_children_or_has_assets(): if node.has_children_or_has_assets():
error = _("Deletion failed and the node contains children or assets") error = _("Deletion failed and the node contains children or assets")
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN) return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
@@ -136,7 +157,7 @@ class NodeChildrenApi(generics.ListCreateAPIView):
return queryset return queryset
class NodeChildrenAsTreeApi(NodeChildrenApi): class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
""" """
节点子节点作为树返回, 节点子节点作为树返回,
[ [
@@ -150,31 +171,23 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
""" """
model = Node model = Node
serializer_class = TreeNodeSerializer
http_method_names = ['get']
def get_queryset(self): def list(self, request, *args, **kwargs):
queryset = super().get_queryset() nodes = self.get_queryset().order_by('value')
queryset = [node.as_tree_node() for node in queryset] nodes = self.serialize_nodes(nodes, with_asset_amount=True)
queryset = self.add_assets_if_need(queryset) assets = self.get_assets()
queryset = sorted(queryset) data = [*nodes, *assets]
return queryset return Response(data=data)
def add_assets_if_need(self, queryset): def get_assets(self):
include_assets = self.request.query_params.get('assets', '0') == '1' include_assets = self.request.query_params.get('assets', '0') == '1'
if not include_assets: if not include_assets:
return queryset return []
assets = self.instance.get_assets().only( assets = self.instance.get_assets().only(
"id", "hostname", "ip", "os", "id", "hostname", "ip", "os",
"org_id", "protocols", "org_id", "protocols", "is_active"
) )
for asset in assets: return self.serialize_assets(assets, self.instance.key)
queryset.append(asset.as_tree_node(self.instance))
return queryset
def check_need_refresh_nodes(self):
if self.request.query_params.get('refresh', '0') == '1':
Node.refresh_nodes()
class NodeAssetsApi(generics.ListAPIView): class NodeAssetsApi(generics.ListAPIView):
@@ -200,14 +213,14 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
nodes_id = request.data.get("nodes") nodes_id = request.data.get("nodes")
children = [get_object_or_none(Node, id=pk) for pk in nodes_id] children = Node.objects.filter(id__in=nodes_id)
for node in children: for node in children:
if not node:
continue
node.parent = instance node.parent = instance
return Response("OK") return Response("OK")
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class NodeAddAssetsApi(generics.UpdateAPIView): class NodeAddAssetsApi(generics.UpdateAPIView):
model = Node model = Node
serializer_class = serializers.NodeAssetsSerializer serializer_class = serializers.NodeAssetsSerializer
@@ -220,6 +233,8 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
instance.assets.add(*tuple(assets)) instance.assets.add(*tuple(assets))
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class NodeRemoveAssetsApi(generics.UpdateAPIView): class NodeRemoveAssetsApi(generics.UpdateAPIView):
model = Node model = Node
serializer_class = serializers.NodeAssetsSerializer serializer_class = serializers.NodeAssetsSerializer
@@ -228,15 +243,17 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
def perform_update(self, serializer): def perform_update(self, serializer):
assets = serializer.validated_data.get('assets') assets = serializer.validated_data.get('assets')
instance = self.get_object() node = self.get_object()
if instance != Node.org_root(): node.assets.remove(*assets)
instance.assets.remove(*tuple(assets))
else: # 把孤儿资产添加到 root 节点
assets = [asset for asset in assets if asset.nodes.count() > 1] orphan_assets = Asset.objects.filter(id__in=[a.id for a in assets], nodes__isnull=True).distinct()
instance.assets.remove(*tuple(assets)) Node.org_root().assets.add(*orphan_assets)
class NodeReplaceAssetsApi(generics.UpdateAPIView): @method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class MoveAssetsToNodeApi(generics.UpdateAPIView):
model = Node model = Node
serializer_class = serializers.NodeAssetsSerializer serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
@@ -244,9 +261,39 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
def perform_update(self, serializer): def perform_update(self, serializer):
assets = serializer.validated_data.get('assets') assets = serializer.validated_data.get('assets')
instance = self.get_object() node = self.get_object()
for asset in assets: self.remove_old_nodes(assets)
asset.nodes.set([instance]) node.assets.add(*assets)
def remove_old_nodes(self, assets):
m2m_model = Asset.nodes.through
# 查询资产与节点关系表,查出要移动资产与节点的所有关系
relates = m2m_model.objects.filter(asset__in=assets).values_list('asset_id', 'node_id')
if relates:
# 对关系以资产进行分组,用来发 `reverse=False` 信号
asset_nodes_mapper = defaultdict(set)
for asset_id, node_id in relates:
asset_nodes_mapper[asset_id].add(node_id)
# 组建一个资产 id -> Asset 的 mapper
asset_mapper = {asset.id: asset for asset in assets}
# 创建删除关系信号发送函数
senders = []
for asset_id, node_id_set in asset_nodes_mapper.items():
senders.append(partial(m2m_changed.send, sender=m2m_model, instance=asset_mapper[asset_id],
reverse=False, model=Node, pk_set=node_id_set))
# 发送 pre 信号
[sender(action=PRE_REMOVE) for sender in senders]
num = len(relates)
asset_ids, node_ids = zip(*relates)
# 删除之前的关系
rows, _i = m2m_model.objects.filter(asset_id__in=asset_ids, node_id__in=node_ids).delete()
if rows != num:
raise SomeoneIsDoingThis
# 发送 post 信号
[sender(action=POST_REMOVE) for sender in senders]
class NodeTaskCreateApi(generics.CreateAPIView): class NodeTaskCreateApi(generics.CreateAPIView):
@@ -267,7 +314,6 @@ class NodeTaskCreateApi(generics.CreateAPIView):
@staticmethod @staticmethod
def refresh_nodes_cache(): def refresh_nodes_cache():
Node.refresh_nodes()
Task = namedtuple('Task', ['id']) Task = namedtuple('Task', ['id'])
task = Task(id="0") task = Task(id="0")
return task return task

View File

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

View File

@@ -65,7 +65,7 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserAssetRelationSerializer serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
filterset_fields = [ filter_fields = [
'id', 'asset', 'systemuser', 'id', 'asset', 'systemuser',
] ]
search_fields = [ search_fields = [
@@ -91,11 +91,11 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserNodeRelationSerializer serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
filterset_fields = [ filter_fields = [
'id', 'node', 'systemuser', 'id', 'node', 'systemuser',
] ]
search_fields = [ search_fields = [
"node__value", "systemuser__name", "systemuser_username" "node__value", "systemuser__name", "systemuser__username"
] ]
def get_objects_attr(self): def get_objects_attr(self):
@@ -112,7 +112,7 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserUserRelationSerializer serializer_class = serializers.SystemUserUserRelationSerializer
model = models.SystemUser.users.through model = models.SystemUser.users.through
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
filterset_fields = [ filter_fields = [
'id', 'user', 'systemuser', 'id', 'user', 'systemuser',
] ]
search_fields = [ search_fields = [

View File

@@ -0,0 +1,6 @@
from rest_framework import status
from common.exceptions import JMSException
class NodeIsBeingUpdatedByOthers(JMSException):
status_code = status.HTTP_409_CONFLICT

View File

@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import coreapi from rest_framework.compat import coreapi, coreschema
from rest_framework import filters from rest_framework import filters
from django.db.models import Q from django.db.models import Q
from common.utils import dict_get_any, is_uuid, get_object_or_none from .models import Label
from .models import Node, Label from assets.utils import is_query_node_all_assets, get_node
class AssetByNodeFilterBackend(filters.BaseFilterBackend): class AssetByNodeFilterBackend(filters.BaseFilterBackend):
@@ -21,51 +21,58 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend):
for field in self.fields for field in self.fields
] ]
@staticmethod def filter_node_related_all(self, queryset, node):
def is_query_all(request): return queryset.filter(
query_all_arg = request.query_params.get('all') Q(nodes__key__istartswith=f'{node.key}:') |
show_current_asset_arg = request.query_params.get('show_current_asset') Q(nodes__key=node.key)
).distinct()
query_all = query_all_arg == '1' def filter_node_related_direct(self, queryset, node):
if show_current_asset_arg is not None: return queryset.filter(nodes__key=node.key).distinct()
query_all = show_current_asset_arg != '1'
return query_all
@staticmethod
def get_query_node(request):
node_id = dict_get_any(request.query_params, ['node', 'node_id'])
if not node_id:
return None, False
if is_uuid(node_id):
node = get_object_or_none(Node, id=node_id)
else:
node = get_object_or_none(Node, key=node_id)
return node, True
@staticmethod
def perform_query(pattern, queryset):
return queryset.filter(nodes__key__regex=pattern).distinct()
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
node, has_query_arg = self.get_query_node(request) node = get_node(request)
if not has_query_arg:
return queryset
if node is None: if node is None:
return queryset return queryset
query_all = self.is_query_all(request)
query_all = is_query_node_all_assets(request)
if query_all: if query_all:
pattern = node.get_all_children_pattern(with_self=True) return self.filter_node_related_all(queryset, node)
else: else:
# pattern = node.get_children_key_pattern(with_self=True) return self.filter_node_related_direct(queryset, node)
# 只显示当前节点下资产
pattern = r"^{}$".format(node.key)
return self.perform_query(pattern, queryset) class FilterAssetByNodeFilterBackend(filters.BaseFilterBackend):
"""
需要与 `assets.api.mixin.FilterAssetByNodeMixin` 配合使用
"""
fields = ['node', 'all']
def get_schema_fields(self, view):
return [
coreapi.Field(
name=field, location='query', required=False,
type='string', example='', description='', schema=None,
)
for field in self.fields
]
def filter_queryset(self, request, queryset, view):
node = view.node
if node is None:
return queryset
query_all = view.is_query_node_all_assets
if query_all:
return queryset.filter(
Q(nodes__key__istartswith=f'{node.key}:') |
Q(nodes__key=node.key)
).distinct()
else:
return queryset.filter(nodes__key=node.key).distinct()
class LabelFilterBackend(filters.BaseFilterBackend): class LabelFilterBackend(filters.BaseFilterBackend):
sep = '#' sep = ':'
query_arg = 'label' query_arg = 'label'
def get_schema_fields(self, view): def get_schema_fields(self, view):
@@ -84,6 +91,8 @@ class LabelFilterBackend(filters.BaseFilterBackend):
q = None q = None
for kv in labels_query: for kv in labels_query:
if '#' in kv:
self.sep = '#'
if self.sep not in kv: if self.sep not in kv:
continue continue
key, value = kv.strip().split(self.sep)[:2] key, value = kv.strip().split(self.sep)[:2]
@@ -111,7 +120,32 @@ class LabelFilterBackend(filters.BaseFilterBackend):
class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend): class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
@staticmethod def filter_node_related_all(self, queryset, node):
def perform_query(pattern, queryset): return queryset.filter(
return queryset.filter(asset__nodes__key__regex=pattern).distinct() Q(asset__nodes__key__istartswith=f'{node.key}:') |
Q(asset__nodes__key=node.key)
).distinct()
def filter_node_related_direct(self, queryset, node):
return queryset.filter(asset__nodes__key=node.key).distinct()
class IpInFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
ips = request.query_params.get('ips')
if not ips:
return queryset
ip_list = [i.strip() for i in ips.split(',')]
queryset = queryset.filter(ip__in=ip_list)
return queryset
def get_schema_fields(self, view):
return [
coreapi.Field(
name='ips', location='query', required=False, type='string',
schema=coreschema.String(
title='ips',
description='ip in filter'
)
)
]

View File

@@ -1,8 +0,0 @@
# -*- coding: utf-8 -*-
#
from .asset import *
from .label import *
from .user import *
from .domain import *
from .cmd_filter import *
from .platform import *

View File

@@ -1,172 +0,0 @@
# -*- coding: utf-8 -*-
#
from itertools import groupby
from django import forms
from django.utils.translation import gettext_lazy as _
from common.utils import get_logger
from orgs.mixins.forms import OrgModelForm
from ..models import Asset, Platform
logger = get_logger(__file__)
__all__ = [
'AssetCreateUpdateForm', 'AssetBulkUpdateForm', 'ProtocolForm',
]
class ProtocolForm(forms.Form):
name = forms.ChoiceField(
choices=Asset.PROTOCOL_CHOICES, label=_("Name"), initial='ssh',
widget=forms.Select(attrs={'class': 'form-control protocol-name'})
)
port = forms.IntegerField(
max_value=65534, min_value=1, label=_("Port"), initial=22,
widget=forms.TextInput(attrs={'class': 'form-control protocol-port'})
)
class AssetCreateUpdateForm(OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_platform_to_name()
self.set_fields_queryset()
def set_fields_queryset(self):
nodes_field = self.fields['nodes']
nodes_choices = []
if self.instance:
nodes_choices = [
(n.id, n.full_value) for n in
self.instance.nodes.all()
]
nodes_field.choices = nodes_choices
@staticmethod
def sorted_platform(platform):
if platform['base'] == 'Other':
return 'zz'
return platform['base']
def set_platform_to_name(self):
choices = []
platforms = Platform.objects.all().values('name', 'base')
platforms_sorted = sorted(platforms, key=self.sorted_platform)
platforms_grouped = groupby(platforms_sorted, key=lambda x: x['base'])
for i in platforms_grouped:
base = i[0]
grouped = sorted(i[1], key=lambda x: x['name'])
grouped = [(j['name'], j['name']) for j in grouped]
choices.append(
(base, grouped)
)
platform_field = self.fields['platform']
platform_field.choices = choices
if self.instance:
self.initial['platform'] = self.instance.platform.name
def add_nodes_initial(self, node):
nodes_field = self.fields['nodes']
nodes_field.choices.append((node.id, node.full_value))
nodes_field.initial = [node]
class Meta:
model = Asset
fields = [
'hostname', 'ip', 'public_ip', 'protocols', 'comment',
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
'domain', 'number',
]
widgets = {
'nodes': forms.SelectMultiple(attrs={
'class': 'nodes-select2', 'data-placeholder': _('Nodes')
}),
'admin_user': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Admin user')
}),
'labels': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Label')
}),
'domain': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Domain')
}),
'platform': forms.Select(attrs={
'class': 'select2', 'data-placeholder': _('Platform')
}),
}
labels = {
'nodes': _("Node"),
}
help_texts = {
'admin_user': _(
'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu'
),
'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"),
'domain': _("If your have some network not connect with each other, you can set domain")
}
class AssetBulkUpdateForm(OrgModelForm):
assets = forms.ModelMultipleChoiceField(
required=True,
label=_('Select assets'), queryset=Asset.objects,
widget=forms.SelectMultiple(
attrs={
'class': 'select2',
'data-placeholder': _('Select assets')
}
)
)
class Meta:
model = Asset
fields = [
'assets', 'admin_user', 'labels', 'platform',
'domain',
]
widgets = {
'labels': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Label')}
),
'nodes': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Node')}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_fields_queryset()
# 重写其他字段为不再required
for name, field in self.fields.items():
if name != 'assets':
field.required = False
def set_fields_queryset(self):
assets_field = self.fields['assets']
if hasattr(self, 'data'):
assets_field.queryset = Asset.objects.all()
def save(self, commit=True):
changed_fields = []
for field in self._meta.fields:
if self.data.get(field) not in [None, '']:
changed_fields.append(field)
cleaned_data = {k: v for k, v in self.cleaned_data.items()
if k in changed_fields}
assets = cleaned_data.pop('assets')
labels = cleaned_data.pop('labels', [])
nodes = cleaned_data.pop('nodes', None)
assets = Asset.objects.filter(id__in=[asset.id for asset in assets])
assets.update(**cleaned_data)
if labels:
for asset in assets:
asset.labels.set(labels)
if nodes:
for asset in assets:
asset.nodes.set(nodes)
return assets

View File

@@ -1,40 +0,0 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
import re
from orgs.mixins.forms import OrgModelForm
from ..models import CommandFilter, CommandFilterRule
__all__ = ['CommandFilterForm', 'CommandFilterRuleForm']
class CommandFilterForm(OrgModelForm):
class Meta:
model = CommandFilter
fields = ['name', 'comment']
class CommandFilterRuleForm(OrgModelForm):
invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
class Meta:
model = CommandFilterRule
fields = [
'filter', 'type', 'content', 'priority', 'action', 'comment'
]
widgets = {
'content': forms.Textarea(attrs={
'placeholder': 'eg:\r\nreboot\r\nrm -rf'
}),
}
def clean_content(self):
content = self.cleaned_data.get("content")
if self.invalid_pattern.search(content):
invalid_char = self.invalid_pattern.pattern.replace('\\', '')
msg = _("Content should not be contain: {}").format(invalid_char)
raise ValidationError(msg)
return content

View File

@@ -1,79 +0,0 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.utils.translation import gettext_lazy as _
from orgs.mixins.forms import OrgModelForm
from ..models import Domain, Asset, Gateway
from .user import PasswordAndKeyAuthForm
__all__ = ['DomainForm', 'GatewayForm']
class DomainForm(forms.ModelForm):
assets = forms.ModelMultipleChoiceField(
queryset=Asset.objects, label=_('Asset'), required=False,
widget=forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Select assets')}
)
)
class Meta:
model = Domain
fields = ['name', 'comment', 'assets']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_fields_queryset()
def set_fields_queryset(self):
assets_field = self.fields.get('assets')
# 没有data代表是渲染表单, 有data代表是提交创建/更新表单
if not self.data:
# 有instance 代表渲染更新表单, 否则是创建表单
# 前端渲染优化, 防止过多资产, 设置assets queryset为none
if self.instance:
assets_field.initial = self.instance.assets.all()
assets_field.queryset = self.instance.assets.all()
else:
assets_field.queryset = Asset.objects.none()
else:
assets_field.queryset = Asset.objects.all()
def save(self, commit=True):
instance = super().save(commit=commit)
assets = self.cleaned_data['assets']
instance.assets.set(assets)
return instance
class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
password_field = self.fields.get('password')
password_field.help_text = _('Password should not contain special characters')
protocol_field = self.fields.get('protocol')
protocol_field.choices = [Gateway.PROTOCOL_CHOICES[0]]
def save(self, commit=True):
# Because we define custom field, so we need rewrite :method: `save`
instance = super().save()
password = self.cleaned_data.get('password')
private_key, public_key = super().gen_keys()
instance.set_auth(password=password, private_key=private_key)
return instance
class Meta:
model = Gateway
fields = [
'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password',
'private_key', 'is_active', 'comment',
]
help_texts = {
'protocol': _("SSH gateway support proxy SSH,RDP,VNC")
}
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
}

View File

@@ -1,46 +0,0 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.utils.translation import gettext_lazy as _
from ..models import Label, Asset
__all__ = ['LabelForm']
class LabelForm(forms.ModelForm):
assets = forms.ModelMultipleChoiceField(
queryset=Asset.objects.none(), label=_('Asset'), required=False,
widget=forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Select assets')}
)
)
class Meta:
model = Label
fields = ['name', 'value', 'assets']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_fields_queryset()
def set_fields_queryset(self):
assets_field = self.fields.get('assets')
# 没有data代表是渲染表单, 有data代表是提交创建/更新表单
if not self.data:
# 有instance 代表渲染更新表单, 否则是创建表单
# 前端渲染优化, 防止过多资产, 设置assets queryset为none
if self.instance:
assets_field.initial = self.instance.assets.all()
assets_field.queryset = self.instance.assets.all()
else:
assets_field.queryset = Asset.objects.none()
else:
assets_field.queryset = Asset.objects.all()
def save(self, commit=True):
label = super().save(commit=commit)
assets = self.cleaned_data['assets']
label.assets.set(assets)
return label

View File

@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
from django import forms
from django.utils.translation import ugettext_lazy as _
from ..models import Platform
__all__ = ['PlatformForm', 'PlatformMetaForm']
class PlatformMetaForm(forms.Form):
SECURITY_CHOICES = (
('rdp', "RDP"),
('nla', "NLA"),
('tls', 'TLS'),
('any', "Any"),
)
CONSOLE_CHOICES = (
(True, _('Yes')),
(False, _('No')),
)
security = forms.ChoiceField(
choices=SECURITY_CHOICES, initial='any', label=_("RDP security"),
required=False,
)
console = forms.ChoiceField(
choices=CONSOLE_CHOICES, initial=False, label=_("RDP console"),
required=False,
)
class PlatformForm(forms.ModelForm):
class Meta:
model = Platform
fields = [
'name', 'base', 'comment',
]
labels = {
'base': _("Base platform")
}

View File

@@ -1,115 +0,0 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.utils.translation import gettext_lazy as _
from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
from orgs.mixins.forms import OrgModelForm
from ..models import AdminUser, SystemUser
logger = get_logger(__file__)
__all__ = [
'FileForm', 'SystemUserForm', 'AdminUserForm', 'PasswordAndKeyAuthForm',
]
class FileForm(forms.Form):
file = forms.FileField()
class PasswordAndKeyAuthForm(forms.ModelForm):
# Form field name can not start with `_`, so redefine it,
password = forms.CharField(
widget=forms.PasswordInput, max_length=128,
strip=True, required=False,
help_text=_('Password or private key passphrase'),
label=_("Password"),
)
# Need use upload private key file except paste private key content
private_key = forms.FileField(required=False, label=_("Private key"))
def clean_private_key(self):
private_key_f = self.cleaned_data['private_key']
password = self.cleaned_data['password']
if private_key_f:
key_string = private_key_f.read()
private_key_f.seek(0)
key_string = key_string.decode()
if not validate_ssh_private_key(key_string, password):
msg = _('Invalid private key, Only support '
'RSA/DSA format key')
raise forms.ValidationError(msg)
return private_key_f
def validate_password_key(self):
password = self.cleaned_data['password']
private_key_f = self.cleaned_data.get('private_key', '')
if not password and not private_key_f:
raise forms.ValidationError(_(
'Password and private key file must be input one'
))
def gen_keys(self):
password = self.cleaned_data.get('password', '') or None
private_key_f = self.cleaned_data['private_key']
public_key = private_key = None
if private_key_f:
private_key = private_key_f.read().strip().decode('utf-8')
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
return private_key, public_key
class AdminUserForm(PasswordAndKeyAuthForm):
def save(self, commit=True):
raise forms.ValidationError("Use api to save")
class Meta:
model = AdminUser
fields = ['name', 'username', 'password', 'private_key', 'comment']
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
}
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
# Admin user assets define, let user select, save it in form not in view
auto_generate_key = forms.BooleanField(initial=True, required=False)
def save(self, commit=True):
raise forms.ValidationError("Use api to save")
class Meta:
model = SystemUser
fields = [
'name', 'username', 'protocol', 'auto_generate_key',
'password', 'private_key', 'auto_push', 'sudo',
'username_same_with_user',
'comment', 'shell', 'priority', 'login_mode', 'cmd_filters',
'sftp_root',
]
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
'cmd_filters': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Command filter')
}),
}
labels = {
'username_same_with_user': _("Username same with user"),
}
help_texts = {
'auto_push': _('Auto push system user to asset'),
'priority': _('1-100, High level will be using login asset as default, '
'if user was granted more than 2 system user'),
'login_mode': _('If you choose manual login mode, you do not '
'need to fill in the username and password.'),
'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig"),
'sftp_root': _("SFTP root dir, tmp, home or custom"),
'username_same_with_user': _("Username is dynamic, When connect asset, using current user's username"),
# 'username_same_with_user': _("用户名是动态的,登录资产时使用当前用户的用户名登录"),
}

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.10 on 2020-07-11 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0049_systemuser_sftp_root'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='created_by',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'),
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.2.10 on 2020-07-13 03:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0050_auto_20200711_1740'),
]
operations = [
migrations.AlterField(
model_name='domain',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='domain',
unique_together={('org_id', 'name')},
),
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.2.10 on 2020-07-15 07:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0051_auto_20200713_1143'),
]
operations = [
migrations.AlterField(
model_name='commandfilter',
name='name',
field=models.CharField(max_length=64, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='commandfilter',
unique_together={('org_id', 'name')},
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 2.2.10 on 2020-07-23 04:32
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0052_auto_20200715_1535'),
]
operations = [
migrations.AlterField(
model_name='adminuser',
name='username',
field=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'),
),
migrations.AlterField(
model_name='authbook',
name='username',
field=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'),
),
migrations.AlterField(
model_name='gateway',
name='username',
field=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'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=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'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.13 on 2020-08-07 02:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0053_auto_20200723_1232'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='token',
field=models.TextField(default='', verbose_name='Token'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql'), ('k8s', 'k8s')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.13 on 2020-08-11 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0054_auto_20200807_1032'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='home',
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='Home'),
),
migrations.AddField(
model_name='systemuser',
name='system_groups',
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='System groups'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.2.13 on 2020-09-04 09:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0055_auto_20200811_1845'),
]
operations = [
migrations.AddField(
model_name='node',
name='assets_amount',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='node',
name='parent_key',
field=models.CharField(db_index=True, default='', max_length=64, verbose_name='Parent key'),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 2.2.13 on 2020-08-21 08:20
from django.db import migrations
from django.db.models import Q
def fill_node_value(apps, schema_editor):
Node = apps.get_model('assets', 'Node')
Asset = apps.get_model('assets', 'Asset')
node_queryset = Node.objects.all()
node_amount = node_queryset.count()
width = len(str(node_amount))
print('\n')
for i, node in enumerate(node_queryset):
print(f'\t{i+1:0>{width}}/{node_amount} compute node[{node.key}]`s assets_amount ...')
assets_amount = Asset.objects.filter(
Q(nodes__key__istartswith=f'{node.key}:') | Q(nodes=node)
).distinct().count()
key = node.key
try:
parent_key = key[:key.rindex(':')]
except ValueError:
parent_key = ''
node.assets_amount = assets_amount
node.parent_key = parent_key
node.save()
print(' ' + '.'*65, end='')
class Migration(migrations.Migration):
dependencies = [
('assets', '0056_auto_20200904_1751'),
]
operations = [
migrations.RunPython(fill_node_value)
]

View File

@@ -0,0 +1,27 @@
# Generated by Django 2.2.13 on 2020-10-23 03:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0057_fill_node_value_assets_amount_and_parent_key'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['-date_created'], 'verbose_name': 'Asset'},
),
migrations.AlterField(
model_name='asset',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='commandfilterrule',
name='content',
field=models.TextField(help_text='One line one command', verbose_name='Content'),
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 2.2.13 on 2020-10-27 11:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0058_auto_20201023_1115'),
]
operations = [
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.AddField(
model_name='systemuser',
name='ad_domain',
field=models.CharField(default='', max_length=256),
),
migrations.AlterField(
model_name='gateway',
name='ip',
field=models.CharField(db_index=True, max_length=128, verbose_name='IP'),
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 2.2.13 on 2020-10-26 11:31
from django.db import migrations, models
def get_node_ancestor_keys(key, with_self=False):
parent_keys = []
key_list = key.split(":")
if not with_self:
key_list.pop()
for i in range(len(key_list)):
parent_keys.append(":".join(key_list))
key_list.pop()
return parent_keys
def migrate_nodes_value_with_slash(apps, schema_editor):
model = apps.get_model("assets", "Node")
db_alias = schema_editor.connection.alias
nodes = model.objects.using(db_alias).filter(value__contains='/')
print('')
print("- Start migrate node value if has /")
for i, node in enumerate(list(nodes)):
new_value = node.value.replace('/', '|')
print("{} start migrate node value: {} => {}".format(i, node.value, new_value))
node.value = new_value
node.save()
def migrate_nodes_full_value(apps, schema_editor):
model = apps.get_model("assets", "Node")
db_alias = schema_editor.connection.alias
nodes = model.objects.using(db_alias).all()
print("- Start migrate node full value")
for i, node in enumerate(list(nodes)):
print("{} start migrate {} node full value".format(i, node.value))
ancestor_keys = get_node_ancestor_keys(node.key, True)
values = model.objects.filter(key__in=ancestor_keys).values_list('key', 'value')
values = [v for k, v in sorted(values, key=lambda x: len(x[0]))]
node.full_value = '/' + '/'.join(values)
node.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0059_auto_20201027_1905'),
]
operations = [
migrations.AddField(
model_name='node',
name='full_value',
field=models.CharField(default='', max_length=4096, verbose_name='Full value'),
),
migrations.RunPython(migrate_nodes_value_with_slash),
migrations.RunPython(migrate_nodes_full_value)
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.2.13 on 2020-11-16 09:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0060_node_full_value'),
]
operations = [
migrations.AlterModelOptions(
name='node',
options={'ordering': ['value'], 'verbose_name': 'Node'},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 2.2.13 on 2020-11-17 11:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0061_auto_20201116_1757'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname', 'ip'], 'verbose_name': 'Asset'},
),
]

View File

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

View File

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

View File

@@ -47,6 +47,10 @@ class AssetManager(OrgManager):
) )
class AssetOrgManager(OrgManager):
pass
class AssetQuerySet(models.QuerySet): class AssetQuerySet(models.QuerySet):
def active(self): def active(self):
return self.filter(is_active=True) return self.filter(is_active=True)
@@ -221,11 +225,12 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw')) hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created')) date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
objects = AssetManager.from_queryset(AssetQuerySet)() objects = AssetManager.from_queryset(AssetQuerySet)()
org_objects = AssetOrgManager.from_queryset(AssetQuerySet)()
_connectivity = None _connectivity = None
def __str__(self): def __str__(self):
@@ -308,6 +313,12 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
} }
return info return info
def nodes_display(self):
names = []
for n in self.nodes.all():
names.append(n.full_value)
return names
def as_node(self): def as_node(self):
from .node import Node from .node import Node
fake_node = Node() fake_node = Node()
@@ -350,36 +361,4 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
class Meta: class Meta:
unique_together = [('org_id', 'hostname')] unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset") verbose_name = _("Asset")
ordering = ["hostname", "ip"]
@classmethod
def generate_fake(cls, count=100):
from .user import AdminUser, SystemUser
from random import seed, choice
from django.db import IntegrityError
from .node import Node
from orgs.utils import get_current_org
from orgs.models import Organization
org = get_current_org()
if not org or not org.is_real():
Organization.default().change_to()
nodes = list(Node.objects.all())
seed()
for i in range(count):
ip = [str(i) for i in random.sample(range(255), 4)]
asset = cls(ip='.'.join(ip),
hostname='.'.join(ip),
admin_user=choice(AdminUser.objects.all()),
created_by='Fake')
try:
asset.save()
asset.protocols = 'ssh/22'
if nodes and len(nodes) > 3:
_nodes = random.sample(nodes, 3)
else:
_nodes = [Node.default_node()]
asset.nodes.set(_nodes)
logger.debug('Generate fake asset : %s' % asset.ip)
except IntegrityError:
print('Error continue')
continue

View File

@@ -3,7 +3,6 @@
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Max from django.db.models import Max
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgManager from orgs.mixins.models import OrgManager
@@ -59,19 +58,17 @@ class AuthBook(BaseUser):
""" """
username = kwargs['username'] username = kwargs['username']
asset = kwargs['asset'] asset = kwargs['asset']
key_lock = 'KEY_LOCK_CREATE_AUTH_BOOK_{}_{}'.format(username, asset.id) with transaction.atomic():
with cache.lock(key_lock): # 使用select_for_update限制并发创建相同的username、asset条目
with transaction.atomic(): instances = cls.objects.select_for_update().filter(username=username, asset=asset)
cls.objects.filter( instances.filter(is_latest=True).update(is_latest=False)
username=username, asset=asset, is_latest=True max_version = cls.get_max_version(username, asset)
).update(is_latest=False) kwargs.update({
max_version = cls.get_max_version(username, asset) 'version': max_version + 1,
kwargs.update({ 'is_latest': True
'version': max_version + 1, })
'is_latest': True obj = cls.objects.create(**kwargs)
}) return obj
obj = cls.objects.create(**kwargs)
return obj
@property @property
def connectivity(self): def connectivity(self):

View File

@@ -158,9 +158,11 @@ class AuthMixin:
if update_fields: if update_fields:
self.save(update_fields=update_fields) self.save(update_fields=update_fields)
def has_special_auth(self, asset=None): def has_special_auth(self, asset=None, username=None):
from .authbook import AuthBook from .authbook import AuthBook
queryset = AuthBook.objects.filter(username=self.username) if username is None:
username = self.username
queryset = AuthBook.objects.filter(username=username)
if asset: if asset:
queryset = queryset.filter(asset=asset) queryset = queryset.filter(asset=asset)
return queryset.exists() return queryset.exists()
@@ -230,7 +232,7 @@ class AuthMixin:
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin): class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True) username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password')) password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key')) private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key')) public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))

View File

@@ -38,27 +38,3 @@ class Cluster(models.Model):
class Meta: class Meta:
ordering = ['name'] ordering = ['name']
verbose_name = _("Cluster") verbose_name = _("Cluster")
@classmethod
def generate_fake(cls, count=5):
from random import seed, choice
import forgery_py
from django.db import IntegrityError
seed()
for i in range(count):
cluster = cls(name=forgery_py.name.full_name(),
bandwidth='200M',
contact=forgery_py.name.full_name(),
phone=forgery_py.address.phone(),
address=forgery_py.address.city() + forgery_py.address.street_address(),
# operator=choice(['北京联通', '北京电信', 'BGP全网通']),
operator=choice([_('Beijing unicom'), _('Beijing telecom'), _('BGP full netcom')]),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:
cluster.save()
logger.debug('Generate fake asset group: %s' % cluster.name)
except IntegrityError:
print('Error continue')
continue

View File

@@ -18,7 +18,7 @@ __all__ = [
class CommandFilter(OrgModelMixin): class CommandFilter(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=64, unique=True, verbose_name=_("Name")) name = models.CharField(max_length=64, verbose_name=_("Name"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active')) is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment")) comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
@@ -29,6 +29,7 @@ class CommandFilter(OrgModelMixin):
return self.name return self.name
class Meta: class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Command filter") verbose_name = _("Command filter")
@@ -51,7 +52,7 @@ class CommandFilterRule(OrgModelMixin):
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type")) type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"), priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)]) validators=[MinValueValidator(1), MaxValueValidator(100)])
content = models.TextField(max_length=1024, verbose_name=_("Content"), help_text=_("One line one command")) 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=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment")) comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)

View File

@@ -9,6 +9,7 @@ import paramiko
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.utils.strings import no_special_chars
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from .base import BaseUser from .base import BaseUser
@@ -17,13 +18,14 @@ __all__ = ['Domain', 'Gateway']
class Domain(OrgModelMixin): class Domain(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name')) name = models.CharField(max_length=128, verbose_name=_('Name'))
comment = models.TextField(blank=True, verbose_name=_('Comment')) comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, null=True, date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created')) verbose_name=_('Date created'))
class Meta: class Meta:
verbose_name = _("Domain") verbose_name = _("Domain")
unique_together = [('org_id', 'name')]
def __str__(self): def __str__(self):
return self.name return self.name
@@ -46,7 +48,7 @@ class Gateway(BaseUser):
(PROTOCOL_SSH, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'), (PROTOCOL_RDP, 'rdp'),
) )
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True) ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port')) 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")) domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
@@ -63,8 +65,8 @@ class Gateway(BaseUser):
def test_connective(self, local_port=None): def test_connective(self, local_port=None):
if local_port is None: if local_port is None:
local_port = self.port local_port = self.port
if self.password and not re.match(r'\w+$', self.password): if self.password and not no_special_chars(self.password):
return False, _("Password should not contain special characters") return False, _("Password should not contains special characters")
client = paramiko.SSHClient() client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

View File

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

View File

@@ -33,21 +33,3 @@ class AssetGroup(models.Model):
def initial(cls): def initial(cls):
asset_group = cls(name=_('Default'), comment=_('Default asset group')) asset_group = cls(name=_('Default'), comment=_('Default asset group'))
asset_group.save() asset_group.save()
@classmethod
def generate_fake(cls, count=100):
from random import seed
import forgery_py
from django.db import IntegrityError
seed()
for i in range(count):
group = cls(name=forgery_py.name.full_name(),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:
group.save()
logger.debug('Generate fake asset group: %s' % group.name)
except IntegrityError:
print('Error continue')
continue

View File

@@ -2,132 +2,35 @@
# #
import uuid import uuid
import re import re
import time
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Q from django.db.models import Q
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.core.cache import cache from django.db.transaction import atomic
from common.utils import get_logger, lazyproperty from common.utils import get_logger
from common.utils.common import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org, current_org from orgs.utils import get_current_org, tmp_to_org
from orgs.models import Organization from orgs.models import Organization
__all__ = ['Node'] __all__ = ['Node', 'FamilyMixin', 'compute_parent_key']
logger = get_logger(__name__) logger = get_logger(__name__)
def compute_parent_key(key):
try:
return key[:key.rindex(':')]
except ValueError:
return ''
class NodeQuerySet(models.QuerySet): class NodeQuerySet(models.QuerySet):
def delete(self): def delete(self):
raise PermissionError("Bulk delete node deny") raise NotImplementedError
class TreeCache:
updated_time_cache_key = 'NODE_TREE_UPDATED_AT_{}'
cache_time = 3600
assets_updated_time_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT_{}'
def __init__(self, tree, org_id):
now = time.time()
self.created_time = now
self.assets_created_time = now
self.tree = tree
self.org_id = org_id
def _has_changed(self, tp="tree"):
if tp == "assets":
key = self.assets_updated_time_cache_key.format(self.org_id)
else:
key = self.updated_time_cache_key.format(self.org_id)
updated_time = cache.get(key, 0)
if updated_time > self.created_time:
return True
else:
return False
@classmethod
def set_changed(cls, tp="tree", t=None, org_id=None):
if org_id is None:
org_id = current_org.id
if tp == "assets":
key = cls.assets_updated_time_cache_key.format(org_id)
else:
key = cls.updated_time_cache_key.format(org_id)
ttl = cls.cache_time
if not t:
t = time.time()
cache.set(key, t, ttl)
def tree_has_changed(self):
return self._has_changed("tree")
def set_tree_changed(self, t=None):
logger.debug("Set tree tree changed")
self.__class__.set_changed(t=t, tp="tree")
def assets_has_changed(self):
return self._has_changed("assets")
def set_tree_assets_changed(self, t=None):
logger.debug("Set tree assets changed")
self.__class__.set_changed(t=t, tp="assets")
def get(self):
if self.tree_has_changed():
self.renew()
return self.tree
if self.assets_has_changed():
self.tree.init_assets()
return self.tree
def renew(self):
new_obj = self.__class__.new(self.org_id)
self.tree = new_obj.tree
self.created_time = new_obj.created_time
self.assets_created_time = new_obj.assets_created_time
@classmethod
def new(cls, org_id=None):
from ..utils import TreeService
logger.debug("Create node tree")
if not org_id:
org_id = current_org.id
with tmp_to_org(org_id):
tree = TreeService.new()
obj = cls(tree, org_id)
obj.tree = tree
return obj
class TreeMixin:
_org_tree_map = {}
@classmethod
def tree(cls):
org_id = current_org.org_id()
t = cls.get_local_tree_cache(org_id)
if t is None:
t = TreeCache.new()
cls._org_tree_map[org_id] = t
return t.get()
@classmethod
def get_local_tree_cache(cls, org_id=None):
t = cls._org_tree_map.get(org_id)
return t
@classmethod
def refresh_tree(cls, t=None):
TreeCache.set_changed(tp="tree", t=t, org_id=current_org.id)
@classmethod
def refresh_node_assets(cls, t=None):
TreeCache.set_changed(tp="assets", t=t, org_id=current_org.id)
class FamilyMixin: class FamilyMixin:
@@ -138,16 +41,16 @@ class FamilyMixin:
@staticmethod @staticmethod
def clean_children_keys(nodes_keys): def clean_children_keys(nodes_keys):
nodes_keys = sorted(list(nodes_keys), key=lambda x: (len(x), x)) sort_key = lambda k: [int(i) for i in k.split(':')]
nodes_keys = sorted(list(nodes_keys), key=sort_key)
nodes_keys_clean = [] nodes_keys_clean = []
for key in nodes_keys[::-1]: base_key = ''
found = False for key in nodes_keys:
for k in nodes_keys: if key.startswith(base_key + ':'):
if key.startswith(k + ':'): continue
found = True nodes_keys_clean.append(key)
break base_key = key
if not found:
nodes_keys_clean.append(key)
return nodes_keys_clean return nodes_keys_clean
@classmethod @classmethod
@@ -175,13 +78,16 @@ class FamilyMixin:
return re.match(children_pattern, self.key) return re.match(children_pattern, self.key)
def get_children(self, with_self=False): def get_children(self, with_self=False):
pattern = self.get_children_key_pattern(with_self=with_self) q = Q(parent_key=self.key)
return Node.objects.filter(key__regex=pattern) if with_self:
q |= Q(key=self.key)
return Node.objects.filter(q)
def get_all_children(self, with_self=False): def get_all_children(self, with_self=False):
pattern = self.get_all_children_pattern(with_self=with_self) q = Q(key__istartswith=f'{self.key}:')
children = Node.objects.filter(key__regex=pattern) if with_self:
return children q |= Q(key=self.key)
return Node.objects.filter(q)
@property @property
def children(self): def children(self):
@@ -191,14 +97,30 @@ class FamilyMixin:
def all_children(self): def all_children(self):
return self.get_all_children(with_self=False) return self.get_all_children(with_self=False)
def create_child(self, value, _id=None): def create_child(self, value=None, _id=None):
with transaction.atomic(): with atomic(savepoint=False):
child_key = self.get_next_child_key() child_key = self.get_next_child_key()
if value is None:
value = child_key
child = self.__class__.objects.create( child = self.__class__.objects.create(
id=_id, key=child_key, value=value id=_id, key=child_key, value=value
) )
return child return child
def get_or_create_child(self, value, _id=None):
"""
:return: Node, bool (created)
"""
children = self.get_children()
exist = children.filter(value=value).exists()
if exist:
child = children.filter(value=value).first()
created = False
else:
child = self.create_child(value, _id)
created = True
return child, created
def get_next_child_key(self): def get_next_child_key(self):
mark = self.child_mark mark = self.child_mark
self.child_mark += 1 self.child_mark += 1
@@ -241,10 +163,13 @@ class FamilyMixin:
ancestor_keys = self.get_ancestor_keys(with_self=with_self) ancestor_keys = self.get_ancestor_keys(with_self=with_self)
return self.__class__.objects.filter(key__in=ancestor_keys) return self.__class__.objects.filter(key__in=ancestor_keys)
@property # @property
def parent_key(self): # def parent_key(self):
parent_key = ":".join(self.key.split(":")[:-1]) # parent_key = ":".join(self.key.split(":")[:-1])
return parent_key # return parent_key
def compute_parent_key(self):
return compute_parent_key(self.key)
def is_parent(self, other): def is_parent(self, other):
return other.is_children(self) return other.is_children(self)
@@ -280,45 +205,63 @@ class FamilyMixin:
sibling = sibling.exclude(key=self.key) sibling = sibling.exclude(key=self.key)
return sibling return sibling
@classmethod
def create_node_by_full_value(cls, full_value):
if not full_value:
return []
nodes_family = full_value.split('/')
nodes_family = [v for v in nodes_family if v]
org_root = cls.org_root()
if nodes_family[0] == org_root.value:
nodes_family = nodes_family[1:]
return cls.create_nodes_recurse(nodes_family, org_root)
@classmethod
def create_nodes_recurse(cls, values, parent=None):
values = [v for v in values if v]
if not values:
return None
if parent is None:
parent = cls.org_root()
value = values[0]
child, created = parent.get_or_create_child(value=value)
if len(values) == 1:
return child
return cls.create_nodes_recurse(values[1:], child)
def get_family(self): def get_family(self):
ancestors = self.get_ancestors() ancestors = self.get_ancestors()
children = self.get_all_children() children = self.get_all_children()
return [*tuple(ancestors), self, *tuple(children)] return [*tuple(ancestors), self, *tuple(children)]
class FullValueMixin:
key = ''
@lazyproperty
def full_value(self):
if self.is_org_root():
return self.value
value = self.tree().get_node_full_tag(self.key)
return value
class NodeAssetsMixin: class NodeAssetsMixin:
key = '' key = ''
id = None id = None
@lazyproperty
def assets_amount(self):
amount = self.tree().assets_amount(self.key)
return amount
def get_all_assets(self): def get_all_assets(self):
from .asset import Asset from .asset import Asset
if self.is_org_root(): q = Q(nodes__key__startswith=f'{self.key}:') | Q(nodes__key=self.key)
return Asset.objects.filter(org_id=self.org_id) return Asset.objects.filter(q).distinct()
pattern = '^{0}$|^{0}:'.format(self.key)
return Asset.objects.filter(nodes__key__regex=pattern).distinct() @classmethod
def get_node_all_assets_by_key_v2(cls, key):
# 最初的写法是:
# Asset.objects.filter(Q(nodes__key__startswith=f'{node.key}:') | Q(nodes__id=node.id))
# 可是 startswith 会导致表关联时 Asset 索引失效
from .asset import Asset
node_ids = cls.objects.filter(
Q(key__startswith=f'{key}:') |
Q(key=key)
).values_list('id', flat=True).distinct()
assets = Asset.objects.filter(
nodes__id__in=list(node_ids)
).distinct()
return assets
def get_assets(self): def get_assets(self):
from .asset import Asset from .asset import Asset
if self.is_org_root(): assets = Asset.objects.filter(nodes=self)
assets = Asset.objects.filter(Q(nodes=self) | Q(nodes__isnull=True))
else:
assets = Asset.objects.filter(nodes=self)
return assets.distinct() return assets.distinct()
def get_valid_assets(self): def get_valid_assets(self):
@@ -327,51 +270,54 @@ class NodeAssetsMixin:
def get_all_valid_assets(self): def get_all_valid_assets(self):
return self.get_all_assets().valid() return self.get_all_assets().valid()
@classmethod
def _get_nodes_all_assets(cls, nodes_keys):
"""
当节点比较多的时候,这种正则方式性能差极了
:param nodes_keys:
:return:
"""
from .asset import Asset
nodes_keys = cls.clean_children_keys(nodes_keys)
nodes_children_pattern = set()
for key in nodes_keys:
children_pattern = cls.get_node_all_children_key_pattern(key)
nodes_children_pattern.add(children_pattern)
pattern = '|'.join(nodes_children_pattern)
return Asset.objects.filter(nodes__key__regex=pattern).distinct()
@classmethod @classmethod
def get_nodes_all_assets_ids(cls, nodes_keys): def get_nodes_all_assets_ids(cls, nodes_keys):
nodes_keys = cls.clean_children_keys(nodes_keys) assets_ids = cls.get_nodes_all_assets(nodes_keys).values_list('id', flat=True)
assets_ids = set()
for key in nodes_keys:
node_assets_ids = cls.tree().all_assets(key)
assets_ids.update(set(node_assets_ids))
return assets_ids return assets_ids
@classmethod @classmethod
def get_nodes_all_assets(cls, nodes_keys, extra_assets_ids=None): def get_nodes_all_assets(cls, nodes_keys, extra_assets_ids=None):
from .asset import Asset from .asset import Asset
nodes_keys = cls.clean_children_keys(nodes_keys) nodes_keys = cls.clean_children_keys(nodes_keys)
assets_ids = cls.get_nodes_all_assets_ids(nodes_keys) q = Q()
node_ids = ()
for key in nodes_keys:
q |= Q(key__startswith=f'{key}:')
q |= Q(key=key)
if q:
node_ids = Node.objects.filter(q).distinct().values_list('id', flat=True)
q = Q(nodes__id__in=list(node_ids))
if extra_assets_ids: if extra_assets_ids:
assets_ids.update(set(extra_assets_ids)) q |= Q(id__in=extra_assets_ids)
return Asset.objects.filter(id__in=assets_ids) if q:
return Asset.org_objects.filter(q).distinct()
else:
return Asset.objects.none()
class SomeNodesMixin: class SomeNodesMixin:
key = '' key = ''
default_key = '1' default_key = '1'
default_value = 'Default' default_value = 'Default'
ungrouped_key = '-10'
ungrouped_value = _('ungrouped')
empty_key = '-11' empty_key = '-11'
empty_value = _("empty") empty_value = _("empty")
favorite_key = '-12'
favorite_value = _("favorite") @classmethod
def default_node(cls):
with tmp_to_org(Organization.default()):
defaults = {'value': cls.default_value}
try:
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
except IntegrityError as e:
logger.error("Create default node failed: {}".format(e))
cls.modify_other_org_root_node_key()
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
return obj
def is_default_node(self): def is_default_node(self):
return self.key == self.default_key return self.key == self.default_key
@@ -406,51 +352,18 @@ class SomeNodesMixin:
@classmethod @classmethod
def org_root(cls): def org_root(cls):
root = cls.objects.filter(key__regex=r'^[0-9]+$') root = cls.objects.filter(parent_key='')\
.filter(key__regex=r'^[0-9]+$')\
.exclude(key__startswith='-')\
.order_by('key')
if root: if root:
return root[0] return root[0]
else: else:
return cls.create_org_root_node() return cls.create_org_root_node()
@classmethod
def ungrouped_node(cls):
with tmp_to_org(Organization.system()):
defaults = {'value': cls.ungrouped_value}
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.ungrouped_key
)
return obj
@classmethod
def default_node(cls):
with tmp_to_org(Organization.default()):
defaults = {'value': cls.default_value}
try:
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
except IntegrityError as e:
logger.error("Create default node failed: {}".format(e))
cls.modify_other_org_root_node_key()
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.default_key,
)
return obj
@classmethod
def favorite_node(cls):
with tmp_to_org(Organization.system()):
defaults = {'value': cls.favorite_value}
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.favorite_key
)
return obj
@classmethod @classmethod
def initial_some_nodes(cls): def initial_some_nodes(cls):
cls.default_node() cls.default_node()
cls.ungrouped_node()
cls.favorite_node()
@classmethod @classmethod
def modify_other_org_root_node_key(cls): def modify_other_org_root_node_key(cls):
@@ -482,12 +395,16 @@ class SomeNodesMixin:
logger.info('Modify key ( {} > {} )'.format(old_key, new_key)) logger.info('Modify key ( {} > {} )'.format(old_key, new_key))
class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin, NodeAssetsMixin): class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True) id = models.UUIDField(default=uuid.uuid4, primary_key=True)
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1' key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
value = models.CharField(max_length=128, verbose_name=_("Value")) value = models.CharField(max_length=128, verbose_name=_("Value"))
full_value = models.CharField(max_length=4096, verbose_name=_('Full value'), default='')
child_mark = models.IntegerField(default=0) child_mark = models.IntegerField(default=0)
date_create = models.DateTimeField(auto_now_add=True) date_create = models.DateTimeField(auto_now_add=True)
parent_key = models.CharField(max_length=64, verbose_name=_("Parent key"),
db_index=True, default='')
assets_amount = models.IntegerField(default=0)
objects = OrgManager.from_queryset(NodeQuerySet)() objects = OrgManager.from_queryset(NodeQuerySet)()
is_node = True is_node = True
@@ -495,10 +412,10 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
class Meta: class Meta:
verbose_name = _("Node") verbose_name = _("Node")
ordering = ['key'] ordering = ['parent_key', 'value']
def __str__(self): def __str__(self):
return self.value return self.full_value
# def __eq__(self, other): # def __eq__(self, other):
# if not other: # if not other:
@@ -522,18 +439,19 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
def name(self): def name(self):
return self.value return self.value
def computed_full_value(self):
# 不要在列表中调用该属性
values = self.__class__.objects.filter(
key__in=self.get_ancestor_keys()
).values_list('key', 'value')
values = [v for k, v in sorted(values, key=lambda x: len(x[0]))]
values.append(str(self.value))
return '/' + '/'.join(values)
@property @property
def level(self): def level(self):
return len(self.key.split(':')) return len(self.key.split(':'))
@classmethod
def refresh_nodes(cls):
cls.refresh_tree()
@classmethod
def refresh_assets(cls):
cls.refresh_node_assets()
def as_tree_node(self): def as_tree_node(self):
from common.tree import TreeNode from common.tree import TreeNode
name = '{} ({})'.format(self.value, self.assets_amount) name = '{} ({})'.format(self.value, self.assets_amount)
@@ -568,19 +486,26 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
return return
return super().delete(using=using, keep_parents=keep_parents) return super().delete(using=using, keep_parents=keep_parents)
@classmethod def update_child_full_value(self):
def generate_fake(cls, count=100): nodes = self.get_all_children(with_self=True)
import random sort_key_func = lambda n: [int(i) for i in n.key.split(':')]
org = get_current_org() nodes_sorted = sorted(list(nodes), key=sort_key_func)
if not org or not org.is_real(): nodes_mapper = {n.key: n for n in nodes_sorted}
Organization.default().change_to() if not self.is_org_root():
nodes = list(cls.objects.all()) # 如果是org_root那么parent_key为'', parent为自己所以这种情况不处理
if count > 100: # 更新自己时自己的parent_key获取不到
length = 100 nodes_mapper.update({self.parent_key: self.parent})
else: for node in nodes_sorted:
length = count parent = nodes_mapper.get(node.parent_key)
if not parent:
if node.parent_key:
logger.error(f'Node parent node in mapper: {node.parent_key} {node.value}')
continue
node.full_value = parent.full_value + '/' + node.value
self.__class__.objects.bulk_update(nodes, ['full_value'])
for i in range(length): def save(self, *args, **kwargs):
node = random.choice(nodes) self.full_value = self.computed_full_value()
child = node.create_child('Node {}'.format(i)) instance = super().save(*args, **kwargs)
print("{}. {}".format(i, child)) self.update_child_full_value()
return instance

View File

@@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from common.utils import signer from common.utils import signer
from common.fields.model import JsonListCharField
from .base import BaseUser from .base import BaseUser
from .asset import Asset from .asset import Asset
@@ -64,26 +65,6 @@ class AdminUser(BaseUser):
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
verbose_name = _("Admin user") verbose_name = _("Admin user")
@classmethod
def generate_fake(cls, count=10):
from random import seed
import forgery_py
from django.db import IntegrityError
seed()
for i in range(count):
obj = cls(name=forgery_py.name.full_name(),
username=forgery_py.internet.user_name(),
password=forgery_py.lorem_ipsum.word(),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:
obj.save()
logger.debug('Generate fake asset group: %s' % obj.name)
except IntegrityError:
print('Error continue')
continue
class SystemUser(BaseUser): class SystemUser(BaseUser):
PROTOCOL_SSH = 'ssh' PROTOCOL_SSH = 'ssh'
@@ -91,12 +72,20 @@ class SystemUser(BaseUser):
PROTOCOL_TELNET = 'telnet' PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc' PROTOCOL_VNC = 'vnc'
PROTOCOL_MYSQL = 'mysql' PROTOCOL_MYSQL = 'mysql'
PROTOCOL_ORACLE = 'oracle'
PROTOCOL_MARIADB = 'mariadb'
PROTOCOL_POSTGRESQL = 'postgresql'
PROTOCOL_K8S = 'k8s'
PROTOCOL_CHOICES = ( PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'), (PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'), (PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet'), (PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'), (PROTOCOL_VNC, 'vnc'),
(PROTOCOL_MYSQL, 'mysql'), (PROTOCOL_MYSQL, 'mysql'),
(PROTOCOL_ORACLE, 'oracle'),
(PROTOCOL_MARIADB, 'mariadb'),
(PROTOCOL_POSTGRESQL, 'postgresql'),
(PROTOCOL_K8S, 'k8s'),
) )
LOGIN_AUTO = 'auto' LOGIN_AUTO = 'auto'
@@ -118,6 +107,10 @@ class SystemUser(BaseUser):
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode')) 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) 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")) 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' _prefer = 'system_user'
def __str__(self): def __str__(self):
@@ -140,6 +133,24 @@ class SystemUser(BaseUser):
def login_mode_display(self): def login_mode_display(self):
return self.get_login_mode_display() return self.get_login_mode_display()
@property
def db_application_protocols(self):
return [
self.PROTOCOL_MYSQL, self.PROTOCOL_ORACLE, self.PROTOCOL_MARIADB,
self.PROTOCOL_POSTGRESQL
]
@property
def cloud_application_protocols(self):
return [self.PROTOCOL_K8S]
@property
def application_category_protocols(self):
protocols = []
protocols.extend(self.db_application_protocols)
protocols.extend(self.cloud_application_protocols)
return protocols
def is_need_push(self): def is_need_push(self):
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]: if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
return True return True
@@ -152,11 +163,16 @@ class SystemUser(BaseUser):
@property @property
def is_need_test_asset_connective(self): def is_need_test_asset_connective(self):
return self.protocol not in [self.PROTOCOL_MYSQL] return self.protocol not in self.application_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 @property
def can_perm_to_asset(self): def can_perm_to_asset(self):
return self.protocol not in [self.PROTOCOL_MYSQL] return self.protocol not in self.application_category_protocols
def _merge_auth(self, other): def _merge_auth(self, other):
super()._merge_auth(other) super()._merge_auth(other)
@@ -193,23 +209,3 @@ class SystemUser(BaseUser):
ordering = ['name'] ordering = ['name']
unique_together = [('name', 'org_id')] unique_together = [('name', 'org_id')]
verbose_name = _("System user") verbose_name = _("System user")
@classmethod
def generate_fake(cls, count=10):
from random import seed
import forgery_py
from django.db import IntegrityError
seed()
for i in range(count):
obj = cls(name=forgery_py.name.full_name(),
username=forgery_py.internet.user_name(),
password=forgery_py.lorem_ipsum.word(),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:
obj.save()
logger.debug('Generate fake asset group: %s' % obj.name)
except IntegrityError:
print('Error continue')
continue

39
apps/assets/pagination.py Normal file
View File

@@ -0,0 +1,39 @@
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.request import Request
from assets.models import Node
class AssetLimitOffsetPagination(LimitOffsetPagination):
"""
需要与 `assets.api.mixin.FilterAssetByNodeMixin` 配合使用
"""
def get_count(self, queryset):
"""
1. 如果查询节点下的所有资产,那 count 使用 Node.assets_amount
2. 如果有其他过滤条件使用 super
3. 如果只查询该节点下的资产使用 super
"""
exclude_query_params = {
self.limit_query_param,
self.offset_query_param,
'node', 'all', 'show_current_asset',
'node_id', 'display', 'draw', 'fields_size',
}
for k, v in self._request.query_params.items():
if k not in exclude_query_params and v is not None:
return super().get_count(queryset)
is_query_all = self._view.is_query_node_all_assets
if is_query_all:
node = self._view.node
if not node:
node = Node.org_root()
return node.assets_amount
return super().get_count(queryset)
def paginate_queryset(self, queryset, request: Request, view=None):
self._request = request
self._view = view
return super().paginate_queryset(queryset, request, view=None)

View File

@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from django.db.models import Prefetch, F from django.db.models import F
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer from ..models import Asset, Node, Platform
from ..models import Asset, Node, Label, Platform
from .base import ConnectivitySerializer from .base import ConnectivitySerializer
__all__ = [ __all__ = [
@@ -67,27 +66,41 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
slug_field='name', queryset=Platform.objects.all(), label=_("Platform") slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
) )
protocols = ProtocolsField(label=_('Protocols'), required=False) protocols = ProtocolsField(label=_('Protocols'), required=False)
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)
""" """
资产的数据结构 资产的数据结构
""" """
class Meta: class Meta:
model = Asset model = Asset
list_serializer_class = AdaptedBulkListSerializer fields_mini = ['id', 'hostname', 'ip']
fields = [ fields_small = fields_mini + [
'id', 'ip', 'hostname', 'protocol', 'port', 'protocol', 'port', 'protocols', 'is_active', 'public_ip',
'protocols', 'platform', 'is_active', 'public_ip', 'domain', 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'admin_user', 'nodes', 'labels', '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',
]
read_only_fields = (
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw', 'os', 'os_version', 'os_arch', 'hostname_raw', 'comment',
'created_by', 'date_created', 'hardware_info',
]
fields_fk = [
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
]
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', 'created_by', 'date_created',
) ] + fields_as
extra_kwargs = { extra_kwargs = {
'protocol': {'write_only': True}, 'protocol': {'write_only': True},
'port': {'write_only': True}, 'port': {'write_only': True},
@@ -98,11 +111,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """
queryset = queryset.prefetch_related( queryset = queryset.select_related('admin_user', 'domain', 'platform')
Prefetch('nodes', queryset=Node.objects.all().only('id')), queryset = queryset.prefetch_related('nodes', 'labels')
Prefetch('labels', queryset=Label.objects.all().only('id')),
).select_related('admin_user', 'domain', 'platform') \
.annotate(platform_base=F('platform__base'))
return queryset return queryset
def compatible_with_old_protocol(self, validated_data): def compatible_with_old_protocol(self, validated_data):
@@ -120,33 +130,45 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
if protocols_data: if protocols_data:
validated_data["protocols"] = ' '.join(protocols_data) validated_data["protocols"] = ' '.join(protocols_data)
def perform_nodes_display_create(self, instance, nodes_display):
if not nodes_display:
return
nodes_to_set = []
for full_value in nodes_display:
node = Node.objects.filter(full_value=full_value).first()
if node:
nodes_to_set.append(node)
else:
node = Node.create_node_by_full_value(full_value)
nodes_to_set.append(node)
instance.nodes.set(nodes_to_set)
def create(self, validated_data): def create(self, validated_data):
self.compatible_with_old_protocol(validated_data) self.compatible_with_old_protocol(validated_data)
nodes_display = validated_data.pop('nodes_display', '')
instance = super().create(validated_data) instance = super().create(validated_data)
self.perform_nodes_display_create(instance, nodes_display)
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
nodes_display = validated_data.pop('nodes_display', '')
self.compatible_with_old_protocol(validated_data) self.compatible_with_old_protocol(validated_data)
return super().update(instance, validated_data) instance = super().update(instance, validated_data)
self.perform_nodes_display_create(instance, nodes_display)
return instance
class AssetDisplaySerializer(AssetSerializer): class AssetDisplaySerializer(AssetSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity")) connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
class Meta(AssetSerializer.Meta): class Meta(AssetSerializer.Meta):
fields = [ fields = AssetSerializer.Meta.fields + [
'id', 'ip', 'hostname', 'protocol', 'port', 'connectivity',
'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', 'connectivity',
] ]
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ queryset = super().setup_eager_loading(queryset)
queryset = queryset\ queryset = queryset\
.annotate(admin_user_username=F('admin_user__username')) .annotate(admin_user_username=F('admin_user__username'))
return queryset return queryset

View File

@@ -2,7 +2,6 @@
# #
import re import re
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.fields import ChoiceDisplayField from common.fields import ChoiceDisplayField
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
@@ -27,11 +26,20 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
serializer_choice_field = ChoiceDisplayField # serializer_choice_field = ChoiceDisplayField
invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]') invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
type_display = serializers.ReadOnlyField(source='get_type_display')
action_display = serializers.ReadOnlyField(source='get_action_display')
class Meta: class Meta:
model = CommandFilterRule model = CommandFilterRule
fields_mini = ['id']
fields_small = fields_mini + [
'type', 'type_display', 'content', 'priority',
'action', 'action_display',
'comment', 'created_by', 'date_created', 'date_updated'
]
fields_fk = ['filter']
fields = '__all__' fields = '__all__'
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer

View File

@@ -1,31 +1,44 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.validators import NoSpecialChars
from ..models import Domain, Gateway from ..models import Domain, Gateway
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
class DomainSerializer(BulkOrgResourceModelSerializer): class DomainSerializer(BulkOrgResourceModelSerializer):
asset_count = serializers.SerializerMethodField() asset_count = serializers.SerializerMethodField(label=_('Assets count'))
gateway_count = serializers.SerializerMethodField() application_count = serializers.SerializerMethodField(label=_('Applications count'))
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
class Meta: class Meta:
model = Domain model = Domain
fields = [ fields_mini = ['id', 'name']
'id', 'name', 'asset_count', 'gateway_count', 'comment', 'assets', fields_small = fields_mini + [
'date_created' 'comment', 'date_created'
] ]
read_only_fields = ( 'asset_count', 'gateway_count', 'date_created') fields_m2m = [
'asset_count', 'assets', 'application_count', 'gateway_count',
]
fields = fields_small + fields_m2m
read_only_fields = ('asset_count', 'gateway_count', 'date_created')
extra_kwargs = {
'assets': {'required': False, 'label': _('Assets')},
}
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
@staticmethod @staticmethod
def get_asset_count(obj): def get_asset_count(obj):
return obj.assets.count() return obj.assets.count()
@staticmethod
def get_application_count(obj):
return obj.applications.count()
@staticmethod @staticmethod
def get_gateway_count(obj): def get_gateway_count(obj):
return obj.gateway_set.all().count() return obj.gateway_set.all().count()
@@ -40,6 +53,19 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'private_key', 'public_key', 'domain', 'is_active', 'date_created', 'private_key', 'public_key', 'domain', 'is_active', 'date_created',
'date_updated', 'created_by', 'comment', 'date_updated', 'created_by', 'comment',
] ]
extra_kwargs = {
'password': {'validators': [NoSpecialChars()]}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_limit_to_ssh()
def protocol_limit_to_ssh(self):
protocol_field = self.fields['protocol']
choices = protocol_field.choices
choices.pop('rdp')
protocol_field._choices = choices
class GatewayWithAuthSerializer(GatewaySerializer): class GatewayWithAuthSerializer(GatewaySerializer):
@@ -51,6 +77,8 @@ class GatewayWithAuthSerializer(GatewaySerializer):
return fields return fields
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer): class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):
gateways = GatewayWithAuthSerializer(many=True, read_only=True) gateways = GatewayWithAuthSerializer(many=True, read_only=True)

View File

@@ -16,7 +16,7 @@ class GatheredUserSerializer(OrgResourceModelSerializerMixin):
'present', 'date_created', 'date_updated' 'present', 'date_created', 'date_updated'
] ]
read_only_fields = fields read_only_fields = fields
labels = { extra_kwargs = {
'hostname': _("Hostname"), 'hostname': {'label': _("Hostname")},
'ip': "IP" 'ip': {'label': 'IP'},
} }

View File

@@ -20,6 +20,9 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
read_only_fields = ( read_only_fields = (
'category', 'date_created', 'asset_count', 'get_category_display' 'category', 'date_created', 'asset_count', 'get_category_display'
) )
extra_kwargs = {
'assets': {'required': False}
}
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
@staticmethod @staticmethod

View File

@@ -25,6 +25,9 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
read_only_fields = ['key', 'org_id'] read_only_fields = ['key', 'org_id']
def validate_value(self, data): def validate_value(self, data):
if '/' in data:
error = _("Can't contains: " + "/")
raise serializers.ValidationError(error)
if self.instance: if self.instance:
instance = self.instance instance = self.instance
siblings = instance.get_siblings() siblings = instance.get_siblings()

View File

@@ -6,7 +6,6 @@ from common.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen from common.utils import ssh_pubkey_gen
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Node
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .base import AuthSerializerMixin from .base import AuthSerializerMixin
@@ -33,17 +32,20 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'login_mode', 'login_mode_display', 'login_mode', 'login_mode_display',
'priority', 'username_same_with_user', 'priority', 'username_same_with_user',
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment', 'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root', 'auto_generate_key', 'sftp_root', 'token',
'assets_amount', 'assets_amount', 'date_created', 'created_by',
'home', 'system_groups', 'ad_domain'
] ]
extra_kwargs = { extra_kwargs = {
'password': {"write_only": True}, 'password': {"write_only": True},
'public_key': {"write_only": True}, 'public_key': {"write_only": True},
'private_key': {"write_only": True}, 'private_key': {"write_only": True},
'nodes_amount': {'label': _('Node')}, 'token': {"write_only": True},
'assets_amount': {'label': _('Asset')}, 'nodes_amount': {'label': _('Nodes amount')},
'assets_amount': {'label': _('Assets amount')},
'login_mode_display': {'label': _('Login mode display')}, 'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True}, 'created_by': {'read_only': True},
'ad_domain': {'required': False, 'allow_blank': True, 'label': _('Ad domain')},
} }
def validate_auto_push(self, value): def validate_auto_push(self, value):
@@ -143,16 +145,28 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class SystemUserListSerializer(SystemUserSerializer): class SystemUserListSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta): class Meta(SystemUserSerializer.Meta):
fields = [ fields = [
'id', 'name', 'username', 'protocol', 'id', 'name', 'username', 'protocol',
'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display', 'login_mode', 'login_mode_display',
'priority', "username_same_with_user", 'priority', "username_same_with_user",
'auto_push', 'sudo', 'shell', 'comment', 'auto_push', 'sudo', 'shell', 'comment',
"assets_amount", "assets_amount", 'home', 'system_groups',
'auto_generate_key', 'auto_generate_key', 'ad_domain',
'sftp_root', 'sftp_root',
] ]
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')},
}
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
@@ -169,7 +183,8 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
'login_mode', 'login_mode_display', 'login_mode', 'login_mode_display',
'priority', 'username_same_with_user', 'priority', 'username_same_with_user',
'auto_push', 'sudo', 'shell', 'comment', 'auto_push', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root', 'auto_generate_key', 'sftp_root', 'token',
'ad_domain',
] ]
extra_kwargs = { extra_kwargs = {
'nodes_amount': {'label': _('Node')}, 'nodes_amount': {'label': _('Node')},
@@ -219,15 +234,8 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
'id', 'node', "node_display", 'id', 'node', "node_display",
] ]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tree = Node.tree()
def get_node_display(self, obj): def get_node_display(self, obj):
if hasattr(obj, 'node_key'): return obj.node.full_value
return self.tree.get_node_full_tag(obj.node_key)
else:
return obj.node.full_value
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer): class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
@@ -249,4 +257,8 @@ class SystemUserTaskSerializer(serializers.Serializer):
asset = serializers.PrimaryKeyRelatedField( asset = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, allow_null=True, required=False, write_only=True queryset=Asset.objects, allow_null=True, required=False, write_only=True
) )
assets = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, allow_null=True, required=False, write_only=True,
many=True
)
task = serializers.CharField(read_only=True) task = serializers.CharField(read_only=True)

View File

@@ -1,17 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from collections import defaultdict from operator import add, sub
from assets.utils import is_asset_exists_in_node
from django.db.models.signals import ( from django.db.models.signals import (
post_save, m2m_changed, post_delete post_save, m2m_changed, pre_delete, post_delete, pre_save
) )
from django.db.models.aggregates import Count from django.db.models import Q, F
from django.dispatch import receiver from django.dispatch import receiver
from common.exceptions import M2MReverseNotAllowed
from common.const.signals import PRE_ADD, POST_ADD, POST_REMOVE, PRE_CLEAR, PRE_REMOVE
from common.utils import get_logger from common.utils import get_logger
from common.decorator import on_transaction_commit from common.decorator import on_transaction_commit
from orgs.utils import tmp_to_root_org from .models import Asset, SystemUser, Node, compute_parent_key
from .models import Asset, SystemUser, Node, AuthBook from users.models import User
from .utils import TreeService
from .tasks import ( from .tasks import (
update_assets_hardware_info_util, update_assets_hardware_info_util,
test_asset_connectivity_util, test_asset_connectivity_util,
@@ -34,6 +37,11 @@ def test_asset_conn_on_created(asset):
test_asset_connectivity_util.delay([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) @receiver(post_save, sender=Asset)
@on_transaction_commit @on_transaction_commit
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
@@ -54,17 +62,9 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
instance.nodes.add(Node.org_root()) instance.nodes.add(Node.org_root())
@receiver(post_delete, sender=Asset)
def on_asset_delete(sender, instance=None, **kwargs):
"""
当资产删除时,刷新节点,节点中存在节点和资产的关系
"""
logger.debug("Asset delete signal recv: {}".format(instance))
Node.refresh_assets()
@receiver(post_save, sender=SystemUser, dispatch_uid="jms") @receiver(post_save, sender=SystemUser, dispatch_uid="jms")
def on_system_user_update(sender, instance=None, created=True, **kwargs): @on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
""" """
当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上, 当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上,
其实应该当 用户名,密码,秘钥 sudo等更新时再推送这里偷个懒, 其实应该当 用户名,密码,秘钥 sudo等更新时再推送这里偷个懒,
@@ -74,53 +74,57 @@ def on_system_user_update(sender, instance=None, created=True, **kwargs):
if instance and not created: if instance and not created:
logger.info("System user update signal recv: {}".format(instance)) logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid() assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance, assets) push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])
@receiver(m2m_changed, sender=SystemUser.assets.through) @receiver(m2m_changed, sender=SystemUser.assets.through)
def on_system_user_assets_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs): @on_transaction_commit
def on_system_user_assets_change(instance, action, model, pk_set, **kwargs):
""" """
当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中 当系统用户和资产关系发生变化时,应该重新推送系统用户到新添加的资产中
""" """
if action != "post_add": if action != POST_ADD:
return return
logger.debug("System user assets change signal recv: {}".format(instance)) logger.debug("System user assets change signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == Asset: if model == Asset:
system_users = [instance] system_users_id = [instance.id]
assets = queryset assets_id = pk_set
else: else:
system_users = queryset system_users_id = pk_set
assets = [instance] assets_id = [instance.id]
for system_user in system_users: for system_user_id in system_users_id:
push_system_user_to_assets.delay(system_user, assets) push_system_user_to_assets.delay(system_user_id, assets_id)
@receiver(m2m_changed, sender=SystemUser.users.through) @receiver(m2m_changed, sender=SystemUser.users.through)
def on_system_user_users_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs): @on_transaction_commit
def on_system_user_users_change(sender, instance: SystemUser, action, model, pk_set, reverse, **kwargs):
""" """
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中 当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
""" """
if action != "post_add": if action != POST_ADD:
return return
if reverse:
raise M2MReverseNotAllowed
if not instance.username_same_with_user: if not instance.username_same_with_user:
return return
logger.debug("System user users change signal recv: {}".format(instance)) logger.debug("System user users change signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set) usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
if model == SystemUser:
system_users = queryset for username in usernames:
else: push_system_user_to_assets_manual.delay(instance, username)
system_users = [instance]
for s in system_users:
push_system_user_to_assets_manual.delay(s)
@receiver(m2m_changed, sender=SystemUser.nodes.through) @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): def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
""" """
当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上 当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上
""" """
if action != "post_add": if action != POST_ADD:
return return
logger.info("System user nodes update signal recv: {}".format(instance)) logger.info("System user nodes update signal recv: {}".format(instance))
@@ -135,102 +139,217 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None,
@receiver(m2m_changed, sender=SystemUser.groups.through) @receiver(m2m_changed, sender=SystemUser.groups.through)
def on_system_user_groups_change(sender, instance=None, action=None, model=None, def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
pk_set=None, reverse=False, **kwargs):
""" """
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上 当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
""" """
if action != "post_add" or reverse: if action != POST_ADD:
return return
if reverse:
raise M2MReverseNotAllowed
logger.info("System user groups update signal recv: {}".format(instance)) logger.info("System user groups update signal recv: {}".format(instance))
groups = model.objects.filter(pk__in=pk_set).annotate(users_count=Count("users"))
users = groups.filter(users_count__gt=0).values_list('users', flat=True) users = User.objects.filter(groups__id__in=pk_set).distinct()
instance.users.add(*tuple(users)) instance.users.add(*users)
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_change(sender, instance=None, action='', **kwargs): def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
""" """
资产节点发生变化时,刷新节点 本操作共访问 4 次数据库
"""
if action.startswith('post'):
logger.debug("Asset nodes change signal recv: {}".format(instance))
Node.refresh_assets()
with tmp_to_root_org():
Node.refresh_assets()
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None, **kwargs):
"""
当资产的节点发生变化时,或者 当节点的资产关系发生变化时, 当资产的节点发生变化时,或者 当节点的资产关系发生变化时,
节点下新增的资产,添加到节点关联的系统用户中 节点下新增的资产,添加到节点关联的系统用户中
""" """
if action != "post_add": if action != POST_ADD:
return return
logger.debug("Assets node add signal recv: {}".format(action)) logger.debug("Assets node add signal recv: {}".format(action))
if model == Node: if reverse:
nodes = model.objects.filter(pk__in=pk_set).values_list('key', flat=True)
assets = [instance.id]
else:
nodes = [instance.key] nodes = [instance.key]
assets = model.objects.filter(pk__in=pk_set).values_list('id', flat=True) 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() nodes_ancestors_keys = set()
node_tree = TreeService.new()
for node in nodes: for node in nodes:
ancestors_keys = node_tree.ancestors_ids(nid=node) nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
nodes_ancestors_keys.update(ancestors_keys)
system_users = SystemUser.objects.filter(nodes__key__in=nodes_ancestors_keys)
system_users_assets = defaultdict(set) # 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
for system_user in system_users: system_user_ids = SystemUser.objects.filter(
system_users_assets[system_user].update(set(assets)) nodes__key__in=nodes_ancestors_keys
for system_user, _assets in system_users_assets.items(): ).distinct().values_list('id', flat=True)
system_user.assets.add(*tuple(_assets))
# 查询所有已存在的关系
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
))
push_system_user_to_assets.delay(system_user_id, asset_ids_to_push)
m2m_model.objects.bulk_create(to_create)
def _update_node_assets_amount(node: Node, asset_pk_set: set, operator=add):
"""
一个节点与多个资产关系变化时,更新计数
:param node: 节点实例
:param asset_pk_set: 资产的`id`集合, 内部不会修改该值
:param operator: 操作
* -> Node
# -> Asset
* [3]
/ \
* * [2]
/ \
* * [1]
/ / \
* [a] # # [b]
"""
# 获取节点[1]祖先节点的 `key` 含自己,也就是[1, 2, 3]节点的`key`
ancestor_keys = node.get_ancestor_keys(with_self=True)
ancestors = Node.objects.filter(key__in=ancestor_keys).order_by('-key')
to_update = []
for ancestor in ancestors:
# 迭代祖先节点的`key`,顺序是 [1] -> [2] -> [3]
# 查询该节点及其后代节点是否包含要操作的资产,将包含的从要操作的
# 资产集合中去掉,他们是重复节点,无论增加或删除都不会影响节点的资产数量
asset_pk_set -= set(Asset.objects.filter(
id__in=asset_pk_set
).filter(
Q(nodes__key__istartswith=f'{ancestor.key}:') |
Q(nodes__key=ancestor.key)
).distinct().values_list('id', flat=True))
if not asset_pk_set:
# 要操作的资产集合为空,说明都是重复资产,不用改变节点资产数量
# 而且既然它包含了,它的祖先节点肯定也包含了,所以祖先节点都不用
# 处理了
break
ancestor.assets_amount = operator(F('assets_amount'), len(asset_pk_set))
to_update.append(ancestor)
Node.objects.bulk_update(to_update, fields=('assets_amount', 'parent_key'))
def _remove_ancestor_keys(ancestor_key, tree_set):
# 这里判断 `ancestor_key` 不能是空,防止数据错误导致的死循环
# 判断是否在集合里,来区分是否已被处理过
while ancestor_key and ancestor_key in tree_set:
tree_set.remove(ancestor_key)
ancestor_key = compute_parent_key(ancestor_key)
def _update_nodes_asset_amount(node_keys, asset_pk, operator):
"""
一个资产与多个节点关系变化时,更新计数
:param node_keys: 节点 id 的集合
:param asset_pk: 资产 id
:param operator: 操作
"""
# 所有相关节点的祖先节点,组成一棵局部树
ancestor_keys = set()
for key in node_keys:
ancestor_keys.update(Node.get_node_ancestor_keys(key))
# 相关节点可能是其他相关节点的祖先节点,如果是从相关节点里干掉
node_keys -= ancestor_keys
to_update_keys = []
for key in node_keys:
# 遍历相关节点,处理它及其祖先节点
# 查询该节点是否包含待处理资产
exists = is_asset_exists_in_node(asset_pk, key)
parent_key = compute_parent_key(key)
if exists:
# 如果资产在该节点,那么他及其祖先节点都不用处理
_remove_ancestor_keys(parent_key, ancestor_keys)
continue
else:
# 不存在,要更新本节点
to_update_keys.append(key)
# 这里判断 `parent_key` 不能是空,防止数据错误导致的死循环
# 判断是否在集合里,来区分是否已被处理过
while parent_key and parent_key in ancestor_keys:
exists = is_asset_exists_in_node(asset_pk, parent_key)
if exists:
_remove_ancestor_keys(parent_key, ancestor_keys)
break
else:
to_update_keys.append(parent_key)
ancestor_keys.remove(parent_key)
parent_key = compute_parent_key(parent_key)
Node.objects.filter(key__in=to_update_keys).update(
assets_amount=operator(F('assets_amount'), 1)
)
@receiver(m2m_changed, sender=Asset.nodes.through) @receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_remove(sender, instance=None, action='', model=None, def update_nodes_assets_amount(action, instance, reverse, pk_set, **kwargs):
pk_set=None, **kwargs): # 不允许 `pre_clear` ,因为该信号没有 `pk_set`
# [官网](https://docs.djangoproject.com/en/3.1/ref/signals/#m2m-changed)
refused = (PRE_CLEAR,)
if action in refused:
raise ValueError
""" mapper = {
监控资产删除节点关系, 或节点删除资产,避免产生游离资产 PRE_ADD: add,
""" POST_REMOVE: sub
if action not in ["post_remove", "pre_clear", "post_clear"]: }
if action not in mapper:
return return
if action == "pre_clear":
if model == Node: operator = mapper[action]
instance._nodes = list(instance.nodes.all())
else: if reverse:
instance._assets = list(instance.assets.all()) node: Node = instance
return asset_pk_set = set(pk_set)
logger.debug("Assets node remove signal recv: {}".format(action)) _update_node_assets_amount(node, asset_pk_set, operator)
if action == "post_remove":
queryset = model.objects.filter(pk__in=pk_set)
else: else:
if model == Node: asset_pk = instance.id
queryset = instance._nodes # 与资产直接关联的节点
else: node_keys = set(Node.objects.filter(id__in=pk_set).values_list('key', flat=True))
queryset = instance._assets _update_nodes_asset_amount(node_keys, asset_pk, operator)
if model == Node:
assets = [instance]
else:
assets = queryset
if isinstance(assets, list):
assets_not_has_node = []
for asset in assets:
if asset.nodes.all().count() == 0:
assets_not_has_node.append(asset.id)
else:
assets_not_has_node = assets.annotate(nodes_count=Count('nodes'))\
.filter(nodes_count=0).values_list('id', flat=True)
Node.org_root().assets.add(*tuple(assets_not_has_node))
@receiver([post_save, post_delete], sender=Node) RELATED_NODE_IDS = '_related_node_ids'
def on_node_update_or_created(sender, **kwargs):
# 刷新节点
Node.refresh_nodes() @receiver(pre_delete, sender=Asset)
with tmp_to_root_org(): def on_asset_delete(instance: Asset, using, **kwargs):
Node.refresh_nodes() 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

@@ -9,3 +9,4 @@ from .gather_asset_users import *
from .gather_asset_hardware_info import * from .gather_asset_hardware_info import *
from .push_system_user import * from .push_system_user import *
from .system_user_connectivity import * from .system_user_connectivity import *
from .nodes_amount import *

View File

@@ -85,7 +85,7 @@ def test_asset_user_connectivity_util(asset_user, task_name):
raw, summary = test_user_connectivity( raw, summary = test_user_connectivity(
task_name=task_name, asset=asset_user.asset, task_name=task_name, asset=asset_user.asset,
username=asset_user.username, password=asset_user.password, username=asset_user.username, password=asset_user.password,
private_key=asset_user.private_key private_key=asset_user.private_key_file
) )
except Exception as e: except Exception as e:
logger.warn("Failed run adhoc {}, {}".format(task_name, e)) logger.warn("Failed run adhoc {}, {}".format(task_name, e))

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