Compare commits

..

268 Commits

Author SHA1 Message Date
老广
a025930957 Merge pull request #2642 from jumpserver/dev
Dev
2019-04-30 10:46:28 +08:00
老广
990c78e7cc Merge branch 'master' into dev 2019-04-30 10:46:10 +08:00
BaiJiangJie
0ef12906d3 [Update] 修改翻译信息 (#2643)
* [Update] 更新翻译信息

* [Update] 更新翻译信息
2019-04-30 10:30:58 +08:00
ibuler
61a37731ec Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-04-30 10:20:57 +08:00
ibuler
d3217b6a67 [Update] 修改版本号 2019-04-30 10:20:48 +08:00
BaiJiangJie
04266cc20b [Update] 修改Copyright (#2649) 2019-04-30 10:07:57 +08:00
ibuler
4f36cf7dd1 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-04-29 10:39:51 +08:00
ibuler
490041587b [Update] 修改copyright 2019-04-29 10:39:31 +08:00
BaiJiangJie
3a3da94468 [Update] 修改预留/auth/login/页面方式(admin=1);解决luna页面刷新不跳转openid认证的问题 (#2634) 2019-04-25 18:55:48 +08:00
BaiJiangJie
b7ad6cfe62 [Update] 防止 XSS (#2633)
* [Bugfix] 修改管理用户列表显示bug

* [Bugfix] 修复刷新批量命令页面的bug

* [Update] 防止 XSS
2019-04-25 18:16:41 +08:00
ibuler
4463e7545d [Update] 去掉多余的 2019-04-25 14:34:47 +08:00
ibuler
d0eafc8b8e [Update] Merge 2019-04-25 14:32:53 +08:00
ibuler
8b98c20d68 [Update] xss 2019-04-25 14:31:34 +08:00
BaiJiangJie
caa5060ecd [Update] 控制组织管理员不允许更新、删除超级用户;修复ViewSet API批量更新的bug (#2629)
* [Update] 控制组织管理员不允许编辑(更新、删除)超级用户 - 待续(控制批量更新API)

* [Update] 修改方法名称

* [Update] 控制组织管理员不允许批量更新包含超级用户的用户列表

* [Bugfix] 修复所有ViewSet API进行批量更新时rest_framework_bulk库内部的bug

* [Update] 修改 OpenID Middleware 日志输出模式 info => debug
2019-04-25 10:11:50 +08:00
BaiJiangJie
aabcf7f31c [Update] 授权列表下拉信息添加action字段 (#2618) 2019-04-22 12:31:40 +08:00
ibuler
40d48cdfe4 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-04-22 12:07:59 +08:00
ibuler
8196537878 [Update] 去掉logger.info openid msg 2019-04-22 12:07:48 +08:00
BaiJiangJie
33a00f043b [Bugfix] 修复创建azure类型的录像存储时前端的bug (#2617) 2019-04-22 11:55:14 +08:00
BaiJiangJie
f235e20153 [Feature] 授权规则添加 actions 选项,控制用户对资产的操作行为 (#2610)
* [Feature] 1. perms actions - 添加 Action Model

* [Feature] 2. perms actions - 添加 Action API

* [Feature] 3. perms actions - 授权规则: 添加actions字段

* [Feature] 4. perms actions - 授权规则创建页面: 设置 actions 默认 all

* [Feature] 5. perms actions - 资产授权工具: 动态给system_user设置actions属性; 修改授权相关的API-serializer类: 添加actions字段值

* [Feature] 6. perms actions - 更新API(用户使用系统用户连接资产时权限校验): 添加actions校验

* [Feature] 7. perms actions - 迁移文件中为已经存在的perms添加默认的action

* [Feature] 8. perms actions - 创建授权规则时设置默认action(如果actions字段值为空)

* [Feature] 9. check actions - 修改校验用户资产权限API逻辑(添加actions校验)

* [Feature] 10. check actions - 修改注释

* [Feature] 11. check actions - 添加API: 获取用户指定资产和系统用户被授权的actions

* [Feature] 12. check actions - 添加翻译信息
2019-04-22 11:42:20 +08:00
BaiJiangJie
cf2455c084 [Bugfix] 修复创建azure类型的录像存储时前端的bug (#2608) 2019-04-18 11:50:30 +08:00
BaiJiangJie
fc1068a9dc [Update] 更新翻译 (#2599) 2019-04-15 16:34:56 +08:00
zhnxin
35a0ca1875 Update node.py (#2527)
bugfix: node get_all_children逻辑
2019-04-15 14:43:49 +08:00
八千流
56519354b6 Email reset password (#2547)
* [Update]更改英文登录界面标题,可两行

* [Update] 更改用户通过邮箱修改密码后,该链接就失效

* [Update]更改页面左上角logo_text图片

* [Update] 优化发送邮箱改密连接失效代码

* [Update] 优化发送邮箱改密连接失效代码(2)

* [Update] 优化发送邮箱改密连接失效代码(3)

* [Update] 更新interface一键恢复默认的翻译

* [Update] 更改登录失败 用户名密码错误信息的翻译

* [Update] 优化生成token并设置缓存的代码
2019-04-15 14:37:45 +08:00
jokimina
78e4e13fb9 [Bugfix] IsSuperUserOrAppUser deny app user (#2558) 2019-04-15 14:33:40 +08:00
八千流
699b8d9980 [Update] 修改批量更新资产url过长导致的错误 (#2571) 2019-04-15 14:33:16 +08:00
zbhlove100
ba9581801c Update README.md 2019-04-09 16:57:01 +08:00
zbhlove100
0a5fdf4ea1 功能转为表格 2019-04-09 16:53:54 +08:00
zhangbohan
3849fa2b15 update readme 2019-04-09 15:27:37 +08:00
zhangbohan
0952cbc7c6 英文readme 2019-04-09 12:21:13 +08:00
zhangbohan
bb06c39dd4 更新readme 2019-04-09 12:09:11 +08:00
zhangbohan
d60dc31443 更新readme 2019-04-08 18:30:39 +08:00
老广
76b3cd8edd Merge pull request #2583 from jumpserver/dev
Dev
2019-04-04 12:33:16 +08:00
老广
638ba31694 Merge pull request #2582 from jumpserver/dev_bugfix_assets
[Bugfix] 修复资产对象获取所有节点时的bug
2019-04-04 12:21:42 +08:00
BaiJiangJie
c31b169cae [Bugfix] 修复资产对象获取所有节点时的bug 2019-04-04 12:17:53 +08:00
老广
fc167526ae Merge pull request #2575 from jumpserver/dev
[Bugfix] 修复OpenID判断退出登录的中间件逻辑的bug (#2574)
2019-04-03 12:19:23 +08:00
BaiJiangJie
55eff5eab9 [Bugfix] 修复OpenID判断退出登录的中间件逻辑的bug (#2574) 2019-04-03 12:18:58 +08:00
老广
f5a7f4e086 Merge pull request #2570 from jumpserver/dev
Bugfix (#2569)
2019-04-02 16:26:28 +08:00
老广
f7b0932cdd Bugfix (#2569)
* [Update] 修改Logo 的位置

* [Update] 修改镜像build
2019-04-02 16:25:39 +08:00
老广
ba89ce8fb9 Merge pull request #2560 from jumpserver/dev
[Update] 修复小问题 (#2557)
2019-03-29 19:34:28 +08:00
BaiJiangJie
9d62deeabe [Update] 修复小问题 (#2557)
* [Update] 修复小问题

* [Bugfix] 修复操作日志动作、资源类型过滤不成功的bug
2019-03-29 19:14:18 +08:00
老广
459b41f327 Merge pull request #2559 from jumpserver/dev
[Update] 修复重定向的问题
2019-03-29 15:54:20 +08:00
ibuler
3062e3f64a [Update] 修复重定向的问题 2019-03-29 15:53:31 +08:00
老广
c1362ca4e2 Merge pull request #2552 from jumpserver/dev
Dev
2019-03-28 13:13:36 +08:00
ibuler
9d24912ad9 [Update] Merge 2019-03-28 13:12:19 +08:00
老广
db290609a8 Merge pull request #2551 from jumpserver/dev
Dev
2019-03-28 13:07:34 +08:00
ibuler
4bc5eced6c Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-03-28 13:07:04 +08:00
BaiJiangJie
b82a66c83d [Update] 优化LDAP用户导入功能 (#2550) 2019-03-28 12:50:04 +08:00
ibuler
bf7079df9e [Update] 修改logo 2019-03-28 12:35:16 +08:00
ibuler
f137c5740e [Update] 添加openldap client 2019-03-28 11:53:52 +08:00
ibuler
ee47905966 [Update] 修改超时时间 2019-03-27 20:39:31 +08:00
fit2cloudrd
f6cd193f9e Update README.md (#2548)
* Dev (#2544)

* [Update] 更新翻译 (#2541)

* [Update] 更新缓存机制

* [Update] 增加task最大允许事件,并设置命令最大运行时间为60s

* Update README.md
2019-03-27 18:47:38 +08:00
ibuler
a31775dd23 [Update] 整理 copyright 和 版本 2019-03-27 17:46:35 +08:00
老广
30ba1e5886 Merge pull request #2545 from jumpserver/dev
Dev
2019-03-27 13:04:58 +08:00
ibuler
f97bfa7bf1 [Update] 修复s3创建时端点不含协议的提示 2019-03-27 12:49:46 +08:00
ibuler
ace028fa7f [Update] 统一可更改的interface 2019-03-27 12:20:43 +08:00
老广
69f6401e87 Dev (#2544)
* [Update] 更新翻译 (#2541)

* [Update] 更新缓存机制

* [Update] 增加task最大允许事件,并设置命令最大运行时间为60s
2019-03-27 11:21:21 +08:00
ibuler
bd4d974df1 [Update] merge 2019-03-27 11:13:35 +08:00
ibuler
6e7446f530 [Update] 增加task最大允许事件,并设置命令最大运行时间为60s 2019-03-27 11:12:34 +08:00
ibuler
afe9471aa2 [Update] 更新缓存机制 2019-03-26 19:46:04 +08:00
BaiJiangJie
4d56b84861 [Update] 更新翻译 (#2541) 2019-03-26 15:47:24 +08:00
老广
8fede58c64 Merge pull request #2456 from jumpserver/dev
Dev
2019-03-26 11:39:43 +08:00
ibuler
370904212f [Update] 修改翻译 2019-03-25 20:36:20 +08:00
ibuler
ae03a5aeb7 [Update] 翻译问题 2019-03-25 19:57:18 +08:00
ibuler
24a38841dd [Update] 修改Dockerfile 2019-03-22 18:26:25 +08:00
ibuler
bbc6156bd7 [Update] 数据库支持CA 2019-03-22 18:06:46 +08:00
ibuler
f387df41d7 [Update] 修改版本 2019-03-22 17:23:02 +08:00
ibuler
ceb8b2f5b3 [Update] 修改翻译 2019-03-22 17:16:02 +08:00
ibuler
877781a6ca [Update] 删掉一个migrations 2019-03-22 16:56:46 +08:00
BaiJiangJie
612d5efd1b [Update] 抽象Inventory,更新翻译 (#2530)
* [Update] 抽象Inventory, 支持自定义用户名密码构建Inventory

* [Update] 更新翻译
2019-03-22 16:52:26 +08:00
ibuler
7ea03801d0 [Update] 修改翻译 2019-03-22 15:57:37 +08:00
八千流
ce8f4b4a48 Ldap synchronization (#2512)
* [Add]初步实现ldap一键导入用户到jumpserver用户表里

* [update]增加定时延迟一秒刷新页面

* [Update]更改前端以表格形式显示用户信息,优化代码结构

* [Update]增加用户显示表格

* [Update]settings配置文件取消注释

* [Update]优化ldap同步用功能代码

* [Update]删除ldap同步用户旧html模版

* [Update]修改登录页面图片拉伸问题

* [Update]增加 是否已经导入,在前端提示用户

* [Update]优化ldap同步用户的代码,以及翻译

* [Update] 更新翻译(改密计划) (#2525)

* [Update] 更新翻译(添加改密计划)

* [Update] 更新翻译(改密计划)

* [Update] 更新翻译

* [Update] 更新翻译

* Export login log (#2511)

* [Add]增加登录日志导出功能

* [Update]优化导出登录日志代码

* [Update]优化导出登录日志代码

* [Update]更改导出登录日志按钮
2019-03-22 15:55:46 +08:00
ibuler
15179d2450 [Update] 修改批量命令的翻译 2019-03-22 15:55:20 +08:00
ibuler
9aae106970 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-03-22 15:19:24 +08:00
ibuler
c82044f6bc [Update] 修改一些bug 2019-03-22 15:18:47 +08:00
八千流
e4e6f59589 Export login log (#2511)
* [Add]增加登录日志导出功能

* [Update]优化导出登录日志代码

* [Update]优化导出登录日志代码

* [Update]更改导出登录日志按钮
2019-03-22 15:17:22 +08:00
BaiJiangJie
c8aa9d006f [Update] 更新翻译(改密计划) (#2525)
* [Update] 更新翻译(添加改密计划)

* [Update] 更新翻译(改密计划)

* [Update] 更新翻译

* [Update] 更新翻译
2019-03-22 09:42:39 +08:00
老广
311538dcf8 Bugfix (#2513)
* [Update] 用户页面添加跳转

* [Update] 网关测试支持nat, 修复创建node等id不能指定的问题, 修复settings频繁redis, 没有has_replay录像不可以播放
2019-03-19 19:09:09 +08:00
老广
324cf2469f Bugfix (#2506)
* [Update] 增加清理celery日志

* [Update] 修复两周前会话命令数量系那是问题

* [Update] 修复两周前会话命令数量系那是问题

* [Update] 修改结构

* [Update] 添加datatable失败的日志

* [Update] 转换配置文件格式

* [Update] 添加traceback
2019-03-18 11:34:13 +08:00
ibuler
01745ead1f [Update] 添加migrations 2019-03-18 10:26:40 +08:00
BaiJiangJie
4e705a52eb [Feature] 添加资产用户管理器 (#2489)
* [Feature] 1. 资产用户管理器

* [Feature] 2. 资产用户管理器: 更新AuthBook

* [Feature] 3. 资产用户管理器: 添加 AssetUser API

* [Feature] 4. AssetUser Model: 添加方法 load_related_asset_auth

* [Feature] 5. AdminUser: 更新管理用户获取认证信息时,先加载相关资产的认证

* [Feature] 6. SystemUser: 更新系统用户获取认证信息时,先加载相关资产的认证

* [Feature] 前端页面: 添加资产用户列表页面

* [Feature] 前端页面: 管理用户的资产管理页面添加按钮: 修改资产用户认证信息

* [Feature] 前端页面: 系统用户的资产管理页面添加按钮: 修改资产用户认证信息

* [Feature] 优化: 从管理用户和系统用户的backend中获取相关资产用户的逻辑

* [Update] Fix 1

* [Feature] 优化: SystemUserBackend之filter功能

* [Feature] 优化: AdminUserBackend之filter功能

* [Feature] 优化: AdminUserBackend和SystemUserBackend功能

* [Feature] 更新翻译: 资产用户管理器

* [Update] 更新资产用户列表页名称为: asset_asset_user_list.html

* [Bugfix] 修改bug: SystemUserBackend 根据用户名过滤系统用户

* [Feature] 添加: 资产用户列表中可测试资产用户的连接性

* [Update] 修改: AdHoc model的run_as字段从SystemUser外键修改为username字符串

* [Feature] 添加: 获取系统用户认证信息(对应某个资产)API

* [Update] 更新: API获取asset user时进行排序

* [Bugfix] 修改: 资产用户可连接性CACHE_KEY

* [Update] 更新翻译信息

* [Update] 修改获取资产用户认证信息API的返回响应(200/400)

* [Update] 修改BaseUser获取特定资产的方法名

* [Update] 修改logger输出,AuthBook set_version_and_latest

* [Update] 修改日志输出添加exc_info参数

* [Update] 移除AuthBook迁移文件0026

* [Bugfix] 修复AdminUserBackend获取instances为空的bug
2019-03-18 10:15:33 +08:00
ibuler
9bb58afee1 [Update] bugfix 2019-03-11 10:27:40 +08:00
老广
b45b33380c Monitor (#2485)
* [Update]

* [Update] 修改fields
2019-03-11 10:06:45 +08:00
BaiJiangJie
c86a036ac6 [Update] OpenID认证流程添加详细日志 (#2462)
* [Update] OpenID认证流程添加详细日志

* [Update] 优化日志格式
2019-03-07 18:41:42 +08:00
ibuler
8694511d86 [Update] 完成 2019-03-06 14:58:25 +08:00
ibuler
58c4a46f6e [Update] 用户权限增加cache 2019-03-05 19:47:14 +08:00
ibuler
dfd26d88d4 [Update] 用户资产添加缓存 2019-03-04 20:45:57 +08:00
ibuler
dcf6959cff [Update] 每次启动失效设置的cache 2019-03-04 18:24:02 +08:00
ibuler
924affd978 [Update] 更新移动的model 2019-03-04 17:07:51 +08:00
ibuler
ad6d233c11 [Update] 修改表名称 2019-03-04 15:51:17 +08:00
ibuler
d84ab1d215 [Update] 修改迁移 2019-03-04 15:38:59 +08:00
ibuler
a5fc04e0ce [Update] merge 2019-03-04 11:36:10 +08:00
ibuler
7f71513085 [Update] 去掉fixtures 2019-03-04 11:32:28 +08:00
ibuler
6004ef3f0d [Update] 修改settings 2019-03-04 10:53:49 +08:00
ibuler
e76392a169 Merge branch 'stable' into dev 2019-03-04 10:47:31 +08:00
ibuler
53f0b2e9b0 [Update] 增加清理登陆日志的任务 2019-03-04 10:39:44 +08:00
ibuler
de79e36251 [Update] 增加配置 2019-03-04 10:17:35 +08:00
ibuler
c84e984eae [Update] 任务列表去掉日期 2019-03-01 18:26:13 +08:00
ibuler
6d65c967b1 [Update] config example增加radius 2019-03-01 18:21:14 +08:00
ibuler
8199ea84f4 [Bugfix] 修复有时导致的token解析失败 2019-03-01 13:50:57 +08:00
ibuler
ce1b0da09d [Update] Merge 2019-02-28 18:24:52 +08:00
ibuler
cd6bb848e9 [Update] Squash table 2019-02-28 18:23:39 +08:00
ibuler
34040fcd59 [Update] 移动model 2019-02-28 17:58:53 +08:00
ibuler
1969fb79fe [Update] 修改authentication backends 2019-02-28 15:12:45 +08:00
BaiJiangJie
904f64604b [Bugfix] 修复authentication模块之间引用的bug 2019-02-28 14:41:33 +08:00
ibuler
9b3509208d [Update] 修改authentication目录结构 2019-02-28 11:58:48 +08:00
BaiJiangJie
6700dc969f [Update] users认证逻辑迁移到authentication中 2019-02-27 20:55:28 +08:00
ibuler
21714cc411 [Update] Stash 2019-02-27 08:45:00 +08:00
ibuler
1a247d60e7 [Update] sudo命令添加help text 2019-02-26 16:49:55 +08:00
jokimina
adf8b1f7aa [Bugfix] operation on closed file. (#2438) 2019-02-26 12:42:34 +08:00
老广
69f640daa4 Api (#2439)
* [Update] 迁移settings到独立app

* [Update] 修改settings migrations

* [Update] 修改docs说明
2019-02-26 12:38:20 +08:00
老广
a43314f5be Merge pull request #2432 from jokimina/patch-5
[Bugfix] celery beat crontab task timezone issue
2019-02-25 10:43:52 +08:00
jokimina
599e8a7e37 [Bugfix] crontab task timezone issue 2019-02-24 21:34:45 +08:00
老广
0e00451e1f Merge pull request #2425 from jumpserver/dev
[Update] 修改ldap test密码为空
2019-02-22 19:21:21 +08:00
ibuler
866e5d2011 [Update] 修改ldap test密码为空 2019-02-22 19:14:07 +08:00
老广
232674b1c1 Merge pull request #2423 from jumpserver/dev
Dev
2019-02-22 17:10:05 +08:00
ibuler
ddf60d2512 [Update] 添加jms日志和修改entrypoint 2019-02-22 16:33:27 +08:00
ibuler
3e6e0153cf [Update] 修改entrypoint并增加debug日志 2019-02-22 16:14:50 +08:00
ibuler
6b984aac53 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-02-22 12:49:03 +08:00
ibuler
6d3ee8116e [Update] 修改版本号 2019-02-22 12:48:43 +08:00
老广
904a0f67dd Merge pull request #2422 from jumpserver/dev
Dev
2019-02-22 10:16:08 +08:00
BaiJiangJie
81e1ce2688 [Update] Interface - 1. settings 添加TERMINAL_HEADER_TITLE (#2421) 2019-02-21 19:36:15 +08:00
ibuler
7c422d2ed6 [Update] Merge 2019-02-21 19:24:52 +08:00
ibuler
2a5129c481 [Update] 修改支持记录cmd filter日志 2019-02-21 19:22:23 +08:00
BaiJiangJie
28cdfca14b [Update] 优化 License (#2420)
* [Update] License - 1.添加标签过滤器

* [Update] License - 2.添加翻译信息
2019-02-21 18:49:40 +08:00
ibuler
202aba048b [Update] 修改用户页面授权资产 2019-02-21 17:35:28 +08:00
ibuler
3c2a4703bc [Update] ldap允许空密码 2019-02-21 16:37:36 +08:00
ibuler
4904aac2df [Update] 更新依赖版本 2019-02-21 16:16:08 +08:00
ibuler
303f88d6ee [Update] 更新replay storage 配置 2019-02-21 16:13:08 +08:00
老广
cccc74279d Dev license (#2409)
* [Update] 更新用户组

* [Update] license - 更新翻译

* [Feature] interface - 自定义配置logo,修改前端(待续)

* [Update] interface更新前端页面,两个登陆首页的模版设

* [Update]修改新登陆模版全局变量,国际化翻译,登陆逻辑页面切换

* [Update] 优化Interface前端逻辑

* [Update] Interface 修改前端一些小问题

* [Update] License/Interface 生成翻译信息
2019-02-20 19:20:13 +08:00
老广
16db2abca5 Merge pull request #2419 from jumpserver/dev
Dev
2019-02-20 19:02:40 +08:00
ibuler
859f2d9795 [Update] 支持telnet自定义正则 2019-02-20 19:00:01 +08:00
ibuler
4fd9957bec [Update] 修改celery日志显示 2019-02-20 17:51:53 +08:00
老广
0ac6e6ba2c Merge pull request #2418 from jumpserver/dev
[Update] 修改gunicorn日志
2019-02-20 16:16:44 +08:00
ibuler
785cc04126 [Update] 修改gunicorn日志 2019-02-20 16:11:58 +08:00
老广
f269eae774 Merge pull request #2415 from jumpserver/dev
[Update] 修改导入
2019-02-20 10:41:59 +08:00
ibuler
6f19fcb702 [Update] 修改tasks 2019-02-20 10:40:43 +08:00
老广
b7b6218306 Merge pull request #2414 from jumpserver/dev
[Update] gunicorn的日志
2019-02-19 20:06:37 +08:00
ibuler
5cd809b48a [Update] 修改导入 2019-02-19 20:03:02 +08:00
ibuler
5a1b894138 [Update] gunicorn的日志 2019-02-19 16:59:00 +08:00
老广
4d402617b6 Merge pull request #2410 from jumpserver/dev
Dev
2019-02-19 13:04:00 +08:00
ibuler
666ef366e7 [Bugfix] 修复session不显示在线的问题 2019-02-19 12:50:33 +08:00
ibuler
28d029a553 [Update] 修改settings 2019-02-19 12:18:39 +08:00
老广
00763e986a Merge pull request #2408 from jumpserver/dev
[Update] 修改token有效期
2019-02-19 12:00:04 +08:00
ibuler
f9a7cca478 [Update] 修改token有效期 2019-02-19 10:29:25 +08:00
老广
d09b34e232 Merge pull request #2404 from jumpserver/dev
[Update] 修改配置文件判断
2019-02-18 15:10:36 +08:00
ibuler
2737675c36 [Update] 修改配置文件判断 2019-02-18 15:09:02 +08:00
老广
7591f40b2c Merge pull request #2402 from jumpserver/dev
[Update] 创建子节点支持id
2019-02-18 13:02:15 +08:00
ibuler
c4af6fa72d [Update] 创建子节点支持id 2019-02-18 12:59:36 +08:00
老广
19be7ac580 Merge pull request #2394 from jumpserver/dev
Dev
2019-02-15 11:16:54 +08:00
ibuler
49404f763d [Update] 修改terminal字体 2019-02-15 11:15:46 +08:00
ibuler
87f2a67789 [Update] 修改录像获取 2019-02-15 11:14:37 +08:00
老广
041edb6177 Merge pull request #2392 from jumpserver/dev
Dev
2019-02-15 10:44:32 +08:00
ibuler
df2fad76c7 [Update] 修改Dockerfile 优先使用阿里云镜像 2019-02-14 16:23:56 +08:00
ibuler
94020a8fbb [Update] 修改日志存储 2019-02-14 16:15:28 +08:00
ibuler
cb1e19d28f [Update] 修改录像路径使用utc 2019-02-14 15:47:55 +08:00
ibuler
0980dffb47 [Update] 修复一下新建两个节点的bug 2019-02-13 19:38:51 +08:00
ibuler
4051225ecb [Bugfix] 修复命令执行bug,修复修改日志级别后无法查看日志 2019-02-13 15:55:11 +08:00
老广
507518da04 Merge pull request #2369 from jumpserver/dev
[Update] 修改版本号
2019-01-29 19:33:06 +08:00
ibuler
d3bdbc0b81 [Update] 修改版本号 2019-01-29 19:24:28 +08:00
老广
3b56027edc Merge pull request #2364 from jumpserver/dev
Dev
2019-01-29 17:45:31 +08:00
ibuler
8285610097 [Update] 创建Host Key 2019-01-29 14:07:46 +08:00
ibuler
6acac9cb3d Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-01-29 12:28:14 +08:00
ibuler
9d78f26807 [Update] 创建Host Key 2019-01-29 12:27:51 +08:00
BaiJiangJie
9d53ba22e1 [Update] 取消ceph录像存储,添加命令执行翻译 (#2365) 2019-01-28 14:59:06 +08:00
ibuler
0d0cf04543 [Update] 修改日志查看 2019-01-28 12:18:53 +08:00
ibuler
56a47b6ba3 [Update] 存储添加说明 2019-01-25 14:55:56 +08:00
ibuler
1dbcf4e3ab [Update] 数据库make migration移动到gunicorn 2019-01-24 11:25:13 +08:00
ibuler
95fcd60f64 [Update] 兼容之前的node api 2019-01-22 12:44:01 +08:00
ibuler
178055eb57 [Update] 修改任务小写 2019-01-21 17:43:08 +08:00
老广
01a101a710 [Update] 修改一些terminal storage (#2357) 2019-01-21 17:05:31 +08:00
ibuler
eee6dd1436 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-01-21 12:27:28 +08:00
ibuler
27693c6288 [Update] 修改配置文件 2019-01-21 12:27:20 +08:00
ibuler
55b55f6162 [Update] 修改测试连接节点时,资产为下面的所有资产 2019-01-21 11:58:51 +08:00
ibuler
384cdfbc19 [Update] 启动脚本 2019-01-19 19:29:38 +08:00
ibuler
c8d007f9d7 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-01-19 16:52:59 +08:00
ibuler
d8069f47f2 [Update] 修改配置文件加载 2019-01-17 20:38:56 +08:00
ibuler
d664018bd7 [Update] 修改config.py 2019-01-17 20:32:10 +08:00
ibuler
0f32e78891 [Update] 修改dockerfile 2019-01-17 20:26:47 +08:00
ibuler
6e061d2da5 [Bugfix] 修改小bug 2019-01-17 18:39:38 +08:00
ibuler
b2717133ee [Update] 修改日志目录 2019-01-17 18:06:47 +08:00
老广
2a0e68c58f Bugfix (#2350)
* [Update] 权限页面增加过滤规则

* [Update] 修改terminal注册,更新以后使用api完成

* [Update] 修改terminal注册,更新以后使用api完成

* [Update] 修改更新注册逻辑
2019-01-16 18:13:16 +08:00
老广
176052e8e9 [Update] 权限页面增加过滤规则 (#2349) 2019-01-15 19:01:33 +08:00
老广
d026b31c9f Bugfix (#2346)
* [Update] 修改command Post导致的output错误和定时任务创建问题

* [Update] 修改celery 日志

* [Update] 修改task日志方式

* [Update] 修改Docker file
2019-01-15 10:23:30 +08:00
老广
50c1b3ed4a [Update] 统一coco host key (#2336) 2019-01-10 11:50:08 +08:00
mago960806
131e588d82 is_running()里的打开pid文件的操作已经在get_pid()中执行过了,不需要再次执行 (#2335) 2019-01-10 10:21:05 +08:00
xiaomao
4bf0dfcf7b [bugfix] 解决日志中文报错以及各种utf8问题 (#2328) 2019-01-08 15:27:15 +08:00
老广
49a166552e Bugfix (#2327)
* [Bugfix] 修复两个配置文件冲突问题

* [Update] Docker中不再提供配置文件
2019-01-08 11:15:09 +08:00
老广
0e1d3f93ff [Update] 支持radius认证 (#2323)
* [Update] 支持radius认证

* [Update] 支持radius

* [Update] 增加requirements

* [Update] 修改copyright

* [Update] 修改migrations
2019-01-07 19:20:39 +08:00
老广
35403086ab Config (#2322)
* [Update] 修改配置文件

* [Update] 修改配置文件形式
2019-01-07 18:55:02 +08:00
ibuler
2fde6cfe24 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-01-03 12:41:21 +08:00
老广
ed1dc7a984 Merge pull request #2300 from jumpserver/dev
Dev
2019-01-03 11:52:13 +08:00
老广
9ab3f0441f Merge pull request #2298 from jumpserver/dev_bugfix
[Bugfix] 修复-用户页面资产详情显示bug
2019-01-03 10:41:48 +08:00
BaiJiangJie
d6567f0e57 [Bugfix] 修复-用户页面资产详情显示bug 2019-01-03 10:39:09 +08:00
老广
84bd465b30 Merge pull request #2296 from jumpserver/dev
Dev
2019-01-02 15:39:05 +08:00
ibuler
d0af8eba32 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-01-02 15:35:06 +08:00
ibuler
b2a8415f77 [Update] 修改创建子节点api 2019-01-02 15:33:35 +08:00
老广
545d4fa691 Merge pull request #2287 from jumpserver/dev
[Update] 禁用其他认证方式修改密码 (#2286)
2018-12-27 18:58:52 +08:00
老广
560df5027a [Update] 禁用其他认证方式修改密码 (#2286)
* [Update] 禁用其他认证方式修改密码

* [Update] 禁用其他认证方式修改密码

* [Update] 禁用其他认证方式修改密码
2018-12-27 16:47:40 +08:00
老广
e3db7462f7 Merge pull request #2284 from jumpserver/dev
Dev
2018-12-27 11:45:38 +08:00
老广
b55d137e7f Merge pull request #2283 from jumpserver/dev_bai
[Update] 修改在线会话终端按钮vnc协议disable
2018-12-27 11:44:58 +08:00
BaiJiangJie
7c10f8743f [Update] 修改在线会话终端按钮vnc协议disable 2018-12-27 11:39:11 +08:00
ibuler
c5d1ed126e [Update] 修改Jms脚本 2018-12-26 19:20:32 +08:00
老广
6b02cdfc37 Merge pull request #2279 from jumpserver/dev
Dev
2018-12-26 14:56:25 +08:00
BaiJiangJie
340c615efe [Bugfix] session model 添加 vnc 协议 (#2278)
* [Bugfix] session model 添加 vnc 协议

* [Update] 修改表结构
2018-12-26 14:43:43 +08:00
BaiJiangJie
8e51f97dc7 [Update] 添加资产树右击菜单: 刷新所有节点资产数量 (#2274)
* [Update] 添加资产树右击菜单: 刷新所有节点资产数量

* [Update] 修改右击菜单样式
2018-12-26 13:54:42 +08:00
BaiJiangJie
76a08c9039 [Bugfix] 修复创建/更新 Perms 时,名称重复不提示错误信息的问题 (#2272) 2018-12-26 12:59:09 +08:00
BaiJiangJie
b9b8c35a81 [Update] 修改创建资产选择vnc协议,默认端口5901 (#2276)
* [Update] 修改创建资产选择vnc协议,默认端口5901

* [update] 修改创建命令/录像存储时账户无效的提示信息
2018-12-26 12:57:59 +08:00
老广
e8fba2ec44 Merge pull request #2270 from jumpserver/dev
Dev
2018-12-25 16:56:02 +08:00
老广
61df6f55b9 Merge pull request #2269 from jumpserver/bugfix
[Update] 修改bug
2018-12-25 16:55:13 +08:00
ibuler
ef02b1f83a [Update] 修改bug 2018-12-25 16:53:52 +08:00
老广
0798e3c466 Merge pull request #2266 from jumpserver/dev
Dev
2018-12-25 14:32:47 +08:00
老广
a990098744 [Update] 优化资产选择 (#2267)
* [Update] 优化资产选择

* [Update] 优化资产任务
2018-12-25 13:33:37 +08:00
BaiJiangJie
dab692c0eb [Update] 限制终端设置中心跳间隔和会话保留时长的form最小值 (#2262)
* [Update] 限制终端设置中心跳间隔和会话保留时长的form最小值

* [Update] 删除terminal forms表单的初始化值

* [Update] 取消安全设置中forms的初始化值,并采用默认值;添加密码过期时间的最大值限制
2018-12-25 09:58:01 +08:00
BaiJiangJie
8e93bfecb0 [Bugfix] 修复DEFAULT组织下,批量删除(某组织下)一部分用户失败的bug (#2261) 2018-12-24 15:50:58 +08:00
ibuler
e5953e1932 Merge branch 'dev' of bitbucket.org:jumpserver/core into dev 2018-12-23 22:59:30 +08:00
老广
67b21f0489 Merge pull request #2254 from jumpserver/dev
Dev
2018-12-21 15:58:56 +08:00
ibuler
e15c9e6588 [Update] 修改小bug 2018-12-21 15:58:19 +08:00
ibuler
94d6525548 [Bugfix] 修复小bug 2018-12-21 15:57:23 +08:00
老广
a14d6b298d Merge pull request #2253 from jumpserver/dev
Bugfix (#2252)
2018-12-21 15:48:29 +08:00
老广
e7725e6910 Bugfix (#2252)
* [Update] 修改心跳偶人时间

* [Update] 修改Node比较

* [Bugfix] 修复bug
2018-12-21 15:47:52 +08:00
老广
23bf2b0f8e Merge pull request #2250 from jumpserver/dev
Dev
2018-12-21 15:10:37 +08:00
BaiJiangJie
ac9178cb93 [Update] 更新deb_requirements依赖 (#2239) 2018-12-21 14:59:00 +08:00
老广
7ff39259af Required opt (#2246)
* [Update] 修改心跳偶人时间

* [Update] 去掉required的label

* [Update] 修改默认心跳时间

* [Update] 去掉默认的placeholder

* [Update] 修改utils
2018-12-21 14:57:42 +08:00
ibuler
b2aef87fdd [Update] 修改心跳偶人时间 2018-12-21 10:35:17 +08:00
老广
e1f1bed9c9 Merge pull request #2231 from jumpserver/dev
Dev
2018-12-20 11:44:34 +08:00
老广
08945f0a19 [Bugfix] 修复adhoc 日志查看的bug, 修改config example (#2230) 2018-12-20 11:42:00 +08:00
ibuler
a1b80f5f0b [Update] 清理task adhoc和history 2018-12-20 11:05:36 +08:00
ibuler
7773c30240 [Update] 增加索引 2018-12-20 10:40:52 +08:00
jokimina
231c907c64 [Update] ops adhoc date_created index (#2223) 2018-12-20 10:34:42 +08:00
BaiJiangJie
db8882a2b9 [Bugfix] 修复创建网域时,选择资产的input和弹出table显示不一致的bug (#2224) 2018-12-20 10:32:41 +08:00
ibuler
e5285f312b [Bugfix] 修复vnc录像的bug 2018-12-19 19:55:41 +08:00
老广
332be54b46 Vnc (#2226)
* [Update] 优化授权协议, 支持vnc

* [Update] 添加协议vnc

* [Update] 修改系统用户添加

* [Update] 修改vnc信息
2018-12-19 19:40:58 +08:00
老广
fe7c3c29ad [Update] 优化授权协议, 支持vnc (#2220)
* [Update] 优化授权协议, 支持vnc
2018-12-19 17:03:10 +08:00
老广
0e9ebed19d Merge pull request #2214 from jumpserver/dev
Dev
2018-12-19 13:57:15 +08:00
老广
4a3327bc4b Merge pull request #2213 from jumpserver/bugfix
[Bugfix] 修复录像bug
2018-12-19 13:56:30 +08:00
ibuler
5d47bebb6b [Bugfix] 修复录像bug 2018-12-19 13:55:49 +08:00
老广
2ece3545ed Merge pull request #2210 from jumpserver/dev
Dev
2018-12-19 12:33:34 +08:00
ibuler
a9a1bae805 [Update] 修改列表页,显示可连接状态 2018-12-19 12:32:53 +08:00
ibuler
f35c02b346 [Update] 修改列表页,显示可连接状态 2018-12-19 12:30:51 +08:00
老广
32df515f4b Merge pull request #2209 from jumpserver/dev
Dev
2018-12-19 12:02:16 +08:00
ibuler
71750970b2 [Update] 暂时去掉刷新资产树 2018-12-19 12:01:18 +08:00
ibuler
a85099ee60 [Update] 修改版本号 2018-12-19 11:43:13 +08:00
老广
a5b9b4e1d2 Merge pull request #2206 from jumpserver/dev
Dev
2018-12-19 10:53:00 +08:00
老广
b3079a4a9b [Update] Add index (#2208) 2018-12-19 10:49:30 +08:00
ibuler
9a22874305 [Update] 去掉多余的任务 2018-12-18 20:14:44 +08:00
ibuler
179018bf67 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-12-18 20:07:16 +08:00
ibuler
583214e91a [Update] 优化获取授权资产api 2018-12-18 20:01:59 +08:00
BaiJiangJie
fb44ef0986 [Bugfix] 修复 资产授权/标签 选择资产时,input框内的资产和弹出资产tabl显示不一致的bug (#2205)
* [Bugfix] 修复授权规则,选择资产时,输入框资产和弹出表格中资产显示不一致

* [Bugfix] 修复创建/更新资产标签,选择资产时,输入框资产和弹出表格中资产显示不一致的bug

* [Update] 修复标签/授权选择资产时, 2次初始化table的bug;资产input和弹出table统一的代码逻辑移到js中;
2018-12-18 18:50:02 +08:00
ibuler
90b77fdb08 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-12-18 17:30:59 +08:00
老广
a609f17078 [Update] Stash it (#2197)
* [Update] Stash it

* [Bugfix] 修复错误

* [Update] 修改jms
2018-12-18 17:28:45 +08:00
ibuler
068a280350 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-12-18 11:38:42 +08:00
老广
1293d72189 Session task (#2196)
* [Bugfix] 修复错误

* [Update] 增加会话定期清理
2018-12-18 11:29:21 +08:00
ibuler
164c5ebabd Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-12-18 09:52:25 +08:00
老广
b56d73ba9e Node asset amount (#2192)
* [Bugfix] 修复错误

* [Update] 优化用户api
2018-12-17 20:09:25 +08:00
ibuler
dbdcdb722d Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-12-17 18:21:51 +08:00
老广
517a27ea33 Node asset amount (#2191)
* [Bugfix] 修复错误

* [Update] 修改树结构,统一api
2018-12-17 18:20:44 +08:00
vkill
ab6c88823d Support for TOTP valid_window configuration (#2187) 2018-12-17 14:26:00 +08:00
ibuler
1ff9f0eaa6 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-12-17 11:50:18 +08:00
老广
b95f8a7d6b [Update] 修复获取节点数量比较慢的问题 (#2184) 2018-12-17 11:49:57 +08:00
ibuler
29ff0efdc1 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2018-12-17 11:45:37 +08:00
老广
985bd6fc82 Bugfix2 (#2183)
* [Bugfix] 修复错误

* [Bugfix] 修复一些bug
2018-12-17 11:44:43 +08:00
BaiJiangJie
374039d287 [Update] 终端设置,添加coco端资产列表页面大小配置项 (#2182)
* [Update] 终端设置,添加coco端资产列表页面大小配置项

* [Update] 添加页面大小选项
2018-12-17 11:18:55 +08:00
ibuler
c7ac93fcc1 [Bugfix] 修复错误 2018-12-17 10:21:16 +08:00
老广
ac7e3e7f97 Merge pull request #2161 from jumpserver/dev
[Update] 更改readme
2018-12-13 10:16:08 +08:00
ibuler
4e0b25ae0f [Update] 更改readme 2018-12-13 10:15:31 +08:00
老广
559f4d2f5c Merge pull request #2160 from jumpserver/dev
[Bugfix] 修复access key的错误
2018-12-13 10:08:16 +08:00
ibuler
f40f6bc61e [Bugfix] 修复accekt的错误 2018-12-13 10:06:51 +08:00
ibuler
e9247dd578 [Update] default组织显示所有用户 2018-10-27 11:34:10 +08:00
385 changed files with 12716 additions and 7769 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ dump.rdb
.idea/
db.sqlite3
config.py
config.yml
*.log
host_rsa_key
*.bat

View File

@@ -6,13 +6,14 @@ RUN useradd jumpserver
COPY ./requirements /tmp/requirements
RUN yum -y install epel-release && cd /tmp/requirements && \
yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install -r requirements.txt
RUN yum -y install epel-release && rpm -ivh https://repo.mysql.com/mysql57-community-release-el6.rpm
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && \
pip install -i https://mirrors.aliyun.com/pypi/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
COPY . /opt/jumpserver
COPY config_docker.py /opt/jumpserver/config.py
RUN echo > config.yml
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs

189
README.md
View File

@@ -1,4 +1,4 @@
## Jumpserver
## Jumpserver 多云环境下更好用的堡垒机
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
@@ -8,45 +8,200 @@
----
Jumpserver是全球首款完全开源的堡垒机使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
Jumpserver 是全球首款完全开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 的专业运维审计系统。
Jumpserver使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
Jumpserver 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
Jumpserver采纳分布式架构支持多机房跨区域部署中心节点提供 API各机房部署登录节点可横向扩展、无并发限制。
Jumpserver 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API各机房部署登录节点可横向扩展、无并发限制。
改变世界,从一点点开始。
----
- [English Version](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
### 功能
----
<table class="subscription-level-table">
<tr class="subscription-level-tr-border">
<th style="background-color: #1ab394;color: #ffffff;" colspan="3">Jumpserver提供的堡垒机必备功能</th>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="4">身份验证 Authentication</td>
<td class="features-second-td-background-style" rowspan="3" >登录认证
</td>
<td class="features-third-td-background-style">资源统一登录和认证
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">LDAP认证
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">支持OpenID实现单点登录
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">多因子认证
</td>
<td class="features-third-td-background-style">MFAGoogle Authenticator
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="9">账号管理 Account</td>
<td class="features-second-td-background-style" rowspan="2">集中账号管理
</td>
<td class="features-third-td-background-style">管理用户管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">系统用户管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style" rowspan="4">统一密码管理
</td>
<td class="features-third-td-background-style">资产密码托管
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">自动生成密码
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">密码自动推送
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">密码过期设置
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style" rowspan="2">批量密码变更(X-PACK)
</td>
<td class="features-outline-td-background-style">定期批量修改密码
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style">生成随机密码
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style">多云环境的资产纳管(X-PACK)
</td>
<td class="features-outline-td-background-style">对私有云、公有云资产统一纳管
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="8">授权控制 Authorization</td>
<td class="features-second-td-background-style" rowspan="3">资产授权管理
</td>
<td class="features-third-td-background-style">资产树
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">资产或资产组灵活授权
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">节点内资产自动继承授权
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style">组织管理(X-PACK)
</td>
<td class="features-outline-td-background-style">实现多租户管理,权限隔离
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">多维度授权
</td>
<td class="features-third-td-background-style">可对用户、用户组或系统角色授权
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">指令限制
</td>
<td class="features-third-td-background-style">限制特权指令使用,支持黑白名单
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">统一文件传输
</td>
<td class="features-third-td-background-style">SFTP 文件上传/下载
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">文件管理
</td>
<td class="features-third-td-background-style">Web SFTP 文件管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="6">安全审计 Audit</td>
<td class="features-second-td-background-style" rowspan="2">会话管理
</td>
<td class="features-third-td-background-style">在线会话管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">历史会话管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style" rowspan="2">录像管理
</td>
<td class="features-third-td-background-style">Linux 录像支持
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">Windows 录像支持
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">指令审计
</td>
<td class="features-third-td-background-style">指令记录
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">文件传输审计
</td>
<td class="features-third-td-background-style">上传/下载记录审计
</td>
</tr>
</table>
![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver-14.png "Jumpserver功能")
### 开始使用
----
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
- 快速开始文档 [Docker 安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
- Step by Step 安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
- 也可以查看我们完整文档 [文档](http://docs.jumpserver.org)
### Demo 和 截图
### Demo、视频 和 截图
----
我们提供了DEMO和截图可以让你快速了解Jumpserver
我们提供了 Demo 、演示视频和截图可以让你快速了解 Jumpserver
[DEMO](http://demo.jumpserver.org)
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
- [Demo](https://demo.jumpserver.org/auth/login/?next=/)
- [视频](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
- [截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
### SDK
----
我们还编写了一些SDK供你其它系统快速和Jumpserver APi交互
我们还编写了一些SDK供你其它系统快速和 Jumpserver API 交互
- [python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver其它组件使用这个SDK完成交互
- [java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver其它组件使用这个SDK完成交互
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK
### License & Copyright
Copyright (c) 2014-2018 Beijing Duizhan Tech, Inc., All rights reserved.
Copyright (c) 2014-2019 飞致云 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

58
README_EN.md Normal file
View File

@@ -0,0 +1,58 @@
## Jumpserver
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Ansible](https://img.shields.io/badge/ansible-2.4.2.0-blue.svg?style=plastic)](https://www.ansible.com/)
[![Paramiko](https://img.shields.io/badge/paramiko-2.4.1-green.svg?style=plastic)](http://www.paramiko.org/)
----
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
Jumpserver is the first fully open source bastion in the world, based on the GNU GPL v2.0 open source protocol. Jumpserver is a professional operation and maintenance audit system conforms to 4A specifications.
Jumpserver is developed using Python / Django, conforms to the Web 2.0 specification, and is equipped with the industry-leading Web Terminal solution which have beautiful interface and great user experience.
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
Change the world, starting from little things.
----
### Features
![Jumpserver 功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver148.jpeg "Jumpserver 功能")
### Start
Quick start [Docker Install](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
Step by Step deployment. [Docs](http://docs.jumpserver.org/zh/docs/step_by_step.html)
Full documentation [Docs](http://docs.jumpserver.org)
### Demo、Video 和 Snapshot
We provide online demo, demo video and screenshots to get you started quickly.
[Demo](https://demo.jumpserver.org/auth/login/?next=/)
[Video](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
[Snapshot](http://docs.jumpserver.org/zh/docs/snapshot.html)
### SDK
We provide the SDK for your other systems to quickly interact with the Jumpserver API.
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK thanks to 恺珺 for provide Java SDK
### License & Copyright
Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-2.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -1,5 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
__version__ = "1.4.5"

View File

@@ -5,3 +5,4 @@ from .system_user import *
from .node import *
from .domain import *
from .cmd_filter import *
from .asset_user import *

View File

@@ -14,6 +14,7 @@
# limitations under the License.
from django.db import transaction
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
@@ -24,13 +25,14 @@ from common.utils import get_logger
from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset
from .. import serializers
from ..tasks import test_admin_user_connectability_manual
from ..tasks import test_admin_user_connectivity_manual
logger = get_logger(__file__)
__all__ = [
'AdminUserViewSet', 'ReplaceNodesAdminUserApi',
'AdminUserTestConnectiveApi', 'AdminUserAuthApi',
'AdminUserAssetsListView',
]
@@ -81,12 +83,30 @@ class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Test asset admin user connectivity
Test asset admin user assets_connectivity
"""
queryset = AdminUser.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object()
task = test_admin_user_connectability_manual.delay(admin_user)
task = test_admin_user_connectivity_manual.delay(admin_user)
return Response({"task": task.id})
class AdminUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(AdminUser, pk=pk)
def get_queryset(self):
admin_user = self.get_object()
return admin_user.get_related_assets()

View File

@@ -1,23 +1,29 @@
# -*- coding: utf-8 -*-
#
import uuid
import random
from rest_framework import generics
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
from rest_framework.pagination import LimitOffsetPagination
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.core.cache import cache
from django.db.models import Q
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from ..models import Asset, AdminUser, Node
from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \
test_asset_connectability_manual
test_asset_connectivity_manual
from ..utils import LabelFilter
@@ -25,7 +31,7 @@ logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetListUpdateApi',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
'AssetGatewayApi'
'AssetGatewayApi', 'AssetBulkUpdateSelectAPI'
]
@@ -41,40 +47,46 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,)
def filter_node(self):
def filter_node(self, queryset):
node_id = self.request.query_params.get("node_id")
if not node_id:
return
return queryset
node = get_object_or_404(Node, id=node_id)
show_current_asset = self.request.query_params.get("show_current_asset") in ('1', 'true')
if node.is_root():
if show_current_asset:
self.queryset = self.queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True)
)
return
if show_current_asset:
self.queryset = self.queryset.filter(nodes=node)
if node.is_root() and show_current_asset:
queryset = queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True)
)
elif node.is_root() and not show_current_asset:
pass
elif not node.is_root() and show_current_asset:
queryset = queryset.filter(nodes=node)
else:
self.queryset = self.queryset.filter(
queryset = queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
)
return queryset
def filter_admin_user_id(self):
def filter_admin_user_id(self, queryset):
admin_user_id = self.request.query_params.get('admin_user_id')
if admin_user_id:
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
self.queryset = self.queryset.filter(admin_user=admin_user)
if not admin_user_id:
return queryset
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
queryset = queryset.filter(admin_user=admin_user)
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_node(queryset)
queryset = self.filter_admin_user_id(queryset)
return queryset
def get_queryset(self):
self.queryset = super().get_queryset()\
.prefetch_related('labels', 'nodes')\
.select_related('admin_user')
self.filter_admin_user_id()
self.filter_node()
return self.queryset.distinct()
queryset = super().get_queryset().distinct()
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
@@ -86,6 +98,21 @@ class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
permission_classes = (IsOrgAdmin,)
class AssetBulkUpdateSelectAPI(APIView):
permission_classes = (IsOrgAdmin,)
def post(self, request, *args, **kwargs):
assets_id = request.data.get('assets_id', '')
if assets_id:
spm = uuid.uuid4().hex
key = CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm)
cache.set(key, assets_id, 300)
url = reverse_lazy('assets:asset-bulk-update') + '?spm=%s' % spm
return Response({'url': url})
error = _('Please select assets that need to be updated')
return Response({'error': error}, status=400)
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
"""
Refresh asset hardware info
@@ -103,21 +130,23 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
class AssetAdminUserTestApi(generics.RetrieveAPIView):
"""
Test asset admin user connectivity
Test asset admin user assets_connectivity
"""
queryset = Asset.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
task = test_asset_connectability_manual.delay(asset)
task = test_asset_connectivity_manual.delay(asset)
return Response({"task": task.id})
class AssetGatewayApi(generics.RetrieveAPIView):
queryset = Asset.objects.all()
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
#
from rest_framework.response import Response
from rest_framework import viewsets, status, generics
from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsOrgAdminOrAppUser
from common.utils import get_object_or_none, get_logger
from ..backends.multi import AssetUserManager
from ..models import Asset
from .. import serializers
from ..tasks import test_asset_users_connectivity_manual
__all__ = [
'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi',
]
logger = get_logger(__name__)
class AssetUserViewSet(viewsets.GenericViewSet):
pagination_class = LimitOffsetPagination
serializer_class = serializers.AssetUserSerializer
permission_classes = (IsOrgAdminOrAppUser, )
http_method_names = ['get', 'post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def get_queryset(self):
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
asset = get_object_or_none(Asset, pk=asset_id)
queryset = AssetUserManager.filter(username=username, asset=asset)
return queryset
def filter_queryset(self, queryset):
queryset = sorted(
queryset,
key=lambda q: (q.asset.hostname, q.connectivity, q.username)
)
return queryset
class AssetUserAuthInfoApi(generics.RetrieveAPIView):
serializer_class = serializers.AssetUserAuthInfoSerializer
permission_classes = (IsOrgAdminOrAppUser,)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
status_code = status.HTTP_200_OK
if not instance:
status_code = status.HTTP_400_BAD_REQUEST
return Response(serializer.data, status=status_code)
def get_object(self):
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
asset = get_object_or_none(Asset, pk=asset_id)
try:
instance = AssetUserManager.get(username, asset)
except Exception as e:
logger.error(e, exc_info=True)
return None
else:
return instance
class AssetUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Test asset users connective
"""
def get_asset_users(self):
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
asset = get_object_or_none(Asset, pk=asset_id)
asset_users = AssetUserManager.filter(username=username, asset=asset)
return asset_users
def retrieve(self, request, *args, **kwargs):
asset_users = self.get_asset_users()
task = test_asset_users_connectivity_manual.delay(asset_users)
return Response({"task": task.id})

View File

@@ -51,9 +51,10 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView):
model = Gateway
object = None
def get(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all())
ok, e = self.object.test_connective()
local_port = self.request.data.get('port') or self.object.port
ok, e = self.object.test_connective(local_port=local_port)
if ok:
return Response("ok")
else:

View File

@@ -17,15 +17,14 @@ from rest_framework import generics, mixins, viewsets
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from django.db.models import Count
from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util
from ..tasks import update_assets_hardware_info_util, test_asset_connectivity_util
from .. import serializers
@@ -34,7 +33,8 @@ __all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
'TestNodeConnectiveApi'
'TestNodeConnectiveApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi', 'RefreshAssetsAmount',
]
@@ -61,45 +61,121 @@ class NodeViewSet(viewsets.ModelViewSet):
return super().update(request, *args, **kwargs)
class NodeListAsTreeApi(generics.ListAPIView):
"""
获取节点列表树
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
def get_queryset(self):
queryset = [node.as_tree_node() for node in Node.objects.all()]
return queryset
def filter_queryset(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
queryset = self.refresh_nodes(queryset)
return queryset
@staticmethod
def refresh_nodes(queryset):
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenAsTreeApi(generics.ListAPIView):
"""
节点子节点作为树返回,
[
{
"id": "",
"name": "",
"pId": "",
"meta": ""
}
]
"""
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
node = None
is_root = False
def get_queryset(self):
node_key = self.request.query_params.get('key')
if node_key:
self.node = Node.objects.get(key=node_key)
queryset = self.node.get_children(with_self=False)
else:
self.is_root = True
self.node = Node.root()
queryset = list(self.node.get_children(with_self=True))
nodes_invalid = Node.objects.exclude(key__startswith=self.node.key)
queryset.extend(list(nodes_invalid))
queryset = [node.as_tree_node() for node in queryset]
return queryset
def filter_assets(self, queryset):
include_assets = self.request.query_params.get('assets', '0') == '1'
if not include_assets:
return queryset
assets = self.node.get_assets()
for asset in assets:
queryset.append(asset.as_tree_node(self.node))
return queryset
def filter_queryset(self, queryset):
queryset = self.filter_assets(queryset)
queryset = self.filter_refresh_nodes(queryset)
return queryset
def filter_refresh_nodes(self, queryset):
if self.request.query_params.get('refresh', '0') == '1':
Node.expire_nodes_assets_amount()
Node.expire_nodes_full_value()
return queryset
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
queryset = Node.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
instance = None
def counter(self):
values = [
child.value[child.value.rfind(' '):]
for child in self.get_object().get_children()
if child.value.startswith("新节点 ")
]
values = [int(value) for value in values if value.strip().isdigit()]
count = max(values)+1 if values else 1
return count
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
instance = self.get_object()
if not request.data.get("value"):
request.data["value"] = _("New node {}").format(self.counter())
request.data["value"] = instance.get_next_child_preset_name()
return super().post(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
instance = self.get_object()
value = request.data.get("value")
_id = request.data.get('id') or None
values = [child.value for child in instance.get_children()]
if value in values:
raise ValidationError(
'The same level node name cannot be the same'
)
node = instance.create_child(value=value)
return Response(
{"id": node.id, "key": node.key, "value": node.value},
status=201,
)
node = instance.create_child(value=value, _id=_id)
return Response(self.serializer_class(instance=node).data, status=201)
def get_object(self):
pk = self.kwargs.get('pk') or self.request.query_params.get('id')
if not pk:
node = None
node = Node.root()
else:
node = get_object_or_404(Node, pk=pk)
return node
@@ -107,7 +183,6 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
def get_queryset(self):
queryset = []
query_all = self.request.query_params.get("all")
query_assets = self.request.query_params.get('assets')
node = self.get_object()
if node is None:
@@ -120,23 +195,8 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
else:
children = node.get_children()
queryset.extend(list(children))
if query_assets:
assets = node.get_assets()
for asset in assets:
node_fake = Node()
node_fake.assets__count = 0
node_fake.id = asset.id
node_fake.is_node = False
node_fake.key = node.key + ':0'
node_fake.value = asset.hostname
queryset.append(node_fake)
queryset = sorted(queryset, key=lambda x: x.is_node, reverse=True)
return queryset
def get(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
@@ -217,7 +277,7 @@ class RefreshNodeHardwareInfoApi(APIView):
def get(self, request, *args, **kwargs):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
assets = node.get_all_assets()
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
task_name = _("Update node asset hardware information: {}").format(node.name)
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
@@ -231,8 +291,17 @@ class TestNodeConnectiveApi(APIView):
def get(self, request, *args, **kwargs):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
assets = node.get_all_assets()
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectability_util.delay(assets, task_name=task_name)
task = test_asset_connectivity_util.delay(assets, task_name=task_name)
return Response({"task": task.id})
class RefreshAssetsAmount(APIView):
permission_classes = (IsOrgAdmin,)
model = Node
def get(self, request, *args, **kwargs):
self.model.expire_nodes_assets_amount()
return Response("Ok")

View File

@@ -24,16 +24,16 @@ from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import SystemUser, Asset
from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
test_system_user_connectability_a_asset
test_system_user_connectivity_manual, push_system_user_a_asset_manual, \
test_system_user_connectivity_a_asset
logger = get_logger(__file__)
__all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi',
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
]
@@ -68,6 +68,22 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
return Response(status=204)
class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
"""
Get system user with asset auth info
"""
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.SystemUserAuthSerializer
def get_object(self):
instance = super().get_object()
aid = self.kwargs.get('aid')
asset = get_object_or_404(Asset, pk=aid)
instance.load_specific_asset_auth(asset)
return instance
class SystemUserPushApi(generics.RetrieveAPIView):
"""
Push system user to cluster assets api
@@ -93,15 +109,16 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user)
task = test_system_user_connectivity_manual.delay(system_user)
return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
serializer_class = serializers.AssetSimpleSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
def get_object(self):
@@ -116,6 +133,7 @@ class SystemUserAssetsListView(generics.ListAPIView):
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
@@ -125,15 +143,16 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView):
return Response({"task": task.id})
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectability_a_asset.delay(system_user, asset)
task = test_system_user_connectivity_a_asset.delay(system_user, asset)
return Response({"task": task.id})

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from abc import abstractmethod
class NotSupportError(Exception):
pass
class BaseBackend:
ObjectDoesNotExist = ObjectDoesNotExist
MultipleObjectsReturned = MultipleObjectsReturned
NotSupportError = NotSupportError
MSG_NOT_EXIST = '{} Object matching query does not exist'
MSG_MULTIPLE = '{} get() returned more than one object ' \
'-- it returned {}!'
@classmethod
def get(cls, username, asset):
instances = cls.filter(username, asset)
if len(instances) == 1:
return instances[0]
elif len(instances) == 0:
cls.raise_does_not_exist(cls.__name__)
else:
cls.raise_multiple_return(cls.__name__, len(instances))
@classmethod
@abstractmethod
def filter(cls, username=None, asset=None, latest=True):
"""
:param username: 用户名
:param asset: <Asset>对象
:param latest: 是否是最新记录
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
"""
pass
@classmethod
@abstractmethod
def create(cls, **kwargs):
"""
:param kwargs:
{
name, username, asset, comment, password, public_key, private_key,
(org_id)
}
:return: <AuthBook>对象
"""
pass
@classmethod
def raise_does_not_exist(cls, name):
raise cls.ObjectDoesNotExist(cls.MSG_NOT_EXIST.format(name))
@classmethod
def raise_multiple_return(cls, name, length):
raise cls.MultipleObjectsReturned(cls.MSG_MULTIPLE.format(name, length))

View File

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

31
apps/assets/backends/external/db.py vendored Normal file
View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#
from assets.models import AuthBook
from ..base import BaseBackend
class AuthBookBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, latest=True):
queryset = AuthBook.objects.all()
if username:
queryset = queryset.filter(username=username)
if asset:
queryset = queryset.filter(asset=asset)
if latest:
queryset = queryset.latest_version()
return queryset
@classmethod
def create(cls, **kwargs):
auth_info = {
'password': kwargs.pop('password', ''),
'public_key': kwargs.pop('public_key', ''),
'private_key': kwargs.pop('private_key', '')
}
obj = AuthBook.objects.create(**kwargs)
obj.set_auth(**auth_info)
return obj

16
apps/assets/backends/external/utils.py vendored Normal file
View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
#
# from django.conf import settings
from .db import AuthBookBackend
# from .vault import VaultBackend
def get_backend():
default_backend = AuthBookBackend
# if settings.BACKEND_ASSET_USER_AUTH_VAULT:
# return VaultBackend
return default_backend

19
apps/assets/backends/external/vault.py vendored Normal file
View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
from ..base import BaseBackend
class VaultBackend(BaseBackend):
@classmethod
def get(cls, username, asset):
pass
@classmethod
def filter(cls, username=None, asset=None, latest=True):
pass
@classmethod
def create(cls, **kwargs):
pass

View File

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

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
from assets.models import Asset
from ..base import BaseBackend
from .utils import construct_authbook_object
class AdminUserBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
instances = cls.construct_authbook_objects(username, asset)
return instances
@classmethod
def _get_assets(cls, asset):
if not asset:
assets = Asset.objects.all().prefetch_related('admin_user')
else:
assets = [asset]
return assets
@classmethod
def construct_authbook_objects(cls, username, asset):
instances = []
assets = cls._get_assets(asset)
for asset in assets:
if username and asset.admin_user.username != username:
continue
instance = construct_authbook_object(asset.admin_user, asset)
instances.append(instance)
return instances
@classmethod
def create(cls, **kwargs):
raise cls.NotSupportError("Not support create")

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
from ..base import BaseBackend
from .admin_user import AdminUserBackend
from .system_user import SystemUserBackend
class AssetUserBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
admin_user_instances = AdminUserBackend.filter(username, asset)
system_user_instances = SystemUserBackend.filter(username, asset)
instances = cls._merge_instances(admin_user_instances, system_user_instances)
return instances
@classmethod
def _merge_instances(cls, admin_user_instances, system_user_instances):
admin_user_instances_keyword_list = [
{'username': instance.username, 'asset': instance.asset}
for instance in admin_user_instances
]
instances = [
instance for instance in system_user_instances
if instance.keyword not in admin_user_instances_keyword_list
]
admin_user_instances.extend(instances)
return admin_user_instances
@classmethod
def create(cls, **kwargs):
raise cls.NotSupportError("Not support create")

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
import itertools
from assets.models import Asset
from ..base import BaseBackend
from .utils import construct_authbook_object
class SystemUserBackend(BaseBackend):
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
instances = cls.construct_authbook_objects(username, asset)
return instances
@classmethod
def _distinct_system_users_by_username(cls, system_users):
system_users = sorted(
system_users,
key=lambda su: (su.username, su.priority, su.date_updated),
reverse=True,
)
results = itertools.groupby(system_users, key=lambda su: su.username)
system_users = [next(result[1]) for result in results]
return system_users
@classmethod
def _filter_system_users_by_username(cls, system_users, username):
_system_users = cls._distinct_system_users_by_username(system_users)
if username:
_system_users = [su for su in _system_users if username == su.username]
return _system_users
@classmethod
def _construct_authbook_objects(cls, system_users, asset):
instances = []
for system_user in system_users:
instance = construct_authbook_object(system_user, asset)
instances.append(instance)
return instances
@classmethod
def _get_assets_with_system_users(cls, asset=None):
"""
{ 'asset': set(<SystemUser>, <SystemUser>, ...) }
"""
if not asset:
_assets = Asset.objects.all().prefetch_related('systemuser_set')
else:
_assets = [asset]
assets = {asset: set(asset.systemuser_set.all()) for asset in _assets}
return assets
@classmethod
def construct_authbook_objects(cls, username, asset):
"""
:return: [<AuthBook>, <AuthBook>, ...]
"""
instances = []
assets = cls._get_assets_with_system_users(asset)
for _asset, _system_users in assets.items():
_system_users = cls._filter_system_users_by_username(_system_users, username)
_instances = cls._construct_authbook_objects(_system_users, _asset)
instances.extend(_instances)
return instances
@classmethod
def create(cls, **kwargs):
raise Exception("Not support create")

View File

@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
#
from assets.models import AuthBook
def construct_authbook_object(asset_user, asset):
"""
作用: 将<AssetUser>对象构造成为<AuthBook>对象并返回
:param asset_user: <AdminUser>或<SystemUser>对象
:param asset: <Asset>对象
:return: <AuthBook>对象
"""
fields = [
'id', 'name', 'username', 'comment', 'org_id',
'_password', '_private_key', '_public_key',
'date_created', 'date_updated', 'created_by'
]
obj = AuthBook(asset=asset, version=0, is_latest=True)
for field in fields:
value = getattr(asset_user, field)
setattr(obj, field, value)
return obj

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
#
from .base import BaseBackend
from .external.utils import get_backend
from .internal.asset_user import AssetUserBackend
class AssetUserManager(BaseBackend):
"""
资产用户管理器
"""
external_backend = get_backend()
internal_backend = AssetUserBackend
@classmethod
def filter(cls, username=None, asset=None, **kwargs):
external_instance = list(cls.external_backend.filter(username, asset))
internal_instance = list(cls.internal_backend.filter(username, asset))
instances = cls._merge_instances(external_instance, internal_instance)
return instances
@classmethod
def create(cls, **kwargs):
instance = cls.external_backend.create(**kwargs)
return instance
@classmethod
def _merge_instances(cls, external_instances, internal_instances):
external_instances_keyword_list = [
{'username': instance.username, 'asset': instance.asset}
for instance in external_instances
]
instances = [
instance for instance in internal_instances
if instance.keyword not in external_instances_keyword_list
]
external_instances.extend(instances)
return external_instances

View File

@@ -32,7 +32,22 @@ TEST_SYSTEM_USER_CONN_TASKS = [
}
]
ASSET_USER_CONN_CACHE_KEY = 'ASSET_USER_CONN_{}_{}'
TEST_ASSET_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "ping",
}
}
]
TASK_OPTIONS = {
'timeout': 10,
'forks': 10,
}
CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}'

View File

@@ -41,9 +41,6 @@ class AssetCreateForm(OrgModelForm):
'nodes': _("Node"),
}
help_texts = {
'hostname': '* required',
'ip': '* required',
'port': '* required',
'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'
@@ -80,10 +77,6 @@ class AssetUpdateForm(OrgModelForm):
'nodes': _("Node"),
}
help_texts = {
'hostname': '* required',
'ip': '* required',
'port': '* required',
'cluster': '* required',
'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'
@@ -95,7 +88,7 @@ class AssetUpdateForm(OrgModelForm):
class AssetBulkUpdateForm(OrgModelForm):
assets = forms.ModelMultipleChoiceField(
required=True, help_text='* required',
required=True,
label=_('Select assets'), queryset=Asset.objects.all(),
widget=forms.SelectMultiple(
attrs={
@@ -104,24 +97,12 @@ class AssetBulkUpdateForm(OrgModelForm):
}
)
)
port = forms.IntegerField(
label=_('Port'), required=False, min_value=1, max_value=65535,
)
admin_user = forms.ModelChoiceField(
required=False, queryset=AdminUser.objects,
label=_("Admin user"),
widget=forms.Select(
attrs={
'class': 'select2',
'data-placeholder': _('Admin user')
}
)
)
class Meta:
model = Asset
fields = [
'assets', 'port', 'admin_user', 'labels', 'nodes', 'platform'
'assets', 'port', 'admin_user', 'labels', 'platform',
'protocol', 'domain',
]
widgets = {
'labels': forms.SelectMultiple(
@@ -132,6 +113,13 @@ class AssetBulkUpdateForm(OrgModelForm):
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 重写其他字段为不再required
for name, field in self.fields.items():
if name != 'assets':
field.required = False
def save(self, commit=True):
changed_fields = []
for field in self._meta.fields:

View File

@@ -28,6 +28,15 @@ class DomainForm(forms.ModelForm):
initial['assets'] = kwargs['instance'].assets.all()
super().__init__(*args, **kwargs)
# 前端渲染优化, 防止过多资产
assets_field = self.fields.get('assets')
if not self.data:
instance = kwargs.get('instance')
if instance:
assets_field.queryset = instance.assets.all()
else:
assets_field.queryset = Asset.objects.none()
def save(self, commit=True):
instance = super().save(commit=commit)
assets = self.cleaned_data['assets']
@@ -36,14 +45,12 @@ class DomainForm(forms.ModelForm):
class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
protocol = forms.ChoiceField(
choices=[Gateway.PROTOCOL_CHOICES[0]],
)
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`
@@ -59,11 +66,10 @@ class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
'name', 'ip', 'port', 'username', 'protocol', 'domain', 'password',
'private_key_file', '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')}),
}
help_texts = {
'name': '* required',
'username': '* required',
}

View File

@@ -26,6 +26,15 @@ class LabelForm(forms.ModelForm):
initial['assets'] = kwargs['instance'].assets.all()
super().__init__(*args, **kwargs)
# 前端渲染优化, 防止过多资产
assets_field = self.fields.get('assets')
if not self.data:
instance = kwargs.get('instance')
if instance:
assets_field.queryset = instance.assets.all()
else:
assets_field.queryset = Asset.objects.none()
def save(self, commit=True):
label = super().save(commit=commit)
assets = self.cleaned_data['assets']

View File

@@ -35,8 +35,12 @@ class PasswordAndKeyAuthForm(forms.ModelForm):
if private_key_file:
key_string = private_key_file.read()
private_key_file.seek(0)
key_string = key_string.decode()
if not validate_ssh_private_key(key_string, password):
raise forms.ValidationError(_('Invalid private key'))
msg = _('Invalid private key, Only support '
'RSA/DSA format key')
raise forms.ValidationError(msg)
return private_key_file
def validate_password_key(self):
@@ -80,10 +84,6 @@ class AdminUserForm(PasswordAndKeyAuthForm):
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
}
help_texts = {
'name': '* required',
'username': '* required',
}
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
@@ -99,8 +99,10 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
auto_generate_key = self.cleaned_data.get('auto_generate_key', False)
private_key, public_key = super().gen_keys()
if login_mode == SystemUser.MANUAL_LOGIN or \
protocol in [SystemUser.RDP_PROTOCOL, SystemUser.TELNET_PROTOCOL]:
if login_mode == SystemUser.LOGIN_MANUAL or \
protocol in [SystemUser.PROTOCOL_RDP,
SystemUser.PROTOCOL_TELNET,
SystemUser.PROTOCOL_VNC]:
system_user.auto_push = 0
auto_generate_key = False
system_user.save()
@@ -120,17 +122,18 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
if not self.instance and not auto_generate:
super().validate_password_key()
def is_valid(self):
validated = super().is_valid()
username = self.cleaned_data.get('username')
login_mode = self.cleaned_data.get('login_mode')
if login_mode == SystemUser.AUTO_LOGIN and not username:
self.add_error(
"username", _('* Automatic login mode,'
' must fill in the username.')
)
return False
return validated
def clean_username(self):
username = self.data.get('username')
login_mode = self.data.get('login_mode')
protocol = self.data.get('protocol')
if username:
return username
if login_mode == SystemUser.LOGIN_AUTO and \
protocol != SystemUser.PROTOCOL_VNC:
msg = _('* Automatic login mode must fill in the username.')
raise forms.ValidationError(msg)
return username
class Meta:
model = SystemUser
@@ -147,11 +150,10 @@ class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
}),
}
help_texts = {
'name': '* required',
'username': '* required',
'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.')
'need to fill in the username and password.'),
'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig")
}

View File

@@ -0,0 +1,158 @@
# Generated by Django 2.1.7 on 2019-02-28 10:16
import assets.models.asset
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
replaces = [('assets', '0002_auto_20180105_1807'), ('assets', '0003_auto_20180109_2331'), ('assets', '0004_auto_20180125_1218'), ('assets', '0005_auto_20180126_1637'), ('assets', '0006_auto_20180130_1502'), ('assets', '0007_auto_20180225_1815'), ('assets', '0008_auto_20180306_1804'), ('assets', '0009_auto_20180307_1212')]
dependencies = [
('assets', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='adminuser',
options={'ordering': ['name'], 'verbose_name': 'Admin user'},
),
migrations.AlterModelOptions(
name='asset',
options={'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='assetgroup',
options={'ordering': ['name'], 'verbose_name': 'Asset group'},
),
migrations.AlterModelOptions(
name='cluster',
options={'ordering': ['name'], 'verbose_name': 'Cluster'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'verbose_name': 'System user'},
),
migrations.RemoveField(
model_name='asset',
name='cluster',
),
migrations.AlterField(
model_name='assetgroup',
name='created_by',
field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'),
),
migrations.CreateModel(
name='Label',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('category', models.CharField(choices=[('S', 'System'), ('U', 'User')], default='U', max_length=128, verbose_name='Category')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
options={
'db_table': 'assets_label',
},
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value')},
),
migrations.AddField(
model_name='asset',
name='labels',
field=models.ManyToManyField(blank=True, related_name='assets', to='assets.Label', verbose_name='Labels'),
),
migrations.RemoveField(
model_name='asset',
name='cabinet_no',
),
migrations.RemoveField(
model_name='asset',
name='cabinet_pos',
),
migrations.RemoveField(
model_name='asset',
name='env',
),
migrations.RemoveField(
model_name='asset',
name='remote_card_ip',
),
migrations.RemoveField(
model_name='asset',
name='status',
),
migrations.RemoveField(
model_name='asset',
name='type',
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('key', models.CharField(max_length=64, unique=True, verbose_name='Key')),
('value', models.CharField(max_length=128, verbose_name='Value')),
('child_mark', models.IntegerField(default=0)),
('date_create', models.DateTimeField(auto_now_add=True)),
],
),
migrations.RemoveField(
model_name='asset',
name='groups',
),
migrations.RemoveField(
model_name='systemuser',
name='cluster',
),
migrations.AlterField(
model_name='asset',
name='admin_user',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='assets.AdminUser', verbose_name='Admin user'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AddField(
model_name='asset',
name='nodes',
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
),
migrations.AddField(
model_name='systemuser',
name='nodes',
field=models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes'),
),
migrations.AlterField(
model_name='adminuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='created_by',
field=models.CharField(max_length=128, null=True, verbose_name='Created by'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=128, verbose_name='Username'),
),
]

View File

@@ -0,0 +1,220 @@
# Generated by Django 2.1.7 on 2019-02-28 10:16
import assets.models.utils
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# assets.migrations.0017_auto_20180702_1415
def migrate_win_to_ssh_protocol(apps, schema_editor):
asset_model = apps.get_model("assets", "Asset")
db_alias = schema_editor.connection.alias
asset_model.objects.using(db_alias).filter(platform__startswith='Win').update(protocol='rdp')
class Migration(migrations.Migration):
replaces = [('assets', '0010_auto_20180307_1749'), ('assets', '0011_auto_20180326_0957'), ('assets', '0012_auto_20180404_1302'), ('assets', '0013_auto_20180411_1135'), ('assets', '0014_auto_20180427_1245'), ('assets', '0015_auto_20180510_1235'), ('assets', '0016_auto_20180511_1203'), ('assets', '0017_auto_20180702_1415'), ('assets', '0018_auto_20180807_1116'), ('assets', '0019_auto_20180816_1320')]
dependencies = [
('assets', '0009_auto_20180307_1212'),
]
operations = [
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, unique=True, verbose_name='Value'),
),
migrations.CreateModel(
name='Domain',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
],
),
migrations.CreateModel(
name='Gateway',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
('username', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('ip', models.GenericIPAddressField(db_index=True, verbose_name='IP')),
('port', models.IntegerField(default=22, verbose_name='Port')),
('protocol', models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp')], default='ssh', max_length=16, verbose_name='Protocol')),
('comment', models.CharField(blank=True, max_length=128, null=True, verbose_name='Comment')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Domain', verbose_name='Domain')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='asset',
name='domain',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assets', to='assets.Domain', verbose_name='Domain'),
),
migrations.AddField(
model_name='systemuser',
name='assets',
field=models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets'),
),
migrations.AlterField(
model_name='systemuser',
name='sudo',
field=models.TextField(default='/bin/whoami', verbose_name='Sudo'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_-]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='node',
name='value',
field=models.CharField(max_length=128, verbose_name='Value'),
),
migrations.AddField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AddField(
model_name='systemuser',
name='login_mode',
field=models.CharField(choices=[('auto', 'Automatic login'), ('manual', 'Manually login')], default='auto', max_length=10, verbose_name='Login mode'),
),
migrations.AlterField(
model_name='adminuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.AlterField(
model_name='asset',
name='platform',
field=models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Windows2016', 'Windows(2016)'), ('Other', 'Other')], default='Linux', max_length=128, verbose_name='Platform'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)')], default='ssh', max_length=16, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='username',
field=models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
),
migrations.RunPython(
code=migrate_win_to_ssh_protocol,
),
migrations.AddField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AddField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
),
migrations.AlterField(
model_name='adminuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='asset',
name='hostname',
field=models.CharField(max_length=128, verbose_name='Hostname'),
),
migrations.AlterField(
model_name='gateway',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterField(
model_name='systemuser',
name='name',
field=models.CharField(max_length=128, verbose_name='Name'),
),
migrations.AlterUniqueTogether(
name='adminuser',
unique_together={('name', 'org_id')},
),
migrations.AddField(
model_name='asset',
name='cpu_vcpus',
field=models.IntegerField(null=True, verbose_name='CPU vcpus'),
),
migrations.AlterUniqueTogether(
name='asset',
unique_together={('org_id', 'hostname')},
),
migrations.AlterUniqueTogether(
name='gateway',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='systemuser',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='label',
unique_together={('name', 'value', 'org_id')},
),
]

View File

@@ -13,36 +13,36 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adminuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='asset',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='domain',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='gateway',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='label',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='node',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AlterField(
model_name='systemuser',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]

View File

@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CommandFilter',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('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)),
('name', models.CharField(max_length=64, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
@@ -32,7 +32,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='CommandFilterRule',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('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)),
('type', models.CharField(choices=[('regex', 'Regex'), ('command', 'Command')], default='command', max_length=16, verbose_name='Type')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),

View File

@@ -0,0 +1,23 @@
# Generated by Django 2.1.4 on 2018-12-19 08:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0023_auto_20181016_1650'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=128, verbose_name='Protocol'),
),
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet (beta)'), ('vnc', 'vnc')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 2.1.7 on 2019-02-21 11:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0024_auto_20181219_1614'),
]
operations = [
migrations.AlterModelOptions(
name='commandfilter',
options={'verbose_name': 'Command filter'},
),
migrations.AlterModelOptions(
name='commandfilterrule',
options={'ordering': ('-priority', 'action'), 'verbose_name': 'Command filter rule'},
),
]

View File

@@ -0,0 +1,43 @@
# Generated by Django 2.1.7 on 2019-03-25 12:35
import assets.models.utils
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0025_auto_20190221_1902'),
]
operations = [
migrations.CreateModel(
name='AuthBook',
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)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, max_length=32, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('_password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('_private_key', models.TextField(blank=True, max_length=4096, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key')),
('_public_key', models.TextField(blank=True, max_length=4096, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
('is_latest', models.BooleanField(default=False, verbose_name='Latest version')),
('version', models.IntegerField(default=1, verbose_name='Version')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.Asset', verbose_name='Asset')),
],
options={
'verbose_name': 'AuthBook',
},
),
migrations.AlterModelOptions(
name='node',
options={'ordering': ['key'], 'verbose_name': 'Node'},
),
]

View File

@@ -7,3 +7,4 @@ from .node import *
from .asset import *
from .cmd_filter import *
from .utils import *
from .authbook import *

View File

@@ -13,7 +13,6 @@ from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache
from ..const import ASSET_ADMIN_CONN_CACHE_KEY
from .user import AdminUser, SystemUser
from orgs.mixins import OrgModelMixin, OrgManager
@@ -60,78 +59,65 @@ class Asset(OrgModelMixin):
('Other', 'Other'),
)
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
TELNET_PROTOCOL = 'telnet'
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'),
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet (beta)'),
(PROTOCOL_VNC, 'vnc'),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
hostname = models.CharField(max_length=128, verbose_name=_('Hostname'))
protocol = models.CharField(max_length=128, default=SSH_PROTOCOL, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
protocol = models.CharField(max_length=128, default=PROTOCOL_SSH, choices=PROTOCOL_CHOICES, verbose_name=_('Protocol'))
port = models.IntegerField(default=22, verbose_name=_('Port'))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES, default='Linux', verbose_name=_('Platform'))
domain = models.ForeignKey("assets.Domain", null=True, blank=True,
related_name='assets', verbose_name=_("Domain"),
on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node,
related_name='assets',
verbose_name=_("Nodes"))
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL)
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', verbose_name=_("Nodes"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
# Auth
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT,
null=True, verbose_name=_("Admin user"))
admin_user = models.ForeignKey('assets.AdminUser', on_delete=models.PROTECT, null=True, verbose_name=_("Admin user"))
# Some information
public_ip = models.GenericIPAddressField(max_length=32, blank=True, null=True, verbose_name=_('Public IP'))
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
# Collect
vendor = models.CharField(max_length=64, null=True, blank=True,
verbose_name=_('Vendor'))
model = models.CharField(max_length=54, null=True, blank=True,
verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True,
verbose_name=_('Serial number'))
vendor = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Vendor'))
model = models.CharField(max_length=54, null=True, blank=True, verbose_name=_('Model'))
sn = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Serial number'))
cpu_model = models.CharField(max_length=64, null=True, blank=True,
verbose_name=_('CPU model'))
cpu_model = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU model'))
cpu_count = models.IntegerField(null=True, verbose_name=_('CPU count'))
cpu_cores = models.IntegerField(null=True, verbose_name=_('CPU cores'))
cpu_vcpus = models.IntegerField(null=True, verbose_name=_('CPU vcpus'))
memory = models.CharField(max_length=64, null=True, blank=True,
verbose_name=_('Memory'))
disk_total = models.CharField(max_length=1024, null=True, blank=True,
verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True,
verbose_name=_('Disk info'))
memory = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Memory'))
disk_total = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk total'))
disk_info = models.CharField(max_length=1024, null=True, blank=True, verbose_name=_('Disk info'))
os = models.CharField(max_length=128, null=True, blank=True,
verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True,
verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True,
verbose_name=_('OS arch'))
hostname_raw = models.CharField(max_length=128, blank=True, null=True,
verbose_name=_('Hostname raw'))
os = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('OS'))
os_version = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('OS version'))
os_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=_('OS arch'))
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'))
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'))
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'))
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'))
objects = OrgManager.from_queryset(AssetQuerySet)()
CONNECTIVITY_CACHE_KEY = '_JMS_ASSET_CONNECTIVITY_{}'
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
def __str__(self):
return '{0.hostname}({0.ip})'.format(self)
@@ -167,7 +153,7 @@ class Asset(OrgModelMixin):
nodes = []
for node in self.get_nodes():
_nodes = node.get_ancestor(with_self=True)
_nodes.append(_nodes)
nodes.append(_nodes)
if flat:
nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
return nodes
@@ -197,28 +183,21 @@ class Asset(OrgModelMixin):
return ''
@property
def is_connective(self):
def connectivity(self):
if not self.is_unixlike():
return True
val = cache.get(ASSET_ADMIN_CONN_CACHE_KEY.format(self.hostname))
if val == 1:
return True
else:
return False
return self.REACHABLE
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
cached = cache.get(key, None)
return cached if cached is not None else self.UNKNOWN
def to_json(self):
info = {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
@connectivity.setter
def connectivity(self, value):
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
cache.set(key, value, 3600*2)
def get_auth_info(self):
if self.admin_user:
self.admin_user.load_specific_asset_auth(self)
return {
'username': self.admin_user.username,
'password': self.admin_user.password,
@@ -236,15 +215,25 @@ class Asset(OrgModelMixin):
fake_node.is_node = False
return fake_node
def to_json(self):
info = {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
}
if self.domain and self.domain.gateway_set.all():
info["gateways"] = [d.id for d in self.domain.gateway_set.all()]
return info
def _to_secret_json(self):
"""
Ansible use it create inventory, First using asset user,
otherwise using cluster admin user
Ansible use it create inventory
Todo: May be move to ops implements it
"""
data = self.to_json()
if self.admin_user:
self.admin_user.load_specific_asset_auth(self)
admin_user = self.admin_user
data.update({
'username': admin_user.username,
@@ -255,6 +244,36 @@ class Asset(OrgModelMixin):
})
return data
def as_tree_node(self, parent_node):
from common.tree import TreeNode
icon_skin = 'file'
if self.platform.lower() == 'windows':
icon_skin = 'windows'
elif self.platform.lower() == 'linux':
icon_skin = 'linux'
data = {
'id': str(self.id),
'name': self.hostname,
'title': self.ip,
'pId': parent_node.key,
'isParent': False,
'open': False,
'iconSkin': icon_skin,
'meta': {
'type': 'asset',
'asset': {
'id': self.id,
'hostname': self.hostname,
'ip': self.ip,
'port': self.port,
'platform': self.platform,
'protocol': self.protocol,
}
}
}
tree_node = TreeNode(**data)
return tree_node
class Meta:
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")

View File

@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache
from orgs.mixins import OrgManager
from .base import AssetUser
from ..const import ASSET_USER_CONN_CACHE_KEY
__all__ = ['AuthBook']
class AuthBookQuerySet(models.QuerySet):
def latest_version(self):
return self.filter(is_latest=True)
class AuthBookManager(OrgManager):
pass
class AuthBook(AssetUser):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
version = models.IntegerField(default=1, verbose_name=_('Version'))
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
class Meta:
verbose_name = _('AuthBook')
def _set_latest(self):
self._remove_pre_obj_latest()
self.is_latest = True
self.save()
def _get_pre_obj(self):
pre_obj = self.__class__.objects.filter(
username=self.username, asset=self.asset).latest_version().first()
return pre_obj
def _remove_pre_obj_latest(self):
pre_obj = self._get_pre_obj()
if pre_obj:
pre_obj.is_latest = False
pre_obj.save()
def _set_version(self):
pre_obj = self._get_pre_obj()
if pre_obj:
self.version = pre_obj.version + 1
else:
self.version = 1
self.save()
def set_version_and_latest(self):
self._set_version()
self._set_latest()
@property
def _conn_cache_key(self):
return ASSET_USER_CONN_CACHE_KEY.format(self.id, self.asset.id)
@property
def connectivity(self):
value = cache.get(self._conn_cache_key, self.UNKNOWN)
return value
@connectivity.setter
def connectivity(self, value):
_connectivity = self.UNKNOWN
for host in value.get('dark', {}).keys():
if host == self.asset.hostname:
_connectivity = self.UNREACHABLE
for host in value.get('contacted', {}).keys():
if host == self.asset.hostname:
_connectivity = self.REACHABLE
cache.set(self._conn_cache_key, _connectivity, 3600)
@property
def keyword(self):
return {'username': self.username, 'asset': self.asset}
def __str__(self):
return '{}@{}'.format(self.username, self.asset)

View File

@@ -9,13 +9,17 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen
from common.utils import (
get_signer, ssh_key_string_to_obj, ssh_key_gen, get_logger
)
from common.validators import alphanumeric
from orgs.mixins import OrgModelMixin
from .utils import private_key_validator
signer = get_signer()
logger = get_logger(__file__)
class AssetUser(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
@@ -29,6 +33,13 @@ class AssetUser(OrgModelMixin):
date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
UNREACHABLE, REACHABLE, UNKNOWN = range(0, 3)
CONNECTIVITY_CHOICES = (
(UNREACHABLE, _("Unreachable")),
(REACHABLE, _('Reachable')),
(UNKNOWN, _("Unknown")),
)
@property
def password(self):
if self._password:
@@ -38,8 +49,8 @@ class AssetUser(OrgModelMixin):
@password.setter
def password(self, password_raw):
raise AttributeError("Using set_auth do that")
# self._password = signer.sign(password_raw)
# raise AttributeError("Using set_auth do that")
self._password = signer.sign(password_raw)
@property
def private_key(self):
@@ -48,8 +59,8 @@ class AssetUser(OrgModelMixin):
@private_key.setter
def private_key(self, private_key_raw):
raise AttributeError("Using set_auth do that")
# self._private_key = signer.sign(private_key_raw)
# raise AttributeError("Using set_auth do that")
self._private_key = signer.sign(private_key_raw)
@property
def private_key_obj(self):
@@ -81,6 +92,11 @@ class AssetUser(OrgModelMixin):
else:
return None
@public_key.setter
def public_key(self, public_key_raw):
# raise AttributeError("Using set_auth do that")
self._public_key = signer.sign(public_key_raw)
@property
def public_key_obj(self):
if self.public_key:
@@ -108,6 +124,25 @@ class AssetUser(OrgModelMixin):
def get_auth(self, asset=None):
pass
def load_specific_asset_auth(self, asset):
from ..backends.multi import AssetUserManager
try:
other = AssetUserManager.get(username=self.username, asset=asset)
except Exception as e:
logger.error(e, exc_info=True)
else:
self._merge_auth(other)
def _merge_auth(self, other):
if not other:
return
if other.password:
self.password = other.password
if other.public_key:
self.public_key = other.public_key
if other.private_key:
self.private_key = other.private_key
def clear_auth(self):
self._password = ''
self._private_key = ''

View File

@@ -27,6 +27,9 @@ class CommandFilter(OrgModelMixin):
def __str__(self):
return self.name
class Meta:
verbose_name = _("Command filter")
class CommandFilterRule(OrgModelMixin):
TYPE_REGEX = 'regex'
@@ -58,6 +61,7 @@ class CommandFilterRule(OrgModelMixin):
class Meta:
ordering = ('-priority', 'action')
verbose_name = _("Command filter rule")
@property
def _pattern(self):

View File

@@ -40,15 +40,15 @@ class Domain(OrgModelMixin):
class Gateway(AssetUser):
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, 'rdp'),
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
)
ip = models.GenericIPAddressField(max_length=32, verbose_name=_('IP'), db_index=True)
port = models.IntegerField(default=22, verbose_name=_('Port'))
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=SSH_PROTOCOL, verbose_name=_("Protocol"))
protocol = models.CharField(choices=PROTOCOL_CHOICES, max_length=16, default=PROTOCOL_SSH, verbose_name=_("Protocol"))
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, verbose_name=_("Domain"))
comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Comment"))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
@@ -60,7 +60,9 @@ class Gateway(AssetUser):
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")
def test_connective(self):
def test_connective(self, local_port=None):
if local_port is None:
local_port = self.port
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
proxy = paramiko.SSHClient()
@@ -76,12 +78,11 @@ class Gateway(AssetUser):
paramiko.SSHException) as e:
return False, str(e)
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', self.port), ('127.0.0.1', 0)
)
try:
client.connect("127.0.0.1", port=self.port,
sock = proxy.get_transport().open_channel(
'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0)
)
client.connect("127.0.0.1", port=local_port,
username=self.username,
password=self.password,
key_filename=self.private_key_file,

View File

@@ -5,6 +5,7 @@ import uuid
from django.db import models, transaction
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.cache import cache
from orgs.mixins import OrgModelMixin
@@ -22,10 +23,13 @@ class Node(OrgModelMixin):
date_create = models.DateTimeField(auto_now_add=True)
is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}'
_assets_amount = None
_full_value_cache_key = '_NODE_VALUE_{}'
_assets_amount_cache_key = '_NODE_ASSETS_AMOUNT_{}'
class Meta:
verbose_name = _("Node")
ordering = ['key']
def __str__(self):
return self.full_value
@@ -49,30 +53,65 @@ class Node(OrgModelMixin):
def name(self):
return self.value
@property
def assets_amount(self):
"""
获取节点下所有资产数量速度太慢所以需要重写使用cache等方案
:return:
"""
if self._assets_amount is not None:
return self._assets_amount
cache_key = self._assets_amount_cache_key.format(self.key)
cached = cache.get(cache_key)
if cached is not None:
return cached
assets_amount = self.get_all_assets().count()
cache.set(cache_key, assets_amount, 3600)
return assets_amount
@assets_amount.setter
def assets_amount(self, value):
self._assets_amount = value
def expire_assets_amount(self):
ancestor_keys = self.get_ancestor_keys(with_self=True)
cache_keys = [self._assets_amount_cache_key.format(k) for k in ancestor_keys]
cache.delete_many(cache_keys)
@classmethod
def expire_nodes_assets_amount(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_assets_amount()
return
key = cls._assets_amount_cache_key.format('*')
cache.delete_pattern(key)
@property
def full_value(self):
key = self._full_value_cache_key_prefix.format(self.key)
key = self._full_value_cache_key.format(self.key)
cached = cache.get(key)
if cached:
return cached
value = self.get_full_value()
self.cache_full_value(value)
return value
def get_full_value(self):
# ancestor = [a.value for a in self.get_ancestor(with_self=True)]
if self.is_root():
return self.value
parent_full_value = self.parent.full_value
value = parent_full_value + ' / ' + self.value
key = self._full_value_cache_key.format(self.key)
cache.set(key, value, 3600)
return value
def cache_full_value(self, value):
key = self._full_value_cache_key_prefix.format(self.key)
cache.set(key, value, 3600)
def expire_full_value(self):
key = self._full_value_cache_key_prefix.format(self.key)
key = self._full_value_cache_key.format(self.key)
cache.delete_pattern(key+'*')
@classmethod
def expire_nodes_full_value(cls, nodes=None):
if nodes:
for node in nodes:
node.expire_full_value()
return
key = cls._full_value_cache_key.format('*')
cache.delete_pattern(key+'*')
@property
@@ -85,10 +124,21 @@ class Node(OrgModelMixin):
self.save()
return "{}:{}".format(self.key, mark)
def create_child(self, value):
def get_next_child_preset_name(self):
name = ugettext("New node")
values = [
child.value[child.value.rfind(' '):]
for child in self.get_children()
if child.value.startswith(name)
]
values = [int(value) for value in values if value.strip().isdigit()]
count = max(values) + 1 if values else 1
return '{} {}'.format(name, count)
def create_child(self, value, _id=None):
with transaction.atomic():
child_key = self.get_next_child_key()
child = self.__class__.objects.create(key=child_key, value=value)
child = self.__class__.objects.create(id=_id, key=child_key, value=value)
return child
def get_children(self, with_self=False):
@@ -98,7 +148,7 @@ class Node(OrgModelMixin):
)
def get_all_children(self, with_self=False):
pattern = r'^{0}$|^{0}:' if with_self else r'^{0}'
pattern = r'^{0}$|^{0}:' if with_self else r'^{0}:'
return self.__class__.objects.filter(
key__regex=pattern.format(self.key)
)
@@ -134,7 +184,7 @@ class Node(OrgModelMixin):
pattern = r'^{0}$|^{0}:'.format(self.key)
args = []
kwargs = {}
if self.is_default_node():
if self.is_root():
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
else:
kwargs['nodes__key__regex'] = pattern
@@ -182,17 +232,18 @@ class Node(OrgModelMixin):
child.save()
self.save()
def get_ancestor(self, with_self=False):
if self.is_root():
root = self.__class__.root()
return [root]
_key = self.key.split(':')
def get_ancestor_keys(self, with_self=False):
parent_keys = []
key_list = self.key.split(":")
if not with_self:
_key.pop()
ancestor_keys = []
for i in range(len(_key)):
ancestor_keys.append(':'.join(_key))
_key.pop()
key_list.pop()
for i in range(len(key_list)):
parent_keys.append(":".join(key_list))
key_list.pop()
return parent_keys
def get_ancestor(self, with_self=False):
ancestor_keys = self.get_ancestor_keys(with_self=with_self)
ancestor = self.__class__.objects.filter(
key__in=ancestor_keys
).order_by('key')
@@ -225,11 +276,28 @@ class Node(OrgModelMixin):
@classmethod
def default_node(cls):
defaults = {'value': 'Default'}
return cls.objects.get_or_create(defaults=defaults, key='1')
obj, created = cls.objects.get_or_create(defaults=defaults, key='1')
return obj
@classmethod
def get_tree_name_ref(cls):
pass
def as_tree_node(self):
from common.tree import TreeNode
from ..serializers import NodeSerializer
name = '{} ({})'.format(self.value, self.assets_amount)
node_serializer = NodeSerializer(instance=self)
data = {
'id': self.key,
'name': name,
'title': name,
'pId': self.parent_key,
'isParent': True,
'open': self.is_root(),
'meta': {
'node': node_serializer.data,
'type': 'node'
}
}
tree_node = TreeNode(**data)
return tree_node
@classmethod
def generate_fake(cls, count=100):

View File

@@ -14,7 +14,7 @@ from ..const import SYSTEM_USER_CONN_CACHE_KEY
from .base import AssetUser
__all__ = ['AdminUser', 'SystemUser',]
__all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__)
signer = get_signer()
@@ -31,6 +31,7 @@ class AdminUser(AssetUser):
become_method = models.CharField(choices=BECOME_METHOD_CHOICES, default='sudo', max_length=4)
become_user = models.CharField(default='root', max_length=64)
_become_pass = models.CharField(default='', max_length=128)
CONNECTIVE_CACHE_KEY = '_JMS_ADMIN_USER_CONNECTIVE_{}'
def __str__(self):
return self.name
@@ -67,6 +68,23 @@ class AdminUser(AssetUser):
def assets_amount(self):
return self.get_related_assets().count()
@property
def connectivity(self):
from .asset import Asset
assets = self.get_related_assets().values_list('id', 'hostname', flat=True)
data = {
'unreachable': [],
'reachable': [],
}
for asset_id, hostname in assets:
key = Asset.CONNECTIVITY_CACHE_KEY.format(str(self.id))
value = cache.get(key, Asset.UNKNOWN)
if value == Asset.REACHABLE:
data['reachable'].append(hostname)
elif value == Asset.UNREACHABLE:
data['unreachable'].append(hostname)
return data
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
@@ -94,34 +112,36 @@ class AdminUser(AssetUser):
class SystemUser(AssetUser):
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
TELNET_PROTOCOL = 'telnet'
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet'
PROTOCOL_VNC = 'vnc'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'),
(PROTOCOL_SSH, 'ssh'),
(PROTOCOL_RDP, 'rdp'),
(PROTOCOL_TELNET, 'telnet (beta)'),
(PROTOCOL_VNC, 'vnc'),
)
AUTO_LOGIN = 'auto'
MANUAL_LOGIN = 'manual'
LOGIN_AUTO = 'auto'
LOGIN_MANUAL = 'manual'
LOGIN_MODE_CHOICES = (
(AUTO_LOGIN, _('Automatic login')),
(MANUAL_LOGIN, _('Manually login'))
(LOGIN_AUTO, _('Automatic login')),
(LOGIN_MANUAL, _('Manually login'))
)
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
priority = models.IntegerField(default=20, verbose_name=_("Priority"),
validators=[MinValueValidator(1), MaxValueValidator(100)])
priority = models.IntegerField(default=20, verbose_name=_("Priority"), validators=[MinValueValidator(1), MaxValueValidator(100)])
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
sudo = models.TextField(default='/bin/whoami', verbose_name=_('Sudo'))
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=AUTO_LOGIN, max_length=10, verbose_name=_('Login mode'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
cache_key = "__SYSTEM_USER_CACHED_{}"
SYSTEM_USER_CACHE_KEY = "__SYSTEM_USER_CACHED_{}"
CONNECTIVE_CACHE_KEY = '_JMS_SYSTEM_USER_CONNECTIVE_{}'
def __str__(self):
return '{0.name}({0.username})'.format(self)
@@ -136,34 +156,61 @@ class SystemUser(AssetUser):
'auto_push': self.auto_push,
}
def get_assets(self):
def get_related_assets(self):
assets = set(self.assets.all())
return assets
@property
def assets_connective(self):
_result = cache.get(SYSTEM_USER_CONN_CACHE_KEY.format(self.name), {})
return _result
def connectivity(self):
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
value = cache.get(cache_key, None)
if not value or 'unreachable' not in value:
return {'unreachable': [], 'reachable': []}
else:
return value
@connectivity.setter
def connectivity(self, value):
data = self.connectivity
unreachable = data['unreachable']
reachable = data['reachable']
for host in value.get('dark', {}).keys():
if host not in unreachable:
unreachable.append(host)
if host in reachable:
reachable.remove(host)
for host in value.get('contacted'):
if host not in reachable:
reachable.append(host)
if host in unreachable:
unreachable.remove(host)
cache_key = self.CONNECTIVE_CACHE_KEY.format(str(self.id))
cache.set(cache_key, data, 3600)
@property
def unreachable_assets(self):
return list(self.assets_connective.get('dark', {}).keys())
def assets_unreachable(self):
return self.connectivity.get('unreachable')
@property
def reachable_assets(self):
return self.assets_connective.get('contacted', [])
def assets_reachable(self):
return self.connectivity.get('reachable')
@property
def login_mode_display(self):
return self.get_login_mode_display()
def is_need_push(self):
if self.auto_push and self.protocol == self.__class__.SSH_PROTOCOL:
if self.auto_push and self.protocol == self.PROTOCOL_SSH:
return True
else:
return False
def set_cache(self):
cache.set(self.cache_key.format(self.id), self, 3600)
cache.set(self.SYSTEM_USER_CACHE_KEY.format(self.id), self, 3600)
def expire_cache(self):
cache.delete(self.cache_key.format(self.id))
cache.delete(self.SYSTEM_USER_CACHE_KEY.format(self.id))
@property
def cmd_filter_rules(self):
@@ -184,7 +231,7 @@ class SystemUser(AssetUser):
@classmethod
def get_system_user_by_id_or_cached(cls, sid):
cached = cache.get(cls.cache_key.format(sid))
cached = cache.get(cls.SYSTEM_USER_CACHE_KEY.format(sid))
if cached:
return cached
try:

View File

@@ -8,3 +8,4 @@ from .system_user import *
from .node import *
from .domain import *
from .cmd_filter import *
from .asset_user import *

View File

@@ -3,6 +3,8 @@
from django.core.cache import cache
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from ..models import Node, AdminUser
from ..const import ADMIN_USER_CONN_CACHE_KEY
@@ -18,6 +20,7 @@ class AdminUserSerializer(serializers.ModelSerializer):
reachable_amount = serializers.SerializerMethodField()
class Meta:
list_serializer_class = AdaptedBulkListSerializer
model = AdminUser
fields = '__all__'
@@ -58,7 +61,7 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
管理用户更新关联到的集群
"""
nodes = serializers.PrimaryKeyRelatedField(
many=True, queryset = Node.objects.all()
many=True, queryset=Node.objects.all()
)
class Meta:
@@ -66,4 +69,5 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
fields = ['id', 'nodes']
class TaskIDSerializer(serializers.Serializer):
task = serializers.CharField(read_only=True)

View File

@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
from ..models import Asset
from .system_user import AssetSystemUserSerializer
__all__ = [
'AssetSerializer', 'AssetGrantedSerializer', 'MyAssetGrantedSerializer',
'AssetAsNodeSerializer', 'AssetSimpleSerializer',
]
@@ -18,18 +19,31 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
"""
class Meta:
model = Asset
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
validators = []
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('labels', 'nodes')\
.select_related('admin_user')
return queryset
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend([
'hardware_info', 'is_connective', 'org_name'
'hardware_info', 'connectivity', 'org_name'
])
return fields
class AssetAsNodeSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'port', 'platform', 'protocol']
class AssetGrantedSerializer(serializers.ModelSerializer):
"""
被授权资产的数据结构
@@ -64,3 +78,9 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer):
"is_active", "system_users_join", "org_name",
"os", "platform", "comment", "org_id", "protocol"
)
class AssetSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'port', 'ip', 'connectivity']

View File

@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext as _
from rest_framework import serializers
from ..models import AuthBook
from ..backends.multi import AssetUserManager
__all__ = [
'AssetUserSerializer', 'AssetUserAuthInfoSerializer',
]
class AssetUserSerializer(serializers.ModelSerializer):
password = serializers.CharField(
max_length=256, allow_blank=True, allow_null=True, write_only=True,
required=False, help_text=_('Password')
)
public_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
required=False, help_text=_('Public key')
)
private_key = serializers.CharField(
max_length=4096, allow_blank=True, allow_null=True, write_only=True,
required=False, help_text=_('Private key')
)
class Meta:
model = AuthBook
read_only_fields = (
'date_created', 'date_updated', 'created_by',
'is_latest', 'version', 'connectivity',
)
fields = '__all__'
extra_kwargs = {
'username': {'required': True}
}
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields = [f for f in fields if not f.startswith('_') and f != 'id']
fields.extend(['connectivity'])
return fields
def create(self, validated_data):
kwargs = {
'name': validated_data.get('name'),
'username': validated_data.get('username'),
'asset': validated_data.get('asset'),
'comment': validated_data.get('comment', ''),
'org_id': validated_data.get('org_id', ''),
'password': validated_data.get('password'),
'public_key': validated_data.get('public_key'),
'private_key': validated_data.get('private_key')
}
instance = AssetUserManager.create(**kwargs)
return instance
class AssetUserAuthInfoSerializer(serializers.ModelSerializer):
class Meta:
model = AuthBook
fields = ['password', 'private_key', 'public_key']

View File

@@ -3,6 +3,7 @@
from rest_framework import serializers
from common.fields import ChoiceDisplayField
from common.serializers import AdaptedBulkListSerializer
from ..models import CommandFilter, CommandFilterRule, SystemUser
@@ -12,6 +13,7 @@ class CommandFilterSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilter
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
@@ -21,3 +23,4 @@ class CommandFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterRule
fields = '__all__'
list_serializer_class = AdaptedBulkListSerializer

View File

@@ -2,6 +2,8 @@
#
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from ..models import Domain, Gateway
@@ -12,6 +14,7 @@ class DomainSerializer(serializers.ModelSerializer):
class Meta:
model = Domain
fields = '__all__'
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
def get_asset_count(obj):
@@ -25,6 +28,7 @@ class DomainSerializer(serializers.ModelSerializer):
class GatewaySerializer(serializers.ModelSerializer):
class Meta:
model = Gateway
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'ip', 'port', 'protocol', 'username',
'domain', 'is_active', 'date_created', 'date_updated',

View File

@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.serializers import AdaptedBulkListSerializer
from ..models import Label
@@ -12,7 +13,7 @@ class LabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
fields = '__all__'
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
def get_asset_count(obj):

View File

@@ -1,91 +1,37 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from ..models import Asset, Node
from .asset import AssetGrantedSerializer
__all__ = [
'NodeSerializer', "NodeGrantedSerializer", "NodeAddChildrenSerializer",
'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer",
]
class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
"""
授权资产组
"""
assets_granted = AssetGrantedSerializer(many=True, read_only=True)
assets_amount = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()
name = serializers.SerializerMethodField()
class Meta:
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
def get_assets_amount(obj):
return len(obj.assets_granted)
@staticmethod
def get_name(obj):
return obj.name
@staticmethod
def get_parent(obj):
return obj.parent.id
class NodeSerializer(serializers.ModelSerializer):
assets_amount = serializers.SerializerMethodField()
tree_id = serializers.SerializerMethodField()
tree_parent = serializers.SerializerMethodField()
assets_amount = serializers.IntegerField(read_only=True)
class Meta:
model = Node
fields = [
'id', 'key', 'value', 'assets_amount',
'is_node', 'org_id', 'tree_id', 'tree_parent',
'id', 'key', 'value', 'assets_amount', 'org_id',
]
read_only_fields = [
'key', 'assets_amount', 'org_id',
]
list_serializer_class = BulkListSerializer
def validate(self, data):
value = data.get('value')
def validate_value(self, data):
instance = self.instance if self.instance else Node.root()
children = instance.parent.get_children().exclude(key=instance.key)
values = [child.value for child in children]
if value in values:
if data in values:
raise serializers.ValidationError(
'The same level node name cannot be the same'
)
return data
@staticmethod
def get_assets_amount(obj):
if hasattr(obj, 'assets_amount'):
return obj.assets_amount
return obj.get_all_assets().count()
@staticmethod
def get_tree_id(obj):
return obj.key
@staticmethod
def get_tree_parent(obj):
return obj.parent_key
def get_fields(self):
fields = super().get_fields()
field = fields["key"]
field.required = False
return fields
class NodeAssetsSerializer(serializers.ModelSerializer):
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
@@ -97,3 +43,4 @@ class NodeAssetsSerializer(serializers.ModelSerializer):
class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField()

View File

@@ -1,6 +1,8 @@
from rest_framework import serializers
from ..models import SystemUser
from common.serializers import AdaptedBulkListSerializer
from ..models import SystemUser, Asset
from .base import AuthSerializer
@@ -17,21 +19,22 @@ class SystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
exclude = ('_password', '_private_key', '_public_key')
list_serializer_class = AdaptedBulkListSerializer
def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
fields.extend([
'get_login_mode_display',
'login_mode_display',
])
return fields
@staticmethod
def get_unreachable_assets(obj):
return obj.unreachable_assets
return obj.assets_unreachable
@staticmethod
def get_reachable_assets(obj):
return obj.reachable_assets
return obj.assets_reachable
def get_unreachable_amount(self, obj):
return len(self.get_unreachable_assets(obj))
@@ -41,7 +44,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
@staticmethod
def get_assets_amount(obj):
return len(obj.get_assets())
return len(obj.get_related_assets())
class SystemUserAuthSerializer(AuthSerializer):
@@ -61,13 +64,19 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
"""
查看授权的资产系统用户的数据结构这个和AssetSerializer不同字段少
"""
actions = serializers.SerializerMethodField()
class Meta:
model = SystemUser
fields = (
'id', 'name', 'username', 'priority',
'protocol', 'comment', 'login_mode'
'protocol', 'comment', 'login_mode', 'actions',
)
@staticmethod
def get_actions(obj):
return [action.name for action in obj.actions]
class SystemUserSimpleSerializer(serializers.ModelSerializer):
"""
@@ -75,4 +84,7 @@ class SystemUserSimpleSerializer(serializers.ModelSerializer):
"""
class Meta:
model = SystemUser
fields = ('id', 'name', 'username')
fields = ('id', 'name', 'username')

View File

@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import Signal
on_app_ready = Signal()

View File

@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.db.models.signals import post_save, m2m_changed
from django.db.models.signals import post_save, m2m_changed, post_delete
from django.dispatch import receiver
from common.utils import get_logger
from .models import Asset, SystemUser, Node
from .tasks import update_assets_hardware_info_util, \
test_asset_connectability_util, push_system_user_to_assets
from .models import Asset, SystemUser, Node, AuthBook
from .tasks import (
update_assets_hardware_info_util,
test_asset_connectivity_util,
push_system_user_to_assets
)
logger = get_logger(__file__)
@@ -19,8 +22,8 @@ def update_asset_hardware_info_on_created(asset):
def test_asset_conn_on_created(asset):
logger.debug("Test asset `{}` connectability".format(asset))
test_asset_connectability_util.delay([asset])
logger.debug("Test asset `{}` connectivity".format(asset))
test_asset_connectivity_util.delay([asset])
def set_asset_root_node(asset):
@@ -35,6 +38,17 @@ def on_asset_created_or_update(sender, instance=None, created=False, **kwargs):
update_asset_hardware_info_on_created(instance)
test_asset_conn_on_created(instance)
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_delete, sender=Asset, dispatch_uid="my_unique_identifier")
def on_asset_delete(sender, instance=None, **kwargs):
# 过期节点资产数量
nodes = instance.nodes.all()
Node.expire_nodes_assets_amount(nodes)
@receiver(post_save, sender=SystemUser, dispatch_uid="my_unique_identifier")
def on_system_user_update(sender, instance=None, created=True, **kwargs):
@@ -63,10 +77,14 @@ def on_system_user_assets_change(sender, instance=None, **kwargs):
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_node_changed(sender, instance=None, **kwargs):
logger.debug("Asset nodes change signal received")
if isinstance(instance, Asset):
if kwargs['action'] == 'post_add':
logger.debug("Asset node change signal received")
if kwargs['action'] == 'pre_remove':
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
Node.expire_nodes_assets_amount(nodes)
if kwargs['action'] == 'post_add':
nodes = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
Node.expire_nodes_assets_amount(nodes)
system_users_assets = defaultdict(set)
system_users = SystemUser.objects.filter(nodes__in=nodes)
# 清理节点缓存
@@ -79,9 +97,11 @@ def on_asset_node_changed(sender, instance=None, **kwargs):
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_node_assets_changed(sender, instance=None, **kwargs):
if isinstance(instance, Node):
logger.debug("Node assets change signal {} received".format(instance))
# 当节点和资产关系发生改变时,过期资产数量缓存
instance.expire_assets_amount()
assets = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
if kwargs['action'] == 'post_add':
logger.debug("Node assets change signal received")
# 重新关联系统用户和资产的关系
system_users = SystemUser.objects.filter(nodes=instance)
for system_user in system_users:
@@ -92,3 +112,10 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
def on_node_update_or_created(sender, instance=None, created=False, **kwargs):
if instance and not created:
instance.expire_full_value()
@receiver(post_save, sender=AuthBook)
def on_auth_book_created(sender, instance=None, created=False, **kwargs):
if created:
logger.debug('Receive create auth book object signal.')
instance.set_version_and_latest()

View File

@@ -4,15 +4,15 @@ import re
import os
from celery import shared_task
from ops.celery import app as celery_app
from django.core.cache import cache
from django.utils.translation import ugettext as _
from django.core.cache import cache
from common.utils import capacity_convert, \
sum_capacity, encrypt_password, get_logger
from ops.celery.utils import register_as_period_task, after_app_shutdown_clean, \
after_app_ready_start
from orgs.utils import set_to_root_org
from common.utils import (
capacity_convert, sum_capacity, encrypt_password, get_logger
)
from ops.celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic
)
from .models import SystemUser, AdminUser, Asset
from . import const
@@ -23,7 +23,30 @@ TIMEOUT = 60
logger = get_logger(__file__)
CACHE_MAX_TIME = 60*60*2
disk_pattern = re.compile(r'^hd|sd|xvd|vd')
PERIOD_TASK = os.environ.get("PERIOD_TASK", "off")
PERIOD_TASK = os.environ.get("PERIOD_TASK", "on")
def check_asset_can_run_ansible(asset):
if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
return False
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
return False
return True
def clean_hosts(assets):
clean_assets = []
for asset in assets:
if not check_asset_can_run_ansible(asset):
continue
clean_assets.append(asset)
if not clean_assets:
print(_("No assets matched, stop task"))
return clean_assets
@shared_task
@@ -60,9 +83,12 @@ def set_assets_hardware_info(assets, result, **kwargs):
___cpu_model = 'Unknown'
___cpu_model = ___cpu_model[:64]
___cpu_count = info.get('ansible_processor_count', 0)
___cpu_cores = info.get('ansible_processor_cores', None) or len(info.get('ansible_processor', []))
___cpu_cores = info.get('ansible_processor_cores', None) or \
len(info.get('ansible_processor', []))
___cpu_vcpus = info.get('ansible_processor_vcpus', 0)
___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb')))
___memory = '%s %s' % capacity_convert(
'{} MB'.format(info.get('ansible_memtotal_mb'))
)
disk_info = {}
for dev, dev_info in info.get('ansible_devices', {}).items():
if disk_pattern.match(dev) and dev_info['removable'] == '0':
@@ -96,19 +122,8 @@ def update_assets_hardware_info_util(assets, task_name=None):
if task_name is None:
task_name = _("Update some assets hardware info")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hosts = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
hosts = clean_hosts(assets)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {}
created_by = str(assets[0].org_id)
task, created = update_or_create_ansible_task(
@@ -116,8 +131,6 @@ def update_assets_hardware_info_util(assets, task_name=None):
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
)
result = task.run()
# Todo: may be somewhere using
# Manual run callback function
set_assets_hardware_info(assets, result)
return result
@@ -125,8 +138,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task
def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info: {}").format(asset.hostname)
# task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util(
update_assets_hardware_info_util(
[asset], task_name=task_name
)
@@ -141,123 +153,18 @@ def update_assets_hardware_info_period():
logger.debug("Period task disabled, update assets hardware info pass")
return
from ops.utils import update_or_create_ansible_task
from orgs.models import Organization
orgs = Organization.objects.all().values_list('id', flat=True)
orgs.append('')
task_name = _("Update assets hardware info period")
# for org_id in orgs:
# org_id = str(org_id)
# hostname_list = [
# asset for asset in Asset.objects.all()
# if asset.is_active and asset.is_unixlike()
# ]
# tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
#
# # Only create, schedule by celery beat
# update_or_create_ansible_task(
# task_name, hosts=hostname_list, tasks=tasks, pattern='all',
# options=const.TASK_OPTIONS, run_as_admin=True, created_by='System',
# interval=60*60*24, is_periodic=True, callback=set_assets_hardware_info.name,
# )
## ADMIN USER CONNECTIVE ##
def set_admin_user_connectability_info(result, **kwargs):
admin_user = kwargs.get("admin_user")
task_name = kwargs.get("task_name")
if admin_user is None and task_name is not None:
admin_user = task_name.split(":")[-1]
raw, summary = result
cache_key = const.ADMIN_USER_CONN_CACHE_KEY.format(admin_user)
cache.set(cache_key, summary, CACHE_MAX_TIME)
for i in summary.get('contacted', []):
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
cache.set(asset_conn_cache_key, 1, CACHE_MAX_TIME)
for i, msg in summary.get('dark', {}).items():
asset_conn_cache_key = const.ASSET_ADMIN_CONN_CACHE_KEY.format(i)
cache.set(asset_conn_cache_key, 0, CACHE_MAX_TIME)
logger.error(msg)
@shared_task
def test_admin_user_connectability_util(admin_user, task_name):
"""
Test asset admin user can connect or not. Using ansible api do that
:param admin_user:
:param task_name:
:return:
"""
from ops.utils import update_or_create_ansible_task
assets = admin_user.get_related_assets()
hosts = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skipped: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skipped: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True, created_by=admin_user.org_id,
)
result = task.run()
set_admin_user_connectability_info(result, admin_user=admin_user.name)
return result
@shared_task
@register_as_period_task(interval=3600)
def test_admin_user_connectability_period():
"""
A period task that update the ansible task period
"""
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
task_name = _("Test admin user connectability period: {}").format(admin_user.name)
test_admin_user_connectability_util(admin_user, task_name)
@shared_task
def test_admin_user_connectability_manual(admin_user):
task_name = _("Test admin user connectability: {}").format(admin_user.name)
# task_name = _("测试管理行号可连接性: {}").format(admin_user.name)
return test_admin_user_connectability_util(admin_user, task_name)
@shared_task
def test_asset_connectability_util(assets, task_name=None):
def test_asset_connectivity_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = _("Test assets connectability")
# task_name = _("测试资产可连接性")
hosts = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
task_name = _("Test assets connectivity")
hosts = clean_hosts(assets)
if not hosts:
logger.info(_("No assets, task stop"))
return {}
tasks = const.TEST_ADMIN_USER_CONN_TASKS
created_by = assets[0].org_id
@@ -267,18 +174,20 @@ def test_asset_connectability_util(assets, task_name=None):
)
result = task.run()
summary = result[1]
for k in summary.get('dark'):
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 0, CACHE_MAX_TIME)
for k in summary.get('contacted'):
cache.set(const.ASSET_ADMIN_CONN_CACHE_KEY.format(k), 1, CACHE_MAX_TIME)
for asset in assets:
if asset.hostname in summary.get('dark', {}):
asset.connectivity = asset.UNREACHABLE
elif asset.hostname in summary.get('contacted', []):
asset.connectivity = asset.REACHABLE
else:
asset.connectivity = asset.UNKNOWN
return summary
@shared_task
def test_asset_connectability_manual(asset):
task_name = _("Test assets connectability: {}").format(asset)
summary = test_asset_connectability_util([asset], task_name=task_name)
def test_asset_connectivity_manual(asset):
task_name = _("Test assets connectivity: {}").format(asset)
summary = test_asset_connectivity_util([asset], task_name=task_name)
if summary.get('dark'):
return False, summary['dark']
@@ -286,21 +195,61 @@ def test_asset_connectability_manual(asset):
return True, ""
@shared_task
def test_admin_user_connectivity_util(admin_user, task_name):
"""
Test asset admin user can connect or not. Using ansible api do that
:param admin_user:
:param task_name:
:return:
"""
assets = admin_user.get_related_assets()
hosts = clean_hosts(assets)
if not hosts:
return {}
summary = test_asset_connectivity_util(hosts, task_name)
return summary
@shared_task
@register_as_period_task(interval=3600)
def test_admin_user_connectivity_period():
"""
A period task that update the ansible task period
"""
if PERIOD_TASK != "on":
logger.debug('Period task off, skip')
return
key = '_JMS_TEST_ADMIN_USER_CONNECTIVITY_PERIOD'
prev_execute_time = cache.get(key)
if prev_execute_time:
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
return
cache.set(key, 1, 60*40)
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
task_name = _("Test admin user connectivity period: {}").format(admin_user.name)
test_admin_user_connectivity_util(admin_user, task_name)
cache.set(key, 1, 60*40)
@shared_task
def test_admin_user_connectivity_manual(admin_user):
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
test_admin_user_connectivity_util(admin_user, task_name)
return True
## System user connective ##
@shared_task
def set_system_user_connectablity_info(result, **kwargs):
def set_system_user_connectivity_info(system_user, result):
summary = result[1]
task_name = kwargs.get("task_name")
system_user = kwargs.get("system_user")
if system_user is None:
system_user = task_name.split(":")[-1]
cache_key = const.SYSTEM_USER_CONN_CACHE_KEY.format(str(system_user.id))
cache.set(cache_key, summary, CACHE_MAX_TIME)
system_user.connectivity = summary
@shared_task
def test_system_user_connectability_util(system_user, assets, task_name):
def test_system_user_connectivity_util(system_user, assets, task_name):
"""
Test system cant connect his assets or not.
:param system_user:
@@ -309,56 +258,45 @@ def test_system_user_connectability_util(system_user, assets, task_name):
:return:
"""
from ops.utils import update_or_create_ansible_task
hosts = []
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
hosts = clean_hosts(assets)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {}
task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS,
run_as=system_user, created_by=system_user.org_id,
run_as=system_user.username, created_by=system_user.org_id,
)
result = task.run()
set_system_user_connectablity_info(result, system_user=system_user)
set_system_user_connectivity_info(system_user, result)
return result
@shared_task
def test_system_user_connectability_manual(system_user):
task_name = _("Test system user connectability: {}").format(system_user)
assets = system_user.get_assets()
return test_system_user_connectability_util(system_user, assets, task_name)
def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectivity: {}").format(system_user)
assets = system_user.get_related_assets()
return test_system_user_connectivity_util(system_user, assets, task_name)
@shared_task
def test_system_user_connectability_a_asset(system_user, asset):
task_name = _("Test system user connectability: {} => {}").format(
def test_system_user_connectivity_a_asset(system_user, asset):
task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset
)
return test_system_user_connectability_util(system_user, [asset], task_name)
return test_system_user_connectivity_util(system_user, [asset], task_name)
@shared_task
def test_system_user_connectability_period():
def test_system_user_connectivity_period():
if PERIOD_TASK != "on":
logger.debug("Period task disabled, test system user connectability pass")
logger.debug("Period task disabled, test system user connectivity pass")
return
system_users = SystemUser.objects.all()
for system_user in system_users:
task_name = _("Test system user connectability period: {}").format(system_user)
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
test_system_user_connectability_util(system_user, task_name)
task_name = _("Test system user connectivity period: {}").format(system_user)
assets = system_user.get_related_assets()
test_system_user_connectivity_util(system_user, assets, task_name)
#### Push system user tasks ####
@@ -380,6 +318,24 @@ def get_push_system_user_tasks(system_user):
),
}
})
tasks.extend([
{
'name': 'Check home dir exists',
'action': {
'module': 'stat',
'args': 'path=/home/{}'.format(system_user.username)
},
'register': 'home_existed'
},
{
'name': "Set home dir permission",
'action': {
'module': 'file',
'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username)
},
'when': 'home_existed.stat.exists == true'
}
])
if system_user.public_key:
tasks.append({
'name': 'Set {} authorized key'.format(system_user.username),
@@ -391,6 +347,12 @@ def get_push_system_user_tasks(system_user):
}
})
if system_user.sudo:
sudo = system_user.sudo.replace('\r\n', '\n').replace('\r', '\n')
sudo_list = sudo.split('\n')
sudo_tmp = []
for s in sudo_list:
sudo_tmp.append(s.strip(','))
sudo = ','.join(sudo_tmp)
tasks.append({
'name': 'Set {} sudo setting'.format(system_user.username),
'action': {
@@ -398,8 +360,7 @@ def get_push_system_user_tasks(system_user):
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
"validate='visudo -cf %s'".format(
system_user.username,
system_user.sudo,
system_user.username, sudo,
)
}
})
@@ -415,32 +376,23 @@ def push_system_user_util(system_user, assets, task_name):
logger.info(msg)
return
tasks = get_push_system_user_tasks(system_user)
hosts = []
for asset in assets:
if not asset.is_active:
msg = _("Asset has been disabled, skip: {}").format(asset)
logger.info(msg)
continue
if not asset.support_ansible():
msg = _("Asset may not be support ansible, skip: {}").format(asset)
logger.info(msg)
continue
hosts.append(asset)
hosts = clean_hosts(assets)
if not hosts:
logger.info(_("No assets matched, stop task"))
return {}
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=hosts, tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True,
created_by=system_user.org_id,
)
return task.run()
for host in hosts:
system_user.load_specific_asset_auth(host)
tasks = get_push_system_user_tasks(system_user)
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=[host], tasks=tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True,
created_by=system_user.org_id,
)
task.run()
@shared_task
def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets()
assets = system_user.get_related_assets()
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name=task_name)
@@ -459,10 +411,59 @@ def push_system_user_to_assets(system_user, assets):
return push_system_user_util(system_user, assets, task_name)
@shared_task
@after_app_shutdown_clean_periodic
def test_system_user_connectability_period():
pass
@shared_task
@after_app_shutdown_clean_periodic
def test_admin_user_connectability_period():
pass
@shared_task
def set_asset_user_connectivity_info(asset_user, result):
summary = result[1]
asset_user.connectivity = summary
@shared_task
def test_asset_user_connectivity_util(asset_user, task_name):
"""
:param asset_user: <AuthBook>对象
:param task_name:
:return:
"""
from ops.utils import update_or_create_ansible_task
tasks = const.TEST_ASSET_USER_CONN_TASKS
if not check_asset_can_run_ansible(asset_user.asset):
return
task, created = update_or_create_ansible_task(
task_name, hosts=[asset_user.asset], tasks=tasks, pattern='all',
options=const.TASK_OPTIONS,
run_as=asset_user.username, created_by=asset_user.org_id
)
result = task.run()
set_asset_user_connectivity_info(asset_user, result)
@shared_task
def test_asset_users_connectivity_manual(asset_users):
"""
:param asset_users: <AuthBook>对象
"""
for asset_user in asset_users:
task_name = _("Test asset user connectivity: {}").format(asset_user)
test_asset_user_connectivity_util(asset_user, task_name)
# @shared_task
# @register_as_period_task(interval=3600)
# @after_app_ready_start
# # @after_app_shutdown_clean
# @after_app_shutdown_clean_periodic
# def push_system_user_period():
# for system_user in SystemUser.objects.all():
# push_system_user_related_nodes(system_user)

View File

@@ -57,6 +57,10 @@
<script>
var zTree2, asset_table2 = 0;
function initTable2() {
if(asset_table2){
return
}
var options = {
ele: $('#asset_list_modal_table'),
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
@@ -69,16 +73,16 @@ function initTable2() {
return asset_table2
}
function onSelected2(event, treeNode) {
function onNodeSelected2(event, treeNode) {
var url = asset_table2.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id);
setCookie('node_selected', treeNode.id);
url = setUrlParam(url, "node_id", treeNode.meta.node.id);
asset_table2.ajax.url(url);
asset_table2.ajax.reload();
}
function initTree2() {
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
var setting = {
view: {
dblClickExpand: false,
@@ -89,33 +93,22 @@ function initTree2() {
enable: true
}
},
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
callback: {
onSelected: onSelected2
onSelected: onNodeSelected2
}
};
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
value["pId"] = value["tree_parent"];
{#value["open"] = true;#}
if (value["key"] === "0") {
value["open"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
});
zNodes = data;
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
var root = zTree2.getNodes()[0];
zTree2.expandNode(root);
});
zTree2 = $.fn.zTree.init($("#assetTree2"), setting);
}
$(document).ready(function(){
}).on('show.bs.modal', function () {
initTable2();
initTree2();
})

View File

@@ -0,0 +1,28 @@
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}asset_user_auth_modal{% endblock %}
{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %}
{% block modal_body %}
<form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_modal_confirm').trigger('click'); return false;}">
{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Hostname" %}</label>
<div class="col-sm-10">
<p class="form-control-static" id="id_hostname_p"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Username" %}</label>
<div class="col-sm-10">
<p class="form-control-static" id="id_username_p"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Password" %}</label>
<div class="col-sm-10">
<input class="form-control" id="id_password" type="password" name="password" placeholder="{% trans 'Please input password' %}"/>
</div>
</div>
</form>
{% endblock %}
{% block modal_confirm_id %}btn_asset_user_auth_modal_confirm{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}gateway_test{% endblock %}
{% block modal_title%}{% trans "Test gateway test connection" %}{% endblock %}
{% block modal_body %}
{% load bootstrap3 %}
<form method="post" class="form-horizontal" action="" id="test_gateway_form" style="padding-top: 10px">
<div class="form-group">
<input id="gateway_id" name="gateway_id" hidden>
<label for="port" class="col-sm-2 control-label">{% trans 'SSH Port' %}</label>
<div class="col-sm-9" id="select2-container">
<input id="ssh_test_port" name="port" class="form-control">
<span class="help-block">{% trans 'If use nat, set the ssh real port' %}</span>
</div>
</div>
</form>
{% endblock %}
{% block modal_confirm_id %}btn_gateway_test{% endblock %}

View File

@@ -103,15 +103,17 @@ var need_change_field_login_mode = [
];
function protocolChange() {
if ($(protocol_id + " option:selected").text() === 'rdp') {
var protocol = $(protocol_id + " option:selected").text();
if (protocol === 'rdp' || protocol === 'vnc') {
$('.auth-fields').removeClass('hidden');
$('#command-filter-block').addClass('hidden');
$.each(need_change_field, function (index, value) {
$(value).closest('.form-group').addClass('hidden')
});
}
else if ($(protocol_id + " option:selected").text() === 'telnet (beta)') {
else if (protocol === 'telnet (beta)') {
$('.auth-fields').removeClass('hidden');
$('#command-filter-block').removeClass('hidden');
$.each(need_change_field, function (index, value) {
$(value).closest('.form-group').addClass('hidden')
});
@@ -123,6 +125,7 @@ function protocolChange() {
return
}
authFieldsDisplay();
$('#command-filter-block').removeClass('hidden');
$.each(need_change_field, function (index, value) {
$(value).closest('.form-group').removeClass('hidden')
});

View File

@@ -45,13 +45,11 @@
<table class="table table-striped table-bordered table-hover" id="asset_list_table">
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th>{% trans 'Hostname' %}</th>
<th>{% trans 'IP' %}</th>
<th>{% trans 'Port' %}</th>
<th>{% trans 'Reachable' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
@@ -86,39 +84,76 @@
</div>
</div>
</div>
{% include 'assets/_asset_user_auth_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var reachable = {{ admin_user.REACHABLE }};
var unreachable = {{ admin_user.UNREACHABLE }};
var options = {
ele: $('#asset_list_table'),
buttons: [],
order: [],
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{targets: 0, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 4, createdCell: function (td, cellData) {
if (!cellData) {
{targets: 3, createdCell: function (td, cellData) {
if (cellData === unreachable) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
} else if (cellData === reachable) {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
}}],
ajax_url: '{% url "api-assets:asset-list" %}?admin_user_id={{ admin_user.id }}',
} else {
$(td).html('')
}
}},
{targets: 4, createdCell: function (td, cellData, rowData) {
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
$(td).html(test_btn + update_auth_btn);
}}
],
ajax_url: '{% url "api-assets:admin-user-assets" pk=admin_user.id %}',
columns: [
{data: function(){return ""}}, {data: "hostname" }, {data: "ip" },
{data: "port" }, {data: "is_connective" }],
{data: "hostname" }, {data: "ip" },
{data: "port" }, {data: "connectivity" }, {data: "id"}],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
function initAssetUserAuthModalForm(hostname, username){
$('#id_hostname_p').html(hostname);
$('#id_username_p').html(username);
$('#id_password').parent().removeClass('has-error');
$('#id_password').val('');
}
var assetId ;
$(document).ready(function () {
initTable();
})
.on('click', '.btn-test-asset', function () {
var asset_id = $(this).data('uid');
var the_url = "{% url 'api-assets:asset-alive-test' pk=DEFAULT_PK %}".replace('{{ DEFAULT_PK }}', asset_id);
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
.on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var success = function (data) {
@@ -133,5 +168,38 @@ $(document).ready(function () {
flash_message: false
});
})
.on('click', '.btn-update-asset-user-auth', function() {
assetId = $(this).data('aid');
var hostname = $(this).data('hostname');
var username = '{{ admin_user.username }}';
initAssetUserAuthModalForm(hostname, username);
$("#asset_user_auth_modal").modal();
})
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
var password = $('#id_password').val();
if (password){
var data = {
'name': "{{ admin_user.username }}",
'asset': assetId,
'username': "{{ admin_user.username }}",
'password': password
};
formSubmit({
data: data,
url: "{% url 'api-assets:asset-user-list' %}",
method: 'POST',
success: function () {
toastr.success("{% trans 'Update successfully!' %}");
},
error: function () {
toastr.error("{% trans 'Update failed!' %}");
}
});
$("#asset_user_auth_modal").modal('hide');
}
else{
$('#id_password').parent().addClass('has-error');
}
})
</script>
{% endblock %}

View File

@@ -44,9 +44,10 @@ $(document).ready(function(){
var options = {
ele: $('#admin_user_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{targets: 1, render: function (cellData, tp, rowData, meta) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:admin-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
return detail_btn.replace('{{ DEFAULT_PK }}', rowData.id);
}},
{targets: 4, createdCell: function (td, cellData) {
var innerHtml = "";
@@ -82,7 +83,6 @@ $(document).ready(function(){
innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}},
{targets: 8, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:admin-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
@@ -90,8 +90,8 @@ $(document).ready(function(){
$(td).html(update_btn + del_btn)
}}],
ajax_url: '{% url "api-assets:admin-user-list" %}',
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }]
columns: [{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment"}, {data: "id"}]
};
jumpserver.initServerSideDataTable(options)
})

View File

@@ -0,0 +1,218 @@
{% extends 'base.html' %}
{% load common_tags %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
{% endblock %}
{% 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>
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %}</a>
</li>
<li class="active">
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %}</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 style="float: left">{% trans 'Asset users of' %} <b>{{ asset.hostname }} </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 table-hover" id="asset_user_list">
<thead>
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Password version' %}</th>
<th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Date updated' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
{% if asset.protocol == 'ssh' %}
<tr class="no-borders-tr">
<td>{% trans 'Test connective' %}:</td>
<td>
<span class="pull-right">
<button type="button" class="btn btn-primary btn-xs" id="btn-bulk-test-connective" style="width: 54px">{% trans 'Test' %}</button>
</span>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% include 'assets/_asset_user_auth_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
<script>
function initAssetUserAuthModalForm(hostname){
$('#id_hostname_p').html(hostname);
$('#id_username_p').html(username);
$('#id_password').parent().removeClass('has-error');
$('#id_password').val('');
}
function initAssetUserTable() {
var reachable = {{ asset.admin_user.REACHABLE }};
var unreachable = {{ asset.admin_user.UNREACHABLE }};
var options = {
ele: $('#asset_user_list'),
buttons: [],
order: [],
columnDefs: [
{targets: 3, createdCell: function (td, cellData) {
if (cellData === unreachable) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else if (cellData === reachable) {
$(td).html('<i class="fa fa-check text-navy"></i>')
} else {
$(td).html('')
}
}},
{targets: 4, createdCell: function (td, cellData) {
$(td).html(cellData.slice(0, -6));
}},
{targets: 5, createdCell: function (td, cellData) {
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Update auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);
{% if asset.protocol == 'ssh' %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-connective" data-username="DEFAULT_USERNAME">{% trans "Test" %}</a>'.replace("DEFAULT_USERNAME", cellData);
$(td).html(test_btn + update_auth_btn);
{% else %}
$(td).html(update_auth_btn);
{% endif %}
{#var check_btn = ' <a class="btn btn-xs btn-info btn-check-asset-user-auth" data-username="DEFAULT_USERNAME">{% trans "Check auth" %}</a>'.replace("DEFAULT_USERNAME", cellData);#}
}}
],
ajax_url: '{% url "api-assets:asset-user-list" %}' + '?asset_id={{ asset.id }}',
columns: [
{data: function (){return ''}}, {data: "username" },
{data: "version"}, {data: "connectivity"}, {data: "date_updated"},
{data: "username", orderable: false}
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
}
var username;
$(document).ready(function () {
initAssetUserTable();
})
{#.on('click', '.btn-check-asset-user-auth', function(){#}
{# var username = $(this).data('username');#}
{# var the_url = "{% url 'api-assets:asset-user-auth-info' %}" + '?asset_id={{ asset.id }}' + '&username=' + username;#}
{# $.ajax({#}
{# url: the_url,#}
{# method: 'GET',#}
{# success: function (data) {#}
{# alert("Password: " + data.password);#}
{# }#}
{# });#}
{# })#}
.on('click', '.btn-update-asset-user-auth', function() {
username = $(this).data('username');
var hostname = "{{ asset.hostname }}";
initAssetUserAuthModalForm(hostname, username);
$("#asset_user_auth_modal").modal();
})
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
var password = $('#id_password').val();
if (password){
var data = {
'name': username,
'asset': "{{ asset.id }}",
'username': username,
'password': password
};
formSubmit({
data: data,
url: "{% url 'api-assets:asset-user-list' %}",
method: 'POST',
success: function () {
toastr.success("{% trans 'Update successfully!' %}");
},
error: function () {
toastr.error("{% trans 'Update failed!' %}");
}
});
$("#asset_user_auth_modal").modal('hide');
}
else{
$('#id_password').parent().addClass('has-error');
}
})
.on('click', '.btn-test-connective', function () {
var username = $(this).data('username');
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}" + "&username=" + username;
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
.on('click', '#btn-bulk-test-connective', function () {
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}";
var success = function (data) {
var task_id = data.task;
var url = '{% url "ops:celery-task-log" pk=DEFAULT_PK %}'.replace("{{ DEFAULT_PK }}", task_id);
window.open(url, '', 'width=800,height=600,left=400,top=400')
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
</script>
{% endblock %}

View File

@@ -95,9 +95,12 @@ $(document).ready(function () {
if(protocol === 'rdp'){
port = 3389;
}
if(protocol === 'telnet (beta)'){
else if(protocol === 'telnet (beta)'){
port = 23;
}
else if(protocol === 'vnc'){
port = 5901;
}
$("#id_port").val(port);
});
})

View File

@@ -19,6 +19,9 @@
<li class="active">
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %} </a>
</li>
<li>
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %} </a>
</li>
{% if user.is_superuser %}
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:asset-update' pk=asset.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
@@ -32,7 +35,7 @@
</ul>
</div>
<div class="tab-content">
<div class="col-sm-7" style="padding-left: 0">
<div class="col-sm-8" style="padding-left: 0">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label"><b>{{ asset.hostname }}</b></span>
@@ -139,7 +142,7 @@
</div>
</div>
{% if user.is_superuser or user.is_org_admin %}
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}

View File

@@ -92,8 +92,7 @@
<th class="text-center">{% trans 'Hostname' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Hardware' %}</th>
<th class="text-center">{% trans 'Active' %}</th>
{# <th class="text-center">{% trans 'Reachable' %}</th>#}
<th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
@@ -134,8 +133,11 @@
<li id="menu_refresh_hardware_info" class="btn-refresh-hardware" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh node hardware info' %}</a></li>
<li id="menu_test_connective" class="btn-test-connective" tabindex="-1"><a><i class="fa fa-chain"></i> {% trans 'Test node connective' %}</a></li>
<li class="divider"></li>
<li id="menu_refresh_assets_amount" class="btn-refresh-assets-amount" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh all node assets amount' %}</a></li>
<li class="divider"></li>
<li id="show_current_asset" class="btn-show-current-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-hand-o-up"></i> {% trans 'Display only current node assets' %}</a></li>
<li id="show_all_asset" class="btn-show-all-asset" style="display: none;" tabindex="-1"><a><i class="fa fa-th"></i> {% trans 'Displays all child node assets' %}</a></li>
{# <li id="fresh_tree" class="btn-refresh-tree" tabindex="-1"><a><i class="fa fa-refresh"></i> {% trans 'Refresh' %}</a></li>#}
</ul>
</div>
@@ -147,11 +149,14 @@
<script>
var zTree, rMenu, asset_table, show = 0;
var update_node_action = "";
var current_node_id = null;
var current_node = null;
function initTable() {
var options = {
ele: $('#asset_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'assets:asset-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));
@@ -160,10 +165,12 @@ function initTable() {
$(td).html(rowData.hardware_info)
}},
{targets: 4, createdCell: function (td, cellData) {
if (!cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>')
if (cellData === 1){
$(td).html('<i class="fa fa-circle text-navy"></i>')
} else if (cellData === 0) {
$(td).html('<i class="fa fa-circle text-danger"></i>')
} else {
$(td).html('<i class="fa fa-check text-navy"></i>')
$(td).html('<i class="fa fa-circle text-warning"></i>')
}
}},
@@ -176,8 +183,8 @@ function initTable() {
ajax_url: '{% url "api-assets:asset-list" %}',
columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" },
{data: "cpu_cores"}, {data: "is_active", orderable: false },
{data: "id", orderable: false }
{data: "cpu_cores", orderable: false},
{data: "connectivity", orderable: false}, {data: "id", orderable: false }
],
op_html: $('#actions').html()
};
@@ -191,18 +198,20 @@ function addTreeNode() {
if (!parentNode){
return
}
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.node_id );
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
$.post(url, {}, function (data, status){
if (status === "success") {
var newNode = {
id: data["key"],
name: data["value"],
node_id: data["id"],
pId: parentNode.id
pId: parentNode.id,
meta: {
"node": data
}
};
newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.node_id, parentNode);
var node = zTree.getNodeByParam('id', newNode.id, parentNode);
zTree.editName(node);
} else {
alert("{% trans 'Create node failed' %}")
@@ -218,10 +227,10 @@ function removeTreeNode() {
}
if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.assets_amount !== 0) {
} else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}");
} else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id );
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({
url: url,
method: "DELETE",
@@ -238,8 +247,8 @@ function editTreeNode() {
if (!current_node){
return
}
if (current_node.value) {
current_node.name = current_node.value;
if (current_node) {
current_node.name = current_node.meta.node.value;
}
zTree.editName(current_node);
}
@@ -281,7 +290,7 @@ function onBodyMouseDown(event){
function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.node_id);
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
var data = {"value": treeNode.name};
if (isCancel){
return
@@ -291,13 +300,21 @@ function onRename(event, treeId, treeNode, isCancel){
body: JSON.stringify(data),
method: "PATCH",
success_message: "{% trans 'Rename success' %}",
fail_message: "{% trans 'Rename failed, do not change the root node name' %}"
fail_message: "{% trans 'Rename failed, do not change the root node name' %}",
success: function () {
treeNode.name = treeNode.name + ' (' + treeNode.meta.node.assets_amount + ')'
zTree.updateNode(treeNode);
console.log("Success: " + treeNode.name)
}
})
}
function onSelected(event, treeNode) {
current_node = treeNode;
current_node_id = treeNode.meta.node.id;
zTree.expandNode(current_node, true);
var url = asset_table.ajax.url();
url = setUrlParam(url, "node_id", treeNode.node_id);
url = setUrlParam(url, "node_id", current_node_id);
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
setCookie('node_selected', treeNode.node_id);
asset_table.ajax.url(url);
@@ -305,6 +322,9 @@ function onSelected(event, treeNode) {
}
function selectQueryNode() {
// TODO: 是否应该添加
// 暂时忽略之前选中的内容
return
var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected');
var node;
@@ -329,10 +349,10 @@ function beforeDrag() {
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
var treeNodesNames = [];
$.each(treeNodes, function (index, value) {
treeNodesNames.push(value.value);
treeNodesNames.push(value.name);
});
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.value + "` 下吗?";
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.name + "` 下吗?";
return confirm(msg);
}
@@ -342,10 +362,10 @@ function onDrag(event, treeId, treeNodes) {
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
var treeNodesIds = [];
$.each(treeNodes, function (index, value) {
treeNodesIds.push(value.node_id);
treeNodesIds.push(value.meta.node.id);
});
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.node_id);
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id);
var body = {nodes: treeNodesIds};
APIUpdateAttr({
url: the_url,
@@ -355,6 +375,14 @@ function onDrop(event, treeId, treeNodes, targetNode, moveType) {
}
function initTree() {
if (zTree) {
return
}
var url = '{% url 'api-assets:node-children-tree' %}?assets=0&all=';
var showCurrentAsset = getCookie('show_current_asset');
if (!showCurrentAsset) {
url += '1'
}
var setting = {
view: {
dblClickExpand: false,
@@ -365,6 +393,12 @@ function initTree() {
enable: true
}
},
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: {
enable: true,
showRemoveBtn: false,
@@ -387,26 +421,8 @@ function initTree() {
};
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]){
value["pId"] = value["tree_parent"];
} else {
value["isParent"] = true;
}
value["name"] = value["value"] + ' (' + value['assets_amount'] + ')';
value['value'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
var root = zTree.getNodes()[0];
zTree.expandNode(root);
rMenu = $("#rMenu");
selectQueryNode();
});
zTree = $.fn.zTree.init($("#assetTree"), setting, zNodes);
rMenu = $("#rMenu");
}
function toggle() {
@@ -443,20 +459,15 @@ $(document).ready(function(){
.on('click', '.btn_export', function () {
var $data_table = $('#asset_list_table').DataTable();
var rows = $data_table.rows('.selected').data();
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length === 1) {
current_node = nodes[0];
}
var assets = [];
$.each(rows, function (index, obj) {
assets.push(obj.id)
});
var _node_id = current_node ? current_node.node_id : null;
$.ajax({
url: "{% url "assets:asset-export" %}",
method: 'POST',
data: JSON.stringify({assets_id: assets, node_id: _node_id}),
data: JSON.stringify({assets_id: assets, node_id: current_node_id}),
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
@@ -469,12 +480,8 @@ $(document).ready(function(){
.on('click', '#btn_asset_import', function () {
var $form = $('#fm_asset_import');
var action = $form.attr("action");
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
action = setUrlParam(action, 'node_id', current_node.node_id);
{#action += "?node_id=" + current_node.node_id;#}
if (current_node_id){
action = setUrlParam(action, 'node_id', current_node_id);
$form.attr("action", action)
}
$form.find('.help-block').remove();
@@ -496,25 +503,14 @@ $(document).ready(function(){
})
.on('click', '.btn-create-asset', function () {
var url = "{% url 'assets:asset-create' %}";
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
url += "?node_id=" + current_node.node_id;
if (current_node_id) {
url += "?node_id=" + current_node_id;
}
window.open(url, '_self');
})
.on('click', '.btn-refresh-hardware', function () {
var url = "{% url 'api-assets:node-refresh-hardware-info' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
@@ -531,15 +527,10 @@ $(document).ready(function(){
})
.on('click', '.btn-test-connective', function () {
var url = "{% url 'api-assets:node-test-connective' pk=DEFAULT_PK %}";
var nodes = zTree.getSelectedNodes();
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
} else {
if (!current_node_id) {
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
@@ -567,6 +558,19 @@ $(document).ready(function(){
setCookie('show_current_asset', '');
location.reload();
})
.on('click', '.btn-test-connective', function () {
hideRMenu();
})
.on('click', '#menu_refresh_assets_amount', function () {
hideRMenu();
var url = "{% url 'api-assets:refresh-assets-amount' %}";
APIUpdateAttr({
'url': url,
'method': 'GET'
});
window.location.reload();
})
.on('click', '.btn_asset_delete', function () {
var $this = $(this);
var $data_table = $("#asset_list_table").DataTable();
@@ -654,17 +658,28 @@ $(document).ready(function(){
});
}
function doUpdate() {
var id_list_string = id_list.join(',');
var url = "{% url 'assets:asset-bulk-update' %}?assets_id=" + id_list_string;
location.href = url
var data = {
'assets_id':id_list
};
function error(data) {
toastr.error(JSON.parse(data).error)
}
function success(data) {
location.href = data.url;
}
APIUpdateAttr({
'url': "{% url 'api-assets:asset-bulk-update-select' %}",
'method': 'POST',
'body': JSON.stringify(data),
'flash_message': false,
'success': success,
'error': error,
})
}
function doRemove() {
var current_node;
var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) {
current_node = nodes[0]
} else {
if (!current_node_id) {
return
}
@@ -677,7 +692,7 @@ $(document).ready(function(){
};
APIUpdateAttr({
'url': '/api/assets/v1/nodes/' + current_node.node_id + '/assets/remove/',
'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/',
'method': 'PUT',
'body': JSON.stringify(data),
'success': success
@@ -706,11 +721,7 @@ $(document).ready(function(){
})
.on('click', '#btn_asset_modal_confirm', function () {
var assets_selected = asset_table2.selected;
var current_node;
var nodes = zTree.getSelectedNodes();
if (nodes && nodes.length === 1) {
current_node = nodes[0]
} else {
if (!current_node_id) {
return
}
@@ -722,9 +733,9 @@ $(document).ready(function(){
var url = '';
if (update_node_action === "move") {
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
} else {
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
}
APIUpdateAttr({
@@ -740,6 +751,7 @@ $(document).ready(function(){
}).on('click', '#menu_asset_move', function () {
update_node_action = "move"
})
</script>
{% endblock %}

View File

@@ -40,6 +40,7 @@ function initTable() {
ele: $('#cmd_filter_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url 'assets:cmd-filter-detail' pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -24,14 +24,26 @@
{% block custom_foot_js %}
<script type="text/javascript">
$(document).ready(function () {
console.log($.fn.select2.defaults);
$('.select2').select2().off("select2:open");
}).on('click', '.select2-selection__rendered', function (e) {
e.preventDefault();
$("#asset_list_modal").modal();
initSelectedAssets2Table();
})
.on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected;
var options = [];
$('#id_assets option').each(function (i, v) {
options.push(v.value)
});
asset_table2.selected_rows.forEach(function (i) {
var name = i.hostname + '(' + i.ip + ')';
var option = new Option(name, i.id, false, true);
if (options.indexOf(i.id) === -1) {
$('#id_assets').append(option).trigger('change');
}
});
$('.select2').val(assets).trigger('change');
$("#asset_list_modal").modal('hide');

View File

@@ -15,10 +15,16 @@
<div class="panel-options">
<ul class="nav nav-tabs">
<li>
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
<a href="{% url 'assets:domain-detail' pk=object.id %}"
class="text-center"><i
class="fa fa-laptop"></i> {% trans 'Detail' %}
</a>
</li>
<li class="active">
<a href="{% url 'assets:domain-detail' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Gateway' %} </a>
<a href="{% url 'assets:domain-detail' pk=object.id %}"
class="text-center"><i
class="fa fa-laptop"></i> {% trans 'Gateway' %}
</a>
</li>
</ul>
</div>
@@ -33,7 +39,8 @@
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<a class="dropdown-toggle"
data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
@@ -45,13 +52,17 @@
</div>
<div class="ibox-content">
<div class="uc pull-left m-r-5">
<a href="{% url 'assets:domain-gateway-create' pk=object.id %}" class="btn btn-sm btn-primary"> {% trans "Create gateway" %} </a>
<a href="{% url 'assets:domain-gateway-create' pk=object.id %}"
class="btn btn-sm btn-primary"> {% trans "Create gateway" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="domain_list_table" >
<table class="table table-striped table-bordered table-hover "
id="domain_list_table">
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
<input type="checkbox"
id="check_all"
class="ipt_check_all">
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
@@ -73,6 +84,7 @@
</div>
</div>
</div>
{% include 'assets/_gateway_test_modal.html' %}
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
@@ -84,7 +96,7 @@ function initTable() {
{targets: 7, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:domain-gateway-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-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" data-uid="{{ DEFAULT_PK }}" data-port="PORT">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData).replace("PORT", rowData.port);
if(rowData.protocol === 'rdp'){
test_btn = '<a class="btn btn-xs btn-warning m-l-xs btn-test" disabled data-uid="{{ DEFAULT_PK }}">{% trans "Test connection" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
}
@@ -114,15 +126,21 @@ $(document).ready(function(){
$data_table.ajax.reload();
}, 3000);
}).on('click', '.btn-test', function () {
var $this = $(this);
var uid = $this.data('uid');
$("#ssh_test_port").val($(this).data('port'));
$("#gateway_id").val($(this).data('uid'));
$("#gateway_test").modal('show');
}).on('click', '#btn_gateway_test', function () {
var data = $("#test_gateway_form").serializeObject();
var uid = data.gateway_id;
var the_url = '{% url "api-assets:test-gateway-connective" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
APIUpdateAttr({
url: the_url,
method: "GET",
method: "POST",
body: JSON.stringify({'port': parseInt(data.port)}),
success_message: "{% trans 'Can be connected' %}",
fail_message: "{% trans 'The connection fails' %}"
})
})
});
</script>
{% endblock %}

View File

@@ -41,6 +41,7 @@ function initTable() {
ele: $('#domain_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:domain-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -32,9 +32,22 @@ $(document).ready(function () {
}).on('click', '.select2-selection__rendered', function (e) {
e.preventDefault();
$("#asset_list_modal").modal();
initSelectedAssets2Table();
})
.on('click', '#btn_asset_modal_confirm', function () {
var assets = asset_table2.selected;
var options = [];
$('#id_assets option').each(function (i, v) {
options.push(v.value)
});
asset_table2.selected_rows.forEach(function (i) {
var name = i.hostname + '(' + i.ip + ')';
var option = new Option(name, i.id, false, true);
if (options.indexOf(i.id) === -1) {
$('#id_assets').append(option).trigger('change');
}
});
$('#id_assets').val(assets).trigger('change');
$("#asset_list_modal").modal('hide');
})

View File

@@ -30,6 +30,7 @@ function initTable() {
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{# var detail_btn = '<a href="{% url "assets:label-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';#}
cellData = htmlEscape(cellData);
var detail_btn = '<a>' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -132,35 +132,40 @@
</div>
</div>
</div>
{% include 'assets/_asset_user_auth_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
<script>
function initAssetsTable() {
var unreachable = {{ system_user.unreachable_assets|safe }};
var connectivity = {{ system_user.connectivity | safe }};
var options = {
ele: $('#system_user_list'),
buttons: [],
order: [],
columnDefs: [
{targets: 0, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 3, createdCell: function (td, cellData) {
if (unreachable.indexOf(cellData) >= 0) {
if (connectivity.unreachable.indexOf(cellData) >= 0) {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else {
} else if (connectivity.reachable.indexOf(cellData) >= 0 ) {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
} else {
$(td).html('')
}
}},
{targets: 4, createdCell: function (td, cellData) {
{targets: 4, createdCell: function (td, cellData, rowData) {
var push_btn = '';
{% if system_user.auto_push %}
push_btn = '<a class="btn btn-xs btn-primary btn-push-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Push" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{% endif %}
var test_btn = ' <a class="btn btn-xs btn-info btn-test-asset" data-uid="{{ DEFAULT_PK }}" >{% trans "Test" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
{#var unbound_btn = '<a class="btn btn-xs btn-danger m-l-xs btn-asset-unbound" data-uid="{{ DEFAULT_PK }}">{% trans "Unbound" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);#}
$(td).html(push_btn + test_btn);
var update_auth_btn = ' <a class="btn btn-xs btn-primary btn-update-asset-user-auth" data-aid="{{ DEFAULT_PK }}" data-hostname="hostname777">{% trans "Update auth" %}</a>'.replace("{{ DEFAULT_PK }}", cellData).replace("hostname777", rowData.hostname);
$(td).html(push_btn + test_btn + update_auth_btn);
}}
],
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
@@ -200,6 +205,15 @@ function updateSystemUserNode(nodes) {
}
jumpserver.nodes_selected = {};
function initAssetUserAuthModalForm(hostname, username){
$('#id_hostname_p').html(hostname);
$('#id_username_p').html(username);
$('#id_password').parent().removeClass('has-error');
$('#id_password').val('');
}
var assetId;
$(document).ready(function () {
$('.select2').select2()
.on('select2:select', function(evt) {
@@ -313,6 +327,38 @@ $(document).ready(function () {
error: error
})
})
.on('click', '.btn-update-asset-user-auth', function() {
assetId = $(this).data('aid');
var hostname = $(this).data('hostname');
var username = '{{ system_user.username }}';
initAssetUserAuthModalForm(hostname, username);
$("#asset_user_auth_modal").modal();
})
.on('click', '#btn_asset_user_auth_modal_confirm', function(){
var password = $('#id_password').val();
if (password){
var data = {
'name': "{{ system_user.username }}",
'asset': assetId,
'username': "{{ system_user.username }}",
'password': password
};
formSubmit({
data: data,
url: "{% url 'api-assets:asset-user-list' %}",
method: 'POST',
success: function () {
toastr.success("{% trans 'Update successfully!' %}");
},
error: function () {
toastr.error("{% trans 'Update failed!' %}");
}
});
$("#asset_user_auth_modal").modal('hide');
}
else{
$('#id_password').parent().addClass('has-error');
}
})
</script>
{% endblock %}

View File

@@ -49,6 +49,7 @@ function initTable() {
ele: $('#system_user_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
@@ -95,7 +96,7 @@ function initTable() {
}}],
ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" },
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "login_mode_display"}, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
],
op_html: $('#actions').html()

View File

@@ -17,7 +17,6 @@
<div class="file-manager ">
<div id="assetTree" class="ztree">
</div>
<div class="clearfix"></div>
</div>
</div>
@@ -46,6 +45,7 @@
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Active' %}</th>
<th class="text-center">{% trans 'System users' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
@@ -62,16 +62,19 @@
{% block custom_foot_js %}
<script>
var treeUrl = "{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0&cache_policy=1";
var zTree, asset_table, show=0;
var inited = false;
var url;
function initTable() {
if (inited){
return
} else {
inited = true;
}
url = "{% url 'api-perms:my-assets' %}";
url = "{% url 'api-perms:my-assets' %}?cache_policy=1";
var options = {
ele: $('#user_assets_table'),
columnDefs: [
@@ -92,13 +95,18 @@ function initTable() {
users.push(data.name);
});
$(td).html(users.join(', '))
}},
{targets: 5, createdCell: function (td, cellData) {
var conn_btn = '<a href="{% url "luna-view" %}?login_to=' + cellData +'" class="btn btn-xs btn-primary">{% trans "Connect" %}</a>'.replace("{{ DEFAULT_PK }}", cellData);
$(td).html(conn_btn)
}}
],
ajax_url: url,
columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" },
{data: "is_active", orderable: false },
{data: "system_users_granted", orderable: false}
{data: "system_users_granted", orderable: false},
{data: "id", orderable: false}
]
};
asset_table = jumpserver.initServerSideDataTable(options);
@@ -106,7 +114,7 @@ function initTable() {
}
function onSelected(event, treeNode) {
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}?cache_policy=1';
var node_id = treeNode.meta.node.id;
url = url.replace("{{ DEFAULT_PK }}", node_id);
setCookie('node_selected', treeNode.id);
@@ -118,7 +126,7 @@ function initTree() {
var setting = {
view: {
dblClickExpand: false,
showLine: true
showLine: true,
},
data: {
simpleData: {
@@ -131,10 +139,14 @@ function initTree() {
};
var zNodes = [];
$.get("{% url 'api-perms:my-nodes-assets-as-tree' %}?show_assets=0", function(data, status){
$.get(treeUrl, function(data, status){
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
rootNodeAddDom(zTree, function () {
treeUrl = treeUrl.replace('cache_policy=1', 'cache_policy=2');
initTree();
});
});
}
@@ -159,7 +171,7 @@ $(document).ready(function () {
'comment': "{% trans 'Comment' %}"
{#'date_joined': "{% trans 'Date joined' %}",#}
};
$.each(data, function(index, value){
$.each(data.results, function(index, value){
if(value.id === asset_id){
for(var i in desc){
trs += "<tr class='no-borders-tr'>\n" +

View File

@@ -17,6 +17,7 @@ router.register(r'nodes', api.NodeViewSet, 'node')
router.register(r'domain', api.DomainViewSet, 'domain')
router.register(r'gateway', api.GatewayViewSet, 'gateway')
router.register(r'cmd-filter', api.CommandFilterViewSet, 'cmd-filter')
router.register(r'asset-user', api.AssetUserViewSet, 'asset-user')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
@@ -24,35 +25,49 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r
urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('asset/update/select/',
api.AssetBulkUpdateSelectAPI.as_view(), name='asset-bulk-update-select'),
path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/',
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
path('assets/<uuid:pk>/gateway/',
api.AssetGatewayApi.as_view(), name='asset-gateway'),
path('asset-user/auth-info/',
api.AssetUserAuthInfoApi.as_view(), name='asset-user-auth-info'),
path('asset-user/test-connective/',
api.AssetUserTestConnectiveApi.as_view(), name='asset-user-connective'),
path('admin-user/<uuid:pk>/nodes/',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
path('admin-user/<uuid:pk>/auth/',
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
path('admin-user/<uuid:pk>/connective/',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
path('admin-user/<uuid:pk>/assets/',
api.AdminUserAssetsListView.as_view(), name='admin-user-assets'),
path('system-user/<uuid:pk>/auth-info/',
api.SystemUserAuthInfoApi.as_view(), name='system-user-auth-info'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/auth-info/',
api.SystemUserAssetAuthInfoApi.as_view(), name='system-user-asset-auth-info'),
path('system-user/<uuid:pk>/assets/',
api.SystemUserAssetsListView.as_view(), name='system-user-assets'),
path('system-user/<uuid:pk>/push/',
api.SystemUserPushApi.as_view(), name='system-user-push'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/push/',
api.SystemUserPushToAssetApi.as_view(), name='system-user-push-to-asset'),
path('system-user/<uuid:pk>/asset/<uuid:aid>/test/',
api.SystemUserTestAssetConnectabilityApi.as_view(), name='system-user-test-to-asset'),
api.SystemUserTestAssetConnectivityApi.as_view(), name='system-user-test-to-asset'),
path('system-user/<uuid:pk>/connective/',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
path('system-user/<uuid:pk>/cmd-filter-rules/',
api.SystemUserCommandFilterRuleListApi.as_view(), name='system-user-cmd-filter-rule-list'),
path('nodes/tree/', api.NodeListAsTreeApi.as_view(), name='node-tree'),
path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'),
path('nodes/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
@@ -70,9 +85,12 @@ urlpatterns = [
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
path('nodes/<uuid:pk>/test-connective/',
api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
path('nodes/refresh-assets-amount/',
api.RefreshAssetsAmount.as_view(), name='refresh-assets-amount'),
path('gateway/<uuid:pk>/test-connective/',
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
]
urlpatterns += router.urls + cmd_filter_router.urls

View File

@@ -15,6 +15,8 @@ urlpatterns = [
path('asset/<uuid:pk>/update/', views.AssetUpdateView.as_view(), name='asset-update'),
path('asset/<uuid:pk>/delete/', views.AssetDeleteView.as_view(), name='asset-delete'),
path('asset/update/', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'),
# Asset user view
path('asset/<uuid:pk>/asset-user/', views.AssetUserListView.as_view(), name='asset-user-list'),
# User asset view
path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'),

View File

@@ -102,7 +102,7 @@ class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
'app': _('Assets'),
'action': _('Admin user detail'),
"total_amount": len(self.queryset),
'unreachable_amount': len([asset for asset in self.queryset if asset.is_connective is False])
'unreachable_amount': len([asset for asset in self.queryset if asset.connectivity is False])
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@@ -28,13 +28,14 @@ from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger
from common.permissions import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from orgs.utils import current_org
from .. import forms
from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
__all__ = [
'AssetListView', 'AssetCreateView', 'AssetUpdateView',
'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView',
'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView',
'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView',
]
@@ -56,6 +57,20 @@ class AssetListView(AdminUserRequiredMixin, TemplateView):
return super().get_context_data(**kwargs)
class AssetUserListView(AdminUserRequiredMixin, DetailView):
model = Asset
context_object_name = 'asset'
template_name = 'assets/asset_asset_user_list.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Asset user list'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserAssetListView(LoginRequiredMixin, TemplateView):
template_name = 'assets/user_asset_list.html'
@@ -106,15 +121,12 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
form = None
def get(self, request, *args, **kwargs):
assets_id = self.request.GET.get('assets_id', '')
self.id_list = [i for i in assets_id.split(',')]
spm = request.GET.get('spm', '')
assets_id = cache.get(CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm))
if kwargs.get('form'):
self.form = kwargs['form']
elif assets_id:
self.form = self.form_class(
initial={'assets': self.id_list}
)
self.form = self.form_class(initial={'assets': assets_id})
else:
self.form = self.form_class()
return super().get(request, *args, **kwargs)
@@ -216,7 +228,6 @@ class AssetExportView(LoginRequiredMixin, View):
return HttpResponse('Json object not valid', status=400)
if not assets_id:
print(node_id)
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
assets = node.get_all_assets()
for asset in assets:

View File

@@ -1,4 +1,2 @@
# -*- coding: utf-8 -*-
#
from users.models import LoginLog

View File

@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default=None, max_length=36, null=True),
field=models.CharField(blank=True, db_index=True, default=None, max_length=36, null=True),
),
]

View File

@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ftplog',
name='org_id',
field=models.CharField(blank=True, default='', max_length=36, verbose_name='Organization'),
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
]

View File

@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='OperateLog',
fields=[
('org_id', models.CharField(blank=True, default='', max_length=36, verbose_name='Organization')),
('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)),
('user', models.CharField(max_length=128, verbose_name='User')),
('action', models.CharField(choices=[('create', 'Create'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action')),
@@ -38,14 +38,4 @@ class Migration(migrations.Migration):
('datetime', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='UserLoginLog',
fields=[
],
options={
'proxy': True,
'indexes': [],
},
bases=('users.loginlog',),
),
]

View File

@@ -0,0 +1,55 @@
# Generated by Django 2.1.7 on 2019-02-28 09:15
from django.db import migrations, models, connection
import django.utils.timezone
import uuid
class Migration(migrations.Migration):
dependencies = [
('audits', '0004_operatelog_passwordchangelog_userloginlog'),
('users', '0019_auto_20190304_1459'),
]
state_operations = [
migrations.CreateModel(
name='UserLoginLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True,
serialize=False)),
('username',
models.CharField(max_length=128, verbose_name='Username')),
('type',
models.CharField(choices=[('W', 'Web'), ('T', 'Terminal')],
max_length=2, verbose_name='Login type')),
('ip', models.GenericIPAddressField(verbose_name='Login ip')),
('city', models.CharField(blank=True, max_length=254, null=True,
verbose_name='Login city')),
('user_agent',
models.CharField(blank=True, max_length=254, null=True,
verbose_name='User agent')),
('mfa', models.SmallIntegerField(
choices=[(0, 'Disabled'), (1, 'Enabled'), (2, '-')],
default=2, verbose_name='MFA')),
('reason', models.SmallIntegerField(
choices=[(0, '-'), (1, 'Username/password check failed'),
(2, 'MFA authentication failed'),
(3, 'Username does not exist'),
(4, 'Password expired')], default=0,
verbose_name='Reason')),
('status', models.BooleanField(
choices=[(True, 'Success'), (False, 'Failed')],
default=True, max_length=2, verbose_name='Status')),
('datetime',
models.DateTimeField(default=django.utils.timezone.now,
verbose_name='Date login')),
],
options={
'ordering': ['-datetime', 'username'],
},
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]

View File

@@ -1,10 +1,11 @@
import uuid
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from orgs.mixins import OrgModelMixin
from .hands import LoginLog
__all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
@@ -55,6 +56,67 @@ class PasswordChangeLog(models.Model):
return "{} change {}'s password".format(self.change_by, self.user)
class UserLoginLog(LoginLog):
class UserLoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
('W', 'Web'),
('T', 'Terminal'),
)
MFA_DISABLED = 0
MFA_ENABLED = 1
MFA_UNKNOWN = 2
MFA_CHOICE = (
(MFA_DISABLED, _('Disabled')),
(MFA_ENABLED, _('Enabled')),
(MFA_UNKNOWN, _('-')),
)
REASON_NOTHING = 0
REASON_PASSWORD = 1
REASON_MFA = 2
REASON_NOT_EXIST = 3
REASON_PASSWORD_EXPIRED = 4
REASON_CHOICE = (
(REASON_NOTHING, _('-')),
(REASON_PASSWORD, _('Username/password check failed')),
(REASON_MFA, _('MFA authentication failed')),
(REASON_NOT_EXIST, _("Username does not exist")),
(REASON_PASSWORD_EXPIRED, _("Password expired")),
)
STATUS_CHOICE = (
(True, _('Success')),
(False, _('Failed'))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(max_length=128, verbose_name=_('Username'))
type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type'))
ip = models.GenericIPAddressField(verbose_name=_('Login ip'))
city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('Login city'))
user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_('User agent'))
mfa = models.SmallIntegerField(default=MFA_UNKNOWN, choices=MFA_CHOICE, verbose_name=_('MFA'))
reason = models.SmallIntegerField(default=0, choices=REASON_CHOICE, verbose_name=_('Reason'))
status = models.BooleanField(max_length=2, default=True, choices=STATUS_CHOICE, verbose_name=_('Status'))
datetime = models.DateTimeField(default=timezone.now, verbose_name=_('Date login'))
@classmethod
def get_login_logs(cls, date_form=None, date_to=None, user=None, keyword=None):
login_logs = cls.objects.all()
if date_form and date_to:
login_logs = login_logs.filter(
datetime__gt=date_form, datetime__lt=date_to
)
if user:
login_logs = login_logs.filter(username=user)
if keyword:
login_logs = login_logs.filter(
Q(ip__contains=keyword) |
Q(city__contains=keyword) |
Q(username__contains=keyword)
)
return login_logs
class Meta:
proxy = True
ordering = ['-datetime', 'username']

View File

@@ -6,14 +6,17 @@ from django.dispatch import receiver
from django.db import transaction
from jumpserver.utils import current_request
from common.utils import get_request_ip
from common.utils import get_request_ip, get_logger
from users.models import User
from .models import OperateLog, PasswordChangeLog
logger = get_logger(__name__)
MODELS_NEED_RECORD = (
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
'Domain', 'Gateway', 'Organization', 'AssetPermission',
'Domain', 'Gateway', 'Organization', 'AssetPermission', 'CommandFilter',
'CommandFilterRule', 'License', 'Setting', 'Account', 'SyncInstanceTask',
)
@@ -26,11 +29,16 @@ def create_operate_log(action, sender, resource):
return
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
data = {
"user": str(user), 'action': action, 'resource_type': resource_type,
'resource': str(resource), 'remote_addr': remote_addr,
}
with transaction.atomic():
OperateLog.objects.create(
user=user, action=action, resource_type=resource_type,
resource=resource, remote_addr=remote_addr
)
try:
OperateLog.objects.create(**data)
except Exception as e:
logger.error("Create operate log error: {}".format(e))
@receiver(post_save, dispatch_uid="my_unique_identifier")

21
apps/audits/tasks.py Normal file
View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
#
import datetime
from django.utils import timezone
from django.conf import settings
from celery import shared_task
from ops.celery.decorator import register_as_period_task
from .models import UserLoginLog
@register_as_period_task(interval=3600*24)
@shared_task
def clean_login_log_period():
now = timezone.now()
try:
days = int(settings.LOGIN_LOG_KEEP_DAYS)
except ValueError:
days = 90
expired_day = now - datetime.timedelta(days=days)
UserLoginLog.objects.filter(datetime__lt=expired_day).delete()

View File

@@ -17,10 +17,10 @@
<div class="form-group" id="date">
<div class="input-daterange input-group" id="datepicker">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}">
<input type="text" id="id_date_from" class="input-sm form-control" style="width: 100px;" name="date_from" value="{{ date_from|date:'Y-m-d'}}">
{# <input type="text" class="input-sm form-control" style="width: 100px;" name="date_from" >#}
<span class="input-group-addon">to</span>
<input type="text" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}">
<input type="text" id="id_date_to" class="input-sm form-control" style="width: 100px;" name="date_to" value="{{ date_to|date:'Y-m-d'}}">
</div>
</div>
<div class="input-group">
@@ -32,7 +32,7 @@
</select>
</div>
<div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
<input type="text" id="search" class="form-control input-sm" name="keyword" placeholder="{% trans 'Search' %}" value="{{ keyword }}">
</div>
<div class="input-group">
<div class="input-group-btn">
@@ -43,38 +43,57 @@
</div>
</form>
{% endblock %}
{% block table_container %}
<table class="table table-striped table-bordered table-hover " id="login_log_table" >
<thead>
<tr>
<th class="text-center">{% trans 'ID' %}</th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Type' %}</th>
<th class="text-center">{% trans 'UA' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'City' %}</th>
<th class="text-center">{% trans 'MFA' %}</th>
<th class="text-center">{% trans 'Reason' %}</th>
<th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Date' %}</th>
</tr>
<thead>
<tbody>
{% for login_log in object_list %}
<tr class="gradeX">
<td class="text-center">{{ forloop.counter }}</td>
<td class="text-center">{{ login_log.username }}</td>
<td class="text-center">{{ login_log.get_type_display }}</td>
<td class="text-center">
<span href="javascript:void(0);" data-toggle="tooltips" title="{{ login_log.user_agent }}">{{ login_log.user_agent | truncatechars:20 }}</span>
</td>
<td class="text-center">{{ login_log.ip }}</td>
<td class="text-center">{{ login_log.city }}</td>
<td class="text-center">{{ login_log.get_mfa_display }}</td>
<td class="text-center">{{ login_log.get_reason_display }}</td>
<td class="text-center">{{ login_log.get_status_display }}</td>
<td class="text-center">{{ login_log.datetime }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="actions" class="" style="margin-top: -20px">
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="export">{% trans 'Export' %}</option>
</select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary btn_export">
{% trans 'Submit' %}
</button>
</div>
</div>
</div>
{% block table_head %}
<th class="text-center">{% trans 'ID' %}</th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Type' %}</th>
<th class="text-center">{% trans 'UA' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'City' %}</th>
<th class="text-center">{% trans 'MFA' %}</th>
<th class="text-center">{% trans 'Reason' %}</th>
<th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Date' %}</th>
{% endblock %}
{% block table_body %}
{% for login_log in object_list %}
<tr class="gradeX">
<td class="text-center">{{ forloop.counter }}</td>
<td class="text-center">{{ login_log.username }}</td>
<td class="text-center">{{ login_log.get_type_display }}</td>
<td class="text-center">
<span href="javascript:void(0);" data-toggle="tooltips" title="{{ login_log.user_agent }}">{{ login_log.user_agent | truncatechars:20 }}</span>
</td>
<td class="text-center">{{ login_log.ip }}</td>
<td class="text-center">{{ login_log.city }}</td>
<td class="text-center">{{ login_log.get_mfa_display }}</td>
<td class="text-center">{{ login_log.get_reason_display }}</td>
<td class="text-center">{{ login_log.get_status_display }}</td>
<td class="text-center">{{ login_log.datetime }}</td>
</tr>
{% endfor %}
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
@@ -95,6 +114,29 @@
width: 'auto'
});
})
.on('click', '.btn_export', function () {
var date_form = $('#id_date_from').val();
var date_to = $('#id_date_to').val();
var user = $('.select2 option:selected').val();
var keyword = $('#search').val();
$.ajax({
url: "{% url "audits:login-log-export" %}",
method: 'POST',
data: JSON.stringify({
'date_form':date_form,
'date_to':date_to,
'user':user,
'keyword':keyword
}),
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
},
error: function () {
toastr.error('Export failed');
}
})
})
</script>
{% endblock %}

View File

@@ -37,7 +37,7 @@
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="asset">
<select class="select2 form-control" name="action">
<option value="">{% trans 'Action' %}</option>
{% for k, v in actions.items %}
<option value="{{ k }}" {% if k == action %} selected {% endif %}>{{ v }}</option>
@@ -45,7 +45,7 @@
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="system_user">
<select class="select2 form-control" name="resource_type">
<option value="">{% trans 'Resource Type' %}</option>
{% for r in resource_type_list %}
<option value="{{ r }}" {% if r == resource_type %} selected {% endif %}>{{ r }}</option>

View File

@@ -14,4 +14,5 @@ urlpatterns = [
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
path('command-execution-log/', views.CommandExecutionListView.as_view(), name='command-execution-log-list'),
path('login-log/export/', views.LoginLogExportView.as_view(), name='login-log-export'),
]

22
apps/audits/utils.py Normal file
View File

@@ -0,0 +1,22 @@
import csv
import codecs
from django.http import HttpResponse
def get_excel_response(filename):
excel_response = HttpResponse(content_type='text/csv')
excel_response[
'Content-Disposition'] = 'attachment; filename="%s"' % filename
excel_response.write(codecs.BOM_UTF8)
return excel_response
def write_content_to_excel(response, header=None, login_logs=None, fields=None):
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
if header:
writer.writerow(header)
if login_logs:
for log in login_logs:
data = [getattr(log, field.name) for field in fields]
writer.writerow(data)
return response

View File

@@ -1,26 +1,43 @@
import csv
import json
import uuid
import codecs
from django.conf import settings
from django.urls import reverse
from django.utils import timezone
from django.core.cache import cache
from django.http import HttpResponse, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import ListView
from django.utils.translation import ugettext as _
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from audits.utils import get_excel_response, write_content_to_excel
from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from ops.views import CommandExecutionListView as UserCommandExecutionListView
from users.models import User
from .models import FTPLog, OperateLog, PasswordChangeLog, UserLoginLog
def get_resource_type_list():
from users.models import User, UserGroup
from assets.models import Asset, Node, AdminUser, SystemUser, Domain, Gateway
from assets.models import (
Asset, Node, AdminUser, SystemUser, Domain, Gateway, CommandFilter,
CommandFilterRule,
)
from orgs.models import Organization
from perms.models import AssetPermission
models = [
User, UserGroup, Asset, Node, AdminUser, SystemUser, Domain,
Gateway, Organization, AssetPermission
Gateway, Organization, AssetPermission, CommandFilter, CommandFilterRule
]
return [model._meta.verbose_name for model in models]
@@ -219,14 +236,49 @@ class CommandExecutionListView(UserCommandExecutionListView):
return users
def get_context_data(self, **kwargs):
context = {
context = super().get_context_data(**kwargs)
context.update({
'app': _('Audits'),
'action': _('Command execution list'),
'action': _('Command execution log'),
'date_from': self.date_from,
'date_to': self.date_to,
'user_list': self.get_user_list(),
'keyword': self.keyword,
'user_id': self.user_id,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
})
return super().get_context_data(**context)
@method_decorator(csrf_exempt, name='dispatch')
class LoginLogExportView(LoginRequiredMixin, View):
def get(self, request):
fields = [
field for field in UserLoginLog._meta.fields
]
filename = 'login-logs-{}.csv'.format(
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
)
excel_response = get_excel_response(filename)
header = [field.verbose_name for field in fields]
login_logs = cache.get(request.GET.get('spm', ''), [])
response = write_content_to_excel(excel_response, login_logs=login_logs,
header=header, fields=fields)
return response
def post(self, request):
try:
date_form = json.loads(request.body).get('date_form', [])
date_to = json.loads(request.body).get('date_to', [])
user = json.loads(request.body).get('user', [])
keyword = json.loads(request.body).get('keyword', [])
login_logs = UserLoginLog.get_login_logs(
date_form=date_form, date_to=date_to, user=user, keyword=keyword)
except ValueError:
return HttpResponse('Json object not valid', status=400)
spm = uuid.uuid4().hex
cache.set(spm, login_logs, 300)
url = reverse('audits:login-log-export') + '?spm=%s' % spm
return JsonResponse({'redirect': url})

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
#
from .auth import *

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.core.cache import cache
@@ -14,16 +15,21 @@ from rest_framework.views import APIView
from common.utils import get_logger, get_request_ip
from common.permissions import IsOrgAdminOrAppUser
from orgs.mixins import RootOrgViewMixin
from ..serializers import UserSerializer
from ..tasks import write_login_log_async
from ..models import User, LoginLog
from ..utils import check_user_valid, check_otp_code, \
increase_login_failed_count, is_block_login, \
clean_failed_count
from ..hands import Asset, SystemUser
from users.serializers import UserSerializer
from users.models import User
from assets.models import Asset, SystemUser
from audits.models import UserLoginLog as LoginLog
from users.utils import (
check_user_valid, check_otp_code, increase_login_failed_count,
is_block_login, clean_failed_count
)
from ..signals import post_auth_success, post_auth_failed
logger = get_logger(__name__)
__all__ = [
'UserAuthApi', 'UserConnectionTokenApi', 'UserOtpAuthApi',
]
class UserAuthApi(RootOrgViewMixin, APIView):
@@ -46,37 +52,22 @@ class UserAuthApi(RootOrgViewMixin, APIView):
username = request.data.get('username', '')
exist = User.objects.filter(username=username).first()
reason = LoginLog.REASON_PASSWORD if exist else LoginLog.REASON_NOT_EXIST
data = {
'username': username,
'mfa': LoginLog.MFA_UNKNOWN,
'reason': reason,
'status': False
}
self.write_login_log(request, data)
self.send_auth_signal(success=False, username=username, reason=reason)
increase_login_failed_count(username, ip)
return Response({'msg': msg}, status=401)
if user.password_has_expired:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_PASSWORD_EXPIRED,
'status': False
}
self.write_login_log(request, data)
self.send_auth_signal(
success=False, username=username,
reason=LoginLog.REASON_PASSWORD_EXPIRED
)
msg = _("The user {} password has expired, please update.".format(
user.username))
logger.info(msg)
return Response({'msg': msg}, status=401)
if not user.otp_enabled:
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
self.send_auth_signal(success=True, user=user)
# 登陆成功,清除原来的缓存计数
clean_failed_count(username, ip)
token = user.create_bearer_token(request)
@@ -91,7 +82,7 @@ class UserAuthApi(RootOrgViewMixin, APIView):
'code': 101,
'msg': _('Please carry seed value and '
'conduct MFA secondary certification'),
'otp_url': reverse('api-users:user-otp-auth'),
'otp_url': reverse('api-auth:user-otp-auth'),
'seed': seed,
'user': self.serializer_class(user).data
}, status=300
@@ -108,22 +99,14 @@ class UserAuthApi(RootOrgViewMixin, APIView):
)
return user, msg
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent,
}
data.update(tmp_data)
write_login_log_async.delay(**data)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)
class UserConnectionTokenApi(RootOrgViewMixin, APIView):
@@ -167,29 +150,6 @@ class UserConnectionTokenApi(RootOrgViewMixin, APIView):
return super().get_permissions()
class UserToken(APIView):
permission_classes = (AllowAny,)
def post(self, request):
if not request.user.is_authenticated:
username = request.data.get('username', '')
email = request.data.get('email', '')
password = request.data.get('password', '')
public_key = request.data.get('public_key', '')
user, msg = check_user_valid(
username=username, email=email,
password=password, public_key=public_key)
else:
user = request.user
msg = None
if user:
token = user.create_bearer_token(request)
return Response({'Token': token, 'Keyword': 'Bearer'}, status=200)
else:
return Response({'error': msg}, status=406)
class UserOtpAuthApi(RootOrgViewMixin, APIView):
permission_classes = (AllowAny,)
serializer_class = UserSerializer
@@ -197,52 +157,25 @@ class UserOtpAuthApi(RootOrgViewMixin, APIView):
def post(self, request):
otp_code = request.data.get('otp_code', '')
seed = request.data.get('seed', '')
user = cache.get(seed, None)
if not user:
return Response(
{'msg': _('Please verify the user name and password first')},
status=401
)
if not check_otp_code(user.otp_secret_key, otp_code):
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_MFA,
'status': False
}
self.write_login_log(request, data)
self.send_auth_signal(success=False, username=user.username, reason=LoginLog.REASON_MFA)
return Response({'msg': _('MFA certification failed')}, status=401)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(request, data)
self.send_auth_signal(success=True, user=user)
token = user.create_bearer_token(request)
return Response(
{
'token': token,
'user': self.serializer_class(user).data
}
)
data = {'token': token, 'user': self.serializer_class(user).data}
return Response(data)
@staticmethod
def write_login_log(request, data):
login_ip = request.data.get('remote_addr', None)
login_type = request.data.get('login_type', '')
user_agent = request.data.get('HTTP_USER_AGENT', '')
if not login_ip:
login_ip = get_request_ip(request)
tmp_data = {
'ip': login_ip,
'type': login_type,
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)
def send_auth_signal(self, success=True, user=None, username='', reason=''):
if success:
post_auth_success.send(sender=self.__class__, user=user, request=self.request)
else:
post_auth_failed.send(
sender=self.__class__, username=username,
request=self.request, reason=reason
)

View File

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

View File

@@ -8,13 +8,13 @@ from django.core.cache import cache
from django.conf import settings
from django.utils.translation import ugettext as _
from django.utils.six import text_type
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions
from rest_framework.authentication import CSRFCheck
from common.utils import get_object_or_none, make_signature, http_to_unixtime
from .models import User, AccessKey, PrivateToken
from ..models import AccessKey, PrivateToken
def get_request_date_header(request):
@@ -42,7 +42,6 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
失败
"""
keyword = 'Sign'
model = AccessKey
def authenticate(self, request):
auth = authentication.get_authorization_header(request).split()
@@ -109,7 +108,7 @@ class AccessKeyAuthentication(authentication.BaseAuthentication):
class AccessTokenAuthentication(authentication.BaseAuthentication):
keyword = 'Bearer'
model = User
model = get_user_model()
expiration = settings.TOKEN_EXPIRATION or 3600
def authenticate(self, request):
@@ -133,10 +132,9 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(token)
@staticmethod
def authenticate_credentials(token):
def authenticate_credentials(self, token):
user_id = cache.get(token)
user = get_object_or_none(User, id=user_id)
user = get_object_or_none(self.model, id=user_id)
if not user:
msg = _('Invalid token or cache refreshed.')

View File

@@ -16,13 +16,13 @@ class LDAPAuthorizationBackend(LDAPBackend):
"""
def authenticate(self, request=None, username=None, password=None, **kwargs):
if password or self.settings.PERMIT_EMPTY_PASSWORD:
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
else:
logger.debug('Rejecting empty password for {}'.format(username))
user = None
logger.info('Authentication LDAP backend')
if not username:
logger.info('Authenticate failed: username is None')
return None
ldap_user = LDAPUser(self, username=username.strip(), request=request)
user = self.authenticate_ldap_user(ldap_user, password)
logger.info('Authenticate user: {}'.format(user))
return user
def get_user(self, user_id):
@@ -88,7 +88,10 @@ class LDAPUser(_LDAPUser):
def _populate_user_from_attributes(self):
super()._populate_user_from_attributes()
if not hasattr(self._user, 'email') or '@' not in self._user.email:
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX)
if '@' not in self._user.username:
email = '{}@{}'.format(self._user.username, settings.EMAIL_SUFFIX)
else:
email = self._user.username
setattr(self._user, 'email', email)

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
#
from .backends import *
from .middleware import *
from .utils import *

View File

@@ -4,16 +4,19 @@
from django.contrib.auth import get_user_model
from django.conf import settings
from . import client
from common.utils import get_logger
from authentication.openid.models import OIDT_ACCESS_TOKEN
from .utils import new_client
from .models import OIDT_ACCESS_TOKEN
UserModel = get_user_model()
logger = get_logger(__file__)
client = new_client()
BACKEND_OPENID_AUTH_CODE = \
'authentication.openid.backends.OpenIDAuthorizationCodeBackend'
__all__ = [
'OpenIDAuthorizationCodeBackend', 'OpenIDAuthorizationPasswordBackend',
]
class BaseOpenIDAuthorizationBackend(object):
@@ -39,41 +42,41 @@ class BaseOpenIDAuthorizationBackend(object):
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, **kwargs):
logger.info('1.openid code backend')
logger.info('Authentication OpenID code backend')
code = kwargs.get('code')
redirect_uri = kwargs.get('redirect_uri')
if not code or not redirect_uri:
logger.info('Authenticate failed: No code or No redirect uri')
return None
try:
oidt_profile = client.update_or_create_from_code(
code=code,
redirect_uri=redirect_uri
)
code=code, redirect_uri=redirect_uri
)
except Exception as e:
logger.error(e)
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
else:
# Check openid user single logout or not with access_token
request.session[OIDT_ACCESS_TOKEN] = oidt_profile.access_token
user = oidt_profile.user
logger.info('Authenticate success: user -> {}'.format(user))
return user if self.user_can_authenticate(user) else None
class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
logger.info('2.openid password backend')
logger.info('Authentication OpenID password backend')
if not settings.AUTH_OPENID:
logger.info('Authenticate failed: AUTH_OPENID is False')
return None
elif not username:
logger.info('Authenticate failed: Not username')
return None
try:
@@ -82,9 +85,10 @@ class OpenIDAuthorizationPasswordBackend(BaseOpenIDAuthorizationBackend):
)
except Exception as e:
logger.error(e)
logger.info('Authenticate failed: get oidt_profile: {}'.format(e))
else:
user = oidt_profile.user
logger.info('Authenticate success: user -> {}'.format(user))
return user if self.user_can_authenticate(user) else None

View File

@@ -6,12 +6,13 @@ from django.contrib.auth import logout
from django.utils.deprecation import MiddlewareMixin
from django.contrib.auth import BACKEND_SESSION_KEY
from . import client
from common.utils import get_logger
from .backends import BACKEND_OPENID_AUTH_CODE
from authentication.openid.models import OIDT_ACCESS_TOKEN
from .utils import new_client
from .models import OIDT_ACCESS_TOKEN
BACKEND_OPENID_AUTH_CODE = 'OpenIDAuthorizationCodeBackend'
logger = get_logger(__file__)
__all__ = ['OpenIDAuthenticationMiddleware']
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
@@ -20,23 +21,25 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
"""
def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID:
logger.debug("Not settings.AUTH_OPENID")
return
# Don't need check single logout if user not authenticated
if not request.user.is_authenticated:
logger.debug("User is not authenticated")
return
elif request.session[BACKEND_SESSION_KEY] != BACKEND_OPENID_AUTH_CODE:
elif not request.session[BACKEND_SESSION_KEY].endswith(
BACKEND_OPENID_AUTH_CODE):
logger.debug("BACKEND_SESSION_KEY is not BACKEND_OPENID_AUTH_CODE")
return
# Check openid user single logout or not with access_token
client = new_client()
try:
client.openid_connect_client.userinfo(
token=request.session.get(OIDT_ACCESS_TOKEN))
token=request.session.get(OIDT_ACCESS_TOKEN)
)
except Exception as e:
logout(request)
logger.error(e)

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