Compare commits

...

325 Commits
1.3.2 ... 1.4.4

Author SHA1 Message Date
老广
69ab3e3542 Merge pull request #2028 from jumpserver/dev
Dev
2018-11-13 15:17:47 +08:00
ibuler
ff1b902b2e Merge remote-tracking branch 'github/dev' into dev 2018-11-13 15:01:07 +08:00
BaiJiangJie
52647da79b [Update] 修改jumpserver功能图片 (#2020) 2018-11-13 14:59:50 +08:00
BaiJiangJie
28b6144189 [Bugfix] 修复创建用户common_settings.SITE_URL 为None的bug (#2027) 2018-11-13 14:59:29 +08:00
ibuler
f7daf26a03 [Update] 修改celery运行,最大20个进程 2018-11-12 17:47:41 +08:00
ibuler
e1673334af Merge remote-tracking branch 'github/dev' into dev 2018-11-12 16:46:21 +08:00
BaiJiangJie
fd6e561d4b [Bugfix] 资产/用户等升序排序问题,显示创建oss录像存储bucket输入框 (#2017)
* [Bugfix] 修复资产列表,用户列表排序只有降序的问题

* [update] 系统设置创建oss录像存储, 添加bucket输入框
2018-11-12 16:45:57 +08:00
ibuler
4cdddaa493 [Bugfix] 修复网域网关列表的bug 2018-11-12 16:43:35 +08:00
老广
9491827e01 Merge pull request #2014 from jumpserver/dev
Dev
2018-11-11 12:04:00 +08:00
老广
5459d1114f Merge branch 'master' into dev 2018-11-11 11:39:02 +08:00
ibuler
6acda27d67 [Update] 更新版本 2018-11-11 10:38:15 +08:00
BaiJiangJie
0f9326bd8f [Update] 更新OpenID依赖 (#2013) 2018-11-09 15:00:37 +08:00
BaiJiangJie
e09f3ca4fd [Update] 支持 OpenID 认证 (#2008)
* [Update] core支持openid登录,coco还不支持

* [Update] coco支持openid登录

* [Update] 修改注释

* [Update] 修改 OpenID Auth Code Backend 用户认证失败返回None, 不是Anonymoususer

* [Update] 修改OpenID Code用户认证异常捕获

* [Update] 修改OpenID Auth Middleware, check用户是否单点退出的异常捕获

* [Update] 修改config_example Auth OpenID 配置

* [Update] 登录页面添加 更多登录方式

* [Update] 重构OpenID认证架构

* [Update] 修改小细节

* [Update] OpenID用户认证成功后,更新用户来源

* [update] 添加OpenID用户登录成功日志
2018-11-09 14:54:38 +08:00
ibuler
1fcb272ddc [Bugfix] 修复coco tree失败的问题,不应该过滤没有asset的node 2018-11-09 12:33:45 +08:00
ibuler
b577c626f7 [Bugfix] 修复luna取到协议不同的系统用户 2018-11-09 11:23:41 +08:00
ibuler
2e4e5503cc [Bugfix] 修复获取common settings时数据库问题 2018-11-07 11:26:39 +08:00
liuzheng712
4212cb3600 feat: add tencentcloud-sdk-python==3.0.32 2018-11-05 12:48:32 +08:00
ibuler
b8874e1855 [Update] 修改版本号 2018-11-05 11:52:44 +08:00
ibuler
9bb498f7b3 [Update] 修改sdk版本 2018-11-05 11:25:22 +08:00
ibuler
e38d089056 Merge remote-tracking branch 'github/dev' into dev 2018-11-05 11:11:33 +08:00
BaiJiangJie
f9e9bf0b2d [Update] 更新用户/授权规则过期时间精确到min (#1993) 2018-11-05 11:11:20 +08:00
BaiJiangJie
1e5387ef47 [Update] 更新组织管理api (#1986)
* [Update] 更新组织管理api

* [Update] 重写-组织管理员/用户API,采用through类

* [Update] 修改OrgMembershipSerializerMixin目录

* [Update] 修改组织管理API,限制http method

* [Update] 修改rpm依赖
2018-11-02 14:38:44 +08:00
BaiJiangJie
f87e08efff [Update] 添加deb依赖,完善用户登录失败日志,修复资产标签bug (#1983)
* [Update] 修改deb依赖

* [Update] 修改记录用户登录失败日志,用户不存在的情况,但是显示登录日志列表还不全..

* [Update] 用户登录日志,记录用户名不存在的情况(Default组织显示所有用户的登录日志)

* [Bugfix] 修复标签名为search, limit时资产列表不显示的bug
2018-11-01 16:28:19 +08:00
BaiJiangJie
82d866db7d [Update] 添加用户授权资产列表页的分页,搜索,排序 (#1963)
* [Update] 分页获取用户授权资产

* [Update] 修改前端-用户授权资产分页

* [Update] 用户授权资产支持搜索

* [Update] 用户授权资产支持排序

* [Update] 用户授权的节点with资产Api,对资产进行排序

* [Update] 获取用户授权的节点下的资产的api,进行分页、排序、查询

* [Update] 抽象用户授权资产列表的查询,排序

* [Update] 优化小细节

* [Update] 删除无用导入

* [Update] 修改AssetFilterMixins目录从common到perms

* [Update] 资产授权规则列表: 添加分页、搜索

* [Update] 添加管理用户,系统用户列表分页、搜索

* [Update] 用户组列表添加分页,搜索

* [Update] 资产标签列表添加分页、搜索

* [Update] 网域网关列表添加分页、搜索

* [Update] 命令过滤列表添加分页、搜索,修改翻译小细节

* [Update] 删除前端注释initDataTable

* [Update] 修改文案,资产组-节点

* [Update] 普通用户资产列表添加分页、搜索
2018-10-31 15:31:09 +08:00
老广
ba0d822734 Merge pull request #1971 from waiwaiku/master
修复子用户资产列表点击无法收起
2018-10-31 15:30:14 +08:00
fangjian
c8568eb244 修复子用户资产列表点击无法收起 2018-10-31 11:26:24 +08:00
ibuler
6e19b9d5bc [Update] Luna支持异步加载 2018-10-30 12:06:39 +08:00
老广
354b728f75 Merge pull request #1945 from jumpserver/dev_storage
[Update] 更新命令/录像存储设置
2018-10-29 10:07:10 +08:00
老广
ce553710ba Merge pull request #1958 from jumpserver/dev_bugfix_ops
[Bugfix] 修复组织管理员刷新硬件信息,获取资产为None(使用fullname)和查看celery log 403问题
2018-10-29 10:04:57 +08:00
BaiJiangJie
4f806f11f2 [Update] 修改命令/录像存储,创建/删除API的权限为SuperUser 2018-10-28 02:17:46 +08:00
老广
0a94a346a0 Merge pull request #1946 from jumpserver/dev_bugfix_perms
[Bugfix] 修复授权规则下Default节点下的资产不显示
2018-10-27 11:14:23 +08:00
老广
d8afe72d4c Merge pull request #1947 from jumpserver/dev_user_paging
[Update] 用户列表页,添加分页,搜索
2018-10-27 11:14:03 +08:00
BaiJiangJie
e2072a1e02 [Update] 修复组织管理员刷新硬件信息,获取资产为None(使用fullname)和查看celery log 403问题 2018-10-26 21:03:09 +08:00
BaiJiangJie
cc387bf511 [Update] 用户列表页,添加分页,搜索 2018-10-24 14:55:04 +08:00
BaiJiangJie
5c002e91ee [Update] 修改获取资产distinct 2018-10-24 13:05:32 +08:00
BaiJiangJie
41a8831034 [Bugfix] 修复授权规则下Default节点下的资产不显示 2018-10-24 12:13:47 +08:00
BaiJiangJie
ebd92c79c7 [Update] 修改翻译小细节 2018-10-24 11:05:39 +08:00
BaiJiangJie
6278900201 [Update] 创建es命令存储,添加提示信息 2018-10-24 10:57:55 +08:00
ibuler
4f580e0df8 [Update] 修改启动脚本 2018-10-24 10:53:00 +08:00
BaiJiangJie
1f502e02c7 [Update] 修改settings为common_settings 2018-10-24 10:48:03 +08:00
BaiJiangJie
cdf8398169 [Update] 修复创建es命令存储校验有效性异常 2018-10-24 10:29:40 +08:00
BaiJiangJie
1bfef829f3 [Update] 修改terminal表单获取storage, 删除无用代码 2018-10-24 10:11:38 +08:00
BaiJiangJie
cc0cf8ed1c Merge branch 'dev' into dev_replay 2018-10-24 09:52:43 +08:00
BaiJiangJie
2791213844 [Update] 更新storage翻译 2018-10-23 20:41:01 +08:00
BaiJiangJie
284e8be45c [Update] 修改系统设置-命令/录像存储页面(添加,删除) 2018-10-23 19:22:18 +08:00
老广
76109f1808 Merge pull request #1938 from jumpserver/dev
Dev
2018-10-23 10:10:53 +08:00
ibuler
54b6e06d1f [Update] 暂时不支持上传目录 2018-10-19 16:30:52 +08:00
ibuler
5c30c76ea3 [Update] 修改jms-storage版本 2018-10-19 10:49:13 +08:00
老广
94b5eb8685 Merge pull request #1929 from jumpserver/dev
Dev
2018-10-19 09:56:52 +08:00
ibuler
c9f4b104c7 Merge remote-tracking branch 'github/dev' into dev 2018-10-19 09:38:18 +08:00
ibuler
3bf1c036c5 [Bugfix] 修改sshpubkey库版本 2018-10-19 09:27:33 +08:00
老广
09fbd3a5ab Merge pull request #1922 from jumpserver/dev
Dev
2018-10-16 17:18:31 +08:00
ibuler
ebecd00581 [Update] 修改优先级逻辑 2018-10-16 16:47:47 +08:00
ibuler
143fa060d1 [Update] 修改节点信息 2018-10-16 13:44:26 +08:00
ibuler
2c18a27e3a [Update] 修改节点创建规则 2018-10-16 12:37:42 +08:00
ibuler
910dd4e593 [Update] 修改节点生成规则 2018-10-16 12:03:11 +08:00
ibuler
11aefa479b [Bugfix] 修复common设置问题 2018-10-16 10:56:25 +08:00
ibuler
abc56016f2 Merge remote-tracking branch 'github/dev' into dev 2018-10-16 10:19:00 +08:00
老广
f44db2a25b Merge pull request #1916 from jumpserver/dev_tag_filter
[Update] 添加(减法)标签过滤器
2018-10-16 10:16:25 +08:00
BaiJiangJie
3fa6807837 [Update] 添加(减法)标签过滤器 2018-10-16 09:53:30 +08:00
ibuler
2c4195d619 Merge remote-tracking branch 'github/dev' into dev 2018-10-15 17:47:32 +08:00
老广
265ef0c8ac Merge pull request #1913 from jumpserver/dev_requirements
[Update] 更新requirements
2018-10-15 17:47:10 +08:00
BaiJiangJie
0b0b06a5c2 [Update] 更新requirements-aliyun 2018-10-15 17:06:01 +08:00
ibuler
d77ba1d5ea Merge remote-tracking branch 'github/dev' into dev 2018-10-15 16:30:08 +08:00
老广
a3bd7cee80 Merge pull request #1912 from jumpserver/dev_cloud_manager
[Update] 修改文案
2018-10-15 16:29:50 +08:00
BaiJiangJie
8d73cd43e1 Merge branch 'dev' into dev_cloud_manager 2018-10-15 16:09:55 +08:00
BaiJiangJie
7e3fd73ae5 [Update] 修改文案 2018-10-15 16:05:53 +08:00
ibuler
70960d2ae4 Merge remote-tracking branch 'github/dev' into dev 2018-10-15 11:34:20 +08:00
老广
d2c574fe9d Merge pull request #1910 from jumpserver/dev_cloud_manager
[Feature] 添加xpack-云管中心模块
2018-10-15 11:30:38 +08:00
BaiJiangJie
a70fcf057b [Update] 修改js,添加3级菜单active属性 2018-10-15 10:48:24 +08:00
ibuler
f37582ec53 [Update] 用户导航加上文件管理 2018-10-15 10:14:20 +08:00
BaiJiangJie
29b87c40fe [Update] 修改云管理模块-文案 2018-10-14 00:03:04 +08:00
BaiJiangJie
1ec77c5bb9 [Update] 添加3级导航菜单-云管理模块 2018-10-12 20:29:04 +08:00
BaiJiangJie
21c71aba93 [Update] Merge branch dev_cloud_manager (baijiangjie_local) to dev 2018-10-12 18:41:01 +08:00
BaiJiangJie
f8db9f480e [Update] 添加翻译-云管理模块 2018-10-12 18:26:56 +08:00
老广
0665644fd0 Merge pull request #1905 from jumpserver/dev
支持命令过滤
2018-10-12 15:33:52 +08:00
ibuler
7bafa546b5 [Update] 修改版本 2018-10-12 15:05:04 +08:00
ibuler
666815b324 [Update] 修改文案 2018-10-12 15:04:01 +08:00
ibuler
532abb86b5 [Update] 添加文案说明和翻译 2018-10-11 11:10:47 +08:00
ibuler
76d4e4ad55 [Update] 修改翻译 2018-10-10 19:31:28 +08:00
ibuler
70fa43adaa [Update] 修改一些逻辑 2018-10-10 19:29:53 +08:00
ibuler
44bf01d4ed [Update] 支持命令过滤 2018-10-10 15:37:20 +08:00
老广
1341983fd3 Merge pull request #1878 from jumpserver/dev
[Update] 更改版本号
2018-10-08 19:13:12 +08:00
ibuler
78936bf9f2 [Update] 更改版本号 2018-10-08 19:03:02 +08:00
老广
9a5d3cb475 Merge pull request #1877 from jumpserver/dev
支持web sftp
2018-10-08 18:57:40 +08:00
ibuler
9bddc29da4 [Update] 暂时不存status数据 2018-09-29 10:44:52 +08:00
ibuler
d68a4d9cae [Update] 修改背景色 2018-09-29 10:28:12 +08:00
ibuler
5457118fb6 [Update] 修改css 2018-09-21 22:49:38 +08:00
ibuler
7ee68f7eeb [Update] 修改static 2018-09-21 20:35:58 +08:00
ibuler
2063f2f257 [Update] Add file to static 2018-09-21 16:37:33 +08:00
ibuler
2637c608a6 [Update] 修改gunicorn配置 2018-09-14 11:29:26 +08:00
老广
32519ea326 Merge pull request #1819 from jumpserver/dev
Dev
2018-09-14 11:08:03 +08:00
ibuler
3ce9d01b6d [Bugfix] 修复coco无法查看资产的bug 2018-09-13 12:16:49 +08:00
ibuler
310bc6ad0b [Update] 修复获取token的bug 2018-09-13 11:41:44 +08:00
ibuler
b54afbe7bb [Bugfix] 修复组织管理员无法查看用户授权的bug 2018-09-13 11:17:55 +08:00
ibuler
ab848afdb9 [Bugfix] 修复创建节点bug 2018-09-12 15:19:40 +08:00
ibuler
5bb867d10d [Update] 添加禁用用户mfa脚本 2018-09-12 11:36:27 +08:00
ibuler
0eda8865e6 [Bugfix] 修复首页显示问题 2018-09-12 11:24:07 +08:00
老广
2a37107abc Merge pull request #1805 from jumpserver/dev
Dev
2018-09-11 16:04:18 +08:00
ibuler
c78107f62f [Bugfix] Remote datepicker bug 2018-09-11 16:01:49 +08:00
ibuler
b022bf36ba [Update] 使用gthread替代eventlet 2018-09-11 12:42:49 +08:00
ibuler
6dc2272a26 [Update] 修复无法取消授权资产的前端bug 2018-09-11 12:17:17 +08:00
老广
1d462aea1b Merge pull request #1796 from jumpserver/dev
Dev
2018-09-07 12:42:55 +08:00
ibuler
88a29c0a93 [Update] 增加手动结束非正常关闭的连接 2018-09-07 12:40:26 +08:00
ibuler
9ffae722f3 [Update] 升级gunicorn版本 2018-09-05 12:26:46 +08:00
老广
9ab2f4bc56 Merge pull request #1780 from jumpserver/dev
[Update] 修改图标
2018-09-05 10:55:26 +08:00
ibuler
41e7f45c20 [Update] 修改图标 2018-09-05 10:53:12 +08:00
老广
9945ac172b Merge pull request #1775 from jumpserver/dev
[Update] 修改web terminal访问bug
2018-09-04 17:10:13 +08:00
ibuler
67ddd42b3d [Update] 修改web terminal访问bug 2018-09-04 17:09:37 +08:00
老广
03adddefa3 Merge pull request #1771 from jumpserver/dev
Dev
2018-09-04 16:05:19 +08:00
ibuler
60b7ccddc0 [Update] 更新翻译 2018-09-04 16:03:42 +08:00
老广
1194932bc0 Merge pull request #1770 from jumpserver/dev
settings加密存储
单独推送系统用户到某个资产
支持了 改密日志 操作日志
翻译更加完善,支持切换语言
2018-09-04 15:29:11 +08:00
ibuler
5c8fd91cf9 [Update] 修复上传录像文件,无法读取的问题 2018-09-04 12:19:20 +08:00
ibuler
bb13003a10 [Bugfix] 修复资产权限 2018-09-03 19:41:44 +08:00
ibuler
9a18817dbb [Bugfix] 修复rdp系统用户密码无法成功的问题 2018-09-03 19:40:06 +08:00
ibuler
2c4966c678 [Update] 修改form 2018-09-03 15:33:35 +08:00
ibuler
d1390a1cd7 [Update] 修改版本 2018-09-03 11:28:56 +08:00
老广
fe45d839fb Dev2 (#1766)
* [Update] 初始化操作日志

* [Feature] 完成操作日志记录

* [Update] 修改mfa失败提示

* [Update] 修改增加created by内容

* [Update] 增加改密日志

* [Update] 登录日志迁移到日志审计中

* [Update] change block user logic, if login success, clean block limit

*  [Update] 更新中/英文翻译(ALL) (#1662)

* Revert "授权页面分页问题"

* 增加命令导出 (#1566)

* [Update] gunicorn不使用eventlet

* [Update] 添加eventlet

* 替换淘宝IP查询接口

* [Feature] 添加命令记录下载功能 (#1559)

* [Feature] 添加命令记录下载功能

* [Update] 文案修改,导出记录、提交,取消全部命令导出

* [Update] 命令导出,修复时间问题

* [Update] paramiko => 2.4.1

* [Update] 修改settings

* [Update] 修改权限判断

* Dev (#1646)

* [Update] 添加org

* [Update] 修改url

* [Update] 完成基本框架

* [Update] 修改一些逻辑

* [Update] 修改用户view

* [Update] 修改资产

* [Update] 修改asset api

* [Update] 修改协议小问题

* [Update] stash it

* [Update] 修改约束

* [Update] 修改外键为org_id

* [Update] 删掉Premiddleware

* [Update] 修改Node

* [Update] 修改get_current_org 为 proxy对象 current_org

* [Bugfix] 解决Node.root() 死循环,移动AdminRequired到permission中 (#1571)

* [Update] 修改permission (#1574)

* Tmp org (#1579)

* [Update] 添加org api, 升级到django 2.0

* [Update] fix some bug

* [Update] 修改一些bug

* [Update] 添加授权规则org (#1580)

* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;

* Tmp org (#1583)

* [Update] 修改一些内容

* [Update] 修改datatable 支持process

* [Bugfix] 修复asset queryset 没有valid方法的bug

* [Update] 在线/历史/命令model添加org;修复命令记录保存org失败bug (#1584)

* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;

* [Update] 在线/历史/命令model添加org

* [Bugfix] 修复命令记录,保存org不成功bug

* [Update] Org功能修改

* [Bugfix] 修复merge带来的问题

* [Update] org admin显示资产详情右侧选项卡;修复资产授权添加用户,会显示其他org用户的bug (#1594)

* [Bugfix] 修复资产授权添加用户,显示其他org的用户bug

* [Update] org admin 显示资产详情右侧选项卡

* Tmp org (#1596)

* [Update] 修改index view

* [Update] 修改nav

* [Update] 修改profile

* [Bugfix] 修复org下普通用户打开web终端看不到已被授权的资产和节点bug

* [Update] 修改get_all_assets

* [Bugfix] 修复节点前面有个空目录

* [Bugfix] 修复merge引起的bug

* [Update] Add init

* [Update] Node get_all_assets 过滤游离资产,条件nodes_key=None -> nodes=None

* [Update] 恢复原来的api地址

* [Update] 修改api

* [Bugfix] 修复org下用户查看我的资产不显示已授权节点/资产的bug

* [Bugfix] Fix perm name unique

* [Bugfix] 修复校验失败api

* [Update] Merge with org

* [Merge] 修改一下bug

* [Update] 暂时修改一些url

* [Update] 修改url 为django 2.0 path

* [Update] 优化datatable 和显示组织优化

* [Update] 升级url

* [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode(… (#1613)

* [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode() method的bug

* [Bugfix] (task任务系统)修复资产连接性测试、硬件刷新和系统用户连接性测试失败等bug

* [Bugfix] 修复一些bug

* [Bugfix] 修复一些bug

*  [Update] 更新org下普通用户的资产详情 (#1619)

* [Update] 更新org下普通用户查看资产详情,只显示数据

* [Update] 优化org下普通用户查看资产详情前端代码

* [Update] 创建/更新用户的role选项;密码强度提示信息中英文; (#1623)

* [Update] 修改 超级管理员/组织管理员 在 创建/更新 用户时role的选项 问题

* [Update] 用户密码强度提示信息支持中英文

* [Update] 修改token返回

* [Update] Asset返回org name

* [Update] 修改支持xpack

* [Update] 修改url

* [Bugfix] 修复不登录就能查看资产的bug

* [Update] 用户修改

* [Bugfix] ...

* [Bugfix] 修复跳转错误的问题

*  [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug; (#1644)

* [Update] 更新xpack下orgs的翻译信息

* [Update] 更新model Label,继承OrgModelMixin;

* [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug;

* [Bugfix] 修复小bug

* [Update] 优化一些api

* [Update] 优化用户资产页面

* [Update] 更新 xpack/orgs 删除功能:限制在当前org下删除当前org (#1645)

* [Update] 修改版本号

* [Update] 添加功能: 语言切换(中/英);修改 header_bar <商业支持、文档>显示方式

* [Update] 中/英切换文案修改;修改django_language key 从 settings 中获取

* [Update] 修改Dashboard页面文案,支持英文

* [Update] 更新中/英文翻译(ALL)

* [Update] 解决翻译文件冲突

* [Update] 系统用户支持单独隋松

* [Update] 重置用户MFA

* [Update] 设置session空闲时间

* [Update] 加密setting配置

* [Update] 修改单独推送和测试资产可连接性

*  [Update] 添加功能:用户个人详情页添加 更改MFA操作 (#1748)

* [Update] 添加功能:用户个人详情页添加 更改MFA操作

* [Update] 删除print

* [Bugfix] 添加部分views的权限控制;从组织移除用户,同时从授权规则和用户组中移除此用户。 (#1746)

* [Bugfix] 修复上传command log 为空

* [Update] 修复执行任务的bug

* [Bugfix] 修复将用户从组内移除,其依然具有之前的组权限的bug, perms and user_groups

* [Bugfix] 修复组管理员可以访问部分url-views的bug(如: /settings/)添加views权限控制

* [Update] 修改日志滚动

* [Bugfix] 修复组织权限控制的bug (#1763)

* [Bugfix] 修复将用户从组内移除,其依然具有之前的组权限的bug, perms and user_groups

* [Bugfix] 修复组管理员可以访问部分url-views的bug(如: /settings/)添加views权限控制
2018-09-03 11:24:25 +08:00
老广
9f96f1c537 Merge pull request #1737 from jumpserver/dev
Dev
2018-08-24 12:12:38 +08:00
老广
dc918c031c Merge pull request #1736 from jumpserver/bugfix_run_adhoc
[Update] 修复执行任务的bug
2018-08-24 12:11:27 +08:00
ibuler
6b047ca702 Merge remote-tracking branch 'github/dev' into bugfix_run_adhoc 2018-08-24 12:09:29 +08:00
ibuler
47d31005b5 [Update] 修复执行任务的bug 2018-08-24 12:08:42 +08:00
老广
57e1ca93f0 Merge pull request #1715 from jumpserver/dev
[Bugfix] 修复上传command log 为空
2018-08-20 08:41:15 -05:00
ibuler
483a7617ce [Bugfix] 修复上传command log 为空 2018-08-20 21:39:15 +08:00
老广
5470ab752e Merge pull request #1702 from jumpserver/dev
Dev
2018-08-16 06:19:27 -05:00
ibuler
2dbd6b6f6e [Bugfix] 修复获取资产失败 2018-08-16 18:29:54 +08:00
老广
504d9242c6 Merge pull request #1700 from jumpserver/dev
Dev
2018-08-16 04:47:13 -05:00
ibuler
14b1e3fa13 [Update] 添加backup脚本 2018-08-16 17:08:45 +08:00
ibuler
7eeca511f1 [Bugfix] 修复打印日志的bug 2018-08-16 16:40:00 +08:00
ibuler
670c8a6d0b [Bugfix] 修复唯一认证 2018-08-16 16:32:49 +08:00
老广
a2aa923abe Merge pull request #1698 from jumpserver/dev
Dev
2018-08-15 23:53:49 -05:00
ibuler
2ac5786ba1 [Bugfix] 修复Hostname可能不唯一引起的任务执行失败 2018-08-16 12:44:39 +08:00
ibuler
5b93a1a0a5 [Bugfix] 修复单页面bug 2018-08-15 17:44:06 +08:00
ibuler
00928dd46d [Update] 不选择节点默认导出所有 2018-08-15 15:01:27 +08:00
ibuler
7ddf7f2a79 [Update] 修改cpu的数量问题 2018-08-15 12:00:47 +08:00
ibuler
3533bf588b [Bugfix] 修复修改资产报错无提示的问题 2018-08-14 18:08:07 +08:00
老广
dea007f27b Merge pull request #1688 from jumpserver/dev
[Update] 支持full value
2018-08-14 03:01:12 -05:00
ibuler
cd2b88caee [Update] 修改节点full value 2018-08-14 15:59:53 +08:00
ibuler
1877511acf [Update] 更改组织同事更改节点名称 2018-08-14 10:16:38 +08:00
ibuler
1c5ce61ed0 [Update] 修改Org刷新cache 2018-08-13 18:26:13 +08:00
老广
b1132bfc37 Merge pull request #1683 from jumpserver/dev
Dev
2018-08-13 02:19:27 -05:00
ibuler
75e67410cf [Bugfix] 修复批量更新资产和用户的bug 2018-08-13 15:01:56 +08:00
ibuler
c9d137bc20 [Update] 修改Mixin,允许org_id不填写 2018-08-13 11:44:44 +08:00
ibuler
d97e606503 [Update] 修改django版本 2018-08-13 11:02:00 +08:00
ibuler
e59b95e97a Merge remote-tracking branch 'github/dev' into dev 2018-08-13 10:46:02 +08:00
ibuler
bb6394150d [Update] 修改批量更新资产的bug 2018-08-12 15:52:16 +08:00
ibuler
2354f0c970 [Bugfix] 修复无法批量禁用启用的bug 2018-08-10 17:11:17 +08:00
老广
ae564ed0d4 Merge pull request #1670 from jumpserver/dev
Dev
2018-08-10 03:16:35 -05:00
ibuler
05ecd7497a [Update] 不可以更改root节点名称 2018-08-10 16:11:40 +08:00
ibuler
6b86b8b485 [Bugfix] 修复资产数量不对的bug 2018-08-10 14:46:04 +08:00
老广
fa0bd85fd4 Merge pull request #1667 from jumpserver/dev
[Bugfix] 修复点击资产报错
2018-08-09 21:07:40 -05:00
ibuler
7da46354ca [Update] 去掉debug 2018-08-10 09:29:26 +08:00
ibuler
e41aad1576 [Bugfix] 修复点击资产报错 2018-08-10 09:12:24 +08:00
ibuler
3f049440b7 [Bugfix] 修复可能引起的创建外键失败 2018-08-08 13:10:58 +08:00
老广
4f532f588b Merge pull request #1651 from jumpserver/dev
Dev
2018-08-07 22:48:26 -05:00
老广
a792781b98 Merge branch 'master' into dev 2018-08-07 22:48:12 -05:00
ibuler
3a4c7846bf [Bugfix] 修改创建第一个节点的Bug 2018-08-08 11:44:06 +08:00
ibuler
ccc292d9a9 [Update] 去掉debug信息 2018-08-07 18:38:46 +08:00
pufer
337338ebf3 增加多OU支持 (#1262)
* 增加多OU支持

* Update models.py

need to import LDAPSearchUnion
2018-08-07 05:11:46 -05:00
ibuler
aa3bc7b53a [Update] 权限修改 2018-08-07 16:20:22 +08:00
老广
d5451a482a Dev (#1646)
* [Update] 添加org

* [Update] 修改url

* [Update] 完成基本框架

* [Update] 修改一些逻辑

* [Update] 修改用户view

* [Update] 修改资产

* [Update] 修改asset api

* [Update] 修改协议小问题

* [Update] stash it

* [Update] 修改约束

* [Update] 修改外键为org_id

* [Update] 删掉Premiddleware

* [Update] 修改Node

* [Update] 修改get_current_org 为 proxy对象 current_org

* [Bugfix] 解决Node.root() 死循环,移动AdminRequired到permission中 (#1571)

* [Update] 修改permission (#1574)

* Tmp org (#1579)

* [Update] 添加org api, 升级到django 2.0

* [Update] fix some bug

* [Update] 修改一些bug

* [Update] 添加授权规则org (#1580)

* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;

* Tmp org (#1583)

* [Update] 修改一些内容

* [Update] 修改datatable 支持process

* [Bugfix] 修复asset queryset 没有valid方法的bug

* [Update] 在线/历史/命令model添加org;修复命令记录保存org失败bug (#1584)

* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;

* [Update] 在线/历史/命令model添加org

* [Bugfix] 修复命令记录,保存org不成功bug

* [Update] Org功能修改

* [Bugfix] 修复merge带来的问题

* [Update] org admin显示资产详情右侧选项卡;修复资产授权添加用户,会显示其他org用户的bug (#1594)

* [Bugfix] 修复资产授权添加用户,显示其他org的用户bug

* [Update] org admin 显示资产详情右侧选项卡

* Tmp org (#1596)

* [Update] 修改index view

* [Update] 修改nav

* [Update] 修改profile

* [Bugfix] 修复org下普通用户打开web终端看不到已被授权的资产和节点bug

* [Update] 修改get_all_assets

* [Bugfix] 修复节点前面有个空目录

* [Bugfix] 修复merge引起的bug

* [Update] Add init

* [Update] Node get_all_assets 过滤游离资产,条件nodes_key=None -> nodes=None

* [Update] 恢复原来的api地址

* [Update] 修改api

* [Bugfix] 修复org下用户查看我的资产不显示已授权节点/资产的bug

* [Bugfix] Fix perm name unique

* [Bugfix] 修复校验失败api

* [Update] Merge with org

* [Merge] 修改一下bug

* [Update] 暂时修改一些url

* [Update] 修改url 为django 2.0 path

* [Update] 优化datatable 和显示组织优化

* [Update] 升级url

* [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode(… (#1613)

* [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode() method的bug

* [Bugfix] (task任务系统)修复资产连接性测试、硬件刷新和系统用户连接性测试失败等bug

* [Bugfix] 修复一些bug

* [Bugfix] 修复一些bug

*  [Update] 更新org下普通用户的资产详情 (#1619)

* [Update] 更新org下普通用户查看资产详情,只显示数据

* [Update] 优化org下普通用户查看资产详情前端代码

* [Update] 创建/更新用户的role选项;密码强度提示信息中英文; (#1623)

* [Update] 修改 超级管理员/组织管理员 在 创建/更新 用户时role的选项 问题

* [Update] 用户密码强度提示信息支持中英文

* [Update] 修改token返回

* [Update] Asset返回org name

* [Update] 修改支持xpack

* [Update] 修改url

* [Bugfix] 修复不登录就能查看资产的bug

* [Update] 用户修改

* [Bugfix] ...

* [Bugfix] 修复跳转错误的问题

*  [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug; (#1644)

* [Update] 更新xpack下orgs的翻译信息

* [Update] 更新model Label,继承OrgModelMixin;

* [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug;

* [Bugfix] 修复小bug

* [Update] 优化一些api

* [Update] 优化用户资产页面

* [Update] 更新 xpack/orgs 删除功能:限制在当前org下删除当前org (#1645)

* [Update] 修改版本号
2018-08-06 23:34:35 -05:00
ibuler
534734881c [Update] 修改版本号 2018-08-07 11:32:11 +08:00
ibuler
8236c7baa0 Merge remote-tracking branch 'github/dev' into dev 2018-08-06 18:17:38 +08:00
BaiJiangJie
96ec5fac99 [Update] 更新 xpack/orgs 删除功能:限制在当前org下删除当前org (#1645) 2018-08-06 05:17:17 -05:00
ibuler
b7fcf80fc5 [Update] 优化用户资产页面 2018-08-06 18:16:25 +08:00
ibuler
de3695bf97 Merge remote-tracking branch 'github/dev' into dev 2018-08-06 17:17:49 +08:00
ibuler
7c814080b2 [Update] 优化一些api 2018-08-06 17:16:52 +08:00
BaiJiangJie
f5531b6065 [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug; (#1644)
* [Update] 更新xpack下orgs的翻译信息

* [Update] 更新model Label,继承OrgModelMixin;

* [Update] xpack/orgs组织添加删除功能-js; 修复Label继承Org后bug;

* [Bugfix] 修复小bug
2018-08-06 03:08:05 -05:00
ibuler
b0aa9f197a [Bugfix] 修复跳转错误的问题 2018-08-06 12:20:27 +08:00
ibuler
fc156e23f3 [Bugfix] ... 2018-08-06 10:39:22 +08:00
ibuler
1a05dab572 [Update] 修改user api权限 2018-08-06 10:28:30 +08:00
ibuler
b8ecb6f81d [Update] 用户修改 2018-08-06 10:24:51 +08:00
ibuler
c01936facc [Bugfix] 修复不登录就能查看资产的bug 2018-08-03 10:32:02 +08:00
ibuler
90c629c837 [Update] 修改url 2018-08-03 10:25:41 +08:00
ibuler
c9d192eefc Merge remote-tracking branch 'github/org' into org 2018-08-02 12:37:17 +08:00
ibuler
9c4ebf9c75 [Update] 修改支持xpack 2018-08-02 12:36:31 +08:00
ibuler
37d89b4ea2 Merge branch 'tmp_org' into org 2018-08-01 13:07:12 +08:00
ibuler
87e0e1f2c4 [Update] Asset返回org name 2018-08-01 13:06:50 +08:00
ibuler
183ff09530 [Update] 修改token返回 2018-08-01 12:35:48 +08:00
BaiJiangJie
485a178c0a [Update] 创建/更新用户的role选项;密码强度提示信息中英文; (#1623)
* [Update] 修改 超级管理员/组织管理员 在 创建/更新 用户时role的选项 问题

* [Update] 用户密码强度提示信息支持中英文
2018-07-31 21:44:43 -05:00
BaiJiangJie
227cc4e965 [Update] 更新org下普通用户的资产详情 (#1619)
* [Update] 更新org下普通用户查看资产详情,只显示数据

* [Update] 优化org下普通用户查看资产详情前端代码
2018-07-30 22:18:38 -05:00
ibuler
01bef95e6e Merge remote-tracking branch 'github/org' into org 2018-07-31 11:01:31 +08:00
ibuler
672dd66023 [Bugfix] 修复一些bug 2018-07-31 11:00:48 +08:00
ibuler
c032294b14 [Bugfix] 修复一些bug 2018-07-31 11:00:21 +08:00
BaiJiangJie
6ce813faf8 [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode(… (#1613)
* [Bugfix] 修复coco启动失败(load_config_from_server)、硬件刷新,测试连接,str 没有 decode() method的bug

* [Bugfix] (task任务系统)修复资产连接性测试、硬件刷新和系统用户连接性测试失败等bug
2018-07-30 21:27:55 -05:00
ibuler
aefc18d73b Merge branch 'org' into tmp_org 2018-07-27 19:02:26 +08:00
ibuler
23815f87c5 [Update] 升级url 2018-07-27 18:56:40 +08:00
ibuler
206e037cf2 [Update] 优化datatable 和显示组织优化 2018-07-27 18:55:07 +08:00
ibuler
492fd98882 [Update] 修改url 为django 2.0 path 2018-07-27 17:43:18 +08:00
ibuler
d92d462dab [Update] 暂时修改一些url 2018-07-27 16:54:40 +08:00
ibuler
8afd5ef90a [Merge] 修改一下bug 2018-07-27 16:21:55 +08:00
ibuler
d3dca5d077 [Update] Merge with org 2018-07-27 16:19:19 +08:00
ibuler
9166a26f80 Merge branch 'org' into tmp_org 2018-07-27 16:16:29 +08:00
ibuler
3039284666 [Bugfix] 修复校验失败api 2018-07-27 16:15:04 +08:00
老广
2f395794ef Merge pull request #1602 from jumpserver/bai_org
[Bugfix] 修复org下用户查看我的资产不显示已授权节点/资产的bug
2018-07-27 03:13:59 -05:00
ibuler
c6d50802db [Bugfix] Fix perm name unique 2018-07-27 15:35:12 +08:00
BaiJiangJie
a10e47f72c Merge branch 'org' into michael_org 2018-07-27 15:24:52 +08:00
BaiJiangJie
3dc214d1fa [Bugfix] 修复org下用户查看我的资产不显示已授权节点/资产的bug 2018-07-27 15:24:09 +08:00
ibuler
f7fb36a176 [Update] 修改api 2018-07-27 15:21:14 +08:00
ibuler
0d7295b60e [Update] 恢复原来的api地址 2018-07-27 15:10:48 +08:00
BaiJiangJie
8f654c37a9 [Update] Node get_all_assets 过滤游离资产,条件nodes_key=None -> nodes=None 2018-07-27 13:01:41 +08:00
ibuler
b29a541aa6 [Update] Add init 2018-07-27 12:55:59 +08:00
ibuler
9fd52f6665 [Bugfix] 修复merge引起的bug 2018-07-27 12:14:13 +08:00
ibuler
f4c86718dc [Update] Merge dev 2018-07-27 12:12:32 +08:00
老广
4ff7a1f066 Merge pull request #1600 from jumpserver/bai_org
[Bugfix] 修复org下普通用户打开web终端看不到已被授权的资产和节点bug
2018-07-26 23:08:55 -05:00
ibuler
eca245fdd5 [Bugfix] 修复节点前面有个空目录 2018-07-27 11:59:39 +08:00
ibuler
7e3cf908a1 [Update] 修改get_all_assets 2018-07-27 11:09:37 +08:00
老广
dded4e10fb Merge pull request #1599 from jumpserver/dev
Dev
2018-07-26 21:45:32 -05:00
BaiJiangJie
45a354f848 [Bugfix] 修复org下普通用户打开web终端看不到已被授权的资产和节点bug 2018-07-27 10:42:32 +08:00
ibuler
8386f107c6 Merge remote-tracking branch 'github/dev' into dev 2018-07-26 19:32:23 +08:00
ibuler
5ce3dd4079 [Update] 添加unblock user脚本 2018-07-26 19:30:37 +08:00
老广
a48fb9de8d Tmp org (#1596)
* [Update] 修改index view

* [Update] 修改nav

* [Update] 修改profile
2018-07-26 05:27:09 -05:00
BaiJiangJie
04e7f54c69 [Update] org admin显示资产详情右侧选项卡;修复资产授权添加用户,会显示其他org用户的bug (#1594)
* [Bugfix] 修复资产授权添加用户,显示其他org的用户bug

* [Update] org admin 显示资产详情右侧选项卡
2018-07-26 05:25:44 -05:00
BaiJiangJie
d649aacfd6 [Update] asset platform 取消*required (#1595) 2018-07-26 05:25:14 -05:00
ibuler
7e65e44a3c [Update] 兼容guacamole手动模式上传system user是uuid 2018-07-26 18:12:25 +08:00
ibuler
74c3f12275 [Update] 添加脚本,将windows协议改为rdp 2018-07-26 14:37:10 +08:00
ibuler
8c12c382a5 [Bugfix] 修复merge带来的问题 2018-07-25 18:21:37 +08:00
ibuler
2ecfecb06f [Update] Merge with dev 2018-07-25 18:18:18 +08:00
BaiJiangJie
ac238aa36e [Update] 修改用户登录失败限制次数,3->7 (#1586)
* [Update] 修改用户登录失败限制次数,3->7

* [Update] 修改用户登录失败限制次数,3->7 - 续
2018-07-25 04:51:09 -05:00
ibuler
2abb9efe96 [Update] Org功能修改 2018-07-25 17:21:13 +08:00
BaiJiangJie
f17727deb9 [Update] 在线/历史/命令model添加org;修复命令记录保存org失败bug (#1584)
* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;

* [Update] 在线/历史/命令model添加org

* [Bugfix] 修复命令记录,保存org不成功bug
2018-07-25 02:13:53 -05:00
老广
36f1165d1b Tmp org (#1583)
* [Update] 修改一些内容

* [Update] 修改datatable 支持process

* [Bugfix] 修复asset queryset 没有valid方法的bug
2018-07-25 02:05:28 -05:00
BaiJiangJie
e7c530d8e6 [Update] 添加授权规则org (#1580)
* [Update] 修复创建授权规则,显示org_name不是有效UUID的bug

* [Update] 更新org之间隔离授权规则,解决QuerySet与Manager问题;修复创建用户,显示org_name不是有效UUID之bug;
2018-07-24 22:28:20 -05:00
老广
b156f4ad16 Tmp org (#1579)
* [Update] 添加org api, 升级到django 2.0

* [Update] fix some bug

* [Update] 修改一些bug
2018-07-24 22:21:12 -05:00
老广
16b23a37fe 增加命令导出 (#1566)
* [Update] gunicorn不使用eventlet

* [Update] 添加eventlet

* 替换淘宝IP查询接口

* [Feature] 添加命令记录下载功能 (#1559)

* [Feature] 添加命令记录下载功能

* [Update] 文案修改,导出记录、提交,取消全部命令导出

* [Update] 命令导出,修复时间问题

* [Update] paramiko => 2.4.1

* [Update] 修改settings

* [Update] 修改权限判断
2018-07-24 22:09:58 -05:00
ibuler
e41add6126 Merge remote-tracking branch 'github/dev' into dev 2018-07-25 10:23:18 +08:00
ibuler
f4c31d8e86 [Update] 修改权限判断 2018-07-25 10:22:32 +08:00
ibuler
28e8f204ec Merge remote-tracking branch 'github/dev' into dev 2018-07-24 16:41:17 +08:00
ibuler
80f147cf13 [Update] 修改settings 2018-07-24 16:40:41 +08:00
老广
c816875f28 [Update] 修改permission (#1574) 2018-07-22 23:55:13 -05:00
ibuler
1c56ba5a11 [Update] paramiko => 2.4.1 2018-07-20 18:48:57 +08:00
老广
2208d6d51e [Bugfix] 解决Node.root() 死循环,移动AdminRequired到permission中 (#1571) 2018-07-20 05:42:01 -05:00
ibuler
e3aa18ff2d [Update] 修改get_current_org 为 proxy对象 current_org 2018-07-20 17:49:47 +08:00
ibuler
b5f6f80ae6 [Update] 修改Node 2018-07-20 13:25:50 +08:00
ibuler
bbe4080008 [Update] 删掉Premiddleware 2018-07-20 12:15:45 +08:00
ibuler
cd797b18fb Merge remote-tracking branch 'github/dev' into dev 2018-07-20 11:10:51 +08:00
ibuler
b6523da603 [Update] 修改外键为org_id 2018-07-20 10:54:16 +08:00
ibuler
c24f1a0517 [Update] 修改约束 2018-07-19 19:24:29 +08:00
BaiJiangJie
83f220d7de [Feature] 添加命令记录下载功能 (#1559)
* [Feature] 添加命令记录下载功能

* [Update] 文案修改,导出记录、提交,取消全部命令导出

* [Update] 命令导出,修复时间问题
2018-07-19 05:36:52 -05:00
老广
8e42a65736 Merge pull request #1560 from wojiushixiaobai/dev
[Update]替换淘宝IP查询接口
2018-07-19 05:03:59 -05:00
wojiushixiaobai
e1fff18ce3 替换淘宝IP查询接口 2018-07-19 14:14:58 +08:00
ibuler
3052744203 [Update] 添加eventlet 2018-07-18 19:17:20 +08:00
ibuler
7924b094f8 [Update] gunicorn不使用eventlet 2018-07-18 16:15:35 +08:00
老广
3d34b06203 Merge pull request #1551 from jumpserver/dev
[Bugfix] 修复转义引起的中文乱码问题
2018-07-18 02:33:33 -05:00
ibuler
c94d018d7e [Bugfix] 修复转义引起的中文乱码问题 2018-07-18 15:31:14 +08:00
老广
53086a8977 Merge pull request #1550 from jumpserver/dev
Dev
2018-07-18 02:28:13 -05:00
ibuler
ce1fc0f3e2 [Bugfix] 修复授权列表引起的分页bug 2018-07-18 15:26:58 +08:00
ibuler
c215278978 [Bugfix] 修复授权列表引起的分页bug 2018-07-18 15:24:58 +08:00
老广
061963a316 Merge pull request #1549 from jumpserver/revert-1515-master
Revert "授权页面分页问题"
2018-07-18 01:56:18 -05:00
老广
97b240cfdd Revert "授权页面分页问题" 2018-07-18 14:55:16 +08:00
ibuler
fd5f562cbf [Update] stash it 2018-07-18 12:57:08 +08:00
老广
722bf786f1 Merge pull request #1542 from jumpserver/dev
Dev to master
2018-07-17 04:26:37 -05:00
老广
2cb5876d1a Merge branch 'master' into dev 2018-07-17 04:25:37 -05:00
ibuler
8883e0090f [Update] 更改版本号 2018-07-17 17:12:28 +08:00
ibuler
790652ff4d [Update] 修改协议小问题 2018-07-17 17:09:36 +08:00
ibuler
ff3f74abe6 [Update] 修改api 2018-07-17 12:06:47 +08:00
老广
1a49cf4d9c Merge pull request #1532 from wojiushixiaobai/dev
[Update]修改url地址
2018-07-16 22:24:09 -05:00
ibuler
4d1da56872 [Update] 修改asset api 2018-07-16 14:47:06 +08:00
老广
ff9e109a2c Merge pull request #1533 from jumpserver/update_unblockuser
[Update] 取消系统用户-清除认证信息,取消-网关rdp协议认证信息,添加解除用户登录限制功能
2018-07-16 01:42:30 -05:00
BaiJiangJie
09e636495e [Update] 模版删除unblock的id属性 2018-07-16 12:22:32 +08:00
BaiJiangJie
14076d8fe1 Merge branch 'dev' of github.com:jumpserver/jumpserver into github_dev 2018-07-16 12:13:59 +08:00
BaiJiangJie
812078331e [Update] 添加解除用户登录限制功能 2018-07-16 12:13:13 +08:00
wojiushixiaobai
696589a3cf 修改url 2018-07-16 12:13:06 +08:00
wojiushixiaobai
7eba46b303 Merge branch 'dev' of https://github.com/jumpserver/jumpserver into dev 2018-07-16 11:01:49 +08:00
wojiushixiaobai
5d63d2369f 合并冲突解决 2018-07-16 11:01:06 +08:00
ibuler
3e17e94245 [Update] 修改资产 2018-07-15 18:39:11 +08:00
ibuler
5648dcd7e7 [Update] 修改用户view 2018-07-15 00:55:05 +08:00
ibuler
28e47f33c1 [Update] 修改一些逻辑 2018-07-14 00:47:21 +08:00
老广
91b3b7ce69 Merge pull request #1515 from wstart/master
授权页面分页问题
2018-07-13 08:22:34 -05:00
老广
8c587a1376 Merge pull request #1523 from wojiushixiaobai/dev
[Update]修改url地址
2018-07-13 08:19:40 -05:00
BaiJiangJie
1182313c1a [Update] 取消系统用户-清除认证信息,取消-网关rdp协议认证信息,添加用户unblock功能 2018-07-13 19:30:48 +08:00
ibuler
7412bdcba7 [Update] 完成基本框架 2018-07-13 15:05:46 +08:00
ibuler
d6ec92d82d [Update] 修改url 2018-07-13 00:31:50 +08:00
ibuler
ad3214641d [Update] 添加org 2018-07-13 00:00:35 +08:00
wojiushixiaobai
9004351ad1 修正字符串 2018-07-12 16:47:35 +08:00
wojiushixiaobai
d3e22a2a90 修改url地址 2018-07-12 10:53:39 +08:00
wstart
fd3df81a64 Update api.py 2018-07-10 17:03:31 +08:00
ycfuck
72517a2c72 授权页面分页
授权页面分页
2018-07-10 16:33:28 +08:00
ibuler
01185a2d07 [Update] 添加gateway api 2018-07-10 16:03:05 +08:00
ibuler
8bfd2be21f [Update] 清除系统用户认证信息时提示 2018-07-10 15:50:15 +08:00
老广
b03ac46df9 Merge pull request #1456 from wojiushixiaobai/dev
[Update]修改提示文字
2018-07-10 02:35:26 -05:00
老广
76be054fcb Merge pull request #1502 from Nevss/patch-1
update docker-guacamole version  to latest
2018-07-10 02:32:06 -05:00
老广
df95c93bb1 Merge pull request #1503 from jumpserver/update_loginlog
[Update] 记录用户登录失败日志,限制用户登录失败次数
2018-07-10 02:31:39 -05:00
ycfuck
eaefb5c669 Revert "资产授权页面增加分页"
This reverts commit 0ddb9476ba.
2018-07-09 16:00:02 +08:00
林峰任1049851
0ddb9476ba 资产授权页面增加分页
资产授权页面增加分页
2018-07-09 15:55:07 +08:00
ibuler
75a9deebdd Merge remote-tracking branch 'github/dev' into dev 2018-07-08 13:03:43 +08:00
ibuler
b6cd4a20c5 [Bugfix] Data table xss attack 2018-07-08 13:02:43 +08:00
BaiJiangJie
435acafccd [Update] 在线会话,历史会话显示登录来源 Web/SSH Terminal 2018-07-06 13:22:20 +08:00
BaiJiangJie
86e4bc5e9a Merge branch 'dev' of github.com:jumpserver/jumpserver into github_dev 2018-07-06 10:57:42 +08:00
ibuler
315609bc45 Merge remote-tracking branch 'github/dev' into dev 2018-07-06 10:35:31 +08:00
ibuler
814e6a7df8 [Update] 修改sshpass 引号引入密码,特殊字符处理 2018-07-06 10:34:16 +08:00
Nevss
7a7c6d40df update docker-guacamole version to latest
update docker-guacamole version  to latest
2018-07-05 19:08:35 +08:00
BaiJiangJie
5d800fa629 [Update] 修改coco端登录限制次数逻辑 2018-07-05 17:07:03 +08:00
BaiJiangJie
bd14266abd [Update] 修改登录日志模型类的MFA字段默认状态为- 2018-07-05 16:45:05 +08:00
BaiJiangJie
88f36c6f02 Merge branch 'dev' of github.com:jumpserver/jumpserver into github_dev 2018-07-05 16:23:48 +08:00
BaiJiangJie
512fc8f8f0 [Update] 优化登录失败次数限制的逻辑,并添加系统安全设置选项 2018-07-05 16:23:33 +08:00
ibuler
37bb344166 [Bugfix] 修复网关端口不对的问题 2018-07-05 10:58:52 +08:00
ibuler
f8c2a445f7 [Bugfix] 修改gateway port 2018-07-05 10:26:16 +08:00
BaiJiangJie
ff9b1a887f [Update] 添加用户登录失败次数限制 2018-07-04 16:55:11 +08:00
BaiJiangJie
43370c547a [Update] 登录日志逻辑修改变量名 2018-07-04 10:57:37 +08:00
BaiJiangJie
2eda58eadd Merge branch 'dev' of github.com:jumpserver/jumpserver into github_dev 2018-07-03 17:48:22 +08:00
BaiJiangJie
e1be867913 [Update] 更新用户登录日志,记录登录失败日志并添加MFA启用状态信息 2018-07-03 17:47:12 +08:00
ibuler
01afcf701c Merge remote-tracking branch 'origin/dev' into dev 2018-07-03 13:24:07 +08:00
ibuler
4002289974 [Bugfix] 修复详情中删除时的bug 2018-07-03 13:23:21 +08:00
ibuler
ef9e03c7ed Merge remote-tracking branch 'github/dev' into dev 2018-07-02 12:11:45 +08:00
老广
442d4e727a Merge pull request #1492 from jumpserver/feature_telnet
[Feature] 添加功能,支持 telnet server.
2018-07-01 22:23:20 -05:00
BaiJiangJie
3993797527 Merge branch 'dev' of github.com:jumpserver/jumpserver into github_dev 2018-06-29 15:00:15 +08:00
BaiJiangJie
f1f06491d6 [Feature] 添加功能, 支持 telnet server. 2018-06-29 14:59:43 +08:00
ibuler
eb95a0a912 [Bugfix] 修改下载录像没有目录的bug 2018-06-27 10:34:16 +08:00
ibuler
401a7f88a8 [Update] 更新依赖storage的版本 2018-06-26 22:27:16 +08:00
ibuler
0a2ff83ca1 Merge remote-tracking branch 'origin/dev' into dev 2018-06-26 22:14:21 +08:00
ibuler
7276bd0b2a [Update] 更新获取session log的方法,以后统一到media/replay中 2018-06-26 22:13:39 +08:00
wojiushixiaobai
2950613b69 修改提示文字 2018-06-20 15:58:01 +08:00
老广
6fa0562d7a Merge pull request #1441 from chnliyong/master
python-gssapi libkrb5-dev for gssapi/gssapi.h
2018-06-18 21:25:15 -05:00
老广
ef22d33afa Merge pull request #1161 from Nidhoggur1993/dev
更新make_migrations.sh的migration逻辑
2018-06-13 21:53:23 -05:00
老广
86404db6c7 Merge pull request #1414 from leostudio/dev
[fixed #1404]新增Redis DB配置
2018-06-13 21:51:53 -05:00
老广
fdf2807d9b Merge pull request #1419 from jumpserver/feature_login_mode
[Update] 添加功能,系统用户选择登录模式(自动/手动登录)
2018-06-13 21:25:00 -05:00
BaiJiangJie
2e6d238c76 [Bugfix] 修复系统用户选择手动登录,ssh命令行连接获取不到login_mode字段值 2018-06-11 19:09:24 +08:00
BaiJiangJie
f5a4370b80 [Update] 系统用户选择手动登录,取消自动推送 2018-06-11 17:56:20 +08:00
BaiJiangJie
db2273ef27 Merge branch 'dev' of github.com:jumpserver/jumpserver into github_dev 2018-06-11 16:28:10 +08:00
BaiJiangJie
dd07fa678f [Update] 添加功能,系统用户选择登录模式(自动/手动登录) 2018-06-11 16:27:40 +08:00
梁志艳
a2f23e9681 新增Redis DB配置 2018-06-09 00:19:57 +08:00
wojiushixiaobai
7c35e75586 更新自动升级脚本 2018-04-25 19:49:22 +08:00
Nidhoggur1993
4fd83bd5be Update make_migrations.sh
增加自动合并而不是在弹出warning结束
2018-04-03 16:02:51 +08:00
chnliyong
6035d1f130 python-gssapi libkrb5-dev for gssapi/gssapi.h 2018-03-10 10:46:28 +08:00
321 changed files with 74777 additions and 5218 deletions

View File

@@ -1,7 +1,8 @@
[简述你的问题]
##### 使用版本
[请提供你使用的Jumpserver版本 0.3.2 或 0.5.0]
[请提供你使用的Jumpserver版本 1.x.x 注: 0.3.x不再提供支持]
##### 问题复现步骤
1. [步骤1]

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ django.db
celerybeat-schedule.db
data/static
docs/_build/
xpack

View File

@@ -1,9 +1,9 @@
## 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-1.11-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Ansible](https://img.shields.io/badge/ansible-2.2.2.0-blue.svg?style=plastic)](https://www.ansible.com/)
[![Paramiko](https://img.shields.io/badge/paramiko-2.1.2-green.svg?style=plastic)](http://www.paramiko.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/)
----
@@ -19,25 +19,25 @@ Jumpserver采纳分布式架构支持多机房跨区域部署中心节点
----
### 功能
![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver13.jpg "Jumpserver功能")
![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver-14.png "Jumpserver功能")
### 开始使用
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/latest/quickstart.html)
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/latest/step_by_step.html)
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
### Demo 和 截图
### Demo 和 截图
我们提供了DEMO和截图可以让你快速了解Jumpserver
[DEMO](http://demo.jumpserver.org)
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
### SDK
### SDK
我们还编写了一些SDK供你其它系统快速和Jumpserver APi交互

View File

@@ -2,4 +2,4 @@
# -*- coding: utf-8 -*-
#
__version__ = "1.3.2"
__version__ = "1.4.4"

View File

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

View File

@@ -17,10 +17,11 @@ from django.db import transaction
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from ..hands import IsSuperUser
from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset
from .. import serializers
from ..tasks import test_admin_user_connectability_manual
@@ -37,21 +38,29 @@ class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet):
"""
Admin user api set, for add,delete,update,list,retrieve resource
"""
filter_fields = ("name", "username")
search_fields = filter_fields
queryset = AdminUser.objects.all()
serializer_class = serializers.AdminUserSerializer
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
class AdminUserAuthApi(generics.UpdateAPIView):
queryset = AdminUser.objects.all()
serializer_class = serializers.AdminUserAuthSerializer
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
class ReplaceNodesAdminUserApi(generics.UpdateAPIView):
queryset = AdminUser.objects.all()
serializer_class = serializers.ReplaceNodeAdminUserSerializer
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
def update(self, request, *args, **kwargs):
admin_user = self.get_object()
@@ -75,7 +84,7 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView):
Test asset admin user connectivity
"""
queryset = AdminUser.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
admin_user = self.get_object()

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
#
import random
from rest_framework import generics
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
@@ -11,8 +13,8 @@ from django.db.models import Q
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from ..hands import IsSuperUser, IsValidUser, IsSuperUserOrAppUser
from ..models import Asset, SystemUser, AdminUser, Node
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..models import Asset, AdminUser, Node
from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \
test_asset_connectability_manual
@@ -22,7 +24,8 @@ from ..utils import LabelFilter
logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetListUpdateApi',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi'
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
'AssetGatewayApi'
]
@@ -36,38 +39,42 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
queryset = Asset.objects.all()
serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination
permission_classes = (IsSuperUserOrAppUser,)
permission_classes = (IsOrgAdminOrAppUser,)
def get_queryset(self):
queryset = super().get_queryset()\
.prefetch_related('labels', 'nodes')\
.select_related('admin_user')
admin_user_id = self.request.query_params.get('admin_user_id')
def filter_node(self):
node_id = self.request.query_params.get("node_id")
show_current_asset = self.request.query_params.get("show_current_asset")
if not node_id:
return
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)
else:
self.queryset = self.queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
)
def filter_admin_user_id(self):
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)
queryset = queryset.filter(admin_user=admin_user)
self.queryset = self.queryset.filter(admin_user=admin_user)
if node_id and show_current_asset:
node = get_object_or_404(Node, id=node_id)
if node.is_root():
queryset = queryset.filter(
Q(nodes=node_id) | Q(nodes__isnull=True)
).distinct()
else:
queryset = queryset.filter(nodes=node).distinct()
if node_id and not show_current_asset:
node = get_object_or_404(Node, id=node_id)
if node.is_root():
queryset = Asset.objects.all()
else:
queryset = queryset.filter(
nodes__key__regex='^{}(:[0-9]+)*$'.format(node.key),
).distinct()
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()
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
@@ -76,7 +83,7 @@ class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
"""
queryset = Asset.objects.all()
serializer_class = serializers.AssetSerializer
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
@@ -85,7 +92,7 @@ class AssetRefreshHardwareApi(generics.RetrieveAPIView):
"""
queryset = Asset.objects.all()
serializer_class = serializers.AssetSerializer
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')
@@ -99,10 +106,27 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView):
Test asset admin user connectivity
"""
queryset = Asset.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
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)
return Response({"task": task.id})
class AssetGatewayApi(generics.RetrieveAPIView):
queryset = Asset.objects.all()
permission_classes = (IsOrgAdminOrAppUser,)
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
if asset.domain and \
asset.domain.gateways.filter(protocol=asset.protocol).exists():
gateway = random.choice(asset.domain.gateways.filter(protocol=asset.protocol))
serializer = serializers.GatewayWithAuthSerializer(instance=gateway)
return Response(serializer.data)
else:
return Response({"msg": "Not have gateway"}, status=404)

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
#
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from django.shortcuts import get_object_or_404
from ..hands import IsOrgAdmin
from ..models import CommandFilter, CommandFilterRule
from .. import serializers
__all__ = ['CommandFilterViewSet', 'CommandFilterRuleViewSet']
class CommandFilterViewSet(BulkModelViewSet):
filter_fields = ("name",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
queryset = CommandFilter.objects.all()
serializer_class = serializers.CommandFilterSerializer
pagination_class = LimitOffsetPagination
class CommandFilterRuleViewSet(BulkModelViewSet):
filter_fields = ("content",)
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self):
fpk = self.kwargs.get('filter_pk')
if not fpk:
return CommandFilterRule.objects.none()
cmd_filter = get_object_or_404(CommandFilter, pk=fpk)
return cmd_filter.rules.all()

View File

@@ -2,12 +2,12 @@
from rest_framework_bulk import BulkModelViewSet
from rest_framework.views import APIView, Response
from rest_framework.generics import RetrieveAPIView
from rest_framework.pagination import LimitOffsetPagination
from django.views.generic.detail import SingleObjectMixin
from common.utils import get_logger
from ..hands import IsSuperUser, IsSuperUserOrAppUser
from common.permissions import IsOrgAdmin, IsAppUser, IsOrgAdminOrAppUser
from ..models import Domain, Gateway
from ..utils import test_gateway_connectability
from .. import serializers
@@ -19,8 +19,13 @@ __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"]
class DomainViewSet(BulkModelViewSet):
queryset = Domain.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.DomainSerializer
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
def get_serializer_class(self):
if self.request.query_params.get('gateway'):
@@ -29,20 +34,21 @@ class DomainViewSet(BulkModelViewSet):
def get_permissions(self):
if self.request.query_params.get('gateway'):
self.permission_classes = (IsSuperUserOrAppUser,)
self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions()
class GatewayViewSet(BulkModelViewSet):
filter_fields = ("domain",)
filter_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = filter_fields
queryset = Gateway.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.GatewaySerializer
pagination_class = LimitOffsetPagination
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
model = Gateway
object = None

View File

@@ -14,10 +14,11 @@
# limitations under the License.
from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination
from django.db.models import Count
from common.utils import get_logger
from ..hands import IsSuperUser
from ..hands import IsOrgAdmin
from ..models import Label
from .. import serializers
@@ -27,12 +28,18 @@ __all__ = ['LabelViewSet']
class LabelViewSet(BulkModelViewSet):
queryset = Label.objects.annotate(asset_count=Count("assets"))
permission_classes = (IsSuperUser,)
filter_fields = ("name", "value")
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LabelSerializer
pagination_class = LimitOffsetPagination
def list(self, request, *args, **kwargs):
if request.query_params.get("distinct"):
self.serializer_class = serializers.LabelDistinctSerializer
self.queryset = self.queryset.values("name").distinct()
return super().list(request, *args, **kwargs)
def get_queryset(self):
self.queryset = Label.objects.annotate(asset_count=Count("assets"))
return self.queryset

View File

@@ -13,16 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from rest_framework import generics, mixins
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 ..hands import IsSuperUser
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import update_assets_hardware_info_util, test_asset_connectability_util
from .. import serializers
@@ -30,18 +31,16 @@ from .. import serializers
logger = get_logger(__file__)
__all__ = [
'NodeViewSet', 'NodeChildrenApi',
'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi',
'NodeReplaceAssetsApi',
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
'TestNodeConnectiveApi'
]
class NodeViewSet(BulkModelViewSet):
class NodeViewSet(viewsets.ModelViewSet):
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
def perform_create(self, serializer):
@@ -49,38 +48,22 @@ class NodeViewSet(BulkModelViewSet):
serializer.validated_data["key"] = child_key
serializer.save()
# class NodeWithAssetsApi(generics.ListAPIView):
# permission_classes = (IsSuperUser,)
# serializers = serializers.NodeSerializer
#
# def get_node(self):
# pk = self.kwargs.get('pk') or self.request.query_params.get('node')
# if not pk:
# node = Node.root()
# else:
# node = get_object_or_404(Node, pk)
# return node
#
# def get_queryset(self):
# queryset = []
# node = self.get_node()
# children = node.get_children()
# assets = node.get_assets()
# queryset.extend(list(children))
#
# for asset in assets:
# node = Node()
# node.id = asset.id
# node.parent = node.id
# node.value = asset.hostname
# queryset.append(node)
# return queryset
def update(self, request, *args, **kwargs):
node = self.get_object()
if node.is_root():
node_value = node.value
post_value = request.data.get('value')
if node_value != post_value:
return Response(
{"msg": _("You can't update the root node name")},
status=400
)
return super().update(request, *args, **kwargs)
class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
instance = None
@@ -126,22 +109,26 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
query_all = self.request.query_params.get("all")
query_assets = self.request.query_params.get('assets')
node = self.get_object()
if node is None:
node = Node.root()
node.assets__count = node.get_all_assets().count()
queryset.append(node)
if query_all:
children = node.get_all_children()
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.parent_id = node.id
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)
@@ -152,7 +139,7 @@ class NodeChildrenApi(mixins.ListModelMixin, generics.CreateAPIView):
class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
def get_queryset(self):
@@ -167,7 +154,7 @@ class NodeAssetsApi(generics.ListAPIView):
class NodeAddChildrenApi(generics.UpdateAPIView):
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeAddChildrenSerializer
instance = None
@@ -185,7 +172,7 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
class NodeAddAssetsApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAssetsSerializer
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@@ -197,7 +184,7 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
class NodeRemoveAssetsApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAssetsSerializer
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@@ -213,7 +200,7 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
class NodeReplaceAssetsApi(generics.UpdateAPIView):
serializer_class = serializers.NodeAssetsSerializer
queryset = Node.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@@ -224,26 +211,28 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
class RefreshNodeHardwareInfoApi(APIView):
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
model = Node
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()
task_name = _("更新节点资产硬件信息: {}".format(node.name))
# 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)
return Response({"task": task.id})
class TestNodeConnectiveApi(APIView):
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
model = Node
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()
task_name = _("测试节点下资产是否可连接: {}".format(node.name))
# 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)
return Response({"task": task.id})

View File

@@ -13,21 +13,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.
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
from rest_framework.pagination import LimitOffsetPagination
from common.utils import get_logger
from ..hands import IsSuperUser, IsSuperUserOrAppUser
from ..models import SystemUser
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
test_system_user_connectability_manual, push_system_user_a_asset_manual, \
test_system_user_connectability_a_asset
logger = get_logger(__file__)
__all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi'
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectabilityApi', 'SystemUserCommandFilterRuleListApi',
]
@@ -35,9 +42,16 @@ class SystemUserViewSet(BulkModelViewSet):
"""
System user api set, for add,delete,update,list,retrieve resource
"""
filter_fields = ("name", "username")
search_fields = filter_fields
queryset = SystemUser.objects.all()
serializer_class = serializers.SystemUserSerializer
permission_classes = (IsSuperUserOrAppUser,)
permission_classes = (IsOrgAdminOrAppUser,)
pagination_class = LimitOffsetPagination
def get_queryset(self):
queryset = super().get_queryset().all()
return queryset
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
@@ -45,7 +59,7 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
Get system user auth info
"""
queryset = SystemUser.objects.all()
permission_classes = (IsSuperUserOrAppUser,)
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.SystemUserAuthSerializer
def destroy(self, request, *args, **kwargs):
@@ -59,7 +73,7 @@ class SystemUserPushApi(generics.RetrieveAPIView):
Push system user to cluster assets api
"""
queryset = SystemUser.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
@@ -75,9 +89,62 @@ class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
Push system user to cluster assets api
"""
queryset = SystemUser.objects.all()
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
task = test_system_user_connectability_manual.delay(system_user)
return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
pagination_class = LimitOffsetPagination
filter_fields = ("hostname", "ip")
search_fields = filter_fields
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=pk)
def get_queryset(self):
system_user = self.get_object()
return system_user.assets.all()
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
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 = push_system_user_a_asset_manual.delay(system_user, asset)
return Response({"task": task.id})
class SystemUserTestAssetConnectabilityApi(generics.RetrieveAPIView):
queryset = SystemUser.objects.all()
permission_classes = (IsOrgAdmin,)
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)
return Response({"task": task.id})
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get_serializer_class(self):
from ..serializers import CommandFilterRuleSerializer
return CommandFilterRuleSerializer
def get_queryset(self):
pk = self.kwargs.get('pk', None)
system_user = get_object_or_404(SystemUser, pk=pk)
return system_user.cmd_filter_rules

View File

@@ -4,3 +4,4 @@ from .asset import *
from .label import *
from .user import *
from .domain import *
from .cmd_filter import *

View File

@@ -3,20 +3,23 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from ..models import Asset, AdminUser
from common.utils import get_logger
from orgs.mixins import OrgModelForm
from ..models import Asset, AdminUser
logger = get_logger(__file__)
__all__ = ['AssetCreateForm', 'AssetUpdateForm', 'AssetBulkUpdateForm']
class AssetCreateForm(forms.ModelForm):
class AssetCreateForm(OrgModelForm):
class Meta:
model = Asset
fields = [
'hostname', 'ip', 'public_ip', 'port', 'comment',
'nodes', 'is_active', 'admin_user', 'labels', 'platform',
'domain',
'domain', 'protocol',
]
widgets = {
@@ -45,18 +48,18 @@ class AssetCreateForm(forms.ModelForm):
'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu'
),
'platform': _("* required Must set exact system platform, Windows, Linux ..."),
'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"),
'domain': _("If your have some network not connect with each other, you can set domain")
}
class AssetUpdateForm(forms.ModelForm):
class AssetUpdateForm(OrgModelForm):
class Meta:
model = Asset
fields = [
'hostname', 'ip', 'port', 'nodes', 'is_active', 'platform',
'public_ip', 'number', 'comment', 'admin_user', 'labels',
'domain',
'domain', 'protocol',
]
widgets = {
'nodes': forms.SelectMultiple(attrs={
@@ -85,12 +88,12 @@ class AssetUpdateForm(forms.ModelForm):
'root or other NOPASSWD sudo privilege user existed in asset,'
'If asset is windows or other set any one, more see admin user left menu'
),
'platform': _("* required Must set exact system platform, Windows, Linux ..."),
'platform': _("Windows 2016 RDP protocol is different, If is window 2016, set it"),
'domain': _("If your have some network not connect with each other, you can set domain")
}
class AssetBulkUpdateForm(forms.ModelForm):
class AssetBulkUpdateForm(OrgModelForm):
assets = forms.ModelMultipleChoiceField(
required=True, help_text='* required',
label=_('Select assets'), queryset=Asset.objects.all(),
@@ -105,7 +108,7 @@ class AssetBulkUpdateForm(forms.ModelForm):
label=_('Port'), required=False, min_value=1, max_value=65535,
)
admin_user = forms.ModelChoiceField(
required=False, queryset=AdminUser.objects.all(),
required=False, queryset=AdminUser.objects,
label=_("Admin user"),
widget=forms.Select(
attrs={

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
#
from django import forms
from orgs.mixins import OrgModelForm
from ..models import CommandFilter, CommandFilterRule
__all__ = ['CommandFilterForm', 'CommandFilterRuleForm']
class CommandFilterForm(OrgModelForm):
class Meta:
model = CommandFilter
fields = ['name', 'comment']
class CommandFilterRuleForm(OrgModelForm):
class Meta:
model = CommandFilterRule
fields = [
'filter', 'type', 'content', 'priority', 'action', 'comment'
]
widgets = {
'content': forms.Textarea(attrs={
'placeholder': 'eg:\r\nreboot\r\nrm -rf'
}),
}

View File

@@ -3,6 +3,7 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from orgs.mixins import OrgModelForm
from ..models import Domain, Asset, Gateway
from .user import PasswordAndKeyAuthForm
@@ -34,7 +35,11 @@ class DomainForm(forms.ModelForm):
return instance
class GatewayForm(PasswordAndKeyAuthForm):
class GatewayForm(PasswordAndKeyAuthForm, OrgModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
password_field = self.fields.get('password')
password_field.help_text = _('Password should not contain special characters')
def save(self, commit=True):
# Because we define custom field, so we need rewrite :method: `save`

View File

@@ -3,8 +3,9 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from ..models import AdminUser, SystemUser
from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
from orgs.mixins import OrgModelForm
from ..models import AdminUser, SystemUser
logger = get_logger(__file__)
__all__ = [
@@ -85,7 +86,7 @@ class AdminUserForm(PasswordAndKeyAuthForm):
}
class SystemUserForm(PasswordAndKeyAuthForm):
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
# Admin user assets define, let user select, save it in form not in view
auto_generate_key = forms.BooleanField(initial=True, required=False)
@@ -93,14 +94,24 @@ class SystemUserForm(PasswordAndKeyAuthForm):
# Because we define custom field, so we need rewrite :method: `save`
system_user = super().save()
password = self.cleaned_data.get('password', '') or None
login_mode = self.cleaned_data.get('login_mode', '') or None
protocol = self.cleaned_data.get('protocol') or None
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]:
system_user.auto_push = 0
auto_generate_key = False
system_user.save()
if auto_generate_key:
logger.info('Auto generate key and set system user auth')
system_user.auto_gen_auth()
else:
system_user.set_auth(password=password, private_key=private_key, public_key=public_key)
system_user.set_auth(password=password, private_key=private_key,
public_key=public_key)
return system_user
def clean(self):
@@ -109,20 +120,38 @@ class SystemUserForm(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
class Meta:
model = SystemUser
fields = [
'name', 'username', 'protocol', 'auto_generate_key',
'password', 'private_key_file', 'auto_push', 'sudo',
'comment', 'shell', 'priority',
'comment', 'shell', 'priority', 'login_mode', 'cmd_filters',
]
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
'cmd_filters': forms.SelectMultiple(attrs={
'class': 'select2', 'data-placeholder': _('Command filter')
}),
}
help_texts = {
'name': '* required',
'username': '* required',
'auto_push': _('Auto push system user to asset'),
'priority': _('High level will be using login asset as default, if user was granted more than 2 system user'),
}
'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.')
}

View File

@@ -11,6 +11,6 @@
"""
from common.mixins import AdminUserRequiredMixin
from common.permissions import IsAppUser, IsSuperUser, IsValidUser, IsSuperUserOrAppUser
from common.permissions import AdminUserRequiredMixin
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
from users.models import User, UserGroup

View File

@@ -1,11 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
from .user import AdminUser, SystemUser
from .user import *
from .label import Label
from .cluster import *
from .group import *
from .domain import *
from .node import *
from .asset import *
from .cmd_filter import *
from .utils import *

View File

@@ -6,13 +6,16 @@ import uuid
import logging
import random
from functools import reduce
from collections import defaultdict
from django.db import models
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
__all__ = ['Asset']
logger = logging.getLogger(__name__)
@@ -31,7 +34,8 @@ def default_cluster():
def default_node():
try:
from .node import Node
return Node.root()
root = Node.root()
return root
except:
return None
@@ -44,12 +48,7 @@ class AssetQuerySet(models.QuerySet):
return self.active()
class AssetManager(models.Manager):
def get_queryset(self):
return AssetQuerySet(self.model, using=self._db)
class Asset(models.Model):
class Asset(OrgModelMixin):
# Important
PLATFORM_CHOICES = (
('Linux', 'Linux'),
@@ -57,16 +56,25 @@ class Asset(models.Model):
('MacOS', 'MacOS'),
('BSD', 'BSD'),
('Windows', 'Windows'),
('Windows2016', 'Windows(2016)'),
('Other', 'Other'),
)
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
TELNET_PROTOCOL = 'telnet'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'),
)
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, unique=True,
verbose_name=_('Hostname'))
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'))
port = models.IntegerField(default=22, verbose_name=_('Port'))
platform = models.CharField(max_length=128, choices=PLATFORM_CHOICES,
default='Linux', verbose_name=_('Platform'))
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)
@@ -80,11 +88,8 @@ class Asset(models.Model):
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'))
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,
@@ -98,6 +103,7 @@ class Asset(models.Model):
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,
@@ -125,7 +131,7 @@ class Asset(models.Model):
comment = models.TextField(max_length=128, default='', blank=True,
verbose_name=_('Comment'))
objects = AssetManager()
objects = OrgManager.from_queryset(AssetQuerySet)()
def __str__(self):
return '{0.hostname}({0.ip})'.format(self)
@@ -140,7 +146,7 @@ class Asset(models.Model):
return False, warning
def is_unixlike(self):
if self.platform not in ("Windows",):
if self.platform not in ("Windows", "Windows2016"):
return True
else:
return False
@@ -159,11 +165,25 @@ class Asset(models.Model):
nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
return nodes
@classmethod
def get_queryset_by_fullname_list(cls, fullname_list):
org_fullname_map = defaultdict(list)
for fullname in fullname_list:
hostname, org = cls.split_fullname(fullname)
org_fullname_map[org].append(hostname)
filter_arg = Q()
for org, hosts in org_fullname_map.items():
if org.is_real():
filter_arg |= Q(hostname__in=hosts, org_id=org.id)
else:
filter_arg |= Q(Q(org_id__isnull=True) | Q(org_id=''), hostname__in=hosts)
return Asset.objects.filter(filter_arg)
@property
def hardware_info(self):
if self.cpu_count:
return '{} Core {} {}'.format(
self.cpu_count * self.cpu_cores,
self.cpu_vcpus or self.cpu_count * self.cpu_cores,
self.memory, self.disk_total
)
else:
@@ -199,6 +219,16 @@ class Asset(models.Model):
'become': self.admin_user.become_info,
}
def as_node(self):
from .node import Node
fake_node = Node()
fake_node.id = self.id
fake_node.key = self.id
fake_node.value = self.hostname
fake_node.asset = self
fake_node.is_node = False
return fake_node
def _to_secret_json(self):
"""
Ansible use it create inventory, First using asset user,
@@ -219,7 +249,7 @@ class Asset(models.Model):
return data
class Meta:
unique_together = ('ip', 'port')
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")
@classmethod

View File

@@ -11,15 +11,16 @@ from django.conf import settings
from common.utils import get_signer, ssh_key_string_to_obj, ssh_key_gen
from common.validators import alphanumeric
from orgs.mixins import OrgModelMixin
from .utils import private_key_validator
signer = get_signer()
class AssetUser(models.Model):
class AssetUser(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
username = models.CharField(max_length=32, verbose_name=_('Username'), validators=[alphanumeric])
name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric])
_password = models.CharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
_private_key = models.TextField(max_length=4096, blank=True, null=True, verbose_name=_('SSH private key'), validators=[private_key_validator, ])
_public_key = models.TextField(max_length=4096, blank=True, verbose_name=_('SSH public key'))

View File

@@ -52,7 +52,8 @@ class Cluster(models.Model):
contact=forgery_py.name.full_name(),
phone=forgery_py.address.phone(),
address=forgery_py.address.city() + forgery_py.address.street_address(),
operator=choice(['北京联通', '北京电信', 'BGP全网通']),
# operator=choice(['北京联通', '北京电信', 'BGP全网通']),
operator=choice([_('Beijing unicom'), _('Beijing telecom'), _('BGP full netcom')]),
comment=forgery_py.lorem_ipsum.sentence(),
created_by='Fake')
try:

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
#
import uuid
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
__all__ = [
'CommandFilter', 'CommandFilterRule'
]
class CommandFilter(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=64, verbose_name=_("Name"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
def __str__(self):
return self.name
class CommandFilterRule(OrgModelMixin):
TYPE_REGEX = 'regex'
TYPE_COMMAND = 'command'
TYPE_CHOICES = (
(TYPE_REGEX, _('Regex')),
(TYPE_COMMAND, _('Command')),
)
ACTION_DENY, ACTION_ALLOW = range(2)
ACTION_CHOICES = (
(ACTION_DENY, _('Deny')),
(ACTION_ALLOW, _('Allow')),
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules')
type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type"))
priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the higher will be match first"),
validators=[MinValueValidator(1), MaxValueValidator(100)])
content = models.TextField(max_length=1024, verbose_name=_("Content"), help_text=_("One line one command"))
action = models.IntegerField(default=ACTION_DENY, choices=ACTION_CHOICES, verbose_name=_("Action"))
comment = models.CharField(max_length=64, blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
created_by = models.CharField(max_length=128, blank=True, default='', verbose_name=_('Created by'))
class Meta:
ordering = ('-priority', 'action')
def __str__(self):
return '{} % {}'.format(self.type, self.content)

View File

@@ -7,18 +7,22 @@ import random
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from .base import AssetUser
__all__ = ['Domain', 'Gateway']
class Domain(models.Model):
class Domain(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
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'))
class Meta:
verbose_name = _("Domain")
def __str__(self):
return self.name
@@ -43,10 +47,13 @@ class Gateway(AssetUser):
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"))
domain = models.ForeignKey(Domain, verbose_name=_("Domain"))
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"))
def __str__(self):
return self.name
class Meta:
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")

View File

@@ -4,9 +4,10 @@
import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
class Label(models.Model):
class Label(OrgModelMixin):
SYSTEM_CATEGORY = "S"
USER_CATEGORY = "U"
CATEGORY_CHOICES = (
@@ -34,4 +35,4 @@ class Label(models.Model):
class Meta:
db_table = "assets_label"
unique_together = ('name', 'value')
unique_together = [('name', 'value', 'org_id')]

View File

@@ -5,12 +5,16 @@ import uuid
from django.db import models, transaction
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.utils import with_cache
from django.core.cache import cache
from orgs.mixins import OrgModelMixin
from orgs.utils import set_current_org, get_current_org
from orgs.models import Organization
__all__ = ['Node']
class Node(models.Model):
class Node(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
key = models.CharField(unique=True, max_length=64, verbose_name=_("Key")) # '1:1:1:1'
value = models.CharField(max_length=128, verbose_name=_("Value"))
@@ -18,11 +22,17 @@ class Node(models.Model):
date_create = models.DateTimeField(auto_now_add=True)
is_node = True
_full_value_cache_key_prefix = '_NODE_VALUE_{}'
class Meta:
verbose_name = _("Node")
def __str__(self):
return self.full_value
def __eq__(self, other):
if not other:
return False
return self.key == other.key
def __gt__(self, other):
@@ -30,12 +40,10 @@ class Node(models.Model):
return True
self_key = [int(k) for k in self.key.split(':')]
other_key = [int(k) for k in other.key.split(':')]
if len(self_key) < len(other_key):
return True
elif len(self_key) > len(other_key):
return False
else:
return self_key[-1] < other_key[-1]
return self_key.__lt__(other_key)
def __lt__(self, other):
return not self.__gt__(other)
@property
def name(self):
@@ -43,10 +51,29 @@ class Node(models.Model):
@property
def full_value(self):
ancestor = [a.value for a in self.get_ancestor(with_self=True)]
key = self._full_value_cache_key_prefix.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
return ' / '.join(ancestor)
parent_full_value = self.parent.full_value
value = parent_full_value + ' / ' + self.value
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)
cache.delete_pattern(key+'*')
@property
def level(self):
@@ -65,7 +92,7 @@ class Node(models.Model):
return child
def get_children(self, with_self=False):
pattern = r'^{0}$|^{}:[0-9]+$' if with_self else r'^{}:[0-9]+$'
pattern = r'^{0}$|^{0}:[0-9]+$' if with_self else r'^{0}:[0-9]+$'
return self.__class__.objects.filter(
key__regex=pattern.format(self.key)
)
@@ -93,62 +120,72 @@ class Node(models.Model):
def get_assets(self):
from .asset import Asset
if self.is_root():
assets = Asset.objects.filter(
Q(nodes__id=self.id) | Q(nodes__isnull=True)
)
if self.is_default_node():
assets = Asset.objects.filter(Q(nodes__id=self.id) | Q(nodes__isnull=True))
else:
assets = self.assets.all()
return assets
assets = Asset.objects.filter(nodes__id=self.id)
return assets.distinct()
def get_valid_assets(self):
return self.get_assets().valid()
def get_all_assets(self):
from .asset import Asset
if self.is_root():
assets = Asset.objects.all()
pattern = r'^{0}$|^{0}:'.format(self.key)
args = []
kwargs = {}
if self.is_default_node():
args.append(Q(nodes__key__regex=pattern) | Q(nodes=None))
else:
pattern = r'^{0}$|^{0}:'.format(self.key)
assets = Asset.objects.filter(nodes__key__regex=pattern)
kwargs['nodes__key__regex'] = pattern
assets = Asset.objects.filter(*args, **kwargs).distinct()
return assets
def get_all_valid_assets(self):
return self.get_all_assets().valid()
def is_default_node(self):
return self.is_root() and self.key == '0'
def is_root(self):
return self.key == '0'
if self.key.isdigit():
return True
else:
return False
@property
def parent_key(self):
parent_key = ":".join(self.key.split(":")[:-1])
return parent_key
@property
def parent(self):
if self.key == "0" or not self.key.startswith("0"):
return self.__class__.root()
parent_key = ":".join(self.key.split(":")[:-1])
if self.is_root():
return self
try:
parent = self.__class__.objects.get(key=parent_key)
parent = self.__class__.objects.get(key=self.parent_key)
return parent
except Node.DoesNotExist:
return self.__class__.root()
@parent.setter
def parent(self, parent):
if self.is_node:
children = self.get_all_children()
old_key = self.key
with transaction.atomic():
self.key = parent.get_next_child_key()
for child in children:
child.key = child.key.replace(old_key, self.key, 1)
child.save()
self.save()
else:
self.key = parent.key+':fake'
if not self.is_node:
self.key = parent.key + ':fake'
return
children = self.get_all_children()
old_key = self.key
with transaction.atomic():
self.key = parent.get_next_child_key()
for child in children:
child.key = child.key.replace(old_key, self.key, 1)
child.save()
self.save()
def get_ancestor(self, with_self=False):
if self.is_root():
ancestor = self.__class__.objects.filter(key='0')
return ancestor
root = self.__class__.root()
return [root]
_key = self.key.split(':')
if not with_self:
_key.pop()
@@ -162,10 +199,46 @@ class Node(models.Model):
return ancestor
@classmethod
def root(cls):
obj, created = cls.objects.get_or_create(
key='0', defaults={"key": '0', 'value': "ROOT"}
)
print(obj)
return obj
def create_root_node(cls):
# 如果使用current_org 在set_current_org时会死循环
_current_org = get_current_org()
with transaction.atomic():
if _current_org.is_root():
key = '0'
elif _current_org.is_default():
key = '1'
else:
set_current_org(Organization.root())
org_nodes_roots = cls.objects.filter(key__regex=r'^[0-9]+$')
org_nodes_roots_keys = org_nodes_roots.values_list('key', flat=True) or ['1']
key = max([int(k) for k in org_nodes_roots_keys])
key = str(key + 1) if key != 0 else '2'
set_current_org(_current_org)
root = cls.objects.create(key=key, value=_current_org.name)
return root
@classmethod
def root(cls):
root = cls.objects.filter(key__regex=r'^[0-9]+$')
if root:
return root[0]
else:
return cls.create_root_node()
@classmethod
def default_node(cls):
defaults = {'value': 'Default'}
return cls.objects.get_or_create(defaults=defaults, key='1')
@classmethod
def get_tree_name_ref(cls):
pass
@classmethod
def generate_fake(cls, count=100):
import random
for i in range(count):
node = random.choice(cls.objects.all())
node.create_child('Node {}'.format(i))

View File

@@ -3,11 +3,11 @@
#
import logging
import uuid
from django.core.cache import cache
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from common.utils import get_signer
from ..const import SYSTEM_USER_CONN_CACHE_KEY
@@ -69,6 +69,7 @@ class AdminUser(AssetUser):
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("Admin user")
@classmethod
@@ -95,18 +96,32 @@ class AdminUser(AssetUser):
class SystemUser(AssetUser):
SSH_PROTOCOL = 'ssh'
RDP_PROTOCOL = 'rdp'
TELNET_PROTOCOL = 'telnet'
PROTOCOL_CHOICES = (
(SSH_PROTOCOL, 'ssh'),
(RDP_PROTOCOL, 'rdp'),
(TELNET_PROTOCOL, 'telnet (beta)'),
)
AUTO_LOGIN = 'auto'
MANUAL_LOGIN = 'manual'
LOGIN_MODE_CHOICES = (
(AUTO_LOGIN, _('Automatic login')),
(MANUAL_LOGIN, _('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=10, verbose_name=_("Priority"))
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'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
cache_key = "__SYSTEM_USER_CACHED_{}"
def __str__(self):
return '{0.name}({0.username})'.format(self)
@@ -144,8 +159,35 @@ class SystemUser(AssetUser):
else:
return False
def set_cache(self):
cache.set(self.cache_key.format(self.id), self, 3600)
def expire_cache(self):
cache.delete(self.cache_key.format(self.id))
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule
rules = CommandFilterRule.objects.filter(
filter__in=self.cmd_filters.all()
).distinct()
return rules
@classmethod
def get_system_user_by_id_or_cached(cls, sid):
cached = cache.get(cls.cache_key.format(sid))
if cached:
return cached
try:
system_user = cls.objects.get(id=sid)
system_user.set_cache()
return system_user
except cls.DoesNotExist:
return None
class Meta:
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("System user")
@classmethod

View File

@@ -7,3 +7,4 @@ from .label import *
from .system_user import *
from .node import *
from .domain import *
from .cmd_filter import *

View File

@@ -58,7 +58,7 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
管理用户更新关联到的集群
"""
nodes = serializers.PrimaryKeyRelatedField(
many=True, queryset=Node.objects.all()
many=True, queryset = Node.objects.all()
)
class Meta:

View File

@@ -4,7 +4,7 @@ from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from ..models import Asset, Node
from ..models import Asset
from .system_user import AssetSystemUserSerializer
__all__ = [
@@ -20,12 +20,12 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
model = Asset
list_serializer_class = BulkListSerializer
fields = '__all__'
validators = [] # If not set to [], partial bulk update will be error
validators = []
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields.extend([
'hardware_info', 'is_connective',
'hardware_info', 'is_connective', 'org_name'
])
return fields
@@ -43,7 +43,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer):
fields = (
"id", "hostname", "ip", "port", "system_users_granted",
"is_active", "system_users_join", "os", 'domain',
"platform", "comment"
"platform", "comment", "protocol", "org_id", "org_name",
)
@staticmethod
@@ -61,6 +61,6 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer):
model = Asset
fields = (
"id", "hostname", "system_users_granted",
"is_active", "system_users_join",
"os", "platform", "comment",
"is_active", "system_users_join", "org_name",
"os", "platform", "comment", "org_id", "protocol"
)

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.fields import ChoiceDisplayField
from ..models import CommandFilter, CommandFilterRule, SystemUser
class CommandFilterSerializer(serializers.ModelSerializer):
rules = serializers.PrimaryKeyRelatedField(queryset=CommandFilterRule.objects.all(), many=True)
system_users = serializers.PrimaryKeyRelatedField(queryset=SystemUser.objects.all(), many=True)
class Meta:
model = CommandFilter
fields = '__all__'
class CommandFilterRuleSerializer(serializers.ModelSerializer):
serializer_choice_field = ChoiceDisplayField
class Meta:
model = CommandFilterRule
fields = '__all__'

View File

@@ -26,7 +26,7 @@ class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
model = Node
fields = [
'id', 'key', 'name', 'value', 'parent',
'assets_granted', 'assets_amount',
'assets_granted', 'assets_amount', 'org_id',
]
@staticmethod
@@ -43,12 +43,16 @@ class NodeGrantedSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class NodeSerializer(serializers.ModelSerializer):
parent = serializers.SerializerMethodField()
assets_amount = serializers.SerializerMethodField()
tree_id = serializers.SerializerMethodField()
tree_parent = serializers.SerializerMethodField()
class Meta:
model = Node
fields = ['id', 'key', 'value', 'parent', 'assets_amount', 'is_node']
fields = [
'id', 'key', 'value', 'assets_amount',
'is_node', 'org_id', 'tree_id', 'tree_parent',
]
list_serializer_class = BulkListSerializer
def validate(self, data):
@@ -63,12 +67,16 @@ class NodeSerializer(serializers.ModelSerializer):
return data
@staticmethod
def get_parent(obj):
return obj.parent.id if obj.is_node else obj.parent_id
def get_assets_amount(obj):
return obj.get_all_assets().count()
@staticmethod
def get_assets_amount(obj):
return obj.get_all_assets().count() if obj.is_node else 0
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()
@@ -78,7 +86,7 @@ class NodeSerializer(serializers.ModelSerializer):
class NodeAssetsSerializer(serializers.ModelSerializer):
assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all())
assets = serializers.PrimaryKeyRelatedField(many=True, queryset = Asset.objects.all())
class Meta:
model = Node

View File

@@ -18,6 +18,13 @@ class SystemUserSerializer(serializers.ModelSerializer):
model = SystemUser
exclude = ('_password', '_private_key', '_public_key')
def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
fields.extend([
'get_login_mode_display',
])
return fields
@staticmethod
def get_unreachable_assets(obj):
return obj.unreachable_assets
@@ -46,7 +53,7 @@ class SystemUserAuthSerializer(AuthSerializer):
model = SystemUser
fields = [
"id", "name", "username", "protocol",
"password", "private_key",
"login_mode", "password", "private_key",
]
@@ -56,7 +63,10 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
"""
class Meta:
model = SystemUser
fields = ('id', 'name', 'username', 'priority', 'protocol', 'comment',)
fields = (
'id', 'name', 'username', 'priority',
'protocol', 'comment', 'login_mode'
)
class SystemUserSimpleSerializer(serializers.ModelSerializer):

View File

@@ -86,3 +86,9 @@ def on_node_assets_changed(sender, instance=None, **kwargs):
system_users = SystemUser.objects.filter(nodes=instance)
for system_user in system_users:
system_user.assets.add(*tuple(assets))
@receiver(post_save, sender=Node)
def on_node_update_or_created(sender, instance=None, created=False, **kwargs):
if instance and not created:
instance.expire_full_value()

View File

@@ -44,7 +44,7 @@ def set_assets_hardware_info(result, **kwargs):
logger.error("Get asset info failed: {}".format(hostname))
continue
asset = get_object_or_none(Asset, hostname=hostname)
asset = Asset.objects.get_object_by_fullname(hostname)
if not asset:
continue
@@ -60,6 +60,7 @@ def set_assets_hardware_info(result, **kwargs):
___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_vcpus = info.get('ansible_processor_vcpus', 0)
___memory = '%s %s' % capacity_convert('{} MB'.format(info.get('ansible_memtotal_mb')))
disk_info = {}
for dev, dev_info in info.get('ansible_devices', {}).items():
@@ -92,10 +93,10 @@ def update_assets_hardware_info_util(assets, task_name=None):
"""
from ops.utils import update_or_create_ansible_task
if task_name is None:
# task_name = _("Update some assets hardware info")
task_name = _("更新资产硬件信息")
task_name = _("Update some assets hardware info")
# task_name = _("更新资产硬件信息")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hostname_list = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
hostname_list = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hostname_list:
logger.info("Not hosts get, may be asset is not active or not unixlike platform")
return {}
@@ -112,8 +113,8 @@ 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")
task_name = _("更新资产硬件信息")
task_name = _("Update asset hardware info")
# task_name = _("更新资产硬件信息")
return update_assets_hardware_info_util([asset], task_name=task_name)
@@ -131,10 +132,10 @@ def update_assets_hardware_info_period():
return
from ops.utils import update_or_create_ansible_task
# task_name = _("Update assets hardware info period")
task_name = _("定期更新资产硬件信息")
task_name = _("Update assets hardware info period")
# task_name = _("定期更新资产硬件信息")
hostname_list = [
asset.hostname for asset in Asset.objects.all()
asset.fullname for asset in Asset.objects.all()
if asset.is_active and asset.is_unixlike()
]
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
@@ -181,7 +182,7 @@ def test_admin_user_connectability_util(admin_user, task_name):
from ops.utils import update_or_create_ansible_task
assets = admin_user.get_related_assets()
hosts = [asset.hostname for asset in assets
hosts = [asset.fullname for asset in assets
if asset.is_active and asset.is_unixlike()]
if not hosts:
return
@@ -209,15 +210,15 @@ def test_admin_user_connectability_period():
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
# task_name = _("Test admin user connectability period: {}".format(admin_user.name))
task_name = _("定期测试管理账号可连接性: {}".format(admin_user.name))
task_name = _("Test admin user connectability period: {}".format(admin_user.name))
# task_name = _("定期测试管理账号可连接性: {}".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)
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)
@@ -226,9 +227,9 @@ def test_asset_connectability_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 = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
task_name = _("Test assets connectability")
# task_name = _("测试资产可连接性")
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts:
logger.info("No hosts, passed")
return {}
@@ -271,16 +272,17 @@ def set_system_user_connectablity_info(result, **kwargs):
@shared_task
def test_system_user_connectability_util(system_user, task_name):
def test_system_user_connectability_util(system_user, assets, task_name):
"""
Test system cant connect his assets or not.
:param system_user:
:param assets:
:param task_name:
:return:
"""
from ops.utils import update_or_create_ansible_task
assets = system_user.get_assets()
hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
# assets = system_user.get_assets()
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
tasks = const.TEST_SYSTEM_USER_CONN_TASKS
if not hosts:
logger.info("No hosts, passed")
@@ -298,7 +300,16 @@ def test_system_user_connectability_util(system_user, task_name):
@shared_task
def test_system_user_connectability_manual(system_user):
task_name = _("Test system user connectability: {}").format(system_user)
return test_system_user_connectability_util(system_user, task_name)
assets = system_user.get_assets()
return test_system_user_connectability_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(
system_user, asset
)
return test_system_user_connectability_util(system_user, [asset], task_name)
@shared_task
@@ -312,8 +323,8 @@ def test_system_user_connectability_period():
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))
task_name = _("Test system user connectability period: {}".format(system_user))
# task_name = _("定期测试系统用户可连接性: {}".format(system_user))
test_system_user_connectability_util(system_user, task_name)
@@ -378,7 +389,7 @@ def push_system_user_util(system_users, assets, task_name):
logger.info("Not tasks, passed")
return {}
hosts = [asset.hostname for asset in assets if asset.is_active and asset.is_unixlike()]
hosts = [asset.fullname for asset in assets if asset.is_active and asset.is_unixlike()]
if not hosts:
logger.info("Not hosts, passed")
return {}
@@ -392,13 +403,23 @@ def push_system_user_util(system_users, assets, task_name):
@shared_task
def push_system_user_to_assets_manual(system_user):
assets = system_user.get_assets()
task_name = "推送系统用户到入资产: {}".format(system_user.name)
# task_name = "推送系统用户到入资产: {}".format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util([system_user], assets, task_name=task_name)
@shared_task
def push_system_user_a_asset_manual(system_user, asset):
task_name = _("Push system users to asset: {} => {}").format(
system_user.name, asset.fullname
)
return push_system_user_util([system_user], [asset], task_name=task_name)
@shared_task
def push_system_user_to_assets(system_user, assets):
task_name = _("推送系统用户到入资产: {}").format(system_user.name)
# task_name = _("推送系统用户到入资产: {}").format(system_user.name)
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util.delay([system_user], assets, task_name)

View File

@@ -71,7 +71,7 @@ function initTable2() {
function onSelected2(event, treeNode) {
var url = asset_table2.ajax.url();
url = setUrlParam(url, "node_id", treeNode.id);
url = setUrlParam(url, "node_id", treeNode.node_id);
setCookie('node_selected', treeNode.id);
asset_table2.ajax.url(url);
asset_table2.ajax.reload();
@@ -97,17 +97,20 @@ function initTree2() {
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["pId"] = value["parent"];
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'] + ')';
value['value'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree2"), setting, zNodes);
zTree2 = $.fn.zTree.getZTreeObj("assetTree2");
var root = zTree2.getNodes()[0];
zTree2.expandNode(root);
});
}

View File

@@ -36,12 +36,13 @@
{% endif %}
<h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.login_mode layout="horizontal" %}
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.priority layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
<h3 id="auth_title_id">{% trans 'Auth' %}</h3>
{% block auth %}
<h3>{% trans 'Auth' %}</h3>
<div class="auto-generate">
<div class="form-group">
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
@@ -61,6 +62,10 @@
</div>
</div>
{% endblock %}
<div id="command-filter-block">
<h3>{% trans 'Command filter' %}</h3>
{% bootstrap_field form.cmd_filters layout="horizontal" %}
</div>
<h3>{% trans 'Other' %}</h3>
{% bootstrap_field form.sudo layout="horizontal" %}
{% bootstrap_field form.shell layout="horizontal" %}
@@ -80,23 +85,43 @@
{% endblock %}
{% block custom_foot_js %}
<script>
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
var login_mode_id = '#' + '{{ form.login_mode.id_for_label }}';
var auto_generate_key = '#'+'{{ form.auto_generate_key.id_for_label }}';
var password_id = '#' + '{{ form.password.id_for_label }}';
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
var shell_id = '#' + '{{ form.shell.id_for_label }}';
var need_change_field = [
auto_generate_key, private_key_id, auto_push_id, sudo_id, shell_id
];
var need_change_field_login_mode = [
auto_generate_key, private_key_id, auto_push_id, password_id
];
function protocolChange() {
if ($(protocol_id + " option:selected").text() === 'rdp') {
$('.auth-fields').removeClass('hidden');
$('#command-filter-block').addClass('hidden');
$.each(need_change_field, function (index, value) {
$(value).closest('.form-group').addClass('hidden')
});
} else {
}
else if ($(protocol_id + " option:selected").text() === 'telnet (beta)') {
$('.auth-fields').removeClass('hidden');
$.each(need_change_field, function (index, value) {
$(value).closest('.form-group').addClass('hidden')
});
}
else {
if($(login_mode_id).val() === 'manual'){
$(sudo_id).closest('.form-group').removeClass('hidden');
$(shell_id).closest('.form-group').removeClass('hidden');
return
}
authFieldsDisplay();
$.each(need_change_field, function (index, value) {
$(value).closest('.form-group').removeClass('hidden')
@@ -111,18 +136,35 @@ function authFieldsDisplay() {
$('.auth-fields').removeClass('hidden');
}
}
function loginModeChange(){
if ($(login_mode_id).val() === 'manual'){
$('#auth_title_id').addClass('hidden');
$.each(need_change_field_login_mode, function(index, value){
$(value).closest('.form-group').addClass('hidden')
})
}
else if($(login_mode_id).val() === 'auto'){
$('#auth_title_id').removeClass('hidden');
$(password_id).closest('.form-group').removeClass('hidden')
protocolChange();
}
}
$(document).ready(function () {
$('.select2').select2();
authFieldsDisplay();
protocolChange();
loginModeChange();
})
.on('change', protocol_id, function(){
protocolChange();
})
.on('change', auto_generate_key, function(){
authFieldsDisplay();
});
})
.on('change', login_mode_id, function(){
loginModeChange();
})
</script>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
<style>
.modal-body {
background-color: white !important;
}
</style>
{% block modal_id %}user_asset_detail_modal{% endblock %}
{% block modal_title %}{% trans "Asset detail" %}{% endblock %}
{% block modal_body %}
<div class="ibox-content" style="background-color: inherit">
<table class="table">
<tbody id="asset_detail_tbody">
</tbody>
</table>
</div>
{% endblock %}
{% block modal_button %}
<button data-dismiss="modal" class="btn btn-white" type="button">{% trans "Close" %}</button>
{% endblock %}

View File

@@ -90,7 +90,7 @@
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Select nodes' %}" id="nodes_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.value }}</option>
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>

View File

@@ -5,8 +5,11 @@
{% block help_message %}
<div class="alert alert-info help-message">
管理用户是服务器的root或拥有 NOPASSWD: ALL sudo权限的用户Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。
Windows或其它硬件可以随意设置一个
{# 管理用户是资产(被控服务器)上的root或拥有 NOPASSWD: ALL sudo权限的用户Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
{# Windows或其它硬件可以随意设置一个#}
{% trans 'Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, '%}
{% trans 'Jumpserver users of the system using the user to `push system user`, `get assets hardware information`, etc. '%}
{% trans 'You can set any one for Windows or other hardware.' %}
</div>
{% endblock %}
@@ -90,7 +93,7 @@ $(document).ready(function(){
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }]
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options)
})
.on('click', '.btn_admin_user_delete', function () {
@@ -107,6 +110,3 @@ $(document).ready(function(){
});
</script>
{% endblock %}

View File

@@ -5,9 +5,9 @@
{% block form %}
<div class="ydxbd" id="formlists" style="display: block;">
<p id="tags_p" class="mgl-5 c02">选择需要修改属性</p>
<p id="tags_p" class="mgl-5 c02">{% trans 'Select properties that need to be modified' %}</p>
<div class="tagBtnList">
<a class="label label-primary" id="change_all" value="1">全选</a>
<a class="label label-primary" id="change_all" value="1">{% trans 'Select all' %}</a>
{% for field in form %}
{% if field.name != 'assets' %}
<a data-id="{{ field.id_for_label }}" class="label label-default label-primary field-tag" value="1">{{ field.label }}</a>

View File

@@ -15,9 +15,10 @@
{% csrf_token %}
<h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}
{% bootstrap_field form.domain layout="horizontal" %}
@@ -85,14 +86,14 @@ $(document).ready(function () {
allowClear: true,
templateSelection: format
});
$("#id_platform").change(function (){
var platform = $("#id_platform option:selected").text();
$("#id_protocol").change(function (){
var protocol = $("#id_protocol option:selected").text();
var port = 22;
if(platform === 'Windows'){
if(protocol === 'rdp'){
port = 3389;
}
if(platform === 'Other'){
port = null;
if(protocol === 'telnet (beta)'){
port = 23;
}
$("#id_port").val(port);
});

View File

@@ -69,6 +69,10 @@
<td>{% trans 'Port' %}:</td>
<td><b>{{ asset.port }}</b></td>
</tr>
<tr>
<td>{% trans 'Protocol' %}:</td>
<td><b>{{ asset.protocol }}</b></td>
</tr>
<tr>
<td>{% trans 'Admin user' %}:</td>
<td><b>{{ asset.admin_user }}</b></td>
@@ -130,7 +134,7 @@
</div>
</div>
</div>
{% if user.is_superuser %}
{% if user.is_superuser or user.is_org_admin %}
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">

View File

@@ -4,12 +4,14 @@
{% block help_message %}
<div class="alert alert-info help-message">
左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产
{# 左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产#}
{% trans 'The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node' %}
</div>
{% endblock %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
{# <link href="https://cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css" rel="stylesheet">#}
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script>
<style type="text/css">
@@ -27,6 +29,10 @@
list-style: none;
background-clip: padding-box;
}
.dataTables_wrapper .dataTables_processing {
opacity: .9;
border: none;
}
div#rMenu li{
margin: 1px 0;
cursor: pointer;
@@ -161,16 +167,6 @@ function initTable() {
}
}},
{#{targets: 5, createdCell: function (td, cellData) {#}
{# if (cellData === 'Unknown'){#}
{# $(td).html('<i class="fa fa-circle text-warning"></i>')#}
{# } else if (!cellData) {#}
{# $(td).html('<i class="fa fa-circle text-danger"></i>')#}
{# } else {#}
{# $(td).html('<i class="fa fa-circle text-navy"></i>')#}
{# }#}
{# }},#}
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:asset-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_asset_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
@@ -178,13 +174,6 @@ 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: "is_connective", orderable: false}, {data: "id", orderable: false }#}
{#],#}
columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" },
{data: "cpu_cores"}, {data: "is_active", orderable: false },
@@ -202,17 +191,18 @@ function addTreeNode() {
if (!parentNode){
return
}
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.id );
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.node_id );
$.post(url, {}, function (data, status){
if (status === "success") {
var newNode = {
id: data["key"],
name: data["value"],
id: data["id"],
node_id: data["id"],
pId: parentNode.id
};
newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.id, parentNode)
var node = zTree.getNodeByParam('id', newNode.node_id, parentNode);
zTree.editName(node);
} else {
alert("{% trans 'Create node failed' %}")
@@ -231,7 +221,7 @@ function removeTreeNode() {
} else if (current_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.id );
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id );
$.ajax({
url: url,
method: "DELETE",
@@ -291,7 +281,7 @@ function onBodyMouseDown(event){
function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.id);
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", treeNode.node_id);
var data = {"value": treeNode.name};
if (isCancel){
return
@@ -299,15 +289,17 @@ function onRename(event, treeId, treeNode, isCancel){
APIUpdateAttr({
url: url,
body: JSON.stringify(data),
method: "PATCH"
method: "PATCH",
success_message: "{% trans 'Rename success' %}",
fail_message: "{% trans 'Rename failed, do not change the root node name' %}"
})
}
function onSelected(event, treeNode) {
var url = asset_table.ajax.url();
url = setUrlParam(url, "node_id", treeNode.id);
url = setUrlParam(url, "node_id", treeNode.node_id);
url = setUrlParam(url, "show_current_asset", getCookie('show_current_asset'));
setCookie('node_selected', treeNode.id);
setCookie('node_selected', treeNode.node_id);
asset_table.ajax.url(url);
asset_table.ajax.reload();
}
@@ -324,7 +316,7 @@ function selectQueryNode() {
node_id = cookie_node_id;
}
node = zTree.getNodesByParam("id", node_id, null);
node = zTree.getNodesByParam("node_id", node_id, null);
if (node){
zTree.selectNode(node[0]);
}
@@ -341,11 +333,7 @@ function beforeDrop(treeId, treeNodes, targetNode, moveType) {
});
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.value + "` 下吗?";
if (confirm(msg)){
return true
} else {
return false
}
return confirm(msg);
}
function onDrag(event, treeId, treeNodes) {
@@ -354,10 +342,10 @@ function onDrag(event, treeId, treeNodes) {
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
var treeNodesIds = [];
$.each(treeNodes, function (index, value) {
treeNodesIds.push(value.id);
treeNodesIds.push(value.node_id);
});
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.id);
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.node_id);
var body = {nodes: treeNodesIds};
APIUpdateAttr({
url: the_url,
@@ -401,16 +389,21 @@ function initTree() {
var zNodes = [];
$.get("{% url 'api-assets:node-list' %}", function(data, status){
$.each(data, function (index, value) {
value["pId"] = value["parent"];
if (value["key"] === "0") {
value["open"] = true;
}
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();
});
@@ -459,10 +452,11 @@ $(document).ready(function(){
$.each(rows, function (index, obj) {
assets.push(obj.id)
});
var _node_id = current_node ? current_node : null;
$.ajax({
url: "{% url "assets:asset-export" %}",
method: 'POST',
data: JSON.stringify({assets_id: assets, node_id: current_node.id}),
data: JSON.stringify({assets_id: assets, node_id: _node_id}),
dataType: "json",
success: function (data, textStatus) {
window.open(data.redirect)
@@ -479,8 +473,8 @@ $(document).ready(function(){
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
action = setUrlParam(action, 'node_id', current_node.id);
{#action += "?node_id=" + current_node.id;#}
action = setUrlParam(action, 'node_id', current_node.node_id);
{#action += "?node_id=" + current_node.node_id;#}
$form.attr("action", action)
}
$form.find('.help-block').remove();
@@ -506,7 +500,7 @@ $(document).ready(function(){
var current_node;
if (nodes && nodes.length ===1 ){
current_node = nodes[0];
url += "?node_id=" + current_node.id;
url += "?node_id=" + current_node.node_id;
}
window.open(url, '_self');
})
@@ -520,7 +514,7 @@ $(document).ready(function(){
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
@@ -545,7 +539,7 @@ $(document).ready(function(){
return null;
}
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.id);
var the_url = url.replace("{{ DEFAULT_PK }}", current_node.node_id);
function success(data) {
rMenu.css({"visibility" : "hidden"});
var task_id = data.task;
@@ -634,6 +628,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected assets !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false
@@ -682,7 +677,7 @@ $(document).ready(function(){
};
APIUpdateAttr({
'url': '/api/assets/v1/nodes/' + current_node.id + '/assets/remove/',
'url': '/api/assets/v1/nodes/' + current_node.node_id + '/assets/remove/',
'method': 'PUT',
'body': JSON.stringify(data),
'success': success
@@ -719,9 +714,7 @@ $(document).ready(function(){
return
}
var data = {
'assets': assets_selected
};
var data = {'assets': assets_selected};
var success = function () {
asset_table2.selected = [];
asset_table2.ajax.reload()
@@ -729,9 +722,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.id);
url = "{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
} else {
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.id);
url = "{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node.node_id);
}
APIUpdateAttr({

View File

@@ -12,7 +12,7 @@
{% block form %}
<form action="" method="post" class="form-horizontal">
{% if form.no_field_errors %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
@@ -21,6 +21,7 @@
<h3>{% trans 'Basic' %}</h3>
{% bootstrap_field form.hostname layout="horizontal" %}
{% bootstrap_field form.ip layout="horizontal" %}
{% bootstrap_field form.protocol layout="horizontal" %}
{% bootstrap_field form.port layout="horizontal" %}
{% bootstrap_field form.platform layout="horizontal" %}
{% bootstrap_field form.public_ip layout="horizontal" %}

View File

@@ -0,0 +1,20 @@
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<form id="groupForm" method="post" class="form-horizontal">
{% csrf_token %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.comment layout="horizontal" %}
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,168 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
{% 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 class="active">
<a href="{% url 'assets:cmd-filter-detail' pk=object.id %}" class="text-center">
<i class="fa fa-laptop"></i> {% trans 'Detail' %}
</a>
</li>
<li>
<li>
<a href="{% url 'assets:cmd-filter-rule-list' pk=object.id %}" class="text-center">
<i class="fa fa-laptop"></i> {% trans 'Rules' %}
</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:cmd-filter-update' pk=object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-del">
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span class="label"><b>{{ object.name }}</b></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td>{% trans 'Name' %}:</td>
<td><b>{{ object.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ object.comment }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ object.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Date updated' %}:</td>
<td><b>{{ object.date_updated }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ object.created_by }}</b></td>
</tr>
</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 'System users' %}
</div>
<div class="panel-body">
<table class="table group_edit" id="table-clusters">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Binding to system user' %}" id="system_users_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for system_user in system_users_remain %}
<option value="{{ system_user.id }}" id="opt_{{ system_user.id }}">{{ system_user }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-primary btn-sm" id="btn-binding-system-users">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for system_user in object.system_users.all %}
<tr>
<td><b class="bdg-system-users" data-gid={{ system_user.id }}>{{ system_user }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-unbound-system-user" data-gid={{ system_user.id }} type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
function updateCMDFilterSystemUsers(system_users) {
var the_url = "{% url 'api-assets:cmd-filter-detail' pk=object.id %}";
var body = {
system_users: Object.assign([], system_users)
};
var success = function(data) {
location.reload();
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
method: 'PATCH',
success: success
});
}
$(document).ready(function () {
$(".select2").select2();
}).on('click', '#btn-binding-system-users', function () {
var origin_system_users = $.map($(".bdg-system-users"), function (s) {
return $(s).data('gid')
});
var new_selected_system_users_id = $.map($("#system_users_selected").select2('data'), function (s) {
return s.id;
});
var system_users = origin_system_users.concat(new_selected_system_users_id);
updateCMDFilterSystemUsers(system_users)
}).on('click', '.btn-unbound-system-user', function () {
var unbound_system_user = $(this).data('gid');
var origin_system_users = $.map($(".bdg-system-users"), function (s) {
return $(s).data('gid')
});
var system_users = $.grep(origin_system_users, function (n, i) {
return n !== unbound_system_user
});
updateCMDFilterSystemUsers(system_users)
})
</script>
{% endblock %}

View File

@@ -0,0 +1,89 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}{% endblock %}
{% block help_message %}
<div class="alert alert-info help-message">
{% trans 'System user bound some command filter, each command filter has some rules,'%}
{% trans 'When user login asset with this system user, then run a command,' %}
{% trans 'The command will be filter by rules, higher priority rule run first,' %}
{% trans 'When a rule matched, if rule action is allow, then allow command execute,' %}
{% trans 'else if action is deny, then command with be deny,' %}
{% trans 'else match next rule, if none matched, allowed' %}
</div>
{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url 'assets:cmd-filter-create' %}" class="btn btn-sm btn-primary"> {% trans "Create command filter" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="cmd_filter_list_table" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Rules' %}</th>
<th class="text-center">{% trans 'System users' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#cmd_filter_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
var detail_btn = '<a href="{% url 'assets:cmd-filter-detail' pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 2, createdCell: function (td, cellData, rowData) {
var filters_list_btn = '<a href="{% url "assets:cmd-filter-rule-list" pk=DEFAULT_PK %}">' + cellData.length + '</a>';
filters_list_btn = filters_list_btn.replace("{{ DEFAULT_PK }}", rowData.id);
$(td).html(filters_list_btn);
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
var system_users_list_btn = '<a href="{% url "assets:cmd-filter-detail" pk=DEFAULT_PK %}">' + cellData.length + '</a>';
system_users_list_btn = system_users_list_btn.replace("{{ DEFAULT_PK }}", rowData.id);
$(td).html(system_users_list_btn);
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:cmd-filter-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);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-assets:cmd-filter-list" %}',
columns: [
{data: "id"}, {data: "name" }, {data: "rules" },
{data: "system_users" }, {data: "comment"}, {data: "id"}
],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#cmd_filter_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var uid = $this.data('uid');
var the_url = '{% url "api-assets:cmd-filter-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
});
</script>
{% endblock %}

View File

@@ -0,0 +1,74 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
{% 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="ibox-title">
<h5>{{ action }}</h5>
<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>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
{% bootstrap_form form layout="horizontal" %}
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var content_origin_placeholder = '';
var content_origin_help_text = '';
var content_ref = '';
var content_help_ref = '';
$(document).ready(function(){
content_ref = $('#id_content');
content_help_ref = content_ref.next();
content_origin_placeholder = content_ref.attr('placeholder');
content_origin_help_text = content_help_ref.html();
}).on('change', '#id_type', function () {
if ($('#id_type :selected').val() === 'regex') {
content_ref.attr('placeholder', 'rm.*|reboot|shutdown');
content_help_ref.html("");
} else {
content_ref.attr('placeholder', content_origin_placeholder);
content_help_ref.html(content_origin_help_text);
}
})
</script>
{% endblock %}

View File

@@ -0,0 +1,115 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href='{% static "css/plugins/select2/select2.min.css" %}' rel="stylesheet">
<script src='{% static "js/plugins/select2/select2.full.min.js" %}'></script>
{% 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:cmd-filter-detail' pk=object.id %}" class="text-center">
<i class="fa fa-laptop"></i> {% trans 'Detail' %}
</a>
</li>
<li class="active">
<a href="{% url 'assets:cmd-filter-rule-list' pk=object.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Rules' %} </a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-12" style="padding-left: 0;">
<div class="" id="content_start">
</div>
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left"><b>{% trans 'Command filter rule list' %}</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">
<div class="uc pull-left m-r-5">
<a href="{% url 'assets:cmd-filter-rule-create' filter_pk=object.id %}" class="btn btn-sm btn-primary"> {% trans "Create rule" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="cmd_filter_rule_list_table" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Type' %}</th>
<th class="text-center">{% trans 'Content' %}</th>
<th class="text-center">{% trans 'Priority' %}</th>
<th class="text-center">{% trans 'Strategy' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
function initTable() {
var options = {
ele: $('#cmd_filter_rule_list_table'),
columnDefs: [
{targets: 6, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:cmd-filter-rule-update" filter_pk=object.id 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);
$(td).html(update_btn + del_btn)
}}
],
ajax_url: '{% url "api-assets:cmd-filter-rule-list" filter_pk=object.id %}',
columns: [
{data: "id"}, {data: "type.display" }, {data: 'content'}, {data: 'priority'},
{data: 'action.display'}, {data: "comment" }, {data: "id"}
],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
})
.on('click', '.btn-delete', function () {
var $this = $(this);
var $data_table = $('#cmd_filter_rule_list_table').DataTable();
var name = $(this).closest("tr").find(":nth-child(2)").children('a').html();
var uid = $this.data('uid');
var the_url = '{% url "api-assets:cmd-filter-rule-detail" filter_pk=object.id pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
})
</script>
{% endblock %}

View File

@@ -98,7 +98,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
@@ -120,8 +120,8 @@ $(document).ready(function(){
APIUpdateAttr({
url: the_url,
method: "GET",
success_message: "可连接",
fail_message: "连接失败"
success_message: "{% trans 'Can be connected' %}",
fail_message: "{% trans 'The connection fails' %}"
})
});
</script>

View File

@@ -1,6 +1,17 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}{% endblock %}
{% block help_message %}
<div class="alert alert-info help-message">
{# 网域功能是为了解决部分环境(如:混合云)无法直接连接而新增的功能,原理是通过网关服务器进行跳转登录。<br>#}
{# JMS => 网域网关 => 目标资产#}
{% trans 'The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.' %}
<br>
{% trans 'JMS => Domain gateway => Target assets' %}
</div>
{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url 'assets:domain-create' %}" class="btn btn-sm btn-primary"> {% trans "Create domain" %} </a>
@@ -51,7 +62,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();
@@ -69,6 +80,3 @@ $(document).ready(function(){
});
</script>
{% endblock %}

View File

@@ -42,7 +42,7 @@
{% bootstrap_field form.domain layout="horizontal" %}
{% block auth %}
<h3>{% trans 'Auth' %}</h3>
<h3 id="auth_title">{% trans 'Auth' %}</h3>
<div class="auth-fields">
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.password layout="horizontal" %}
@@ -72,14 +72,23 @@
var protocol_id = '#' + '{{ form.protocol.id_for_label }}';
var private_key_id = '#' + '{{ form.private_key_file.id_for_label }}';
var port = '#' + '{{ form.port.id_for_label }}';
var username = '#' + '{{ form.username.id_for_label }}';
var password = '#' + '{{ form.password.id_for_label }}';
var auth_title = '#auth_title';
function protocolChange() {
if ($(protocol_id + " option:selected").text() === 'rdp') {
$(port).val(3389);
$(private_key_id).closest('.form-group').addClass('hidden')
{#$(port).val(3389);#}
$(private_key_id).closest('.form-group').addClass('hidden');
$(username).closest('.form-group').addClass('hidden');
$(password).closest('.form-group').addClass('hidden');
$(auth_title).addClass('hidden');
} else {
$(port).val(22);
$(private_key_id).closest('.form-group').removeClass('hidden')
{#$(port).val(22);#}
$(private_key_id).closest('.form-group').removeClass('hidden');
$(username).closest('.form-group').removeClass('hidden');
$(password).closest('.form-group').removeClass('hidden');
$(auth_title).removeClass('hidden');
}
}

View File

@@ -47,7 +47,7 @@ function initTable() {
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
initTable();

View File

@@ -1,4 +1,5 @@
{% extends 'base.html' %}
{% load common_tags %}
{% load static %}
{% load i18n %}
@@ -50,6 +51,7 @@
<th>{% trans 'IP' %}</th>
<th>{% trans 'Port' %}</th>
<th>{% trans 'Reachable' %}</th>
<th>{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
@@ -67,14 +69,6 @@
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td width="50%">{% trans 'Push system user now' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
</span>
</td>
</tr>
<tr>
<td width="50%">{% trans 'Test assets connective' %}:</td>
<td>
<span style="float: right">
@@ -82,6 +76,52 @@
</span>
</td>
</tr>
{% if system_user.auto_push %}
<tr>
<td width="50%">{% trans 'Push system user now' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-push" style="width: 54px">{% trans 'Push' %}</button>
</span>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
</div>
<div class="panel-body">
<table class="table node_edit" id="add-asset2group">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for node in system_user.nodes.all|sort %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
@@ -96,7 +136,7 @@
{% block custom_foot_js %}
<script>
function initAssetsTable() {
var unreachable = {{ system_user.unreachable_assets|safe}};
var unreachable = {{ system_user.unreachable_assets|safe }};
var options = {
ele: $('#system_user_list'),
buttons: [],
@@ -112,27 +152,64 @@ function initAssetsTable() {
} else {
$(td).html('<i class="fa fa-check text-navy"></i>')
}
}},
{targets: 4, createdCell: function (td, cellData) {
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);
}}
],
ajax_url: '{% url "api-assets:asset-list" %}?system_user_id={{ system_user.id }}',
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }],
ajax_url: '{% url "api-assets:system-user-assets" pk=system_user.id %}',
columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }, {data: "id"}],
op_html: $('#actions').html()
};
jumpserver.initServerSideDataTable(options);
}
function updateSystemUserNode(nodes) {
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
var body = {
nodes: Object.assign([], nodes)
};
var success = function(data) {
// remove all the selected groups from select > option and rendered ul element;
$('.select2-selection__rendered').empty();
$('#node_selected').val('');
$.map(jumpserver.nodes_selected, function(node_name, index) {
$('#opt_' + index).remove();
// change tr html of user groups.
$('.node_edit tbody').append(
'<tr>' +
'<td><b class="bdg_node" data-gid="' + index + '">' + node_name + '</b></td>' +
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' +
'</tr>'
)
});
// clear jumpserver.nodes_selected
jumpserver.nodes_selected = {};
};
APIUpdateAttr({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
jumpserver.nodes_selected = {};
$(document).ready(function () {
$('.select2').select2()
.on("select2:select", function (evt) {
var data = evt.params.data;
jumpserver.assets_selected[data.id] = data.text;
jumpserver.asset_groups_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.assets_selected[data.id];
delete jumpserver.asset_groups_selected[data.id];
});
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.nodes_selected[data.id];
});
initAssetsTable();
})
.on('click', '.btn-push', function () {
@@ -140,11 +217,16 @@ $(document).ready(function () {
var error = function (data) {
alert(data)
};
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,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, Go to ops task list seen result" %}"
success: success
});
})
.on('click', '.btn-test-connective', function () {
@@ -152,15 +234,85 @@ $(document).ready(function () {
var error = function (data) {
alert(data)
};
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,
error: error,
method: 'GET',
success_message: "{% trans "Task has been send, seen left assets status" %}"
success: success
});
})
.on('click', '.btn-remove-from-node', function() {
var $this = $(this);
var $tr = $this.closest('tr');
var $badge = $tr.find('.bdg_node');
var gid = $badge.data('gid');
var node_name = $badge.html() || $badge.text();
$('#groups_selected').append(
'<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>'
);
$tr.remove();
var nodes = $('.bdg_node').map(function () {
return $(this).data('gid');
}).get();
updateSystemUserNode(nodes);
})
.on('click', '#btn-add-to-node', function() {
if (Object.keys(jumpserver.nodes_selected).length === 0) {
return false;
}
var nodes = $('.bdg_node').map(function() {
return $(this).data('gid');
}).get();
$.map(jumpserver.nodes_selected, function(value, index) {
nodes.push(index);
});
updateSystemUserNode(nodes);
})
.on('click', '.btn-push-asset', function () {
var $this = $(this);
var asset_id = $this.data('uid');
var the_url = "{% url 'api-assets:system-user-push-to-asset' pk=object.id aid=DEFAULT_PK %}";
the_url = the_url.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')
};
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
error: error
})
})
.on('click', '.btn-test-asset', function () {
var $this = $(this);
var asset_id = $this.data('uid');
var the_url = "{% url 'api-assets:system-user-test-to-asset' pk=object.id aid=DEFAULT_PK %}";
the_url = the_url.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')
};
var error = function (data) {
alert(data)
};
APIUpdateAttr({
url: the_url,
method: 'GET',
success: success,
error: error
})
})
</script>
{% endblock %}

View File

@@ -17,11 +17,11 @@
<li class="active">
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
{# <li>#}
{# <a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">#}
{# <i class="fa fa-bar-chart-o"></i> {% trans 'Attached assets' %}#}
{# </a>#}
{# </li>#}
<li>
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
<i class="fa fa-bar-chart-o"></i> {% trans 'Assets' %}
</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:system-user-update' pk=system_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
@@ -62,6 +62,10 @@
<td>{% trans 'Username' %}:</td>
<td><b>{{ system_user.username }}</b></td>
</tr>
<tr>
<td>{% trans 'Login mode' %}:</td>
<td><b>{{ system_user.get_login_mode_display }}</b></td>
</tr>
<tr>
<td>{% trans 'Protocol' %}:</td>
<td><b id="id_protocol_type">{{ system_user.protocol }}</b></td>
@@ -148,66 +152,51 @@
</span>
</td>
</tr>
<tr>
<td width="50%">{% trans 'Clear auth' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-clear-auth" style="width: 54px">{% trans 'Clear' %}</button>
</span>
</td>
</tr>
{# <tr>#}
{# <td width="50%">{% trans 'Change auth period' %}:</td>#}
{# <td>#}
{# <span style="float: right">#}
{# <button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>#}
{# </span>#}
{# </td>#}
{# </tr>#}
</tbody>
</table>
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Nodes' %}
</div>
<div class="panel-body">
<table class="table node_edit" id="add-asset2group">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Add to node' %}" id="node_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes_remain %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-add-to-node">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for node in system_user.nodes.all %}
<tr>
<td ><b class="bdg_node" data-gid={{ node.id }}>{{ node }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% if system_user.protocol != 'rdp' %}
<div class="col-sm-4" style="padding-left: 0; padding-right: 0">
<div class="panel panel-info">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Command filter' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="{% trans 'Binding command filters' %}" id="command_filters_selected" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for cf in cmd_filters_remain %}
<option value="{{ cf.id }}" id="opt_{{ cf.id }}" >{{ cf }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-info btn-sm" id="btn-binding-command-filters">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
{% for cf in object.cmd_filters.all %}
<tr>
<td><b class="bdg-command-filters" data-gid={{ cf.id }}>{{ cf }}</b></td>
<td>
<button class="btn btn-danger pull-right btn-xs btn-unbound-command-filter" data-gid={{ cf.id }} type="button"><i class="fa fa-minus"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
@@ -215,27 +204,13 @@
{% endblock %}
{% block custom_foot_js %}
<script>
function updateSystemUserNode(nodes) {
function updateCommandFilters(command_filters) {
var the_url = "{% url 'api-assets:system-user-detail' pk=system_user.id %}";
var body = {
nodes: Object.assign([], nodes)
cmd_filters: Object.assign([], command_filters)
};
var success = function(data) {
// remove all the selected groups from select > option and rendered ul element;
$('.select2-selection__rendered').empty();
$('#node_selected').val('');
$.map(jumpserver.nodes_selected, function(node_name, index) {
$('#opt_' + index).remove();
// change tr html of user groups.
$('.node_edit tbody').append(
'<tr>' +
'<td><b class="bdg_node" data-gid="' + index + '">' + node_name + '</b></td>' +
'<td><button class="btn btn-danger btn-xs pull-right btn-remove-from-node" type="button"><i class="fa fa-minus"></i></button></td>' +
'</tr>'
)
});
// clear jumpserver.groups_selected
jumpserver.nodes_selected = {};
location.reload();
};
APIUpdateAttr({
url: the_url,
@@ -243,21 +218,12 @@ function updateSystemUserNode(nodes) {
success: success
});
}
jumpserver.nodes_selected = {};
$(document).ready(function () {
if($('#id_protocol_type').text() === 'rdp'){
$('.only-ssh').addClass('hidden')
}
$(".panel-body .table tr:visible:first").addClass('no-borders-tr');
$('.select2').select2()
.on('select2:select', function(evt) {
var data = evt.params.data;
jumpserver.nodes_selected[data.id] = data.text;
})
.on('select2:unselect', function(evt) {
var data = evt.params.data;
delete jumpserver.nodes_selected[data.id];
});
})
.on('click', '#btn-auto-push', function () {
var checked = $(this).prop('checked');
@@ -269,33 +235,6 @@ $(document).ready(function () {
url: the_url,
body: JSON.stringify(body)
});
})
.on('click', '#btn-add-to-node', function() {
if (Object.keys(jumpserver.nodes_selected).length === 0) {
return false;
}
var nodes = $('.bdg_node').map(function() {
return $(this).data('gid');
}).get();
$.map(jumpserver.nodes_selected, function(value, index) {
nodes.push(index);
});
updateSystemUserNode(nodes);
})
.on('click', '.btn-remove-from-node', function() {
var $this = $(this);
var $tr = $this.closest('tr');
var $badge = $tr.find('.bdg_node');
var gid = $badge.data('gid');
var node_name = $badge.html() || $badge.text();
$('#groups_selected').append(
'<option value="' + gid + '" id="opt_' + gid + '">' + node_name + '</option>'
);
$tr.remove();
var nodes = $('.bdg_node').map(function () {
return $(this).data('gid');
}).get();
updateSystemUserNode(nodes);
}).on('click', '.btn-del', function () {
var $this = $(this);
var name = "{{ system_user.name}}";
@@ -331,13 +270,24 @@ $(document).ready(function () {
success: success,
flash_message: false
});
}).on('click', '.btn-clear-auth', function () {
var the_url = '{% url "api-assets:system-user-auth-info" pk=system_user.id %}';
APIUpdateAttr({
url: the_url,
method: 'DELETE',
success_message: "{% trans 'Clear auth' %}" + " {% trans 'success' %}"
}).on('click', '#btn-binding-command-filters', function () {
var new_selected_cmd_filters = $.map($('#command_filters_selected').select2('data'), function (i) {
return i.id;
});
var origin_cmd_filters = $.map($(".bdg-command-filters"), function (s) {
return $(s).data('gid')
});
var command_filters = new_selected_cmd_filters.concat(origin_cmd_filters);
updateCommandFilters(command_filters)
}).on('click', '.btn-unbound-command-filter', function () {
var unbound_command_filter = $(this).data('gid');
var origin_command_filters = $.map($(".bdg-command-filters"), function (s) {
return $(s).data('gid')
});
var command_filters = $.grep(origin_command_filters, function (n, i) {
return n !== unbound_command_filter
});
updateCommandFilters(command_filters)
})
</script>
{% endblock %}

View File

@@ -3,10 +3,13 @@
{% block help_message %}
<div class="alert alert-info help-message">
系统用户是 Jumpserver跳转登录资产时使用的用户可以理解为登录资产用户如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);
简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。
系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。
目前还不支持Windows的自动推送
{# 系统用户是 Jumpserver跳转登录资产时使用的用户可以理解为登录资产用户如 web, sa, dba(`ssh web@some-host`), 而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host`);#}
{# 简单来说是 用户使用自己的用户名登录Jumpserver, Jumpserver使用系统用户登录资产。#}
{# 系统用户创建时,如果选择了自动推送 Jumpserver会使用ansible自动推送系统用户到资产中如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。#}
{# 目前还不支持Windows的自动推送#}
{% trans 'System user is Jumpserver jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); '%}
{% trans 'In simple terms, users log into Jumpserver using their own username, and Jumpserver uses system users to log into assets. '%}
{% trans 'When system users are created, if you choose auto push Jumpserver to use ansible push system users into the asset, if the asset (Switch, Windows) does not support ansible, please manually fill in the account password. Automatic push for Windows is not currently supported.' %}
</div>
{% endblock %}
@@ -26,6 +29,7 @@
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Protocol' %}</th>
<th class="text-center">{% trans 'Login mode' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Unreachable' %}</th>
@@ -48,7 +52,7 @@ function initTable() {
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
{targets: 5, createdCell: function (td, cellData) {
{targets: 6, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "<span class='text-navy'>" + cellData + "</span>";
@@ -57,7 +61,7 @@ function initTable() {
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData +'">' + innerHtml + '</span>');
}},
{targets: 6, createdCell: function (td, cellData) {
{targets: 7, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData !== 0) {
innerHtml = "<span class='text-danger'>" + cellData + "</span>";
@@ -66,7 +70,7 @@ function initTable() {
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}},
{targets: 7, createdCell: function (td, cellData, rowData) {
{targets: 8, createdCell: function (td, cellData, rowData) {
var val = 0;
var innerHtml = "";
var total = rowData.assets_amount;
@@ -84,19 +88,19 @@ function initTable() {
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}},
{targets: 9, createdCell: function (td, cellData, rowData) {
{targets: 10, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:system-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
var del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_admin_user_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
$(td).html(update_btn + del_btn)
}}],
ajax_url: '{% url "api-assets:system-user-list" %}',
columns: [
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "assets_amount" },
{data: "id" }, {data: "name" }, {data: "username" }, {data: "protocol"}, {data: "get_login_mode_display"}, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }
],
op_html: $('#actions').html()
};
jumpserver.initDataTable(options);
jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
@@ -134,6 +138,7 @@ $(document).ready(function(){
text: "{% trans 'This will delete the selected System Users !!!' %}",
type: "warning",
showCancelButton: true,
cancelButtonText: "{% trans 'Cancel' %}",
confirmButtonColor: "#DD6B55",
confirmButtonText: "{% trans 'Confirm' %}",
closeOnConfirm: false

View File

@@ -4,7 +4,6 @@
{% load bootstrap3 %}
{% block auth %}
<h3>{% trans 'Auth' %}</h3>
{% bootstrap_field form.password layout="horizontal" %}
{% bootstrap_field form.private_key_file layout="horizontal" %}
<div class="form-group">

View File

@@ -55,11 +55,14 @@
</div>
</div>
</div>
{% include 'assets/_user_asset_detail_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
<script>
var zTree, rMenu, asset_table;
var zTree, asset_table, show=0;
var inited = false;
var url;
function initTable() {
@@ -68,14 +71,15 @@ function initTable() {
} else {
inited = true;
}
console.log("init table")
url = "{% url 'api-perms:my-assets' %}";
var options = {
ele: $('#user_assets_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{% 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));
}},
var detail_btn = '<a class="asset_detail" asset-id="rowData_id" data-toggle="modal" data-target="#user_asset_detail_modal" tabindex="0">'+ cellData +'</a>'
$(td).html(detail_btn.replace("rowData_id", rowData.id));
}},
{targets: 3, createdCell: function (td, cellData) {
if (!cellData) {
$(td).html('<i class="fa fa-times text-danger"></i>')
@@ -98,38 +102,18 @@ function initTable() {
{data: "system_users_granted", orderable: false}
]
};
asset_table = jumpserver.initDataTable(options);
asset_table = jumpserver.initServerSideDataTable(options);
return asset_table
}
function onSelected(event, treeNode) {
console.log("select");
url = '{% url "api-perms:my-node-assets" node_id=DEFAULT_PK %}';
url = url.replace("{{ DEFAULT_PK }}", treeNode.id);
initTable();
url = url.replace("{{ DEFAULT_PK }}", treeNode.node_id);
setCookie('node_selected', treeNode.id);
asset_table.ajax.url(url);
asset_table.ajax.reload();
}
function selectQueryNode() {
var query_node_id = $.getUrlParam("node");
var cookie_node_id = getCookie('node_selected');
var node;
var node_id;
if (query_node_id !== null) {
node_id = query_node_id
} else if (cookie_node_id !== null) {
node_id = cookie_node_id;
}
node = zTree.getNodesByParam("id", node_id, null);
if (node){
zTree.selectNode(node[0]);
}
}
function initTree() {
var setting = {
view: {
@@ -149,23 +133,71 @@ function initTree() {
var zNodes = [];
$.get("{% url 'api-perms:my-nodes' %}", function(data, status){
$.each(data, function (index, value) {
value["pId"] = value["parent"];
if (value["key"] === "0") {
value["open"] = true;
value["node_id"] = value["id"];
value["id"] = value["tree_id"];
if (value["tree_id"] !== value["tree_parent"]) {
value["pId"] = value["tree_parent"];
}
value["name"] = value["value"]
value["isParent"] = value["is_node"];
value['name'] = value['value'];
});
zNodes = data;
$.fn.zTree.init($("#assetTree"), setting, zNodes);
zTree = $.fn.zTree.getZTreeObj("assetTree");
rMenu = $("#rMenu");
selectQueryNode();
var root = zTree.getNodes()[0];
zTree.expandNode(root);
});
}
$(document).ready(function () {
initTree();
initTable();
})
.on('click', '.asset_detail', function() {
var data = asset_table.ajax.json();
var asset_id = this.getAttribute("asset-id");
var trs = '';
var desc = {
'hostname': "{% trans 'Hostname' %}",
'ip': "{% trans 'IP' %}",
'port': "{% trans 'Port' %}",
'protocol': "{% trans 'Protocol' %}",
'platform': "{% trans 'Platform' %}",
'os': "{% trans 'OS' %}",
'system_users_join': "{% trans 'System user' %}",
'domain': "{% trans 'Domain' %}",
'is_active': "{% trans 'Is active' %}",
'comment': "{% trans 'Comment' %}"
{#'date_joined': "{% trans 'Date joined' %}",#}
};
$.each(data, function(index, value){
if(value.id === asset_id){
for(var i in desc){
trs += "<tr class='no-borders-tr'>\n" +
"<td>"+ desc[i] + ":</td>"+
"<td><b>"+ (value[i] === null?'':value[i]) + "</b></td>\n" +
"</tr>";
}
}
});
$('#asset_detail_tbody').html(trs)
});
function toggle() {
if (show === 0) {
$("#split-left").hide(500, function () {
$("#split-right").attr("class", "col-lg-12");
$("#toggle-icon").attr("class", "fa fa-angle-right fa-x");
show = 1;
});
} else {
$("#split-right").attr("class", "col-lg-9");
$("#toggle-icon").attr("class", "fa fa-angle-left fa-x");
$("#split-left").show(500);
show = 0;
}
}
</script>
{% endblock %}

View File

@@ -1,59 +1,79 @@
# coding:utf-8
from django.conf.urls import url
from .. import api
from django.urls import path
from rest_framework_nested import routers
# from rest_framework.routers import DefaultRouter
from rest_framework_bulk.routes import BulkRouter
from .. import api
app_name = 'assets'
router = BulkRouter()
router.register(r'v1/assets', api.AssetViewSet, 'asset')
router.register(r'v1/admin-user', api.AdminUserViewSet, 'admin-user')
router.register(r'v1/system-user', api.SystemUserViewSet, 'system-user')
router.register(r'v1/labels', api.LabelViewSet, 'label')
router.register(r'v1/nodes', api.NodeViewSet, 'node')
router.register(r'v1/domain', api.DomainViewSet, 'domain')
router.register(r'v1/gateway', api.GatewayViewSet, 'gateway')
router.register(r'assets', api.AssetViewSet, 'asset')
router.register(r'admin-user', api.AdminUserViewSet, 'admin-user')
router.register(r'system-user', api.SystemUserViewSet, 'system-user')
router.register(r'labels', api.LabelViewSet, 'label')
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')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filter', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
urlpatterns = [
url(r'^v1/assets-bulk/$', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth-info/', api.SystemUserAuthInfoApi.as_view(),
name='system-user-auth-info'),
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/refresh/$',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
url(r'^v1/assets/(?P<pk>[0-9a-zA-Z\-]{36})/alive/$',
api.AssetAdminUserTestApi.as_view(), name='asset-alive-test'),
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/nodes/$',
api.ReplaceNodesAdminUserApi.as_view(), name='replace-nodes-admin-user'),
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/auth/$',
api.AdminUserAuthApi.as_view(), name='admin-user-auth'),
url(r'^v1/admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
api.AdminUserTestConnectiveApi.as_view(), name='admin-user-connective'),
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/push/$',
api.SystemUserPushApi.as_view(), name='system-user-push'),
url(r'^v1/system-user/(?P<pk>[0-9a-zA-Z\-]{36})/connective/$',
api.SystemUserTestConnectiveApi.as_view(), name='system-user-connective'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/$',
api.NodeChildrenApi.as_view(), name='node-children'),
url(r'^v1/nodes/children/$', api.NodeChildrenApi.as_view(), name='node-children-2'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/add/$',
api.NodeAddChildrenApi.as_view(), name='node-add-children'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$',
api.NodeAssetsApi.as_view(), name='node-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$',
api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/replace/$',
api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$',
api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/refresh-hardware-info/$',
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
url(r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$',
api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
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('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('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'),
url(r'^v1/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$',
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
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'),
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/<uuid:pk>/children/',
api.NodeChildrenApi.as_view(), name='node-children'),
path('nodes/children/', api.NodeChildrenApi.as_view(), name='node-children-2'),
path('nodes/<uuid:pk>/children/add/',
api.NodeAddChildrenApi.as_view(), name='node-add-children'),
path('nodes/<uuid:pk>/assets/',
api.NodeAssetsApi.as_view(), name='node-assets'),
path('nodes/<uuid:pk>/assets/add/',
api.NodeAddAssetsApi.as_view(), name='node-add-assets'),
path('nodes/<uuid:pk>/assets/replace/',
api.NodeReplaceAssetsApi.as_view(), name='node-replace-assets'),
path('nodes/<uuid:pk>/assets/remove/',
api.NodeRemoveAssetsApi.as_view(), name='node-remove-assets'),
path('nodes/<uuid:pk>/refresh-hardware-info/',
api.RefreshNodeHardwareInfoApi.as_view(), name='node-refresh-hardware-info'),
path('nodes/<uuid:pk>/test-connective/',
api.TestNodeConnectiveApi.as_view(), name='node-test-connective'),
path('gateway/<uuid:pk>/test-connective/',
api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
]
urlpatterns += router.urls
urlpatterns += router.urls + cmd_filter_router.urls

View File

@@ -1,53 +1,60 @@
# coding:utf-8
from django.conf.urls import url
from django.urls import path
from .. import views
app_name = 'assets'
urlpatterns = [
# Resource asset url
url(r'^$', views.AssetListView.as_view(), name='asset-index'),
url(r'^asset/$', views.AssetListView.as_view(), name='asset-list'),
url(r'^asset/create/$', views.AssetCreateView.as_view(), name='asset-create'),
url(r'^asset/export/$', views.AssetExportView.as_view(), name='asset-export'),
url(r'^asset/import/$', views.BulkImportAssetView.as_view(), name='asset-import'),
url(r'^asset/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AssetDetailView.as_view(), name='asset-detail'),
url(r'^asset/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.AssetUpdateView.as_view(), name='asset-update'),
url(r'^asset/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.AssetDeleteView.as_view(), name='asset-delete'),
url(r'^asset/update/$', views.AssetBulkUpdateView.as_view(), name='asset-bulk-update'),
path('', views.AssetListView.as_view(), name='asset-index'),
path('asset/', views.AssetListView.as_view(), name='asset-list'),
path('asset/create/', views.AssetCreateView.as_view(), name='asset-create'),
path('asset/export/', views.AssetExportView.as_view(), name='asset-export'),
path('asset/import/', views.BulkImportAssetView.as_view(), name='asset-import'),
path('asset/<uuid:pk>/', views.AssetDetailView.as_view(), name='asset-detail'),
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'),
# User asset view
url(r'^user-asset/$', views.UserAssetListView.as_view(), name='user-asset-list'),
path('user-asset/', views.UserAssetListView.as_view(), name='user-asset-list'),
# Resource admin user url
url(r'^admin-user/$', views.AdminUserListView.as_view(), name='admin-user-list'),
url(r'^admin-user/create/$', views.AdminUserCreateView.as_view(), name='admin-user-create'),
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
url(r'^admin-user/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$', views.AdminUserAssetsView.as_view(), name='admin-user-assets'),
path('admin-user/', views.AdminUserListView.as_view(), name='admin-user-list'),
path('admin-user/create/', views.AdminUserCreateView.as_view(), name='admin-user-create'),
path('admin-user/<uuid:pk>/', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
path('admin-user/<uuid:pk>/update/', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
path('admin-user/<uuid:pk>/delete/', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
path('admin-user/<uuid:pk>/assets/', views.AdminUserAssetsView.as_view(), name='admin-user-assets'),
# Resource system user url
url(r'^system-user/$', views.SystemUserListView.as_view(), name='system-user-list'),
url(r'^system-user/create/$', views.SystemUserCreateView.as_view(), name='system-user-create'),
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.SystemUserDetailView.as_view(), name='system-user-detail'),
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.SystemUserUpdateView.as_view(), name='system-user-update'),
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.SystemUserDeleteView.as_view(), name='system-user-delete'),
url(r'^system-user/(?P<pk>[0-9a-zA-Z\-]{36})/asset/$', views.SystemUserAssetView.as_view(), name='system-user-asset'),
path('system-user/', views.SystemUserListView.as_view(), name='system-user-list'),
path('system-user/create/', views.SystemUserCreateView.as_view(), name='system-user-create'),
path('system-user/<uuid:pk>/', views.SystemUserDetailView.as_view(), name='system-user-detail'),
path('system-user/<uuid:pk>/update/', views.SystemUserUpdateView.as_view(), name='system-user-update'),
path('system-user/<uuid:pk>/delete/', views.SystemUserDeleteView.as_view(), name='system-user-delete'),
path('system-user/<uuid:pk>/asset/', views.SystemUserAssetView.as_view(), name='system-user-asset'),
url(r'^label/$', views.LabelListView.as_view(), name='label-list'),
url(r'^label/create/$', views.LabelCreateView.as_view(), name='label-create'),
url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.LabelUpdateView.as_view(), name='label-update'),
url(r'^label/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.LabelDeleteView.as_view(), name='label-delete'),
path('label/', views.LabelListView.as_view(), name='label-list'),
path('label/create/', views.LabelCreateView.as_view(), name='label-create'),
path('label/<uuid:pk>/update/', views.LabelUpdateView.as_view(), name='label-update'),
path('label/<uuid:pk>/delete/', views.LabelDeleteView.as_view(), name='label-delete'),
url(r'^domain/$', views.DomainListView.as_view(), name='domain-list'),
url(r'^domain/create/$', views.DomainCreateView.as_view(), name='domain-create'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/$', views.DomainDetailView.as_view(), name='domain-detail'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainUpdateView.as_view(), name='domain-update'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/delete/$', views.DomainDeleteView.as_view(), name='domain-delete'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/$', views.DomainGatewayListView.as_view(), name='domain-gateway-list'),
path('domain/', views.DomainListView.as_view(), name='domain-list'),
path('domain/create/', views.DomainCreateView.as_view(), name='domain-create'),
path('domain/<uuid:pk>/', views.DomainDetailView.as_view(), name='domain-detail'),
path('domain/<uuid:pk>/update/', views.DomainUpdateView.as_view(), name='domain-update'),
path('domain/<uuid:pk>/delete/', views.DomainDeleteView.as_view(), name='domain-delete'),
path('domain/<uuid:pk>/gateway/', views.DomainGatewayListView.as_view(), name='domain-gateway-list'),
url(r'^domain/(?P<pk>[0-9a-zA-Z\-]{36})/gateway/create/$', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'),
url(r'^domain/gateway/(?P<pk>[0-9a-zA-Z\-]{36})/update/$', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
path('domain/<uuid:pk>/gateway/create/', views.DomainGatewayCreateView.as_view(), name='domain-gateway-create'),
path('domain/gateway/<uuid:pk>/update/', views.DomainGatewayUpdateView.as_view(), name='domain-gateway-update'),
path('cmd-filter/', views.CommandFilterListView.as_view(), name='cmd-filter-list'),
path('cmd-filter/create/', views.CommandFilterCreateView.as_view(), name='cmd-filter-create'),
path('cmd-filter/<uuid:pk>/update/', views.CommandFilterUpdateView.as_view(), name='cmd-filter-update'),
path('cmd-filter/<uuid:pk>/', views.CommandFilterDetailView.as_view(), name='cmd-filter-detail'),
path('cmd-filter/<uuid:pk>/rule/', views.CommandFilterRuleListView.as_view(), name='cmd-filter-rule-list'),
path('cmd-filter/<uuid:filter_pk>/rule/create/', views.CommandFilterRuleCreateView.as_view(), name='cmd-filter-rule-create'),
path('cmd-filter/<uuid:filter_pk>/rule/<uuid:pk>/update/', views.CommandFilterRuleUpdateView.as_view(), name='cmd-filter-rule-update'),
]

View File

@@ -12,8 +12,8 @@ def get_assets_by_id_list(id_list):
return Asset.objects.filter(id__in=id_list)
def get_assets_by_hostname_list(hostname_list):
return Asset.objects.filter(hostname__in=hostname_list)
def get_assets_by_fullname_list(hostname_list):
return Asset.get_queryset_by_fullname_list(hostname_list)
def get_system_user_by_name(name):
@@ -54,7 +54,8 @@ def test_gateway_connectability(gateway):
proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
proxy.connect(gateway.ip, username=gateway.username,
proxy.connect(gateway.ip, gateway.port,
username=gateway.username,
password=gateway.password,
pkey=gateway.private_key_obj)
except(paramiko.AuthenticationException,

View File

@@ -4,3 +4,4 @@ from .system_user import *
from .admin_user import *
from .label import *
from .domain import *
from .cmd_filter import *

View File

@@ -11,7 +11,7 @@ from django.views.generic.detail import DetailView, SingleObjectMixin
from common.const import create_success_msg, update_success_msg
from .. import forms
from ..models import AdminUser, Node
from ..hands import AdminUserRequiredMixin
from common.permissions import AdminUserRequiredMixin
__all__ = [
'AdminUserCreateView', 'AdminUserDetailView',

View File

@@ -8,8 +8,8 @@ import codecs
import chardet
from io import StringIO
from django.conf import settings
from django.db import transaction
from django.contrib import messages
from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, ListView, View
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
@@ -25,11 +25,12 @@ from django.shortcuts import redirect
from django.contrib.messages.views import SuccessMessageMixin
from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger, is_uuid
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 orgs.utils import current_org
from .. import forms
from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
from ..hands import AdminUserRequiredMixin
__all__ = [
@@ -73,14 +74,6 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
template_name = 'assets/asset_create.html'
success_url = reverse_lazy('assets:asset-list')
# def form_valid(self, form):
# print("form valid")
# asset = form.save()
# asset.created_by = self.request.user.username or 'Admin'
# asset.date_created = timezone.now()
# asset.save()
# return super().form_valid(form)
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
node_id = self.request.GET.get("node_id")
@@ -103,29 +96,12 @@ class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
return create_success_msg % ({"name": cleaned_data["hostname"]})
# class AssetModalListView(AdminUserRequiredMixin, ListView):
# paginate_by = settings.DISPLAY_PER_PAGE
# model = Asset
# context_object_name = 'asset_modal_list'
# template_name = 'assets/_asset_list_modal.html'
#
# def get_context_data(self, **kwargs):
# assets = Asset.objects.all()
# assets_id = self.request.GET.get('assets_id', '')
# assets_id_list = [i for i in assets_id.split(',') if i.isdigit()]
# context = {
# 'all_assets': assets_id_list,
# 'assets': assets
# }
# kwargs.update(context)
# return super().get_context_data(**kwargs)
class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
model = Asset
form_class = forms.AssetBulkUpdateForm
template_name = 'assets/asset_bulk_update.html'
success_url = reverse_lazy('assets:asset-list')
success_message = _("Bulk update asset success")
id_list = None
form = None
@@ -147,6 +123,7 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
form = self.form_class(request.POST)
if form.is_valid():
form.save()
messages.success(request, self.success_message)
return redirect(self.success_url)
else:
return self.get(request, form=form, *args, **kwargs)
@@ -186,7 +163,7 @@ class AssetDeleteView(AdminUserRequiredMixin, DeleteView):
success_url = reverse_lazy('assets:asset-list')
class AssetDetailView(DetailView):
class AssetDetailView(LoginRequiredMixin, DetailView):
model = Asset
context_object_name = 'asset'
template_name = 'assets/asset_detail.html'
@@ -203,7 +180,7 @@ class AssetDetailView(DetailView):
@method_decorator(csrf_exempt, name='dispatch')
class AssetExportView(View):
class AssetExportView(LoginRequiredMixin, View):
def get(self, request):
spm = request.GET.get('spm', '')
assets_id_default = [Asset.objects.first().id] if Asset.objects.first() else []
@@ -211,7 +188,7 @@ class AssetExportView(View):
fields = [
field for field in Asset._meta.fields
if field.name not in [
'date_created'
'date_created', 'org_id'
]
]
filename = 'assets-{}.csv'.format(
@@ -234,13 +211,13 @@ class AssetExportView(View):
def post(self, request, *args, **kwargs):
try:
assets_id = json.loads(request.body).get('assets_id', [])
assets_node_id = json.loads(request.body).get('node_id', None)
node_id = json.loads(request.body).get('node_id', None)
except ValueError:
return HttpResponse('Json object not valid', status=400)
if not assets_id and assets_node_id:
assets_node = get_object_or_none(Node, id=assets_node_id)
assets = assets_node.get_all_assets()
if not assets_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:
assets_id.append(asset.id)

View File

@@ -0,0 +1,168 @@
# -*- coding: utf-8 -*-
#
from django.views.generic import TemplateView, CreateView, \
UpdateView, DeleteView, DetailView
from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy
from django.shortcuts import get_object_or_404, reverse
from common.permissions import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg
from ..models import CommandFilter, CommandFilterRule, SystemUser
from ..forms import CommandFilterForm, CommandFilterRuleForm
__all__ = (
"CommandFilterListView", "CommandFilterCreateView",
"CommandFilterUpdateView",
"CommandFilterRuleListView", "CommandFilterRuleCreateView",
"CommandFilterRuleUpdateView", "CommandFilterDetailView",
)
class CommandFilterListView(AdminUserRequiredMixin, TemplateView):
template_name = 'assets/cmd_filter_list.html'
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Command filter list'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandFilterCreateView(AdminUserRequiredMixin, CreateView):
model = CommandFilter
template_name = 'assets/cmd_filter_create_update.html'
form_class = CommandFilterForm
success_url = reverse_lazy('assets:cmd-filter-list')
success_message = create_success_msg
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Create command filter'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandFilterUpdateView(AdminUserRequiredMixin, UpdateView):
model = CommandFilter
template_name = 'assets/cmd_filter_create_update.html'
form_class = CommandFilterForm
success_url = reverse_lazy('assets:cmd-filter-list')
success_message = update_success_msg
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Update command filter'),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandFilterDetailView(AdminUserRequiredMixin, DetailView):
model = CommandFilter
template_name = 'assets/cmd_filter_detail.html'
def get_context_data(self, **kwargs):
system_users_remain = SystemUser.objects\
.exclude(cmd_filters=self.object)\
.exclude(protocol='rdp')
context = {
'app': _('Assets'),
'action': _('Command filter detail'),
'system_users_remain': system_users_remain
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandFilterRuleListView(AdminUserRequiredMixin, SingleObjectMixin, TemplateView):
template_name = 'assets/cmd_filter_rule_list.html'
model = CommandFilter
object = None
def get(self, request, *args, **kwargs):
self.object = self.get_object(queryset=self.model.objects.all())
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Command filter rule list'),
'object': self.get_object()
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandFilterRuleCreateView(AdminUserRequiredMixin, CreateView):
template_name = 'assets/cmd_filter_rule_create_update.html'
model = CommandFilterRule
form_class = CommandFilterRuleForm
success_message = create_success_msg
cmd_filter = None
def get_success_url(self):
return reverse('assets:cmd-filter-rule-list', kwargs={
'pk': self.cmd_filter.id
})
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
form['filter'].initial = self.cmd_filter
form['filter'].field.widget.attrs['readonly'] = 1
return form
def dispatch(self, request, *args, **kwargs):
filter_pk = self.kwargs.get('filter_pk')
self.cmd_filter = get_object_or_404(CommandFilter, pk=filter_pk)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Create command filter rule'),
'object': self.cmd_filter,
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class CommandFilterRuleUpdateView(AdminUserRequiredMixin, UpdateView):
template_name = 'assets/cmd_filter_rule_create_update.html'
model = CommandFilterRule
form_class = CommandFilterRuleForm
success_message = create_success_msg
cmd_filter = None
def get_success_url(self):
return reverse('assets:cmd-filter-rule-list', kwargs={
'pk': self.cmd_filter.id
})
def get_form(self, form_class=None):
form = super().get_form(form_class=form_class)
form['filter'].initial = self.cmd_filter
form['filter'].field.widget.attrs['readonly'] = 1
return form
def dispatch(self, request, *args, **kwargs):
filter_pk = self.kwargs.get('filter_pk')
self.cmd_filter = get_object_or_404(CommandFilter, pk=filter_pk)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),
'action': _('Update command filter rule'),
'object': self.cmd_filter,
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@@ -7,7 +7,7 @@ from django.views.generic.detail import SingleObjectMixin
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy, reverse
from common.mixins import AdminUserRequiredMixin
from common.permissions import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg
from common.utils import get_object_or_none
from ..models import Domain, Gateway
@@ -140,11 +140,6 @@ class DomainGatewayUpdateView(AdminUserRequiredMixin, UpdateView):
domain = self.object.domain
return reverse('assets:domain-gateway-list', kwargs={"pk": domain.id})
def form_valid(self, form):
response = super().form_valid(form)
print(form.cleaned_data)
return response
def get_context_data(self, **kwargs):
context = {
'app': _('Assets'),

View File

@@ -6,7 +6,7 @@ from django.views.generic import TemplateView, CreateView, \
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse_lazy
from common.mixins import AdminUserRequiredMixin
from common.permissions import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg
from ..models import Label
from ..forms import LabelForm
@@ -36,6 +36,7 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView):
form_class = LabelForm
success_url = reverse_lazy('assets:label-list')
success_message = create_success_msg
disable_name = ['draw', 'search', 'limit', 'offset', '_']
def get_context_data(self, **kwargs):
context = {
@@ -45,6 +46,16 @@ class LabelCreateView(AdminUserRequiredMixin, CreateView):
kwargs.update(context)
return super().get_context_data(**kwargs)
def form_valid(self, form):
name = form.cleaned_data.get('name')
if name in self.disable_name:
msg = _(
'Tips: Avoid using label names reserved internally: {}'
).format(', '.join(self.disable_name))
form.add_error("name", msg)
return self.form_invalid(form)
return super().form_valid(form)
class LabelUpdateView(AdminUserRequiredMixin, UpdateView):
model = Label

View File

@@ -9,8 +9,8 @@ from django.views.generic.detail import DetailView
from common.const import create_success_msg, update_success_msg
from ..forms import SystemUserForm
from ..models import SystemUser, Node
from ..hands import AdminUserRequiredMixin
from ..models import SystemUser, Node, CommandFilter
from common.permissions import AdminUserRequiredMixin
__all__ = [
@@ -73,7 +73,7 @@ class SystemUserDetailView(AdminUserRequiredMixin, DetailView):
context = {
'app': _('Assets'),
'action': _('System user detail'),
'nodes_remain': Node.objects.exclude(systemuser=self.object)
'cmd_filters_remain': CommandFilter.objects.exclude(system_users=self.object)
}
kwargs.update(context)
return super().get_context_data(**kwargs)
@@ -91,9 +91,11 @@ class SystemUserAssetView(AdminUserRequiredMixin, DetailView):
context_object_name = 'system_user'
def get_context_data(self, **kwargs):
nodes_remain = sorted(Node.objects.exclude(systemuser=self.object), reverse=True)
context = {
'app': _('assets'),
'action': _('System user asset'),
'nodes_remain': nodes_remain
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

@@ -3,7 +3,7 @@
from rest_framework import viewsets
from common.permissions import IsSuperUserOrAppUser
from common.permissions import IsOrgAdminOrAppUser
from .models import FTPLog
from .serializers import FTPLogSerializer
@@ -11,4 +11,4 @@ from .serializers import FTPLogSerializer
class FTPLogViewSet(viewsets.ModelViewSet):
queryset = FTPLog.objects.all()
serializer_class = FTPLogSerializer
permission_classes = (IsSuperUserOrAppUser,)
permission_classes = (IsOrgAdminOrAppUser,)

View File

@@ -3,3 +3,6 @@ from django.apps import AppConfig
class AuditsConfig(AppConfig):
name = 'audits'
def ready(self):
from . import signals_handler

4
apps/audits/hands.py Normal file
View File

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

View File

@@ -3,8 +3,15 @@ import uuid
from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins import OrgModelMixin
from .hands import LoginLog
class FTPLog(models.Model):
__all__ = [
'FTPLog', 'OperateLog', 'PasswordChangeLog', 'UserLoginLog',
]
class FTPLog(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
@@ -14,3 +21,40 @@ class FTPLog(models.Model):
filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True)
class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create'
ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete'
ACTION_CHOICES = (
(ACTION_CREATE, _("Create")),
(ACTION_UPDATE, _("Update")),
(ACTION_DELETE, _("Delete"))
)
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
action = models.CharField(max_length=16, choices=ACTION_CHOICES, verbose_name=_("Action"))
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True)
def __str__(self):
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
class PasswordChangeLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
user = models.CharField(max_length=128, verbose_name=_('User'))
change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
remote_addr = models.CharField(max_length=15, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True)
def __str__(self):
return "{} change {}'s password".format(self.change_by, self.user)
class UserLoginLog(LoginLog):
class Meta:
proxy = True

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
#
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.db import transaction
from jumpserver.utils import current_request
from common.utils import get_request_ip
from users.models import User
from .models import OperateLog, PasswordChangeLog
MODELS_NEED_RECORD = (
'User', 'UserGroup', 'Asset', 'Node', 'AdminUser', 'SystemUser',
'Domain', 'Gateway', 'Organization', 'AssetPermission',
)
def create_operate_log(action, sender, resource):
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
return
model_name = sender._meta.object_name
if model_name not in MODELS_NEED_RECORD:
return
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
with transaction.atomic():
OperateLog.objects.create(
user=user, action=action, resource_type=resource_type,
resource=resource, remote_addr=remote_addr
)
@receiver(post_save, dispatch_uid="my_unique_identifier")
def on_object_created_or_update(sender, instance=None, created=False, **kwargs):
if created:
action = OperateLog.ACTION_CREATE
else:
action = OperateLog.ACTION_UPDATE
create_operate_log(action, sender, instance)
@receiver(post_delete, dispatch_uid="my_unique_identifier")
def on_object_delete(sender, instance=None, **kwargs):
create_operate_log(OperateLog.ACTION_DELETE, sender, instance)
@receiver(post_save, sender=User, dispatch_uid="my_unique_identifier")
def on_user_change_password(sender, instance=None, **kwargs):
if hasattr(instance, '_set_password'):
if not current_request or not current_request.user.is_authenticated:
return
with transaction.atomic():
PasswordChangeLog.objects.create(
user=instance, change_by=current_request.user,
remote_addr=get_request_ip(current_request),
)

View File

@@ -66,7 +66,6 @@
{% endblock %}
{% block table_head %}
<th class="text-center"></th>
{# <th class="text-center">{% trans 'ID' %}</th>#}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
@@ -82,7 +81,6 @@
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
<td class="text-center"><input type="checkbox" value="{{ object.id }}"></td>
{# <td class="text-center">#}
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
{# </td>#}

View File

@@ -51,6 +51,9 @@
<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 %}
@@ -65,6 +68,9 @@
</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 %}
@@ -74,12 +80,7 @@
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function() {
$('table').DataTable({
"searching": false,
"bInfo" : false,
"paging": false,
"order": []
});
jumpserver.initStaticTable('table');
$('#date .input-daterange').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",

View File

@@ -0,0 +1,119 @@
{% extends '_base_list.html' %}
{% load i18n %}
{% load static %}
{% load terminal_tags %}
{% load common_tags %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content_left_head %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline">
<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' }}">
<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' }}">
</div>
</div>
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="asset">
<option value="">{% trans 'Action' %}</option>
{% for k, v in actions.items %}
<option value="{{ k }}" {% if k == action %} selected {% endif %}>{{ v }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="system_user">
<option value="">{% trans 'Resource Type' %}</option>
{% for r in resource_type_list %}
<option value="{{ r }}" {% if r == resource_type %} selected {% endif %}>{{ r }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans 'Search' %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_head %}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
<th class="text-center">{% trans 'Resource Type' %}</th>
<th class="text-center">{% trans 'Resource' %}</th>
<th class="text-center">{% trans 'Remote addr' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
{% endblock %}
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
{# <td class="text-center">#}
{# <a href="{% url 'terminal:object-detail' pk=object.id %}">{{ forloop.counter }}</a>#}
{# </td>#}
<td class="text-center">{{ object.user }}</td>
<td class="text-center">{{ object.get_action_display }}</td>
<td class="text-center">{{ object.resource_type }}</td>
<td class="text-center">{{ object.resource }}</td>
<td class="text-center">{{ object.remote_addr }}</td>
<td class="text-center">{{ object.datetime }}</td>
</tr>
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function() {
$('table').DataTable({
"searching": false,
"paging": false,
"bInfo" : false,
"order": [],
"language": jumpserver.language
});
$('.select2').select2({
dropdownAutoWidth: true,
width: "auto"
});
$('.input-daterange.input-group').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
})
</script>
{% endblock %}

View File

@@ -0,0 +1,96 @@
{% extends '_base_list.html' %}
{% load i18n %}
{% load static %}
{% load terminal_tags %}
{% load common_tags %}
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/datepicker/datepicker3.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style>
#search_btn {
margin-bottom: 0;
}
</style>
{% endblock %}
{% block content_left_head %}
{% endblock %}
{% block table_search %}
<form id="search_form" method="get" action="" class="pull-right form-inline">
<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' }}">
<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' }}">
</div>
</div>
<div class="input-group">
<select class="select2 form-control" name="user">
<option value="">{% trans 'User' %}</option>
{% for u in user_list %}
<option value="{{ u }}" {% if u == user %} selected {% endif %}>{{ u }}</option>
{% endfor %}
</select>
</div>
<div class="input-group">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
{% trans 'Search' %}
</button>
</div>
</div>
</form>
{% endblock %}
{% block table_head %}
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Change by' %}</th>
<th class="text-center">{% trans 'Remote addr' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
{% endblock %}
{% block table_body %}
{% for object in object_list %}
<tr class="gradeX">
<td class="text-center">{{ object.user }}</td>
<td class="text-center">{{ object.change_by }}</td>
<td class="text-center">{{ object.remote_addr }}</td>
<td class="text-center">{{ object.datetime }}</td>
</tr>
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
{% endblock %}
{% block custom_foot_js %}
<script src="{% static 'js/plugins/datepicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function() {
$('table').DataTable({
"searching": false,
"paging": false,
"bInfo" : false,
"order": [],
"language": jumpserver.language
});
$('.select2').select2({
dropdownAutoWidth: true,
width: "auto"
});
$('.input-daterange.input-group').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
});
})
</script>
{% endblock %}

View File

@@ -9,10 +9,9 @@ from .. import api
app_name = "audits"
router = DefaultRouter()
router.register(r'v1/ftp-log', api.FTPLogViewSet, 'ftp-log')
router.register(r'ftp-log', api.FTPLogViewSet, 'ftp-log')
urlpatterns = [
# url(r'^v1/celery/task/(?P<pk>[0-9a-zA-Z\-]{36})/log/$', api.CeleryTaskLogApi.as_view(), name='celery-task-log'),
]
urlpatterns += router.urls

View File

@@ -1,8 +1,7 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
from django.conf.urls import url
from django.urls import path
from .. import views
__all__ = ["urlpatterns"]
@@ -10,5 +9,8 @@ __all__ = ["urlpatterns"]
app_name = "audits"
urlpatterns = [
url(r'^ftp-log/$', views.FTPLogListView.as_view(), name='ftp-log-list'),
path('login-log/', views.LoginLogListView.as_view(), name='login-log-list'),
path('ftp-log/', views.FTPLogListView.as_view(), name='ftp-log-list'),
path('operate-log/', views.OperateLogListView.as_view(), name='operate-log-list'),
path('password-change-log/', views.PasswordChangeLogList.as_view(), name='password-change-log-list'),
]

View File

@@ -1,10 +1,26 @@
from django.conf import settings
from django.views.generic import ListView
from django.utils.translation import ugettext as _
from django.db.models import Q
from common.mixins import AdminUserRequiredMixin, DatetimeSearchMixin
from common.mixins import DatetimeSearchMixin
from common.permissions import AdminUserRequiredMixin
from .models import FTPLog
from orgs.utils import current_org
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 orgs.models import Organization
from perms.models import AssetPermission
models = [
User, UserGroup, Asset, Node, AdminUser, SystemUser, Domain,
Gateway, Organization, AssetPermission
]
return [model._meta.verbose_name for model in models]
class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
@@ -52,3 +68,129 @@ class FTPLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class OperateLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
model = OperateLog
template_name = 'audits/operate_log_list.html'
paginate_by = settings.DISPLAY_PER_PAGE
user = action = resource_type = ''
date_from = date_to = None
actions_dict = dict(OperateLog.ACTION_CHOICES)
def get_queryset(self):
self.queryset = super().get_queryset()
self.user = self.request.GET.get('user')
self.action = self.request.GET.get('action')
self.resource_type = self.request.GET.get('resource_type')
filter_kwargs = dict()
filter_kwargs['datetime__gt'] = self.date_from
filter_kwargs['datetime__lt'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if self.action:
filter_kwargs['action'] = self.action
if self.resource_type:
filter_kwargs['resource_type'] = self.resource_type
if filter_kwargs:
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
return self.queryset
def get_context_data(self, **kwargs):
context = {
'user_list': current_org.get_org_users(),
'actions': self.actions_dict,
'resource_type_list': get_resource_type_list(),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'action': self.action,
'resource_type': self.resource_type,
"app": _("Audits"),
"action": _("Operate log"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class PasswordChangeLogList(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
model = PasswordChangeLog
template_name = 'audits/password_change_log_list.html'
paginate_by = settings.DISPLAY_PER_PAGE
user = ''
date_from = date_to = None
def get_queryset(self):
self.queryset = super().get_queryset()
self.user = self.request.GET.get('user')
filter_kwargs = dict()
filter_kwargs['datetime__gt'] = self.date_from
filter_kwargs['datetime__lt'] = self.date_to
if self.user:
filter_kwargs['user'] = self.user
if filter_kwargs:
self.queryset = self.queryset.filter(**filter_kwargs).order_by('-datetime')
return self.queryset
def get_context_data(self, **kwargs):
context = {
'user_list': current_org.get_org_users(),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
"app": _("Audits"),
"action": _("Password change log"),
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class LoginLogListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
template_name = 'audits/login_log_list.html'
model = UserLoginLog
paginate_by = settings.DISPLAY_PER_PAGE
user = keyword = ""
date_to = date_from = None
@staticmethod
def get_org_users():
users = current_org.get_org_users().values_list('username', flat=True)
return users
def get_queryset(self):
if current_org.is_default():
queryset = super().get_queryset()
else:
users = self.get_org_users()
queryset = super().get_queryset().filter(username__in=users)
self.user = self.request.GET.get('user', '')
self.keyword = self.request.GET.get("keyword", '')
queryset = queryset.filter(
datetime__gt=self.date_from, datetime__lt=self.date_to
)
if self.user:
queryset = queryset.filter(username=self.user)
if self.keyword:
queryset = queryset.filter(
Q(ip__contains=self.keyword) |
Q(city__contains=self.keyword) |
Q(username__contains=self.keyword)
)
return queryset
def get_context_data(self, **kwargs):
context = {
'app': _('Users'),
'action': _('Login log'),
'date_from': self.date_from,
'date_to': self.date_to,
'user': self.user,
'keyword': self.keyword,
'user_list': self.get_org_users(),
}
kwargs.update(context)
return super().get_context_data(**kwargs)

View File

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,10 @@
from django.apps import AppConfig
class AuthenticationConfig(AppConfig):
name = 'authentication'
def ready(self):
from . import signals_handlers
super().ready()

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
#
from django.conf import settings
from .models import Client
def new_client():
"""
:return: authentication.models.Client
"""
return Client(
server_url=settings.AUTH_OPENID_SERVER_URL,
realm_name=settings.AUTH_OPENID_REALM_NAME,
client_id=settings.AUTH_OPENID_CLIENT_ID,
client_secret=settings.AUTH_OPENID_CLIENT_SECRET
)
client = new_client()

View File

@@ -0,0 +1,90 @@
# coding:utf-8
#
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
UserModel = get_user_model()
logger = get_logger(__file__)
BACKEND_OPENID_AUTH_CODE = \
'authentication.openid.backends.OpenIDAuthorizationCodeBackend'
class BaseOpenIDAuthorizationBackend(object):
@staticmethod
def user_can_authenticate(user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_active = getattr(user, 'is_active', None)
return is_active or is_active is None
def get_user(self, user_id):
try:
user = UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
class OpenIDAuthorizationCodeBackend(BaseOpenIDAuthorizationBackend):
def authenticate(self, request, **kwargs):
logger.info('1.openid code backend')
code = kwargs.get('code')
redirect_uri = kwargs.get('redirect_uri')
if not code or not redirect_uri:
return None
try:
oidt_profile = client.update_or_create_from_code(
code=code,
redirect_uri=redirect_uri
)
except Exception as e:
logger.error(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
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')
if not settings.AUTH_OPENID:
return None
elif not username:
return None
try:
oidt_profile = client.update_or_create_from_password(
username=username, password=password
)
except Exception as e:
logger.error(e)
else:
user = oidt_profile.user
return user if self.user_can_authenticate(user) else None

View File

@@ -0,0 +1,42 @@
# coding:utf-8
#
from django.conf import settings
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
logger = get_logger(__file__)
class OpenIDAuthenticationMiddleware(MiddlewareMixin):
"""
Check openid user single logout (with access_token)
"""
def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID:
return
# Don't need check single logout if user not authenticated
if not request.user.is_authenticated:
return
elif request.session[BACKEND_SESSION_KEY] != BACKEND_OPENID_AUTH_CODE:
return
# Check openid user single logout or not with access_token
try:
client.openid_connect_client.userinfo(
token=request.session.get(OIDT_ACCESS_TOKEN))
except Exception as e:
logout(request)
logger.error(e)

View File

@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
from django.db import transaction
from django.contrib.auth import get_user_model
from keycloak.realm import KeycloakRealm
from keycloak.keycloak_openid import KeycloakOpenID
from ..signals import post_create_openid_user
OIDT_ACCESS_TOKEN = 'oidt_access_token'
class OpenIDTokenProfile(object):
def __init__(self, user, access_token, refresh_token):
"""
:param user: User object
:param access_token:
:param refresh_token:
"""
self.user = user
self.access_token = access_token
self.refresh_token = refresh_token
def __str__(self):
return "{}'s OpenID token profile".format(self.user.username)
class Client(object):
def __init__(self, server_url, realm_name, client_id, client_secret):
self.server_url = server_url
self.realm_name = realm_name
self.client_id = client_id
self.client_secret = client_secret
self.realm = self.new_realm()
self.openid_client = self.new_openid_client()
self.openid_connect_client = self.new_openid_connect_client()
def new_realm(self):
"""
:param authentication.openid.models.Realm realm:
:return keycloak.realm.Realm:
"""
return KeycloakRealm(
server_url=self.server_url,
realm_name=self.realm_name,
headers={}
)
def new_openid_connect_client(self):
"""
:rtype: keycloak.openid_connect.KeycloakOpenidConnect
"""
openid_connect = self.realm.open_id_connect(
client_id=self.client_id,
client_secret=self.client_secret
)
return openid_connect
def new_openid_client(self):
"""
:rtype: keycloak.keycloak_openid.KeycloakOpenID
"""
return KeycloakOpenID(
server_url='%sauth/' % self.server_url,
realm_name=self.realm_name,
client_id=self.client_id,
client_secret_key=self.client_secret,
)
def update_or_create_from_password(self, username, password):
"""
Update or create an user based on an authentication username and password.
:param str username: authentication username
:param str password: authentication password
:return: authentication.models.OpenIDTokenProfile
"""
token_response = self.openid_client.token(
username=username, password=password
)
return self._update_or_create(token_response=token_response)
def update_or_create_from_code(self, code, redirect_uri):
"""
Update or create an user based on an authentication code.
Response as specified in:
https://tools.ietf.org/html/rfc6749#section-4.1.4
:param str code: authentication code
:param str redirect_uri:
:rtype: authentication.models.OpenIDTokenProfile
"""
token_response = self.openid_connect_client.authorization_code(
code=code, redirect_uri=redirect_uri)
return self._update_or_create(token_response=token_response)
def _update_or_create(self, token_response):
"""
Update or create an user based on a token response.
`token_response` contains the items returned by the OpenIDConnect Token API
end-point:
- id_token
- access_token
- expires_in
- refresh_token
- refresh_expires_in
:param dict token_response:
:rtype: authentication.openid.models.OpenIDTokenProfile
"""
userinfo = self.openid_connect_client.userinfo(
token=token_response['access_token'])
with transaction.atomic():
user, _ = get_user_model().objects.update_or_create(
username=userinfo.get('preferred_username', ''),
defaults={
'email': userinfo.get('email', ''),
'first_name': userinfo.get('given_name', ''),
'last_name': userinfo.get('family_name', '')
}
)
oidt_profile = OpenIDTokenProfile(
user=user,
access_token=token_response['access_token'],
refresh_token=token_response['refresh_token'],
)
if user:
post_create_openid_user.send(sender=user.__class__, user=user)
return oidt_profile
def __str__(self):
return self.client_id
class Nonce(object):
"""
The openid-login is stored in cache as a temporary object, recording the
user's redirect_uri and next_pat
"""
def __init__(self, redirect_uri, next_path):
import uuid
self.state = uuid.uuid4()
self.redirect_uri = redirect_uri
self.next_path = next_path

View File

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
#
import logging
from django.urls import reverse
from django.conf import settings
from django.core.cache import cache
from django.views.generic.base import RedirectView
from django.contrib.auth import authenticate, login
from django.http.response import (
HttpResponseBadRequest,
HttpResponseServerError,
HttpResponseRedirect
)
from . import client
from .models import Nonce
from users.models import LoginLog
from users.tasks import write_login_log_async
from common.utils import get_request_ip
logger = logging.getLogger(__name__)
def get_base_site_url():
return settings.BASE_SITE_URL
class LoginView(RedirectView):
def get_redirect_url(self, *args, **kwargs):
nonce = Nonce(
redirect_uri=get_base_site_url() + reverse(
"authentication:openid-login-complete"),
next_path=self.request.GET.get('next')
)
cache.set(str(nonce.state), nonce, 24*3600)
self.request.session['openid_state'] = str(nonce.state)
authorization_url = client.openid_connect_client.\
authorization_url(
redirect_uri=nonce.redirect_uri, scope='code',
state=str(nonce.state)
)
return authorization_url
class LoginCompleteView(RedirectView):
def get(self, request, *args, **kwargs):
if 'error' in request.GET:
return HttpResponseServerError(self.request.GET['error'])
if 'code' not in self.request.GET and 'state' not in self.request.GET:
return HttpResponseBadRequest()
if self.request.GET['state'] != self.request.session['openid_state']:
return HttpResponseBadRequest()
nonce = cache.get(self.request.GET['state'])
if not nonce:
return HttpResponseBadRequest()
user = authenticate(
request=self.request,
code=self.request.GET['code'],
redirect_uri=nonce.redirect_uri
)
cache.delete(str(nonce.state))
if not user:
return HttpResponseBadRequest()
login(self.request, user)
data = {
'username': user.username,
'mfa': int(user.otp_enabled),
'reason': LoginLog.REASON_NOTHING,
'status': True
}
self.write_login_log(data)
return HttpResponseRedirect(nonce.next_path or '/')
def write_login_log(self, data):
login_ip = get_request_ip(self.request)
user_agent = self.request.META.get('HTTP_USER_AGENT', '')
tmp_data = {
'ip': login_ip,
'type': 'W',
'user_agent': user_agent
}
data.update(tmp_data)
write_login_log_async.delay(**data)

View File

@@ -0,0 +1,4 @@
from django.dispatch import Signal
post_create_openid_user = Signal(providing_args=('user',))

View File

@@ -0,0 +1,33 @@
from django.http.request import QueryDict
from django.contrib.auth.signals import user_logged_out
from django.dispatch import receiver
from django.conf import settings
from .openid import client
from .signals import post_create_openid_user
@receiver(user_logged_out)
def on_user_logged_out(sender, request, user, **kwargs):
if not settings.AUTH_OPENID:
return
query = QueryDict('', mutable=True)
query.update({
'redirect_uri': settings.BASE_SITE_URL
})
openid_logout_url = "%s?%s" % (
client.openid_connect_client.get_url(
name='end_session_endpoint'),
query.urlencode()
)
request.COOKIES['next'] = openid_logout_url
@receiver(post_create_openid_user)
def on_post_create_openid_user(sender, user=None, **kwargs):
if user and user.username != 'admin':
user.source = user.SOURCE_OPENID
user.save()

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,16 @@
# coding:utf-8
#
from django.urls import path
from authentication.openid import views
app_name = 'authentication'
urlpatterns = [
# openid
path('openid/login/', views.LoginView.as_view(), name='openid-login'),
path('openid/login/complete/', views.LoginCompleteView.as_view(),
name='openid-login-complete'),
# other
]

View File

@@ -0,0 +1 @@

View File

@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
#
import os
import json
import jms_storage
from rest_framework.views import Response, APIView
from ldap3 import Server, Connection
@@ -8,12 +11,13 @@ from django.core.mail import get_connection, send_mail
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from .permissions import IsSuperUser
from .permissions import IsOrgAdmin, IsSuperUser
from .serializers import MailTestSerializer, LDAPTestSerializer
from .models import Setting
class MailTestingAPI(APIView):
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = MailTestSerializer
success_message = _("Test mail sent to {}, please check")
@@ -37,7 +41,7 @@ class MailTestingAPI(APIView):
class LDAPTestingAPI(APIView):
permission_classes = (IsSuperUser,)
permission_classes = (IsOrgAdmin,)
serializer_class = LDAPTestSerializer
success_message = _("Test ldap success")
@@ -48,7 +52,7 @@ class LDAPTestingAPI(APIView):
bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"]
password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"]
use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False)
search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
search_ougroup = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
@@ -64,18 +68,19 @@ class LDAPTestingAPI(APIView):
except Exception as e:
return Response({"error": str(e)}, status=401)
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
return Response({"error": "Search no entry matched"}, status=401)
users = []
for entry in conn.entries:
user = {}
for attr, mapping in attr_map.items():
if hasattr(entry, mapping):
user[attr] = getattr(entry, mapping)
users.append(user)
for search_ou in str(search_ougroup).split("|"):
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
return Response({"error": _("Search no entry matched in ou {}").format(search_ou)}, status=401)
for entry in conn.entries:
user = {}
for attr, mapping in attr_map.items():
if hasattr(entry, mapping):
user[attr] = getattr(entry, mapping)
users.append(user)
if len(users) > 0:
return Response({"msg": _("Match {} s users").format(len(users))})
else:
@@ -84,9 +89,93 @@ class LDAPTestingAPI(APIView):
return Response({"error": str(serializer.errors)}, status=401)
class ReplayStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
if storage_data.get('TYPE') == 'ceph':
port = storage_data.get('PORT')
if port.isdigit():
storage_data['PORT'] = int(storage_data.get('PORT'))
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response({"error": _("Error: Account invalid")}, status=401)
Setting.save_storage('TERMINAL_REPLAY_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod
def is_valid(storage_data):
if storage_data.get('TYPE') == 'server':
return True
storage = jms_storage.get_object_storage(storage_data)
target = 'tests.py'
src = os.path.join(settings.BASE_DIR, 'common', target)
return storage.is_valid(src, target)
class ReplayStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_REPLAY_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class CommandStorageCreateAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_data = request.data
storage_name = storage_data.pop('NAME')
data = {storage_name: storage_data}
if not self.is_valid(storage_data):
return Response({"error": _("Error: Account invalid")}, status=401)
Setting.save_storage('TERMINAL_COMMAND_STORAGE', data)
return Response({"msg": _('Create succeed')}, status=200)
@staticmethod
def is_valid(storage_data):
if storage_data.get('TYPE') == 'server':
return True
try:
storage = jms_storage.get_log_storage(storage_data)
except Exception:
return False
return storage.ping()
class CommandStorageDeleteAPI(APIView):
permission_classes = (IsSuperUser,)
def post(self, request):
storage_name = str(request.data.get('name'))
Setting.delete_storage('TERMINAL_COMMAND_STORAGE', storage_name)
return Response({"msg": _('Delete succeed')}, status=200)
class DjangoSettingsAPI(APIView):
def get(self, request):
return Response('Danger, Close now')
if not settings.DEBUG:
return Response("Not in debug mode")
data = {}
for k, v in settings.__dict__.items():
if k and k.isupper():
try:
json.dumps(v)
data[k] = v
except (json.JSONDecodeError, TypeError):
data[k] = str(v)
return Response(data)

View File

@@ -13,7 +13,7 @@ from .utils import get_signer
signer = get_signer()
class DictField(forms.Field):
class FormDictField(forms.Field):
widget = forms.Textarea
def to_python(self, value):
@@ -23,6 +23,7 @@ class DictField(forms.Field):
# RadioSelect will provide. Because bool("True") == bool('1') == True,
# we don't need to handle that explicitly.
if isinstance(value, six.string_types):
value = value.replace("'", '"')
try:
value = json.loads(value)
return value
@@ -62,7 +63,7 @@ class EncryptMixin:
def get_prep_value(self, value):
if value is None:
return value
return signer.sign(value).decode('utf-8')
return signer.sign(value)
class EncryptTextField(EncryptMixin, models.TextField):
@@ -74,3 +75,32 @@ class EncryptCharField(EncryptMixin, models.CharField):
kwargs['max_length'] = 2048
super().__init__(*args, **kwargs)
class FormEncryptMixin:
pass
class FormEncryptCharField(FormEncryptMixin, forms.CharField):
pass
class FormEncryptDictField(FormEncryptMixin, FormDictField):
pass
class ChoiceDisplayField(serializers.ChoiceField):
def __init__(self, *args, **kwargs):
super(ChoiceDisplayField, self).__init__(*args, **kwargs)
self.choice_strings_to_display = {
six.text_type(key): value for key, value in self.choices.items()
}
def to_representation(self, value):
if value is None:
return value
return {
'value': self.choice_strings_to_values.get(six.text_type(value),
value),
'display': self.choice_strings_to_display.get(six.text_type(value),
value),
}

View File

@@ -1,69 +1,63 @@
# -*- coding: utf-8 -*-
#
import json
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.utils.html import escape
from django.db import transaction
from django.conf import settings
from .models import Setting
from .fields import DictField
def to_model_value(value):
try:
return json.dumps(value)
except json.JSONDecodeError:
return None
def to_form_value(value):
try:
data = json.loads(value)
if isinstance(data, dict):
data = value
return data
except json.JSONDecodeError:
return ""
from .models import Setting, common_settings
from .fields import FormDictField, FormEncryptCharField, \
FormEncryptMixin, FormEncryptDictField
class BaseForm(forms.Form):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
db_settings = Setting.objects.all()
for name, field in self.fields.items():
db_value = getattr(db_settings, name).value
db_value = getattr(common_settings, name)
django_value = getattr(settings, name) if hasattr(settings, name) else None
if db_value is None and django_value is None:
continue
if db_value is False or db_value:
field.initial = to_form_value(db_value)
if isinstance(db_value, dict):
db_value = json.dumps(db_value)
initial_value = db_value
elif django_value is False or django_value:
field.initial = to_form_value(to_model_value(django_value))
initial_value = django_value
else:
initial_value = ''
field.initial = initial_value
def save(self, category="default"):
if not self.is_bound:
raise ValueError("Form is not bound")
db_settings = Setting.objects.all()
if self.is_valid():
with transaction.atomic():
for name, value in self.cleaned_data.items():
field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value:
continue
if value == to_form_value(getattr(db_settings, name).value):
continue
defaults = {
'name': name,
'category': category,
'value': to_model_value(value)
}
Setting.objects.update_or_create(defaults=defaults, name=name)
else:
# db_settings = Setting.objects.all()
if not self.is_valid():
raise ValueError(self.errors)
with transaction.atomic():
for name, value in self.cleaned_data.items():
field = self.fields[name]
if isinstance(field.widget, forms.PasswordInput) and not value:
continue
if value == getattr(common_settings, name):
continue
encrypted = True if isinstance(field, FormEncryptMixin) else False
try:
setting = Setting.objects.get(name=name)
except Setting.DoesNotExist:
setting = Setting()
setting.name = name
setting.category = category
setting.encrypted = encrypted
setting.cleaned_value = value
setting.save()
class BasicSettingForm(BaseForm):
SITE_URL = forms.URLField(
@@ -88,7 +82,7 @@ class EmailSettingForm(BaseForm):
EMAIL_HOST_USER = forms.CharField(
max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org'
)
EMAIL_HOST_PASSWORD = forms.CharField(
EMAIL_HOST_PASSWORD = FormEncryptCharField(
max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput,
required=False, help_text=_("Some provider use token except password")
)
@@ -109,26 +103,24 @@ class LDAPSettingForm(BaseForm):
AUTH_LDAP_BIND_DN = forms.CharField(
label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org'
)
AUTH_LDAP_BIND_PASSWORD = forms.CharField(
AUTH_LDAP_BIND_PASSWORD = FormEncryptCharField(
label=_("Password"), initial='',
widget=forms.PasswordInput, required=False
)
AUTH_LDAP_SEARCH_OU = forms.CharField(
label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org'
label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org',
help_text=_("Use | split User OUs")
)
AUTH_LDAP_SEARCH_FILTER = forms.CharField(
label=_("User search filter"), initial='(cn=%(user)s)',
help_text=_("Choice may be (cn|uid|sAMAccountName)=%(user)s)")
)
AUTH_LDAP_USER_ATTR_MAP = DictField(
AUTH_LDAP_USER_ATTR_MAP = FormDictField(
label=_("User attr map"),
initial=json.dumps({
"username": "cn",
"name": "sn",
"email": "mail"
}),
help_text=_(
"User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr")
"User attr map present how to map LDAP user attr to jumpserver, "
"username,name,email is jumpserver attr"
)
)
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
@@ -143,34 +135,26 @@ class TerminalSettingForm(BaseForm):
('hostname', _('Hostname')),
('ip', _('IP')),
)
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by")
)
TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
initial=5, label=_("Heartbeat interval"), help_text=_("Units: seconds")
)
TERMINAL_PASSWORD_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Password auth")
)
TERMINAL_PUBLIC_KEY_AUTH = forms.BooleanField(
initial=True, required=False, label=_("Public key auth")
)
TERMINAL_COMMAND_STORAGE = DictField(
label=_("Command storage"), help_text=_(
"Set terminal storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
)
TERMINAL_HEARTBEAT_INTERVAL = forms.IntegerField(
initial=5, label=_("Heartbeat interval"), help_text=_("Units: seconds")
)
TERMINAL_REPLAY_STORAGE = DictField(
label=_("Replay storage"), help_text=_(
"Set replay storage setting, `default` is the using as default,"
"You can set other storage and some terminal using"
)
TERMINAL_ASSET_LIST_SORT_BY = forms.ChoiceField(
choices=SORT_BY_CHOICES, initial='hostname', label=_("List sort by")
)
class TerminalCommandStorage(BaseForm):
pass
class SecuritySettingForm(BaseForm):
# MFA全局设置
# MFA global setting
SECURITY_MFA_AUTH = forms.BooleanField(
initial=False, required=False,
label=_("MFA Secondary certification"),
@@ -179,38 +163,60 @@ class SecuritySettingForm(BaseForm):
'authentication (valid for all users, including administrators)'
)
)
# 最小长度
# limit login count
SECURITY_LOGIN_LIMIT_COUNT = forms.IntegerField(
initial=7, min_value=3,
label=_("Limit the number of login failures")
)
# limit login time
SECURITY_LOGIN_LIMIT_TIME = forms.IntegerField(
initial=30, min_value=5,
label=_("No logon interval"),
help_text=_(
"Tip :(unit/minute) if the user has failed to log in for a limited "
"number of times, no login is allowed during this time interval."
)
)
SECURITY_MAX_IDLE_TIME = forms.IntegerField(
initial=30, required=False,
label=_("Connection max idle time"),
help_text=_(
'If idle time more than it, disconnect connection(only ssh now) '
'Unit: minute'
),
)
# min length
SECURITY_PASSWORD_MIN_LENGTH = forms.IntegerField(
initial=6, label=_("Password minimum length"),
min_value=6
)
# 大写字母
# upper case
SECURITY_PASSWORD_UPPER_CASE = forms.BooleanField(
initial=False, required=False,
label=_("Must contain capital letters"),
help_text=_(
'After opening, the user password changes '
'and resets must contain uppercase letters')
)
# 小写字母
# lower case
SECURITY_PASSWORD_LOWER_CASE = forms.BooleanField(
initial=False, required=False,
label=_("Must contain lowercase letters"),
help_text=_('After opening, the user password changes '
'and resets must contain lowercase letters')
)
# 数字
# number
SECURITY_PASSWORD_NUMBER = forms.BooleanField(
initial=False, required=False,
label=_("Must contain numeric characters"),
help_text=_('After opening, the user password changes '
'and resets must contain numeric characters')
)
# 特殊字符
SECURITY_PASSWORD_SPECIAL_CHAR= forms.BooleanField(
# special char
SECURITY_PASSWORD_SPECIAL_CHAR = forms.BooleanField(
initial=False, required=False,
label=_("Must contain special characters"),
help_text=_('After opening, the user password changes '
'and resets must contain special characters')
)

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