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
logs/*
### 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
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
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt || pip install -r requirements.txt
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
# 构建运行时环境
FROM python:3.8.6-slim
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
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
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
ENV LC_ALL=zh_CN.UTF-8
EXPOSE 8070
EXPOSE 8080

View File

@@ -2,6 +2,11 @@
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](https://hub.docker.com/u/jumpserver)
|Developer Wanted|
|------------------|
|JumpServer 正在寻找开发者,一起为改变世界做些贡献吧,哪怕一点点,联系我 <ibuler@fit2cloud.com> |
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
@@ -20,7 +25,24 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- 无插件: 仅需浏览器,极致的 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>
</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>
## 快速开始
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
- [完整文档](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 .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 import generics
from common.exceptions import JMSException
from ..hands import IsOrgAdmin, IsAppUser
from ..models import RemoteApp
from .. import models
from ..serializers import RemoteAppSerializer, RemoteAppConnectionInfoSerializer
@@ -14,14 +15,26 @@ __all__ = [
class RemoteAppViewSet(OrgBulkModelViewSet):
model = RemoteApp
filter_fields = ('name',)
model = models.RemoteApp
filter_fields = ('name', 'type', 'comment')
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = RemoteAppSerializer
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
model = RemoteApp
model = models.Application
permission_classes = (IsAppUser, )
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 = [
{'name': 'mysql_workbench_ip'},
{'name': 'mysql_workbench_name'},
{'name': 'mysql_workbench_port'},
{'name': 'mysql_workbench_username'},
{'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 .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 .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
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer
from .. import models
__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):
@@ -24,3 +45,6 @@ class DatabaseAppSerializer(BulkOrgResourceModelSerializer):
'created_by', 'date_created', 'date_updated'
'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
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from common.fields.serializer import CustomMetaDictField
from common.utils import get_logger
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Asset
from .. import const
from ..models import RemoteApp
from ..models import RemoteApp, Category, Application
logger = get_logger(__file__)
__all__ = [
'RemoteAppSerializer', 'RemoteAppConnectionInfoSerializer',
]
class CharPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def to_internal_value(self, data):
instance = super().to_internal_value(data)
return str(instance.id)
def to_representation(self, value):
# value is instance.id
if self.pk_field is not None:
return self.pk_field.to_representation(value)
return value
class RemoteAppAttrsSerializer(serializers.Serializer):
asset_info = serializers.SerializerMethodField()
asset = CharPrimaryKeyRelatedField(queryset=Asset.objects, required=False, label=_("Asset"))
path = serializers.CharField(max_length=128, label=_('Application path'))
@staticmethod
def get_asset_info(obj):
asset_info = {}
asset_id = obj.get('asset')
if not asset_id:
return asset_info
try:
asset = Asset.objects.get(id=asset_id)
asset_info.update({
'id': str(asset.id),
'hostname': asset.hostname
})
except ObjectDoesNotExist as e:
logger.error(e)
return asset_info
class ChromeAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
chrome_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
chrome_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
chrome_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class MySQLWorkbenchAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files\MySQL\MySQL Workbench 8.0 CE\MySQLWorkbench.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
mysql_workbench_ip = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('IP'))
mysql_workbench_port = serializers.IntegerField(required=False, label=_('Port'))
mysql_workbench_name = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Database'))
mysql_workbench_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
mysql_workbench_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class VMwareClientAttrsSerializer(RemoteAppAttrsSerializer):
REMOTE_APP_PATH = 'C:\Program Files (x86)\VMware\Infrastructure\Virtual Infrastructure Client\Launcher\VpxClient.exe'
path = serializers.CharField(max_length=128, label=_('Application path'), default=REMOTE_APP_PATH)
vmware_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target URL'))
vmware_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
vmware_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
class CustomRemoteAppAttrsSeralizers(RemoteAppAttrsSerializer):
custom_cmdline = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Operating parameter'))
custom_target = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Target url'))
custom_username = serializers.CharField(max_length=128, allow_blank=True, required=False, label=_('Username'))
custom_password = serializers.CharField(max_length=128, allow_blank=True, required=False, write_only=True, label=_('Password'))
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):
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
default_type = const.REMOTE_APP_TYPE_CHROME
@@ -24,8 +141,9 @@ class RemoteAppParamsDictField(CustomMetaDictField):
convert_key_to_upper = False
# TODO: DELETE
class RemoteAppSerializer(BulkOrgResourceModelSerializer):
params = RemoteAppParamsDictField()
params = RemoteAppParamsDictField(label=_('Parameters'))
type_fields_map = const.REMOTE_APP_TYPE_FIELDS_MAP
class Meta:
@@ -39,6 +157,10 @@ class RemoteAppSerializer(BulkOrgResourceModelSerializer):
'created_by', 'date_created', 'asset_info',
'get_type_display'
]
extra_kwargs = {
'asset_info': {'label': _('Asset info')},
'get_type_display': {'label': _('Type for display')},
}
def process_params(self, instance, validated_data):
new_params = copy.deepcopy(validated_data.get('params', {}))
@@ -66,21 +188,3 @@ class RemoteAppSerializer(BulkOrgResourceModelSerializer):
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'
router = BulkRouter()
router.register(r'applications', api.ApplicationViewSet, 'application')
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
urlpatterns = [
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),

View File

@@ -1,23 +1,7 @@
# coding:utf-8
from django.urls import path
from .. import views
app_name = 'applications'
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 .asset 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.models import Count
@@ -49,7 +36,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets'))
queryset = queryset.annotate(assets_amount=Count('assets'))
return queryset
def destroy(self, request, *args, **kwargs):
@@ -107,7 +94,6 @@ class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self):

View File

@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
#
import random
from rest_framework.response import Response
from assets.api import FilterAssetByNodeMixin
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView
from django.shortcuts import get_object_or_404
@@ -17,7 +14,7 @@ from .. import serializers
from ..tasks import (
update_asset_hardware_info_manual, test_asset_connectivity_manual
)
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
from ..filters import FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
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.
"""
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")
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_classes = {
@@ -41,7 +41,7 @@ class AssetViewSet(OrgBulkModelViewSet):
'display': serializers.AssetDisplaySerializer,
}
permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
def set_assets_node(self, assets):
if not isinstance(assets, list):
@@ -74,12 +74,16 @@ class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']
filter_fields = ['name', 'base']
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):
if request.method.lower() in ['delete', 'put', 'patch'] and \
obj.internal:
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
self.permission_denied(
request, message={"detail": "Internal platform"}
)
@@ -111,7 +115,6 @@ class AssetTaskCreateApi(generics.CreateAPIView):
class AssetGatewayListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer
model = Asset
def get_queryset(self):
asset_id = self.kwargs.get('pk')

View File

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

View File

@@ -1,7 +1,9 @@
# ~*~ coding: utf-8 ~*~
from rest_framework.views import APIView, Response
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.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
@@ -42,6 +44,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all())
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)
if ok:
return Response("ok")

View File

@@ -18,5 +18,5 @@ class GatheredUserViewSet(OrgModelViewSet):
permission_classes = [IsOrgAdmin]
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']

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 ~*~
from functools import partial
from collections import namedtuple, defaultdict
from collections import namedtuple
from rest_framework import status
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from rest_framework.decorators import action
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404, Http404
from django.utils.decorators import method_decorator
from django.db.models.signals import m2m_changed
from common.const.http import POST
from common.exceptions import SomeoneIsDoingThis
from common.const.signals import PRE_REMOVE, POST_REMOVE
from assets.models import Asset
from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from common.const.distributed_lock_key import UPDATE_NODE_TREE_LOCK_KEY
from orgs.mixins.api import OrgModelViewSet
from orgs.mixins import generics
from orgs.lock import org_level_transaction_lock
from orgs.utils import current_org
from assets.tasks import check_node_assets_amount_task
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import (
@@ -18,12 +30,13 @@ from ..tasks import (
test_node_assets_connectivity_manual,
)
from .. import serializers
from .mixin import SerializeToTreeNodeMixin
logger = get_logger(__file__)
__all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'MoveAssetsToNodeApi',
'NodeAddChildrenApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi',
'NodeTaskCreateApi',
@@ -37,6 +50,11 @@ class NodeViewSet(OrgModelViewSet):
permission_classes = (IsOrgAdmin,)
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接口创建
def perform_create(self, serializer):
child_key = Node.org_root().get_next_child_key()
@@ -52,6 +70,9 @@ class NodeViewSet(OrgModelViewSet):
def destroy(self, request, *args, **kwargs):
node = self.get_object()
if node.is_org_root():
error = _("You can't delete the root node ({})".format(node.value))
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
if node.has_children_or_has_assets():
error = _("Deletion failed and the node contains children or assets")
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
@@ -136,7 +157,7 @@ class NodeChildrenApi(generics.ListCreateAPIView):
return queryset
class NodeChildrenAsTreeApi(NodeChildrenApi):
class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
"""
节点子节点作为树返回,
[
@@ -150,31 +171,23 @@ class NodeChildrenAsTreeApi(NodeChildrenApi):
"""
model = Node
serializer_class = TreeNodeSerializer
http_method_names = ['get']
def get_queryset(self):
queryset = super().get_queryset()
queryset = [node.as_tree_node() for node in queryset]
queryset = self.add_assets_if_need(queryset)
queryset = sorted(queryset)
return queryset
def list(self, request, *args, **kwargs):
nodes = self.get_queryset().order_by('value')
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
assets = self.get_assets()
data = [*nodes, *assets]
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'
if not include_assets:
return queryset
return []
assets = self.instance.get_assets().only(
"id", "hostname", "ip", "os",
"org_id", "protocols",
"org_id", "protocols", "is_active"
)
for asset in assets:
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()
return self.serialize_assets(assets, self.instance.key)
class NodeAssetsApi(generics.ListAPIView):
@@ -200,14 +213,14 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
def put(self, request, *args, **kwargs):
instance = self.get_object()
nodes_id = request.data.get("nodes")
children = [get_object_or_none(Node, id=pk) for pk in nodes_id]
children = Node.objects.filter(id__in=nodes_id)
for node in children:
if not node:
continue
node.parent = instance
return Response("OK")
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class NodeAddAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
@@ -220,6 +233,8 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
instance.assets.add(*tuple(assets))
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='patch')
@method_decorator(org_level_transaction_lock(UPDATE_NODE_TREE_LOCK_KEY), name='put')
class NodeRemoveAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
@@ -228,15 +243,17 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
def perform_update(self, serializer):
assets = serializer.validated_data.get('assets')
instance = self.get_object()
if instance != Node.org_root():
instance.assets.remove(*tuple(assets))
else:
assets = [asset for asset in assets if asset.nodes.count() > 1]
instance.assets.remove(*tuple(assets))
node = self.get_object()
node.assets.remove(*assets)
# 把孤儿资产添加到 root 节点
orphan_assets = Asset.objects.filter(id__in=[a.id for a in assets], nodes__isnull=True).distinct()
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
serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,)
@@ -244,9 +261,39 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
def perform_update(self, serializer):
assets = serializer.validated_data.get('assets')
instance = self.get_object()
for asset in assets:
asset.nodes.set([instance])
node = self.get_object()
self.remove_old_nodes(assets)
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):
@@ -267,7 +314,6 @@ class NodeTaskCreateApi(generics.CreateAPIView):
@staticmethod
def refresh_nodes_cache():
Node.refresh_nodes()
Task = namedtuple('Task', ['id'])
task = Task(id="0")
return task

View File

@@ -3,7 +3,8 @@ from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.drf.filters import CustomFilter
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.utils import tmp_to_org
@@ -12,14 +13,14 @@ from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer
from ..tasks import (
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
push_system_user_a_asset_manual,
push_system_user_to_assets
)
logger = get_logger(__file__)
__all__ = [
'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
"""
model = SystemUser
filter_fields = ("name", "username")
filter_fields = ("name", "username", "protocol")
search_fields = filter_fields
serializer_class = serializers.SystemUserSerializer
serializer_classes = {
@@ -82,18 +83,18 @@ class SystemUserTaskApi(generics.CreateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.SystemUserTaskSerializer
def do_push(self, system_user, asset=None):
if asset is None:
def do_push(self, system_user, assets_id=None):
if assets_id is None:
task = push_system_user_to_assets_manual.delay(system_user)
else:
username = self.request.query_params.get('username')
task = push_system_user_a_asset_manual.delay(
system_user, asset, username=username
task = push_system_user_to_assets.delay(
system_user.id, assets_id, username=username
)
return task
@staticmethod
def do_test(system_user, asset=None):
def do_test(system_user):
task = test_system_user_connectivity_manual.delay(system_user)
return task
@@ -104,11 +105,16 @@ class SystemUserTaskApi(generics.CreateAPIView):
def perform_create(self, serializer):
action = serializer.validated_data["action"]
asset = serializer.validated_data.get('asset')
assets = serializer.validated_data.get('assets') or []
system_user = self.get_object()
if action == 'push':
task = self.do_push(system_user, asset)
assets = [asset] if asset else assets
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:
task = self.do_test(system_user, asset)
task = self.do_test(system_user)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
@@ -125,3 +131,18 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
pk = self.kwargs.get('pk', None)
system_user = get_object_or_404(SystemUser, pk=pk)
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
model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
filter_fields = [
'id', 'asset', 'systemuser',
]
search_fields = [
@@ -91,11 +91,11 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
filter_fields = [
'id', 'node', 'systemuser',
]
search_fields = [
"node__value", "systemuser__name", "systemuser_username"
"node__value", "systemuser__name", "systemuser__username"
]
def get_objects_attr(self):
@@ -112,7 +112,7 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserUserRelationSerializer
model = models.SystemUser.users.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
filter_fields = [
'id', 'user', 'systemuser',
]
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 -*-
#
import coreapi
from rest_framework.compat import coreapi, coreschema
from rest_framework import filters
from django.db.models import Q
from common.utils import dict_get_any, is_uuid, get_object_or_none
from .models import Node, Label
from .models import Label
from assets.utils import is_query_node_all_assets, get_node
class AssetByNodeFilterBackend(filters.BaseFilterBackend):
@@ -21,51 +21,58 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend):
for field in self.fields
]
@staticmethod
def is_query_all(request):
query_all_arg = request.query_params.get('all')
show_current_asset_arg = request.query_params.get('show_current_asset')
def filter_node_related_all(self, queryset, node):
return queryset.filter(
Q(nodes__key__istartswith=f'{node.key}:') |
Q(nodes__key=node.key)
).distinct()
query_all = query_all_arg == '1'
if show_current_asset_arg is not None:
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_node_related_direct(self, queryset, node):
return queryset.filter(nodes__key=node.key).distinct()
def filter_queryset(self, request, queryset, view):
node, has_query_arg = self.get_query_node(request)
if not has_query_arg:
return queryset
node = get_node(request)
if node is None:
return queryset
query_all = self.is_query_all(request)
query_all = is_query_node_all_assets(request)
if query_all:
pattern = node.get_all_children_pattern(with_self=True)
return self.filter_node_related_all(queryset, node)
else:
# pattern = node.get_children_key_pattern(with_self=True)
# 只显示当前节点下资产
pattern = r"^{}$".format(node.key)
return self.perform_query(pattern, queryset)
return self.filter_node_related_direct(queryset, node)
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):
sep = '#'
sep = ':'
query_arg = 'label'
def get_schema_fields(self, view):
@@ -84,6 +91,8 @@ class LabelFilterBackend(filters.BaseFilterBackend):
q = None
for kv in labels_query:
if '#' in kv:
self.sep = '#'
if self.sep not in kv:
continue
key, value = kv.strip().split(self.sep)[:2]
@@ -111,7 +120,32 @@ class LabelFilterBackend(filters.BaseFilterBackend):
class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
@staticmethod
def perform_query(pattern, queryset):
return queryset.filter(asset__nodes__key__regex=pattern).distinct()
def filter_node_related_all(self, queryset, node):
return queryset.filter(
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):
def active(self):
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'))
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'))
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)()
org_objects = AssetOrgManager.from_queryset(AssetQuerySet)()
_connectivity = None
def __str__(self):
@@ -308,6 +313,12 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
}
return info
def nodes_display(self):
names = []
for n in self.nodes.all():
names.append(n.full_value)
return names
def as_node(self):
from .node import Node
fake_node = Node()
@@ -350,36 +361,4 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
class Meta:
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")
@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
ordering = ["hostname", "ip"]

View File

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

View File

@@ -158,9 +158,11 @@ class AuthMixin:
if 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
queryset = AuthBook.objects.filter(username=self.username)
if username is None:
username = self.username
queryset = AuthBook.objects.filter(username=username)
if asset:
queryset = queryset.filter(asset=asset)
return queryset.exists()
@@ -230,7 +232,7 @@ class AuthMixin:
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
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'))
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'))

View File

@@ -38,27 +38,3 @@ class Cluster(models.Model):
class Meta:
ordering = ['name']
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):
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'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
@@ -29,6 +29,7 @@ class CommandFilter(OrgModelMixin):
return self.name
class Meta:
unique_together = [('org_id', 'name')]
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"))
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"),
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"))
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)

View File

@@ -9,6 +9,7 @@ import paramiko
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils.strings import no_special_chars
from orgs.mixins.models import OrgModelMixin
from .base import BaseUser
@@ -17,13 +18,14 @@ __all__ = ['Domain', 'Gateway']
class Domain(OrgModelMixin):
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'))
date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created'))
class Meta:
verbose_name = _("Domain")
unique_together = [('org_id', 'name')]
def __str__(self):
return self.name
@@ -46,7 +48,7 @@ class Gateway(BaseUser):
(PROTOCOL_SSH, 'ssh'),
(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'))
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"))
@@ -63,8 +65,8 @@ class Gateway(BaseUser):
def test_connective(self, local_port=None):
if local_port is None:
local_port = self.port
if self.password and not re.match(r'\w+$', self.password):
return False, _("Password should not contain special characters")
if self.password and not no_special_chars(self.password):
return False, _("Password should not contains special characters")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

View File

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

View File

@@ -33,21 +33,3 @@ class AssetGroup(models.Model):
def initial(cls):
asset_group = cls(name=_('Default'), comment=_('Default asset group'))
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 re
import time
from django.db import models, transaction
from django.db.models import Q
from django.db.utils import IntegrityError
from django.utils.translation import ugettext_lazy as _
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.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
__all__ = ['Node']
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key']
logger = get_logger(__name__)
def compute_parent_key(key):
try:
return key[:key.rindex(':')]
except ValueError:
return ''
class NodeQuerySet(models.QuerySet):
def delete(self):
raise PermissionError("Bulk delete node deny")
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)
raise NotImplementedError
class FamilyMixin:
@@ -138,16 +41,16 @@ class FamilyMixin:
@staticmethod
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 = []
for key in nodes_keys[::-1]:
found = False
for k in nodes_keys:
if key.startswith(k + ':'):
found = True
break
if not found:
nodes_keys_clean.append(key)
base_key = ''
for key in nodes_keys:
if key.startswith(base_key + ':'):
continue
nodes_keys_clean.append(key)
base_key = key
return nodes_keys_clean
@classmethod
@@ -175,13 +78,16 @@ class FamilyMixin:
return re.match(children_pattern, self.key)
def get_children(self, with_self=False):
pattern = self.get_children_key_pattern(with_self=with_self)
return Node.objects.filter(key__regex=pattern)
q = Q(parent_key=self.key)
if with_self:
q |= Q(key=self.key)
return Node.objects.filter(q)
def get_all_children(self, with_self=False):
pattern = self.get_all_children_pattern(with_self=with_self)
children = Node.objects.filter(key__regex=pattern)
return children
q = Q(key__istartswith=f'{self.key}:')
if with_self:
q |= Q(key=self.key)
return Node.objects.filter(q)
@property
def children(self):
@@ -191,14 +97,30 @@ class FamilyMixin:
def all_children(self):
return self.get_all_children(with_self=False)
def create_child(self, value, _id=None):
with transaction.atomic():
def create_child(self, value=None, _id=None):
with atomic(savepoint=False):
child_key = self.get_next_child_key()
if value is None:
value = child_key
child = self.__class__.objects.create(
id=_id, key=child_key, value=value
)
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):
mark = self.child_mark
self.child_mark += 1
@@ -241,10 +163,13 @@ class FamilyMixin:
ancestor_keys = self.get_ancestor_keys(with_self=with_self)
return self.__class__.objects.filter(key__in=ancestor_keys)
@property
def parent_key(self):
parent_key = ":".join(self.key.split(":")[:-1])
return parent_key
# @property
# def parent_key(self):
# parent_key = ":".join(self.key.split(":")[:-1])
# return parent_key
def compute_parent_key(self):
return compute_parent_key(self.key)
def is_parent(self, other):
return other.is_children(self)
@@ -280,45 +205,63 @@ class FamilyMixin:
sibling = sibling.exclude(key=self.key)
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):
ancestors = self.get_ancestors()
children = self.get_all_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:
key = ''
id = None
@lazyproperty
def assets_amount(self):
amount = self.tree().assets_amount(self.key)
return amount
def get_all_assets(self):
from .asset import Asset
if self.is_org_root():
return Asset.objects.filter(org_id=self.org_id)
pattern = '^{0}$|^{0}:'.format(self.key)
return Asset.objects.filter(nodes__key__regex=pattern).distinct()
q = Q(nodes__key__startswith=f'{self.key}:') | Q(nodes__key=self.key)
return Asset.objects.filter(q).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):
from .asset import Asset
if self.is_org_root():
assets = Asset.objects.filter(Q(nodes=self) | Q(nodes__isnull=True))
else:
assets = Asset.objects.filter(nodes=self)
assets = Asset.objects.filter(nodes=self)
return assets.distinct()
def get_valid_assets(self):
@@ -327,51 +270,54 @@ class NodeAssetsMixin:
def get_all_valid_assets(self):
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
def get_nodes_all_assets_ids(cls, nodes_keys):
nodes_keys = cls.clean_children_keys(nodes_keys)
assets_ids = set()
for key in nodes_keys:
node_assets_ids = cls.tree().all_assets(key)
assets_ids.update(set(node_assets_ids))
assets_ids = cls.get_nodes_all_assets(nodes_keys).values_list('id', flat=True)
return assets_ids
@classmethod
def get_nodes_all_assets(cls, nodes_keys, extra_assets_ids=None):
from .asset import Asset
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:
assets_ids.update(set(extra_assets_ids))
return Asset.objects.filter(id__in=assets_ids)
q |= Q(id__in=extra_assets_ids)
if q:
return Asset.org_objects.filter(q).distinct()
else:
return Asset.objects.none()
class SomeNodesMixin:
key = ''
default_key = '1'
default_value = 'Default'
ungrouped_key = '-10'
ungrouped_value = _('ungrouped')
empty_key = '-11'
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):
return self.key == self.default_key
@@ -406,51 +352,18 @@ class SomeNodesMixin:
@classmethod
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:
return root[0]
else:
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
def initial_some_nodes(cls):
cls.default_node()
cls.ungrouped_node()
cls.favorite_node()
@classmethod
def modify_other_org_root_node_key(cls):
@@ -482,12 +395,16 @@ class SomeNodesMixin:
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)
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
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)
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)()
is_node = True
@@ -495,10 +412,10 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
class Meta:
verbose_name = _("Node")
ordering = ['key']
ordering = ['parent_key', 'value']
def __str__(self):
return self.value
return self.full_value
# def __eq__(self, other):
# if not other:
@@ -522,18 +439,19 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
def name(self):
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
def level(self):
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):
from common.tree import TreeNode
name = '{} ({})'.format(self.value, self.assets_amount)
@@ -568,19 +486,26 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
return
return super().delete(using=using, keep_parents=keep_parents)
@classmethod
def generate_fake(cls, count=100):
import random
org = get_current_org()
if not org or not org.is_real():
Organization.default().change_to()
nodes = list(cls.objects.all())
if count > 100:
length = 100
else:
length = count
def update_child_full_value(self):
nodes = self.get_all_children(with_self=True)
sort_key_func = lambda n: [int(i) for i in n.key.split(':')]
nodes_sorted = sorted(list(nodes), key=sort_key_func)
nodes_mapper = {n.key: n for n in nodes_sorted}
if not self.is_org_root():
# 如果是org_root那么parent_key为'', parent为自己所以这种情况不处理
# 更新自己时自己的parent_key获取不到
nodes_mapper.update({self.parent_key: self.parent})
for node in nodes_sorted:
parent = nodes_mapper.get(node.parent_key)
if not parent:
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):
node = random.choice(nodes)
child = node.create_child('Node {}'.format(i))
print("{}. {}".format(i, child))
def save(self, *args, **kwargs):
self.full_value = self.computed_full_value()
instance = super().save(*args, **kwargs)
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 common.utils import signer
from common.fields.model import JsonListCharField
from .base import BaseUser
from .asset import Asset
@@ -64,26 +65,6 @@ class AdminUser(BaseUser):
unique_together = [('name', 'org_id')]
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):
PROTOCOL_SSH = 'ssh'
@@ -91,12 +72,20 @@ class SystemUser(BaseUser):
PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_MYSQL = 'mysql'
PROTOCOL_ORACLE = 'oracle'
PROTOCOL_MARIADB = 'mariadb'
PROTOCOL_POSTGRESQL = 'postgresql'
PROTOCOL_K8S = 'k8s'
PROTOCOL_CHOICES = (
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet'),
(PROTOCOL_VNC, 'vnc'),
(PROTOCOL_MYSQL, 'mysql'),
(PROTOCOL_ORACLE, 'oracle'),
(PROTOCOL_MARIADB, 'mariadb'),
(PROTOCOL_POSTGRESQL, 'postgresql'),
(PROTOCOL_K8S, 'k8s'),
)
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'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
token = models.TextField(default='', verbose_name=_('Token'))
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
ad_domain = models.CharField(default='', max_length=256)
_prefer = 'system_user'
def __str__(self):
@@ -140,6 +133,24 @@ class SystemUser(BaseUser):
def login_mode_display(self):
return self.get_login_mode_display()
@property
def db_application_protocols(self):
return [
self.PROTOCOL_MYSQL, self.PROTOCOL_ORACLE, self.PROTOCOL_MARIADB,
self.PROTOCOL_POSTGRESQL
]
@property
def cloud_application_protocols(self):
return [self.PROTOCOL_K8S]
@property
def application_category_protocols(self):
protocols = []
protocols.extend(self.db_application_protocols)
protocols.extend(self.cloud_application_protocols)
return protocols
def is_need_push(self):
if self.auto_push and self.protocol in [self.PROTOCOL_SSH, self.PROTOCOL_RDP]:
return True
@@ -152,11 +163,16 @@ class SystemUser(BaseUser):
@property
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
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):
super()._merge_auth(other)
@@ -193,23 +209,3 @@ class SystemUser(BaseUser):
ordering = ['name']
unique_together = [('name', 'org_id')]
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 -*-
#
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 orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.serializers import AdaptedBulkListSerializer
from ..models import Asset, Node, Label, Platform
from ..models import Asset, Node, Platform
from .base import ConnectivitySerializer
__all__ = [
@@ -67,27 +66,41 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
)
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:
model = Asset
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'ip', 'hostname', 'protocol', 'port',
'protocols', 'platform', 'is_active', 'public_ip', 'domain',
'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',
fields_mini = ['id', 'hostname', 'ip']
fields_small = fields_mini + [
'protocol', 'port', 'protocols', 'is_active', 'public_ip',
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw',
'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',
)
] + fields_as
extra_kwargs = {
'protocol': {'write_only': True},
'port': {'write_only': True},
@@ -98,11 +111,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related(
Prefetch('nodes', queryset=Node.objects.all().only('id')),
Prefetch('labels', queryset=Label.objects.all().only('id')),
).select_related('admin_user', 'domain', 'platform') \
.annotate(platform_base=F('platform__base'))
queryset = queryset.select_related('admin_user', 'domain', 'platform')
queryset = queryset.prefetch_related('nodes', 'labels')
return queryset
def compatible_with_old_protocol(self, validated_data):
@@ -120,33 +130,45 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
if 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):
self.compatible_with_old_protocol(validated_data)
nodes_display = validated_data.pop('nodes_display', '')
instance = super().create(validated_data)
self.perform_nodes_display_create(instance, nodes_display)
return instance
def update(self, instance, validated_data):
nodes_display = validated_data.pop('nodes_display', '')
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):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
class Meta(AssetSerializer.Meta):
fields = [
'id', 'ip', 'hostname', 'protocol', 'port',
'protocols', 'is_active', 'public_ip',
'number', 'vendor', 'model', 'sn',
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
'hostname_raw', 'comment', 'created_by', 'date_created',
'hardware_info', 'connectivity',
fields = AssetSerializer.Meta.fields + [
'connectivity',
]
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = super().setup_eager_loading(queryset)
queryset = queryset\
.annotate(admin_user_username=F('admin_user__username'))
return queryset

View File

@@ -2,7 +2,6 @@
#
import re
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.fields import ChoiceDisplayField
from common.serializers import AdaptedBulkListSerializer
@@ -27,11 +26,20 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
serializer_choice_field = ChoiceDisplayField
# serializer_choice_field = ChoiceDisplayField
invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]')
type_display = serializers.ReadOnlyField(source='get_type_display')
action_display = serializers.ReadOnlyField(source='get_action_display')
class Meta:
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__'
list_serializer_class = AdaptedBulkListSerializer

View File

@@ -1,31 +1,44 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.validators import NoSpecialChars
from ..models import Domain, Gateway
from .base import AuthSerializerMixin
class DomainSerializer(BulkOrgResourceModelSerializer):
asset_count = serializers.SerializerMethodField()
gateway_count = serializers.SerializerMethodField()
asset_count = serializers.SerializerMethodField(label=_('Assets count'))
application_count = serializers.SerializerMethodField(label=_('Applications count'))
gateway_count = serializers.SerializerMethodField(label=_('Gateways count'))
class Meta:
model = Domain
fields = [
'id', 'name', 'asset_count', 'gateway_count', 'comment', 'assets',
'date_created'
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'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
@staticmethod
def get_asset_count(obj):
return obj.assets.count()
@staticmethod
def get_application_count(obj):
return obj.applications.count()
@staticmethod
def get_gateway_count(obj):
return obj.gateway_set.all().count()
@@ -40,6 +53,19 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'private_key', 'public_key', 'domain', 'is_active', 'date_created',
'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):
@@ -51,6 +77,8 @@ class GatewayWithAuthSerializer(GatewaySerializer):
return fields
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):
gateways = GatewayWithAuthSerializer(many=True, read_only=True)

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,20 @@
# -*- 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 (
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 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.decorator import on_transaction_commit
from orgs.utils import tmp_to_root_org
from .models import Asset, SystemUser, Node, AuthBook
from .utils import TreeService
from .models import Asset, SystemUser, Node, compute_parent_key
from users.models import User
from .tasks import (
update_assets_hardware_info_util,
test_asset_connectivity_util,
@@ -34,6 +37,11 @@ def test_asset_conn_on_created(asset):
test_asset_connectivity_util.delay([asset])
@receiver(pre_save, sender=Node)
def on_node_pre_save(sender, instance: Node, **kwargs):
instance.parent_key = instance.compute_parent_key()
@receiver(post_save, sender=Asset)
@on_transaction_commit
def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
@@ -54,17 +62,9 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
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")
def on_system_user_update(sender, instance=None, created=True, **kwargs):
@on_transaction_commit
def on_system_user_update(instance: SystemUser, created, **kwargs):
"""
当系统用户更新时,可能更新了秘钥,用户名等,这时要自动推送系统用户到资产上,
其实应该当 用户名,密码,秘钥 sudo等更新时再推送这里偷个懒,
@@ -74,53 +74,57 @@ def on_system_user_update(sender, instance=None, created=True, **kwargs):
if instance and not created:
logger.info("System user update signal recv: {}".format(instance))
assets = instance.assets.all().valid()
push_system_user_to_assets.delay(instance, assets)
push_system_user_to_assets.delay(instance.id, [_asset.id for _asset in assets])
@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
logger.debug("System user assets change signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == Asset:
system_users = [instance]
assets = queryset
system_users_id = [instance.id]
assets_id = pk_set
else:
system_users = queryset
assets = [instance]
for system_user in system_users:
push_system_user_to_assets.delay(system_user, assets)
system_users_id = pk_set
assets_id = [instance.id]
for system_user_id in system_users_id:
push_system_user_to_assets.delay(system_user_id, assets_id)
@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
if reverse:
raise M2MReverseNotAllowed
if not instance.username_same_with_user:
return
logger.debug("System user users change signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == SystemUser:
system_users = queryset
else:
system_users = [instance]
for s in system_users:
push_system_user_to_assets_manual.delay(s)
usernames = model.objects.filter(pk__in=pk_set).values_list('username', flat=True)
for username in usernames:
push_system_user_to_assets_manual.delay(instance, username)
@receiver(m2m_changed, sender=SystemUser.nodes.through)
@on_transaction_commit
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
"""
当系统用户和节点关系发生变化时,应该将节点下资产关联到新的系统用户上
"""
if action != "post_add":
if action != POST_ADD:
return
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)
def on_system_user_groups_change(sender, instance=None, action=None, model=None,
pk_set=None, reverse=False, **kwargs):
def on_system_user_groups_change(instance, action, pk_set, reverse, **kwargs):
"""
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
"""
if action != "post_add" or reverse:
if action != POST_ADD:
return
if reverse:
raise M2MReverseNotAllowed
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)
instance.users.add(*tuple(users))
users = User.objects.filter(groups__id__in=pk_set).distinct()
instance.users.add(*users)
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_change(sender, instance=None, action='', **kwargs):
def on_asset_nodes_add(instance, action, reverse, pk_set, **kwargs):
"""
资产节点发生变化时,刷新节点
"""
if action.startswith('post'):
logger.debug("Asset nodes change signal recv: {}".format(instance))
Node.refresh_assets()
with tmp_to_root_org():
Node.refresh_assets()
本操作共访问 4 次数据库
@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
logger.debug("Assets node add signal recv: {}".format(action))
if model == Node:
nodes = model.objects.filter(pk__in=pk_set).values_list('key', flat=True)
assets = [instance.id]
else:
if reverse:
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()
node_tree = TreeService.new()
for node in nodes:
ancestors_keys = node_tree.ancestors_ids(nid=node)
nodes_ancestors_keys.update(ancestors_keys)
system_users = SystemUser.objects.filter(nodes__key__in=nodes_ancestors_keys)
nodes_ancestors_keys.update(Node.get_node_ancestor_keys(node, with_self=True))
system_users_assets = defaultdict(set)
for system_user in system_users:
system_users_assets[system_user].update(set(assets))
for system_user, _assets in system_users_assets.items():
system_user.assets.add(*tuple(_assets))
# 查询所有祖先节点关联的系统用户,都是要跟资产建立关系的
system_user_ids = SystemUser.objects.filter(
nodes__key__in=nodes_ancestors_keys
).distinct().values_list('id', flat=True)
# 查询所有已存在的关系
m2m_model = SystemUser.assets.through
exist = set(m2m_model.objects.filter(
systemuser_id__in=system_user_ids, asset_id__in=asset_ids
).values_list('systemuser_id', 'asset_id'))
# TODO 优化
to_create = []
for system_user_id in system_user_ids:
asset_ids_to_push = []
for asset_id in asset_ids:
if (system_user_id, asset_id) in exist:
continue
asset_ids_to_push.append(asset_id)
to_create.append(m2m_model(
systemuser_id=system_user_id,
asset_id=asset_id
))
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)
def on_asset_nodes_remove(sender, instance=None, action='', model=None,
pk_set=None, **kwargs):
def update_nodes_assets_amount(action, instance, reverse, pk_set, **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
"""
监控资产删除节点关系, 或节点删除资产,避免产生游离资产
"""
if action not in ["post_remove", "pre_clear", "post_clear"]:
mapper = {
PRE_ADD: add,
POST_REMOVE: sub
}
if action not in mapper:
return
if action == "pre_clear":
if model == Node:
instance._nodes = list(instance.nodes.all())
else:
instance._assets = list(instance.assets.all())
return
logger.debug("Assets node remove signal recv: {}".format(action))
if action == "post_remove":
queryset = model.objects.filter(pk__in=pk_set)
operator = mapper[action]
if reverse:
node: Node = instance
asset_pk_set = set(pk_set)
_update_node_assets_amount(node, asset_pk_set, operator)
else:
if model == Node:
queryset = instance._nodes
else:
queryset = instance._assets
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))
asset_pk = instance.id
# 与资产直接关联的节点
node_keys = set(Node.objects.filter(id__in=pk_set).values_list('key', flat=True))
_update_nodes_asset_amount(node_keys, asset_pk, operator)
@receiver([post_save, post_delete], sender=Node)
def on_node_update_or_created(sender, **kwargs):
# 刷新节点
Node.refresh_nodes()
with tmp_to_root_org():
Node.refresh_nodes()
RELATED_NODE_IDS = '_related_node_ids'
@receiver(pre_delete, sender=Asset)
def on_asset_delete(instance: Asset, using, **kwargs):
node_ids = set(Node.objects.filter(
assets=instance
).distinct().values_list('id', flat=True))
setattr(instance, RELATED_NODE_IDS, node_ids)
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=PRE_REMOVE
)
@receiver(post_delete, sender=Asset)
def on_asset_post_delete(instance: Asset, using, **kwargs):
node_ids = getattr(instance, RELATED_NODE_IDS, None)
if node_ids:
m2m_changed.send(
sender=Asset.nodes.through, instance=instance, reverse=False,
model=Node, pk_set=node_ids, using=using, action=POST_REMOVE
)

View File

@@ -9,3 +9,4 @@ from .gather_asset_users import *
from .gather_asset_hardware_info import *
from .push_system_user 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(
task_name=task_name, asset=asset_user.asset,
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:
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