Compare commits

...

568 Commits
1.5.6 ... v2.1

Author SHA1 Message Date
Bai
a52b8a13fd fix(common) 修复管理员未设置Email主题前缀导致发送邮件失败的问题 2020-09-02 10:24:37 +08:00
ibuler
8f0cd12a3c ci(Dockerfile): 修改依赖的setuptools版本,导致的ldap无法安装问题 2020-09-02 10:05:36 +08:00
ibuler
1551aa418f perf(config): 修改默认登录日志保存时间 2020-08-18 05:21:20 -05:00
xinwen
37b1038725 fix(csv): 修复JMSCSVParser调用serializer导致循环调用问题 2020-08-13 12:48:47 +08:00
ibuler
526babb724 fix(auth): 修复radius decode error的问题 2020-08-03 10:33:02 +08:00
ibuler
a3c1d1e730 ci: 修改docker构建的问题 2020-07-29 19:49:51 +08:00
ibuler
8905613e50 ci(dockerfile): 修改dockerfile构建 2020-07-28 19:28:49 +08:00
Bai
d6d85dff40 fix(perms): 修复perms api UserPermissionMixin 中 kwargs 参数传递(未发现引出其他问题) 2020-07-23 11:04:11 +08:00
github-actions
28b92a0d80 fix(cas): 修复cas校验不同的问题 2020-07-22 17:22:54 +08:00
ibuler
e4d2b5e98d fix(es): 修复es7数据结构引起的命令无法查询的问题
- 更新了 jms-storage版本依赖
2020-07-22 17:14:59 +08:00
github-actions
1e1cd2e1aa ci(github): 添加通用action 2020-07-21 14:33:08 +08:00
Bai
5065490a25 fix(authentication): 修复用户认证Radius认证中参数传递错误问题 *kwargs -> **kwargs 2020-07-21 10:31:57 +08:00
BaiJiangJie
070af8c491 fix(radius): 修复radius认证失败问题 (#4342) (#4343)
* fix(radius): 修复radius认证失败问题,添加get_django_user方法参数(django-radius==1.4.0 中添加了额外参数)

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

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

* [Update] 修改redis-cache 版本
2020-06-02 15:44:45 +08:00
ibuler
50bea55732 merge: with dev 2020-06-02 15:43:42 +08:00
ibuler
a4ece2b271 feat: audits中添加id字段 2020-06-02 15:41:27 +08:00
xinwen
b460e4abaa [Fix] 日志审计 日期过滤与排序bug (#4063) 2020-06-02 15:40:07 +08:00
BaiJiangJie
087ba9ae95 Merge pull request #4059 from jumpserver/lina
Lina
2020-06-01 14:05:22 +08:00
BaiJiangJie
19926e67e1 Merge pull request #4058 from jumpserver/dev
Dev
2020-06-01 14:02:56 +08:00
BaiJiangJie
af15622319 Merge pull request #4057 from jumpserver/dev_ldap
[Update] 添加LDAP USER_QUERY_FIELD字段值为username,解决数据库因大小敏感导致LDAP用户认证首次成功…
2020-06-01 12:51:30 +08:00
Bai
9850633350 [Update] 添加LDAP USER_QUERY_FIELD字段值为username,解决数据库因大小敏感导致LDAP用户认证首次成功,再次失败的问题;如果不配置此字段,则查询使用小写,创建可能会使用大写,最终导致username重复而创建用户失败; 2020-06-01 12:06:39 +08:00
ibuler
5f2345852d Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-06-01 11:41:55 +08:00
ibuler
ad1c17aa7b feat: 仅支持fields_size=mini,small 2020-06-01 10:10:28 +08:00
xinwen
3a79bfd5f6 [Update] 添加一些 i18n (#4052) 2020-05-29 15:18:25 +08:00
Bai
5ee8519274 [Update] 修改AccessKey序列类 2020-05-29 14:04:50 +08:00
Bai
ff546774e9 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-28 20:46:30 +08:00
Bai
1f4fc9b6f0 [Update] 修改API-Key序列类 2020-05-28 20:46:17 +08:00
xinwen
f8142e23cd [Fix] 权限管理-> 资产与数据库 Bug (#4049) 2020-05-28 19:08:48 +08:00
Bai
196f1654ab Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-28 17:47:27 +08:00
Bai
3b8a24eeb7 [Update] 添加用户profile public-key序列类 2020-05-28 17:47:18 +08:00
xinwen
7dde15cb04 [Feature] 权限管理-> 远程应用 添加 api (#4040)
* [Feature] 权限管理-> 远程应用 添加 api

* [Feature] 添加 RelationMixin
2020-05-28 17:00:42 +08:00
Bai
3e5d949610 [Update] 添加用户profile password序列类 2020-05-28 16:10:28 +08:00
Bai
25c3691f6b Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-28 15:03:07 +08:00
Bai
f3bc6c0b22 [Update] 修改用户profile序列类 2020-05-28 15:02:53 +08:00
ibuler
caf0d85939 [feat] 修改remote app, database app的过滤参数 2020-05-28 14:47:30 +08:00
ibuler
a463f632e8 [Update] 优化重定向 2020-05-27 20:33:09 +08:00
ibuler
a0e6d09770 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-27 19:51:45 +08:00
BaiJiangJie
54623a5b06 Merge pull request #4047 from jumpserver/lina_dev
Lina dev
2020-05-27 18:46:48 +08:00
Bai
7afff5e392 [Update] merge from dev to lina 2020-05-27 18:44:12 +08:00
ibuler
9a5fee5a4c Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-27 18:03:14 +08:00
ibuler
a840e611cd [Update] 修改core 的base url 2020-05-27 18:02:18 +08:00
Bai
566419cac4 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-27 17:27:38 +08:00
Bai
7b362bfc76 [Update] 用户序列类添加mfa相关字段 2020-05-27 17:27:28 +08:00
ibuler
f528dd4888 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-26 19:00:00 +08:00
ibuler
3c95c6fe11 [Feat] 添加失效用户权限的api 2020-05-26 18:56:03 +08:00
BaiJiangJie
0b17d55f30 Merge pull request #4041 from jumpserver/dev_oidc
[Update] 修改依赖版本号 jumpserver-django-oidc-rp==0.3.7.5
2020-05-26 14:23:52 +08:00
Bai
3861943518 [Update] 修改依赖版本号 jumpserver-django-oidc-rp==0.3.7.5 2020-05-26 14:23:04 +08:00
老广
71a72dd957 Merge pull request #4033 from jumpserver/update-i18n
[Update] i18n
2020-05-25 01:53:02 -05:00
老广
78ac1968dd Merge pull request #4039 from jumpserver/asset_permission-add-field
[Update] perms.serializers.asset_permission.AssetPermissionSerializer…
2020-05-25 01:52:35 -05:00
xinwen
93453cc8c3 [Update] perms.serializers.asset_permission.AssetPermissionSerializer 添加字段 2020-05-25 14:50:07 +08:00
xinwen
11527b9033 [Update] i18n 2020-05-22 18:18:40 +08:00
ibuler
2ff2266417 [Update] 修改 SerializerMixin 还原之前的更改 2020-05-21 20:35:44 +08:00
老广
1ef1bea703 Merge pull request #4029 from jumpserver/dev
修改了readme,增加安全建议
2020-05-21 07:06:49 -05:00
ibuler
6804f23b51 [Update] 修改readme 2020-05-21 20:01:53 +08:00
ibuler
ea7e1f19b2 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-05-21 19:55:36 +08:00
ibuler
3320e6105c [Update] 删掉没用的内容 2020-05-21 19:00:47 +08:00
老广
072e74ce49 Merge pull request #4027 from jumpserver/update-org-user
[Update] org retrieve api
2020-05-21 03:15:05 -05:00
xinwen
492b1c4311 [Update] org retrieve api 2020-05-21 16:09:42 +08:00
老广
0babada459 Merge pull request #4025 from jumpserver/update-org-user-add-id
[Update] orgs.serializers.OrgReadSerializer add `id`
2020-05-21 02:43:35 -05:00
xinwen
7b0993959e [Update] orgs.serializers.OrgReadSerializer add id 2020-05-21 15:40:41 +08:00
ibuler
701582fe38 [Update] 修改license的判断方法 2020-05-21 14:47:00 +08:00
BaiJiangJie
d289960ff2 Merge pull request #3998 from jumpserver/dev
Dev
2020-05-21 14:00:37 +08:00
BaiJiangJie
6adf37a30d Merge pull request #4022 from jumpserver/dev_tran
[Update] 修改翻译信息
2020-05-20 19:43:43 +08:00
ibuler
b371676813 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-20 19:35:48 +08:00
Bai
2e0eab9289 [Update] 修改翻译信息 2020-05-20 19:35:35 +08:00
ibuler
0cac8d66b3 [Update] 修改index api 2020-05-20 19:35:33 +08:00
BaiJiangJie
2053e9210e Merge pull request #4021 from jumpserver/dev_asset
[Bugfix] 修复只显示当前节点下资产不生效的问题
2020-05-20 18:21:59 +08:00
Bai
7c44f74068 [Bugfix] 修复只显示当前节点下资产不生效的问题 2020-05-20 18:20:01 +08:00
老广
3e0f5af848 Merge pull request #4019 from jumpserver/update-some-i18n
[Update] 一些本地化
2020-05-20 05:17:12 -05:00
xinwen
245d28b03d [Update] 一些本地化 2020-05-20 17:45:50 +08:00
Bai
75f4f6d0a2 [Update] 资产授权序列类is_expired, is_valid设置为read_only 2020-05-19 20:50:33 +08:00
Bai
f9e167cb0e Merge branch 'lina' of https://github.com/jumpserver/jumpserver into lina 2020-05-19 20:36:41 +08:00
Bai
f224e49de7 [Update] 去掉资产授权序列类中不存在的字段 2020-05-19 20:36:17 +08:00
BaiJiangJie
4ec4869896 Merge pull request #4012 from jumpserver/dev_permsasset
[Update] 授权资产树返回org_id字段值
2020-05-19 19:37:13 +08:00
Bai
63ff868553 [Update] 授权资产树返回org_id字段值 2020-05-19 19:36:19 +08:00
BaiJiangJie
234130091b Merge pull request #4011 from jumpserver/dev_conf
[Update] 修改配置文件demo
2020-05-19 18:38:47 +08:00
Bai
216e0f28b9 [Update] 修改配置文件demo 2020-05-19 18:36:56 +08:00
BaiJiangJie
f3483484d7 Merge pull request #4010 from jumpserver/dev_openid
Dev openid
2020-05-19 18:35:30 +08:00
Bai
d704a35ead [Update] 修改依赖版本号 jumpserver-django-oidc-rp==0.3.7.4 2020-05-19 18:33:33 +08:00
Bai
b97deec1de [Update] openid 继续使用配置项 base_site_url 2020-05-19 18:24:26 +08:00
BaiJiangJie
5b6488e1b2 Merge pull request #4008 from jumpserver/dev_bugfix
Dev bugfix
2020-05-19 15:31:10 +08:00
Bai
be3fdac8a9 [Update] 修复API-key不能删除的问题 2020-05-19 15:28:31 +08:00
Bai
96afd82341 [Update] 修复任务列表,执行历史中stat汇总数据显示问题 2020-05-19 15:15:17 +08:00
Bai
728e4b7edd [Update] 修复授权模块的信号处理监听不到的问题 2020-05-19 11:47:49 +08:00
Bai
73c3de97b8 [Update] 取消用户资产页面中,资产详情系统用户值为undefine的问题 2020-05-18 18:14:19 +08:00
Bai
e98626988b [Update] 解决开启OpenID,为用户强制启用MFA导致,登录时报错的问题 2020-05-18 18:02:37 +08:00
Bai
01a52812f0 [Update] 修改用户MFA禁用/更新的View 2020-05-18 15:49:53 +08:00
Bai
041d99f0be [Update] 更新remote-app用户/用户组时自动刷新页面 2020-05-18 15:40:54 +08:00
Bai
75edc26a10 [Update] 修改翻译,禁用资产提示不支持ansible的错误提示 2020-05-18 15:25:24 +08:00
Bai
2f7b169405 [Update] 修改授权用户/用户组页面删除用户组失败的问题 2020-05-18 15:24:55 +08:00
ibuler
76ef9b292b [Update] 修改public api 2020-05-18 14:55:16 +08:00
ibuler
1540cbdcaa [Update] 修改csv render 2020-05-18 11:52:50 +08:00
BaiJiangJie
eefe0709f8 Merge pull request #4007 from maninhill/patch-1
Update README.md
2020-05-17 07:44:37 +08:00
maninhill
5ae6e81a1d Update README.md 2020-05-17 00:02:35 +08:00
BaiJiangJie
8a62488cb9 Merge pull request #4003 from jumpserver/dev_v
[Update] 修改版本号1.5.9
2020-05-14 16:15:17 +08:00
Bai
e951b64b0a [Update] 修改版本号1.5.9 2020-05-14 16:14:17 +08:00
ibuler
5d129fd0da Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-14 14:51:05 +08:00
ibuler
b529127461 [Update] 修改user role 2020-05-14 14:49:54 +08:00
BaiJiangJie
2e40b9607e Merge pull request #4002 from jumpserver/dev_bai
[Update] 用户资产详情Modal添加comment
2020-05-14 14:28:24 +08:00
Bai
d878089ebd [Update] 用户资产详情Modal添加comment 2020-05-14 14:26:00 +08:00
BaiJiangJie
9d32285446 Merge pull request #3997 from jumpserver/dev_bai
[Update] 修改向导文案
2020-05-14 11:01:35 +08:00
Bai
4658a4c90f [Update] 修改向导文案 2020-05-14 11:00:24 +08:00
Bai
d06ea2944e [Update] 修改Dashboard API 2020-05-14 10:53:30 +08:00
wojiushixiaobai
d50ea83f40 [Fix]修改文档链接 2020-05-13 15:21:04 +08:00
ibuler
154aad1e22 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-13 11:06:24 +08:00
ibuler
17163dd909 [Update] 用户Profile添加在当前组织的角色 2020-05-13 11:05:58 +08:00
BaiJiangJie
b789a8bb05 Merge pull request #3994 from jumpserver/lina_assetuser
[Update] 修复AssetUserViewSet 使用Option方法时error
2020-05-12 20:32:58 +08:00
Bai
9341ce9f84 [Update] 修复AssetUserViewSet 使用Option方法时error 2020-05-12 20:31:32 +08:00
ibuler
4d0f7d0254 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-05-12 17:57:51 +08:00
ibuler
195cbbbe42 [Update] 修改profile serialzier 2020-05-12 17:57:35 +08:00
xinwen
6e5e340a25 Merge pull request #3993 from jumpserver/update-assets-filter
[Update] Asset filter 添加 platform__base 字段
2020-05-12 17:26:59 +08:00
xinwen
eb74d13059 [Update] Asset filter 添加 platform__base 字段 2020-05-12 17:24:51 +08:00
老广
16f916c40a Merge pull request #3992 from jumpserver/update-assets-filter
Update assets filter
2020-05-12 04:11:49 -05:00
xinwen
4dd6d4498b [Update] Asset filter 添加 platform__name 字段 2020-05-12 17:09:36 +08:00
ibuler
dd5bf546df [Update] 修改serialzier_class 2020-05-12 15:37:37 +08:00
ibuler
d6debde566 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-11 19:28:24 +08:00
ibuler
efc66cc7ee [Update] 默认关闭debug 2020-05-11 19:28:02 +08:00
BaiJiangJie
b310731ba7 Merge pull request #3990 from jumpserver/lina_dev
Lina dev
2020-05-11 17:45:51 +08:00
Bai
4bd2681bf0 [Update] Merge from dev to lina 2020-05-11 17:43:48 +08:00
BaiJiangJie
38e09753f4 Merge pull request #3989 from jumpserver/dev_master
Dev master
2020-05-11 17:38:46 +08:00
Bai
98e3adbb11 [Update] Merge from master to dev 2020-05-11 17:38:01 +08:00
BaiJiangJie
c700b101c1 Merge pull request #3988 from jumpserver/master_otp
[Bugfix] 修复用户页面更新MFA时解除管理员强制启用的Bug
2020-05-11 16:58:40 +08:00
Bai
7b3647e78a [Bugfix] 修复用户页面更新MFA时解除管理员强制启用的Bug 2020-05-11 16:56:52 +08:00
BaiJiangJie
f4f042c407 Merge pull request #3987 from jumpserver/master_index
[Update] 仪表盘缓存key添加组织id
2020-05-11 15:20:00 +08:00
Bai
0a8eeca629 [Update] 仪表盘缓存key添加组织id 2020-05-11 15:12:42 +08:00
BaiJiangJie
fdd55511a6 Merge pull request #3986 from jumpserver/lina_dev
Lina dev
2020-05-11 14:04:52 +08:00
Bai
0cff6ab29b [Update] Merge fromo dev to lina 2020-05-11 14:03:24 +08:00
ibuler
cda677a30f [update] 修改remote apps的serializer 2020-05-11 12:35:33 +08:00
ibuler
5571651c02 [Update] 升级requirements 2020-05-09 16:13:06 +08:00
ibuler
2680396680 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-09 16:08:32 +08:00
ibuler
227f97c2f5 [Update] 修改remote apps 字段 2020-05-09 16:08:09 +08:00
老广
ad3231c8a3 Merge pull request #3982 from jumpserver/update-ops-migrate
[Update] 重建 ops 0018 迁移脚本
2020-05-09 01:54:37 -05:00
ibuler
e39d8dce3c [Update] 修改fields 2020-05-09 14:52:44 +08:00
ibuler
0bdc425c55 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-05-09 14:51:47 +08:00
ibuler
0a7f63cc5e [Update] 修改remote app api 2020-05-09 14:51:19 +08:00
xinwen
c6e0e9a79a [Update] 重建 ops 0018 迁移脚本 2020-05-09 14:50:22 +08:00
BaiJiangJie
4cebfc7f6a [Update] 修改翻译 (#3981)
* [Update] 修改翻译

* [Update] 修改翻译2
2020-05-09 13:29:39 +08:00
老广
7fde392774 Merge pull request #3980 from jumpserver/update-audits-verbose
[Update] audits 模块为一些 models 字段添加 verbose 信息
2020-05-08 22:40:34 -05:00
xinwen
3ee051303a [Update] audits 模块为一些 models 字段添加 verbose 信息 2020-05-09 11:29:25 +08:00
老广
9fa31be4bf Merge pull request #3976 from jumpserver/add-audits-apis
[Add] audits apis
2020-05-08 08:04:14 -05:00
老广
ae2e4049db Merge pull request #3979 from jumpserver/lina_setting_dict_field
AUTH_LDAP_USER_ATTR_MAP to Dict field
2020-05-08 08:02:04 -05:00
Eric
48b71bb11b AUTH_LDAP_USER_ATTR_MAP to Dict field 2020-05-08 20:58:13 +08:00
BaiJiangJie
7e4edc3c63 Merge pull request #3978 from jumpserver/dev_index
[Update] 修改dashboard中使用的变量名
2020-05-08 20:03:08 +08:00
Bai
11b3c57c92 [Update] 修改dashboard中使用的变量名 2020-05-08 20:02:00 +08:00
xinwen
e339ed1fb3 [Add] audits apis 2020-05-08 17:59:58 +08:00
BaiJiangJie
2d48c8028b Merge pull request #3975 from jumpserver/dev_master
Dev master
2020-05-08 16:42:32 +08:00
Bai
185c53d311 [Update] Merge from master to dev 2020-05-08 16:40:07 +08:00
BaiJiangJie
2f030c02ec Merge pull request #3973 from jumpserver/master_org
[Update] 优化仪表盘、组织详情页
2020-05-08 16:27:49 +08:00
老广
f4457ff1e2 Merge pull request #3972 from jumpserver/PERM_CACHE_CONFIG
Bugfix Perm cache config
2020-05-08 03:04:33 -05:00
ibuler
c6ed6d8acb [Update] 去掉debug信息 2020-05-08 15:58:00 +08:00
ibuler
331cfe2aed [Bugfix] 修复节点数量缓存显示不对的bug 2020-05-08 15:53:58 +08:00
Bai
e48dbabef2 [Update] 修改翻译 2020-05-08 15:50:01 +08:00
Bai
181973f235 [Update] 优化仪表盘页面加载 2020-05-08 15:41:56 +08:00
ibuler
440a2ad241 Merge branch 'PERM_CACHE_CONFIG' into dev 2020-05-08 14:25:36 +08:00
ibuler
1936a6d5ee [Update] 去掉perms cache enable settings 2020-05-08 14:25:28 +08:00
老广
4517a92b2b Merge pull request #3971 from jumpserver/add-ftp-log-api
[Add] ftp-logs api
2020-05-08 00:52:03 -05:00
ibuler
44a2a51f59 Merge branch 'PERM_CACHE_CONFIG' into dev 2020-05-08 13:13:47 +08:00
ibuler
2d4498578a [Update] 修改授权缓存配置的默认值 2020-05-08 13:13:38 +08:00
xinwen
ac902501ec [Add] ftp-logs api 2020-05-08 12:46:18 +08:00
老广
363b5d04d9 Merge pull request #3969 from jumpserver/update-login-logs
[Update] login-logs api 添加 `mfa_display`
2020-05-07 22:23:37 -05:00
xinwen
dec89ae5ee [Update] login-logs api 添加 mfa_display 2020-05-08 10:08:57 +08:00
老广
5d37269a6c Merge pull request #3967 from jumpserver/lina_status_code
[Update] Reponse status code from 401 to 400
2020-05-07 04:03:34 -05:00
Eric
9a39ccd37d [Update] Reponse status code from 401 to 400 2020-05-07 16:57:57 +08:00
BaiJiangJie
5896ea9c63 Merge pull request #3963 from jumpserver/dev_oidc
[Update] 修改依赖版本jumpserver-django-oidc-rp==0.3.7.3
2020-05-06 21:06:27 +08:00
Bai
8323de1c07 [Update] 修改依赖版本jumpserver-django-oidc-rp==0.3.7.3 2020-05-06 21:05:23 +08:00
ibuler
8c0bf0b71b [Update] Login log 添加display field 2020-05-06 19:35:19 +08:00
老广
5812c50a33 Merge pull request #3960 from jumpserver/lina_setting
[Update] save settings
2020-05-06 04:05:51 -05:00
Eric
7b339df430 [Update] save settings 2020-05-06 16:36:36 +08:00
Bai
aec78dc3c7 [Update] 添加组织详情->组织用户tab页,解决组织详情页面加载超时问题 2020-05-06 15:31:08 +08:00
ibuler
6bb13a26f5 [Update] 修改用户serializer 2020-04-30 16:58:08 +08:00
ibuler
b92137afd9 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-04-30 16:53:21 +08:00
老广
962763dc7b Merge pull request #3954 from xuxinwen/feature-audits-apis
[Feature] 添加 login-logs API
2020-04-30 03:52:57 -05:00
xuxinwen
f4eca83a49 [feature] 添加 login-logs API 2020-04-30 16:40:55 +08:00
老广
02135ea04f Merge pull request #3948 from jumpserver/settings_api
[Update] Add settings api
2020-04-29 18:25:10 +08:00
Eric
79eb838250 [Update] setting fields automatically generated by serializer 2020-04-29 18:10:42 +08:00
Eric
82710294f4 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into settings_api 2020-04-29 17:09:50 +08:00
Eric
2d18acf6f7 [Update] settings api 2020-04-29 17:04:58 +08:00
BaiJiangJie
a860bed34f [Update] 修改config_example配置(openid) (#3951)
* [Update] 修改config_example配置(openid)

* [Update] 修改config_example配置(openid)2
2020-04-29 16:57:10 +08:00
ibuler
f8c323cf5c [Update] 优化user group serializer 2020-04-29 16:01:14 +08:00
ibuler
b6f5b335bd Merge branch 'api_perf' into lina 2020-04-29 15:47:46 +08:00
Eric
f451f8a979 Merge branch 'lina' of https://github.com/jumpserver/jumpserver into settings_api 2020-04-29 14:33:22 +08:00
ibuler
5c4dfabc48 [Update] 修改settings 2020-04-29 14:32:51 +08:00
BaiJiangJie
72ccaf7b1c Merge pull request #3949 from jumpserver/dev_openid
Dev openid
2020-04-29 14:25:32 +08:00
Bai
e4b788a012 [Update] 修改依赖版本 jumpserver-dajngo-oidc-rp (0.3.7.2) 2020-04-29 14:07:54 +08:00
Bai
6eaba4e2fb [Update] openid配置分块 2020-04-29 14:03:48 +08:00
ibuler
0c0c0e6d6f Merge branch 'dev' into lina 2020-04-29 13:00:47 +08:00
Eric
20cf7c7c52 [Update] Add settings api 2020-04-29 11:09:35 +08:00
ibuler
230d6137f3 Merge branch 'lina' of github.com:jumpserver/jumpserver into lina 2020-04-29 10:21:06 +08:00
ibuler
aa9533eb5b [Update] Groups users字段可以显示 2020-04-29 10:19:25 +08:00
Bai
23f9454e5d [Update] 修改Url name (oidc => openi) 2020-04-29 01:12:00 +08:00
Bai
9eee79f7d4 [Update] 调整openid backend顺序;openid 用户创建/更新添加日志输出 2020-04-29 00:43:54 +08:00
Bai
c0089a98f4 [Update] 修改openid信号名称 2020-04-28 22:29:56 +08:00
Bai
87242c13a1 [Update] 修改信号监听kwargs 2020-04-28 21:06:13 +08:00
Bai
184432a2a6 [Update] 更新OpenID的配置项以及对应的信号监听 2020-04-28 21:00:22 +08:00
BaiJiangJie
555861c319 Merge pull request #3943 from jumpserver/dev_conf
[Update] 修改config_example(openid)
2020-04-27 12:50:03 +08:00
Bai
a505995f49 [Update] 修改config_example(openid) 2020-04-27 11:36:11 +08:00
BaiJiangJie
b477b649f5 Merge pull request #3942 from jumpserver/dev_docker
[Update] 修改Dockerfile pypi 镜像源(阿里=>清华)
2020-04-26 22:41:49 +08:00
Michael Bai
6e18383531 [Update] 修改Dockerfile pypi 镜像源(阿里=>清华) 2020-04-26 22:40:56 +08:00
BaiJiangJie
7833ff6671 Dev oidc (#3941)
* [Update] oidc_rp获取token添加headers base64编码

* [Update] 移除对oidc_rp的支持

* [Update] 移除对oidc_rp的支持2

* [Update] 修改OpenID配置(添加新配置项,并对旧配置项做兼容)

* [Update] 移除所有与Keycloak相关的模块

* [Update] 添加jumpserver-django-oidc-rp的使用

* [Update] 更新登录重定向地址(oidc)

* [Update] oidc添加一些配置参数;处理用户登录/创建/更新等信号

* [Update] 修改退出登录逻辑

* [Update] 添加oidc user登录成功的信号机制

* [Update] 修改mfa认证choices内容 (otp => code)

* [Update] 添加OpenID backend password 认证失败信号机制;修改引入common包问题

* [Update] 用户Token/Auth API 校验用户时,传入request参数(解决登录成功日志记录的问题)

* [Update] 添加依赖jumpserver-django-oidc-rp==0.3.7.1

* [Update] oidc认证模块说明
2020-04-26 20:36:17 +08:00
BaiJiangJie
5d433456d4 Merge pull request #3937 from jumpserver/dev_index
[Update] 更新仪表盘数据显示问题
2020-04-24 11:30:30 +08:00
Bai
fc5ec3f21c [Update] 更新仪表盘数据显示问题 2020-04-24 11:29:01 +08:00
ibuler
efb5d4135a [Update] 优化api字段显示 2020-04-23 11:14:02 +08:00
ibuler
5f2c9c3801 [Update] 添加 authentication backend header 2020-04-22 15:13:04 +08:00
BaiJiangJie
56f38e57bc Merge pull request #3932 from jumpserver/dev_oidc
[Update] 添加oidc-op配置信息
2020-04-22 11:11:47 +08:00
Bai
586c04cba6 [Update] 添加oidc-op配置信息 2020-04-22 11:09:13 +08:00
Bai
306605915c [Update] 添加依赖 django-oidc-rp==0.3.4 2020-04-22 00:31:59 +08:00
BaiJiangJie
272701a8fd Dev oidc (#3930)
* [Update] 添加django-oidc-rp支持

* [Update] 添加django-oidc-rp支持2

* [Update] 调试django-oidc-rp对keycloak的支持

* [Update] 调试django-oidc-rp对keycloak的支持2

* [Update] 修改oidc_rp创建用户/更新用户的功能

* [Update] oidc_rp添加支持password认证

* [Update] 重写oidc_rp end session view

* [Update] 优化 oidc_rp view backend url 等引用关系
2020-04-22 00:22:24 +08:00
BaiJiangJie
9febe488b5 Merge pull request #3929 from jumpserver/lina_terminal
[Update] add terminal model fileds
2020-04-21 22:23:53 +08:00
Eric
c8c6ba1c19 [Update] add terminal model fileds 2020-04-21 17:37:29 +08:00
老广
2df7bd8510 Merge pull request #3926 from jumpserver/fix_otp_error
Fix otp error
2020-04-20 17:00:54 +08:00
老广
c16a986c4b Merge pull request #3925 from jumpserver/bugfix_many_sqlquery_perm_asset
[Bugfix] 修复查询授权时额外查询platform的问题
2020-04-20 16:58:14 +08:00
ibuler
44db0e8a5d [Bugfix] 修复查询授权时额外查询platform的问题 2020-04-20 16:56:47 +08:00
ibuler
91c994924f [Update] 修改报错文案 2020-04-20 10:44:45 +08:00
ibuler
61407331bc [Bugfix] 修复otp登录时导致的500 2020-04-20 10:37:07 +08:00
BaiJiangJie
1b1a686b96 Merge pull request #3922 from jumpserver/dev
Dev
2020-04-18 23:19:11 +08:00
BaiJiangJie
cc30a20b7c Merge pull request #3921 from jumpserver/dev
Dev
2020-04-18 23:15:42 +08:00
BaiJiangJie
0379e5160c [Bugfix] 修复真实组织下,新建的用户角色显示不稳定的问题2 (#3919)
* [Bugfix] 修复真实组织下,新建的用户角色显示不稳定的问题2

* [Bugfix] 修复真实组织下,新建的用户角色显示不稳定的问题3

* [Bugfix] 修复真实组织下,新建的用户角色显示不稳定的问题4
2020-04-18 23:14:25 +08:00
BaiJiangJie
386ce629ac Merge pull request #3918 from jumpserver/fix_connectivity
[Update] 修复资产页面可连接性
2020-04-18 10:42:55 +08:00
BaiJiangJie
5677bf0995 Merge pull request #3915 from jumpserver/dev
Dev
2020-04-18 01:27:09 +08:00
BaiJiangJie
c17dc26f8c Merge pull request #3916 from jumpserver/dev_org
[Bugfix] 修复真实组织下,新建的用户角色显示不稳定的问题
2020-04-18 01:24:21 +08:00
Bai
b4cc2bbff9 [Bugfix] 修复真实组织下,新建的用户角色显示不稳定的问题 2020-04-18 01:22:48 +08:00
BaiJiangJie
e74289b223 [Update] 修改用户密码重置页面错误信息提示2 (#3914) 2020-04-17 23:41:50 +08:00
ibuler
98afa032a7 [Update] 修复资产页面可连接性 2020-04-17 16:32:38 +08:00
BaiJiangJie
0d3fab216b Merge pull request #3910 from jumpserver/dev_user
[Update] 修改用户密码重置页面错误信息提示
2020-04-17 11:27:26 +08:00
Bai
081f4b1c0a [Update] 修改用户密码重置页面错误信息提示 2020-04-17 11:26:29 +08:00
BaiJiangJie
e9fe5b3004 Merge pull request #3908 from jumpserver/dev
Dev
2020-04-16 18:30:54 +08:00
BaiJiangJie
1de2923dd3 Merge pull request #3907 from jumpserver/dev_user
[Update] 修改忘记密码页面的错误信息提示
2020-04-16 18:30:07 +08:00
Bai
6890d549ed [Update] 修改忘记密码页面的错误信息提示 2020-04-16 18:29:02 +08:00
老广
9426f58a6b Merge pull request #3904 from jumpserver/dev
[Bugfix] 修复显示未授权资产的bug
2020-04-15 23:17:08 -05:00
ibuler
6a2a0013a8 [Update] 去掉debug 2020-04-16 12:12:24 +08:00
ibuler
864a4c0485 [Bugfix] 修复显示未授权资产的bug 2020-04-16 12:08:52 +08:00
BaiJiangJie
8fa08d7ea3 Merge pull request #3902 from jumpserver/dev
Dev
2020-04-16 11:31:38 +08:00
ibuler
3648a1458b [Update] 修改command options get view filter 2020-04-16 11:27:46 +08:00
BaiJiangJie
5001f48982 Merge pull request #3899 from jumpserver/dev
Dev
2020-04-14 19:46:36 +08:00
BaiJiangJie
93bf15adc9 Merge pull request #3898 from jumpserver/dev_email
[Update] 修改测试邮件后,配置获取不稳定的Bug(显示给settings赋值导致)
2020-04-14 19:45:06 +08:00
Bai
83eb8f77d1 [Update] 修改测试邮件后,配置获取不稳定的Bug(显示给settings赋值导致) 2020-04-14 19:40:56 +08:00
BaiJiangJie
4eaaa2462b Merge pull request #3897 from jumpserver/dev
Dev
2020-04-14 17:53:52 +08:00
BaiJiangJie
0910236f12 Merge pull request #3896 from jumpserver/dev_bai
Dev bai
2020-04-14 17:52:49 +08:00
Bai
77149bf36c [Update] 修改监控多个会话时,总是覆盖同一个tab页 2020-04-14 17:50:47 +08:00
Bai
6337d0d0a1 [Update] 修改会话can_join为property 2020-04-14 16:12:24 +08:00
BaiJiangJie
093e3924a2 Merge pull request #3894 from jumpserver/dev
Dev
2020-04-14 16:00:08 +08:00
BaiJiangJie
e143408e57 Merge pull request #3893 from jumpserver/dev_req
[Update] 修改依赖版本jms-storage=0.0.29
2020-04-14 15:58:24 +08:00
Bai
dfdf33bbb5 [Update] 修改依赖版本jms-storage=0.0.29 2020-04-14 15:54:59 +08:00
BaiJiangJie
8fd5e6521f Merge pull request #3892 from jumpserver/dev_bai
Dev bai
2020-04-14 13:20:50 +08:00
BaiJiangJie
6a2b9cd9bf Merge pull request #3891 from jumpserver/dev_bai
[Update] 修复系统设置-终端设置-资产排序设置不生效的Bug
2020-04-14 13:19:57 +08:00
Bai
55fa5800b1 [Update] 修复系统设置-终端设置-资产排序设置不生效的Bug 2020-04-14 13:18:33 +08:00
BaiJiangJie
6cdba2e8d2 Merge pull request #3890 from jumpserver/dev_bai
Dev bai
2020-04-14 12:56:20 +08:00
BaiJiangJie
043aa08f23 Merge pull request #3889 from jumpserver/dev_bai
Dev bai
2020-04-14 12:55:35 +08:00
Bai
63519ec076 [Update] 修改命令记录/操作/改密日志搜索字段没清空的问题 2020-04-14 12:51:13 +08:00
Bai
48fa26a3bc [Update] 修改操作日志搜索字段没清空的问题 2020-04-14 11:03:12 +08:00
Bai
d969563e43 [Update] 修复自定义平台列表在详情页删除失败的bug 2020-04-14 10:42:40 +08:00
BaiJiangJie
287236a447 Merge pull request #3885 from jumpserver/dev_trans
[Update] 更新翻译
2020-04-13 19:23:38 +08:00
Bai
4fb4a9f622 [Update] 更新翻译 2020-04-13 19:21:53 +08:00
BaiJiangJie
4dcd4749c3 Merge pull request #3884 from jumpserver/dev
Dev
2020-04-13 18:24:54 +08:00
BaiJiangJie
508d1c2c1f Merge pull request #3883 from jumpserver/master_bai
Master bai
2020-04-13 18:23:43 +08:00
BaiJiangJie
600ea42633 Merge pull request #3882 from jumpserver/master_bai
[Update] 修改OpenID用户获取username的问题
2020-04-13 18:22:23 +08:00
Bai
41cbd3e0f6 [Update] 修改OpenID用户获取username的问题 2020-04-13 18:21:49 +08:00
BaiJiangJie
e593ccb01c Merge pull request #3879 from jumpserver/dev_bai
Dev bai
2020-04-13 14:22:31 +08:00
Bai
d5e1ca7908 [Update] 添加翻译 2020-04-13 14:19:42 +08:00
Bai
06fb502a1d [Update] 修改文案: 加入 -> 监控 2020-04-13 14:17:08 +08:00
Bai
3f6d7637c3 [Update] 更新用户添加CAS来源选项 2020-04-13 13:40:13 +08:00
ibuler
0d2b4d7ca3 [Update] 添加ids filter 2020-04-13 10:40:13 +08:00
BaiJiangJie
42c5c02709 Merge pull request #3871 from jumpserver/lina_dev
Lina dev
2020-04-10 14:50:05 +08:00
Bai
d1d73da322 [Update] Merge from dev 2020-04-10 14:46:35 +08:00
BaiJiangJie
88fcf8dbd7 Merge pull request #3869 from jumpserver/lina_user
[Update] 修改用户Profile序列类返回的admin_orgs
2020-04-10 12:49:57 +08:00
Bai
396bc9b6ae [Update] 修改用户ViewSet序列类返回的admin_orgs(解决组织管理员登录的Bug) 2020-04-10 12:47:16 +08:00
BaiJiangJie
27ffed1be9 Merge pull request #3868 from jumpserver/dev_user
[Update] 修改用户ViewSet序列类返回的admin_orgs(解决组织管理员登录的Bug)
2020-04-10 12:40:12 +08:00
Bai
af9d42695f [Update] 修改用户ViewSet序列类返回的admin_orgs(解决组织管理员登录的Bug) 2020-04-10 12:38:05 +08:00
BaiJiangJie
422b4424fe Merge pull request #3867 from jumpserver/dev_ab
[Update] 修复AuthBook删除失败的Bug
2020-04-09 23:40:18 +08:00
Michael Bai
dc172e1ef0 [Update] 修复AuthBook删除失败的Bug 2020-04-09 23:39:17 +08:00
ibuler
cd7946f3f0 [Update] 修改一些bug 2020-04-09 18:58:22 +08:00
BaiJiangJie
4994a4a387 Dev bai (#3865)
* [Update] 更新config-example(CAS)

* [Update] 更新config-example(CAS)2

* [Update] 更新config-example(CAS)3
2020-04-09 13:39:11 +08:00
BaiJiangJie
e26eba9919 Merge pull request #3864 from jumpserver/1.5.8
1.5.8
2020-04-09 11:36:38 +08:00
Bai
44cd82f3e1 [Update] 修改翻译 2020-04-09 10:59:00 +08:00
Bai
adf14b0f2e [Update] 修改版本号 2020-04-09 10:46:24 +08:00
Bai
f5a7c4131d [Update] 修改校验join session的url-name 2020-04-09 10:43:25 +08:00
ibuler
e7031d0ac1 [Update] 修改serailizer mixin 2020-04-09 10:33:20 +08:00
Bai
ef27188f86 [Update] 修改Bug: CommandViewSet的Option方法Error 2020-04-08 18:08:22 +08:00
BaiJiangJie
a7734812fc Merge pull request #3860 from jumpserver/1.5.8_bai
[Update] 命令记录导出: 只导出满足搜索条件的数据
2020-04-08 01:35:16 +08:00
Michael Bai
fb62fbde6c [Update] 命令记录导出: 只导出满足搜索条件的数据 2020-04-08 01:34:10 +08:00
BaiJiangJie
521f4ea86b Merge pull request #3857 from jumpserver/1.5.8_bai
1.5.8 bai
2020-04-07 12:32:54 +08:00
Bai
e9827c8b25 [Update] 添加会话加入校验API 2020-04-07 12:26:47 +08:00
Bai
29b099efc0 [Update] 添加加入会话按钮 2020-04-02 19:45:56 +08:00
Bai
e9fc56c056 [Update] 修改日志日期显示问题 2020-04-02 16:02:55 +08:00
Bai
e9103ee608 [Update] 对自定义MetaDict字段值进行strip 2020-04-02 15:14:30 +08:00
ibuler
f8dae2a3c9 [Update] 时区允许设置 2020-04-01 17:27:32 +08:00
BaiJiangJie
52c6244b2b Merge pull request #3842 from jumpserver/dev
Dev
2020-03-28 21:36:54 +08:00
BaiJiangJie
7977294c5c Merge pull request #3841 from jumpserver/dev
Dev
2020-03-28 21:36:09 +08:00
BaiJiangJie
e1331084e5 Merge pull request #3840 from jumpserver/dev_readme
Dev readme
2020-03-28 21:35:23 +08:00
Michael Bai
5268c0faae [Update] OpenID登录成功后,如果用户没有email字段则构造默认的email 2020-03-28 21:34:17 +08:00
Michael Bai
b2d9b69874 [Update] 修改readme 2020-03-28 21:26:27 +08:00
BaiJiangJie
336589af98 Merge pull request #3836 from jumpserver/1.5.8_cap
1.5.8 cap
2020-03-27 11:28:39 +08:00
Bai
5087c0e06f Merge branch 'dev' into 1.5.8_cap 2020-03-27 11:26:09 +08:00
BaiJiangJie
43ecd39d0e Merge pull request #3835 from jumpserver/dev
Dev
2020-03-27 11:09:09 +08:00
BaiJiangJie
209b23c5ce Merge pull request #3834 from jumpserver/dev_req
[Update] 修改依赖jms-storage==0.0.28
2020-03-27 11:08:42 +08:00
Bai
7e7d4401e6 [Update] 修改依赖jms-storage==0.0.28 2020-03-27 11:07:55 +08:00
BaiJiangJie
b4c3471d2c Merge pull request #3833 from jumpserver/dev
Dev
2020-03-27 10:48:09 +08:00
BaiJiangJie
f4a822062f Merge pull request #3829 from jumpserver/dev_bai
[Update] 优化创建AuthBook对象
2020-03-26 13:13:35 +08:00
Bai
4583beaec0 [Update] 创建AuthBook对象的方法从ModelManager中移动到Model中,保留原生的对象create方法;同时添加事务处理; 2020-03-26 13:11:38 +08:00
BaiJiangJie
42a6feb35e Merge pull request #3822 from jumpserver/dev_lock
[Update] 优化创建AuthBook逻辑;添加锁机制(基于redis-lock);
2020-03-24 23:20:38 +08:00
Bai
4503df910d [Update] 优化创建AuthBook逻辑;添加锁机制(基于redis-lock); 2020-03-24 18:40:20 +08:00
BaiJiangJie
a09b0c6c06 Merge pull request #3818 from jumpserver/dev_bai
[Update] 修改任务执行日期显示本地时间
2020-03-24 08:57:45 +08:00
Michael Bai
55c7e06185 [Update] 修改任务执行日期显示本地时间 2020-03-24 01:37:09 +08:00
老广
a95a0da6f7 Merge pull request #3816 from jumpserver/dev
[Bugfix] 修复因数据不支持timezone引起的仪表盘数据为空的bug
2020-03-23 17:56:45 +08:00
ibuler
0fd43f48f0 [Bugfix] 修复因数据不支持timezone引起的仪表盘数据为空的bug 2020-03-23 17:55:28 +08:00
ibuler
dffd05cd20 [Update] 修改组织获取优先级 2020-03-23 10:52:26 +08:00
BaiJiangJie
681046119d Merge pull request #3811 from jumpserver/dev
Dev
2020-03-23 03:22:00 +08:00
BaiJiangJie
b19c49da41 Merge pull request #3810 from jumpserver/dev_bai
[Update] 修改翻译;升级依赖;
2020-03-23 03:20:30 +08:00
Michael Bai
cc31c04b5f [Update] 升级依赖 2020-03-23 03:15:51 +08:00
Michael Bai
89f62d8e6b [Update] 更新翻译 2020-03-23 03:06:53 +08:00
Michael Bai
41b73c3701 [Update] 修改提示播放器下载地址 2020-03-23 03:02:22 +08:00
BaiJiangJie
cff3a790ef Merge pull request #3809 from jumpserver/dev_bai
[Update] 修改Node generate fake时死循环的问题
2020-03-23 02:50:06 +08:00
Michael Bai
8328edd69c [Update] 修改Node generate fake时死循环的问题 2020-03-23 02:32:24 +08:00
ibuler
9bf5d6dd45 [Update] 用户profile中添加orgs 2020-03-20 18:21:27 +08:00
BaiJiangJie
ac9a0c6d26 Merge pull request #3803 from jumpserver/dev
[Update] 禁用ansible连接复用
2020-03-20 17:55:38 +08:00
ibuler
064cb16d25 [Update] 禁用ansible连接复用 2020-03-20 16:45:38 +08:00
BaiJiangJie
f89b1fd44b Merge pull request #3798 from jumpserver/dev
[Bugfix] 修复重置并下载ssh key的bug
2020-03-19 16:28:34 +08:00
ibuler
b25096925b [Bugfix] 修复重置并下载ssh key的bug 2020-03-19 16:28:08 +08:00
BaiJiangJie
e8ff576324 Merge pull request #3797 from jumpserver/dev
[Update] 修复收藏资产数量不对的问题
2020-03-19 15:43:44 +08:00
ibuler
5ac1467564 [Update] 修复收藏数量不对的问题 2020-03-19 15:41:41 +08:00
BaiJiangJie
1c54e5acd8 Merge pull request #3796 from jumpserver/dev
Dev
2020-03-19 14:26:23 +08:00
ibuler
b8d0272e37 [Update] 修改测试系统用户显示 2020-03-19 11:47:39 +08:00
ibuler
0b7a90b83c Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-03-19 11:13:34 +08:00
ibuler
7cf000262d [Update] django cas ng 退出逻辑有问题,暂时屏蔽 2020-03-19 11:11:18 +08:00
BaiJiangJie
fa7e0d84f9 Merge pull request #3794 from jumpserver/dev_bai
[Update] 修改批量移除节点时不选择节点给出提示信息
2020-03-19 10:27:26 +08:00
Bai
98f6d0146c [Update] 修改批量移除节点时不选择节点给出提示信息 2020-03-19 10:21:19 +08:00
ibuler
d0fac3f838 [Bugfix] 修复task 列表、 2020-03-18 19:58:08 +08:00
ibuler
a41988d2b5 [Bugfix] 修改配置文件,支持cas proxy 2020-03-18 19:10:27 +08:00
BaiJiangJie
b801d2f2e9 Update README.md 2020-03-18 17:16:53 +08:00
BaiJiangJie
6bed9210dd Merge pull request #3793 from jumpserver/dev_bai
[Update] 修改readme2
2020-03-18 17:15:15 +08:00
Bai
eac4630272 [Update] 修改readme2 2020-03-18 17:14:21 +08:00
BaiJiangJie
11a0d72b01 Merge pull request #3792 from jumpserver/dev_readme
[Update] 修改readme
2020-03-18 17:09:21 +08:00
Bai
7dcd04ca1a [Update] 修改readme 2020-03-18 17:03:49 +08:00
BaiJiangJie
ccfb151fb1 Merge pull request #3791 from jumpserver/dev_version
[Update] 更新版本号
2020-03-18 16:43:12 +08:00
Bai
ae98eee0c7 [Update] 更新版本号 2020-03-18 16:34:37 +08:00
BaiJiangJie
829a4406a2 Merge pull request #3790 from jumpserver/dev_bai
[Update] 修改执行task时切换root组织2
2020-03-18 14:04:14 +08:00
Bai
91c42e8530 [Update] 修改执行task时切换root组织2 2020-03-18 14:02:36 +08:00
BaiJiangJie
250acea751 Merge pull request #3789 from jumpserver/dev_bai
[Update] 修改执行task时切换root组织
2020-03-18 13:49:04 +08:00
Bai
00d9f71384 [Update] 修改执行task时切换root组织 2020-03-18 13:45:49 +08:00
BaiJiangJie
ffce909ee3 Merge pull request #3783 from jumpserver/bugfix
Bugfix
2020-03-18 11:05:51 +08:00
BaiJiangJie
0e62ea787c Merge pull request #3785 from jumpserver/login_image_2
[Fix] 修改登录图片亮度
2020-03-18 10:44:55 +08:00
jym503558564
cedb862420 [Fix] 修改登录图片亮度 2020-03-17 17:28:53 +08:00
Bai
a6054ff6a5 [Update] 修复批量删除资产,当待删除数量和资产总数相等时,提示删除失败。 2020-03-17 17:06:59 +08:00
Bai
a67b445026 [Update] 修复录像存储ceph的help_text 2020-03-17 16:15:37 +08:00
BaiJiangJie
8d33990050 Merge pull request #3782 from jumpserver/login_image_2
[Fix] 修改登录界面LOGO
2020-03-17 15:32:18 +08:00
jym503558564
516e75cbf4 [Fix] 修改登录界面LOGO 2020-03-17 15:30:59 +08:00
Bai
15ca775005 [Update] 删除推送系统用户时关于task的log 2020-03-17 15:21:12 +08:00
Bai
41ca43bf33 Merge branch 'bugfix' of https://github.com/jumpserver/jumpserver into bugfix 2020-03-17 15:02:56 +08:00
Bai
c6a604fd5f [Update] 修复显示创建者字段值的问题 2020-03-17 15:02:44 +08:00
ibuler
8e84efb296 [Update] 修改一些文案和翻译 2020-03-17 13:51:41 +08:00
ibuler
caee286973 [Update] 修改华为云始终有效的bug 2020-03-17 10:59:49 +08:00
ibuler
672b82c3d6 [Bugfix] 修复可能无法找到command execution的bug 2020-03-16 19:24:48 +08:00
ibuler
55554a025f [Bugfix] 修复动态系统用户更新用户时无法推送的bug, 修复通过relation api无法触发m2m_change的bug 2020-03-16 19:15:29 +08:00
ibuler
c91ce2b99f [Bugfix] 修复动态系统用户返回auth info的bug 2020-03-16 16:59:45 +08:00
ibuler
02a901467b [Bugfix] 修复动态系统用户登录windows的bug 2020-03-16 16:13:54 +08:00
ibuler
00b3c7c945 [Bugfix] 修复动态系统用户执行批量命令的bug 2020-03-16 15:23:15 +08:00
ibuler
2f9598ba49 [Update] 修复ops task中任务某版本任务的bug 2020-03-16 14:41:24 +08:00
ibuler
3138abb00c [Bugfix] 修复ops列表任务无法删除的bug 2020-03-16 14:18:29 +08:00
ibuler
3afb8647bd Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2020-03-13 13:57:42 +08:00
ibuler
0682f4fc90 [Update] 修改readme 2020-03-13 13:57:21 +08:00
BaiJiangJie
44ec69fdbd Merge pull request #3775 from jumpserver/dev_bai
[Update] 修改只有local用户需要发送创建成功邮件、发送密码过期邮件;
2020-03-13 12:36:40 +08:00
Bai
04945809a5 [Update] 修改只有local用户需要发送创建成功邮件、发送密码过期邮件; 2020-03-13 12:33:09 +08:00
BaiJiangJie
e38b113d7e Merge pull request #3771 from jumpserver/dev_trans
[Update] 更新翻译
2020-03-12 19:05:50 +08:00
Bai
eb448dc3f2 [Update] 更新翻译 2020-03-12 19:00:22 +08:00
BaiJiangJie
954a97bba7 Merge pull request #3770 from jumpserver/dev_req
[Update] 更新依赖jms-storage=0.0.27
2020-03-12 16:34:33 +08:00
Bai
41e03c629f [Update] 更新依赖jms-storage=0.0.27 2020-03-12 16:33:33 +08:00
老广
1fd2e782f8 1.5.7 Merge to dev (#3766)
* [Update] 暂存,优化解决不了问题

* [Update] 待续(小白)

* [Update] 修改asset user

* [Update] 计划再次更改

* [Update] 修改asset user

* [Update] 暂存与喜爱

* [Update] Add id in

* [Update] 阶段性完成ops task该做

* [Update] 修改asset user api

* [Update] 修改asset user 任务,查看认证等

* [Update] 基本完成asset user改造

* [Update] dynamic user only allow 1

* [Update] 修改asset user task

* [Update] 修改node admin user task api

* [Update] remove file header license

* [Update] 添加sftp root

* [Update] 暂存

* [Update] 暂存

* [Update] 修改翻译

* [Update] 修改系统用户改为同名后,用户名改为空

* [Update] 基本完成CAS调研

* [Update] 支持cas server

* [Update] 支持cas server

* [Update] 添加requirements

* [Update] 为方便调试添加mysql ipython到包中

* [Update] 添加huaweiyun翻译

* [Update] 增加下载session 录像

* [Update] 只有第一次通知replay离线的使用方法

* [Update] 暂存一下

* [Bugfix] 获取系统用户信息报错

* [Bugfix] 修改system user info

* [Update] 改成清理10天git status

* [Update] 修改celery日志保留时间

* [Update]修复部分pip包依赖的版本不兼容问题 (#3672)

* [Update] 修复用户更新页面会清空用户public_key的问题

* Fix broken dependencies

Co-authored-by: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com>

* [Update] 修改获取系统用户auth info

* [Update] Remove log

* [Bugfix] 修复sftp home设置的bug

* [Update] 授权的系统用户添加sftp root

* [Update] 修改系统用户关联的用户

* [Update] 修改placeholder

* [Update] 优化获取授权的系统用户

* [Update] 修改tasks

* [Update] tree service update

* [Update] 暂存

* [Update] 基本完成用户授权树和资产树改造

* [Update] Dashbaord perf

* [update] Add huawei cloud sdk requirements

* [Updte] 优化dashboard页面

* [Update] system user auth info 添加id

* [Update] 修改系统用户serializer

* [Update] 优化api

* [Update] LDAP Test Util (#3720)

* [Update] LDAPTestUtil 1

* [Update] LDAPTestUtil 2

* [Update] LDAPTestUtil 3

* [Update] LDAPTestUtil 4

* [Update] LDAPTestUtil 5

* [Update] LDAPTestUtil 6

* [Update] LDAPTestUtil 7

* [Update] session 已添加is success,并且添加display serializer

* [Bugfix] 修复无法删除空节点的bug

* [Update] 命令记录分组织显示

* [Update] Session is_success 添加迁移文件

* [Update] 批量命令添加org_id

* [Update] 修复一些文案,修改不绑定MFA,不能ssh登录

* [Update] 修改replay api, 返回session信息

* [Update] 解决无效es导致访问命令记录页面失败的问题

* [Update] 拆分profile view

* [Update] 修改一个翻译

* [Update] 修改aysnc api框架

* [Update] 命令列表添加risk level

* [Update] 完成录像打包下载

* [Update] 更改登陆otp页面

* [Update] 修改command 存储redis_level

* [Update] 修改翻译

* [Update] 修改系统用户的用户列表字段

* [Update] 使用新logo和统一Jumpserver为JumpServer

* [Update] 优化cloud task

* [Update] 统一period task

* [Update] 统一period form serializer字段

* [Update] 修改period task

* [Update] 修改资产网关信息

* [Update] 用户授权资产树资产信息添加domain

* [Update] 修改翻译

* [Update] 测试可连接性

* 1.5.7 bai (#3764)

* [Update] 修复index页面Bug;修复测试资产用户可连接性问题;

* [Update] 修改测试资产用户可连接

* [Bugfix] 修复backends问题

* [Update] 修改marksafe依赖版本

* [Update] 修改测试资产用户可连接性

* [Update] 修改检测服务器性能时获取percent值

* [Update] 更新依赖boto3=1.12.14

Co-authored-by: Yanzhe Lee <lee.yanzhe@yanzhe.org>
Co-authored-by: BaiJiangJie <32935519+BaiJiangJie@users.noreply.github.com>
Co-authored-by: Bai <bugatti_it@163.com>
2020-03-12 16:24:38 +08:00
老广
1f6a8e8f02 Merge pull request #3698 from jumpserver/readme
[Update] 修改README中 jumpserver ==> JumpServer
2020-03-12 10:39:26 +08:00
老广
eca0a9a7d7 Merge pull request #3723 from jumpserver/settings_bug
Settings bug
2020-02-26 16:19:39 +08:00
老广
775f8f2ffd Merge pull request #3722 from jumpserver/settings_bug
Settings bug
2020-02-26 16:18:24 +08:00
ibuler
1e8ef8c925 [Update] Merge with master 2020-02-26 16:15:42 +08:00
ibuler
c354b55f61 [update] 修复获取settings有问题的bug 2020-02-26 16:12:41 +08:00
ibuler
76ac0215fe [update] 修复获取settings有问题的bug 2020-02-26 16:12:08 +08:00
老广
4f93a3ca92 Merge pull request #3716 from jumpserver/dev
Dev
2020-02-24 12:08:32 +08:00
ibuler
bcae30814d [Update] 修改导入 2020-02-24 12:06:02 +08:00
老广
98bb6c63f5 [Update] 修改切换组织后页面跳转 (#3715) 2020-02-24 12:00:20 +08:00
老广
8b8b11ce1e [Update] 恢复到原来的sql (#3707) 2020-02-15 20:49:20 +08:00
BaiJiangJie
5f61f2b555 Merge pull request #3702 from jumpserver/dev_interface
[Update] 修改终端获取登录标题配置的逻辑
2020-02-14 10:45:37 +08:00
Bai
62d2e01cdf [Update] 修改终端获取登录标题配置的逻辑 2020-02-14 10:40:18 +08:00
jym503558564
5e89ee9202 [Update] 修改README中 jumpserver ==> JumpServer 2020-02-13 19:58:14 +08:00
老广
5d313a827b [Update] 优化session 索引 is_finished (#3697) 2020-02-13 15:17:13 +08:00
ibuler
edf6baa52d [Update] 修改api创建的token有效期是600s 2020-02-07 17:24:52 +08:00
老广
3c69860b24 [Update] 优化sql, or方式改为union (#3682)
* [Update] 优化sql, or方式改为union

* [Update] 优化union操作,直接union后,queryset 的一些参数不能使用,如annoate, 如filter assets__isnull=True
2020-02-05 15:56:28 +08:00
老广
aa2255a87e [Update] sql优化,查询用户资产权限时,使用union替代or (#3681) 2020-02-05 12:10:24 +08:00
494 changed files with 11808 additions and 26501 deletions

View File

@@ -2,7 +2,7 @@
##### 使用版本
[请提供你使用的Jumpserver版本 1.x.x 注: 0.3.x不再提供支持]
[请提供你使用的JumpServer版本 如 2.0.1 注: 1.4及以下版本不再提供支持]
##### 问题复现步骤
1. [步骤1]

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

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

View File

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

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

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

4
.gitignore vendored
View File

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

View File

@@ -1,19 +1,33 @@
FROM registry.fit2cloud.com/public/python:v3
FROM registry.fit2cloud.com/public/python:v3 as stage-build
MAINTAINER Jumpserver Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM registry.fit2cloud.com/public/python:v3
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
ENV MYSQL_MIRROR=$MYSQL_MIRROR
WORKDIR /opt/jumpserver
COPY ./requirements ./requirements
RUN useradd jumpserver
COPY ./requirements /tmp/requirements
RUN yum -y install epel-release && \
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && pip install wheel && \
pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
RUN yum -y install $(cat requirements/rpm_requirements.txt)
RUN pip install --upgrade pip setuptools==49.6.0 wheel -i ${PIP_MIRROR} && \
pip config set global.index-url ${PIP_MIRROR}
RUN pip install -r requirements/requirements.txt || pip install -r requirements/requirements.txt
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
COPY . /opt/jumpserver
RUN echo > config.yml
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs

View File

@@ -1,26 +1,50 @@
# Jumpserver 多云环境下更好用的堡垒机
# JumpServer 多云环境下更好用的堡垒机
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![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/)
[![Django](https://img.shields.io/badge/django-2.2-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Docker Pulls](https://img.shields.io/docker/pulls/jumpserver/jms_all.svg)](https://hub.docker.com/u/jumpserver)
Jumpserver 是全球首款完全开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 机制的运维安全审计系统。
JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 规范的运维安全审计系统。
Jumpserver 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 使用 Python / Django 为主进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
改变世界,从一点点开始。
注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 Jumpserver 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
> 注: [KubeOperator](https://github.com/KubeOperator/KubeOperator) 是 JumpServer 团队在 Kubernetes 领域的的又一全新力作,欢迎关注和使用。
## 核心功能列表
## 特色优势
- 开源: 零门槛,线上快速获取和安装, 修复版本视情况而定;
, 修复版本视情况而定- 分布式: 轻松支持大规模并发访问;
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
- 多云支持: 一套系统,同时管理不同云上面的资产;
- 云端存储: 审计录像云端存储,永不丢失;
- 多租户: 一套系统,多个子公司和部门同时使用。
## 版本说明
自 v2.0.0 发布后, JumpServer 版本号命名将变更为v大版本.功能版本.Bug修复版本。比如
```
v2.0.1 是 v2.0.0 之后的Bug修复版本
v2.1.0 是 v2.0.0 之后的功能版本。
```
像其它优秀开源项目一样JumpServer 每个月会发布一个功能版本,并同时维护 3 个功能版本。比如:
```
在 v2.4 发布前,我们会同时维护 v2.1、v2.2、v2.3
在 v2.4 发布后,我们会同时维护 v2.2、v2.3、v2.4v2.1 会停止维护。
```
## 功能列表
<table>
<tr>
<td rowspan="7">身份认证<br>Authentication</td>
<td rowspan="4">登录认证</td>
<td rowspan="8">身份认证<br>Authentication</td>
<td rowspan="5">登录认证</td>
<td>资源统一登录与认证</td>
</tr>
<tr>
@@ -32,6 +56,9 @@ Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向
<tr>
<td>OpenID 认证(实现单点登录)</td>
</tr>
<tr>
<td>CAS 认证 (实现单点登录)</td>
</tr>
<tr>
<td rowspan="2">MFA认证</td>
<td>MFA 二次认证Google Authenticator</td>
@@ -169,29 +196,36 @@ Jumpserver 采纳分布式架构,支持多机房跨区域部署,支持横向
</tr>
</table>
## 安装及使用指南
- [Docker 快速安装文档](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
- [Step by Step 安装文档](http://docs.jumpserver.org/zh/docs/step_by_step.html)
- [完整文档](http://docs.jumpserver.org)
## 演示视频和截屏
我们提供了演示视频和系统截图可以让你快速了解 Jumpserver
## 快速开始
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
- [完整文档](https://docs.jumpserver.org)
- [演示视频](https://jumpserver.oss-cn-hangzhou.aliyuncs.com/jms-media/%E3%80%90%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%E3%80%91Jumpserver%20%E5%A0%A1%E5%9E%92%E6%9C%BA%20V1.5.0%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91%20-%20final.mp4)
- [系统截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
## SDK
## 案例研究
我们编写了一些SDK供您的其它系统快速和 Jumpserver API 交互:
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver 其它组件使用这个 SDK 完成交互
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的 Java 版本的 SDK
## 安全说明
JumpServer是一款安全产品请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) 部署安装.
如果你发现安全问题,可以直接联系我们:
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755
## License & Copyright
Copyright (c) 2014-2019 飞致云 FIT2CLOUD, All rights reserved.
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

View File

@@ -15,7 +15,7 @@ __all__ = [
class RemoteAppViewSet(OrgBulkModelViewSet):
model = RemoteApp
filter_fields = ('name',)
filter_fields = ('name', 'type', 'comment')
search_fields = filter_fields
permission_classes = (IsOrgAdmin,)
serializer_class = RemoteAppSerializer

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
Other module of this app shouldn't connect with other app.
:copyright: (c) 2014-2018 by Jumpserver Team.
:copyright: (c) 2014-2018 by JumpServer Team.
:license: GPL v2, see LICENSE for more details.
"""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,4 @@
# ~*~ coding: utf-8 ~*~
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
#
# Licensed under the GNU General Public License v2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.gnu.org/licenses/gpl-2.0.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.db import transaction
from django.db.models import Count
@@ -49,7 +36,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets'))
queryset = queryset.annotate(assets_amount=Count('assets'))
return queryset
def destroy(self, request, *args, **kwargs):

View File

@@ -1,9 +1,6 @@
# -*- coding: utf-8 -*-
#
import random
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView
from django.shortcuts import get_object_or_404
@@ -17,14 +14,14 @@ from .. import serializers
from ..tasks import (
update_asset_hardware_info_manual, test_asset_connectivity_manual
)
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend
from ..filters import AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend
logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetPlatformRetrieveApi',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
'AssetGatewayApi', 'AssetPlatformViewSet',
'AssetGatewayListApi', 'AssetPlatformViewSet',
'AssetTaskCreateApi',
]
@@ -33,12 +30,18 @@ class AssetViewSet(OrgBulkModelViewSet):
API endpoint that allows Asset to be viewed or edited.
"""
model = Asset
filter_fields = ("hostname", "ip", "systemuser__id", "admin_user__id")
filter_fields = (
"hostname", "ip", "systemuser__id", "admin_user__id", "platform__base",
"is_active", 'ip'
)
search_fields = ("hostname", "ip")
ordering_fields = ("hostname", "ip", "port", "cpu_cores")
serializer_class = serializers.AssetSerializer
serializer_classes = {
'default': serializers.AssetSerializer,
'display': serializers.AssetDisplaySerializer,
}
permission_classes = (IsOrgAdminOrAppUser,)
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend]
extra_filter_backends = [AssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
def set_assets_node(self, assets):
if not isinstance(assets, list):
@@ -71,62 +74,53 @@ class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']
filter_fields = ['name', 'base']
search_fields = ['name']
def get_permissions(self):
if self.request.method.lower() in ['get', 'options']:
self.permission_classes = (IsOrgAdmin,)
return super().get_permissions()
def check_object_permissions(self, request, obj):
if request.method.lower() in ['delete', 'put', 'patch'] and \
obj.internal:
if request.method.lower() in ['delete', 'put', 'patch'] and obj.internal:
self.permission_denied(
request, message={"detail": "Internal platform"}
)
return super().check_object_permissions(request, obj)
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
"""
Refresh asset hardware info
"""
class AssetTaskCreateApi(generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetSerializer
serializer_class = serializers.AssetTaskSerializer
permission_classes = (IsOrgAdmin,)
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
task = update_asset_hardware_info_manual.delay(asset)
return Response({"task": task.id})
def get_object(self):
pk = self.kwargs.get("pk")
instance = get_object_or_404(Asset, pk=pk)
return instance
def perform_create(self, serializer):
asset = self.get_object()
action = serializer.validated_data["action"]
if action == "refresh":
task = update_asset_hardware_info_manual.delay(asset)
else:
task = test_asset_connectivity_manual.delay(asset)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
class AssetAdminUserTestApi(generics.RetrieveAPIView):
"""
Test asset admin user assets_connectivity
"""
model = Asset
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
task = test_asset_connectivity_manual.delay(asset)
return Response({"task": task.id})
class AssetGatewayApi(generics.RetrieveAPIView):
class AssetGatewayListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer
model = Asset
def retrieve(self, request, *args, **kwargs):
asset_id = kwargs.get('pk')
def get_queryset(self):
asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
if asset.domain and \
asset.domain.gateways.filter(protocol='ssh').exists():
gateway = random.choice(asset.domain.gateways.filter(protocol='ssh'))
serializer = serializers.GatewayWithAuthSerializer(instance=gateway)
return Response(serializer.data)
else:
return Response({"msg": "Not have gateway"}, status=404)
if not asset.domain:
return []
queryset = asset.domain.gateways.filter(protocol='ssh')
return queryset

View File

@@ -1,26 +1,24 @@
# -*- coding: utf-8 -*-
#
from rest_framework.response import Response
from rest_framework import generics
from rest_framework import filters
from rest_framework_bulk import BulkModelViewSet
from django.shortcuts import get_object_or_404
from django.http import Http404
import coreapi
from django.conf import settings
from rest_framework.response import Response
from rest_framework import generics, filters
from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsOrgAdminOrAppUser, NeedMFAVerify
from common.utils import get_object_or_none, get_logger
from common.mixins import CommonApiMixin
from ..backends import AssetUserManager
from ..models import Asset, Node, SystemUser, AdminUser
from ..models import Asset, Node, SystemUser
from .. import serializers
from ..tasks import test_asset_users_connectivity_manual
from ..tasks import (
test_asset_users_connectivity_manual, push_system_user_a_asset_manual
)
__all__ = [
'AssetUserViewSet', 'AssetUserAuthInfoApi', 'AssetUserTestConnectiveApi',
'AssetUserExportViewSet',
'AssetUserViewSet', 'AssetUserAuthInfoViewSet', 'AssetUserTaskCreateAPI',
]
@@ -34,10 +32,17 @@ class AssetUserFilterBackend(filters.BaseFilterBackend):
value = request.GET.get(field)
if not value:
continue
if field in ("node_id", "system_user_id", "admin_user_id"):
if field == "node_id":
value = get_object_or_none(Node, pk=value)
kwargs["node"] = value
continue
elif field == "asset_id":
field = "asset"
kwargs[field] = value
return queryset.filter(**kwargs)
if kwargs:
queryset = queryset.filter(**kwargs)
logger.debug("Filter {}".format(kwargs))
return queryset
class AssetUserSearchBackend(filters.BaseFilterBackend):
@@ -45,72 +50,75 @@ class AssetUserSearchBackend(filters.BaseFilterBackend):
value = request.GET.get('search')
if not value:
return queryset
_queryset = AssetUserManager.none()
for field in view.search_fields:
if field in ("node_id", "system_user_id", "admin_user_id"):
continue
_queryset |= queryset.filter(**{field: value})
return _queryset.distinct()
queryset = queryset.search(value)
return queryset
class AssetUserLatestFilterBackend(filters.BaseFilterBackend):
def get_schema_fields(self, view):
return [
coreapi.Field(
name='latest', location='query', required=False,
type='string', example='1',
description='Only the latest version'
)
]
def filter_queryset(self, request, queryset, view):
latest = request.GET.get('latest') == '1'
if latest:
queryset = queryset.distinct()
return queryset
class AssetUserViewSet(CommonApiMixin, BulkModelViewSet):
serializer_class = serializers.AssetUserSerializer
serializer_classes = {
'default': serializers.AssetUserWriteSerializer,
'display': serializers.AssetUserReadSerializer,
'retrieve': serializers.AssetUserReadSerializer,
}
permission_classes = [IsOrgAdminOrAppUser]
http_method_names = ['get', 'post']
filter_fields = [
"id", "ip", "hostname", "username", "asset_id", "node_id",
"system_user_id", "admin_user_id"
"id", "ip", "hostname", "username",
"asset_id", "node_id",
"prefer", "prefer_id",
]
search_fields = filter_fields
filter_backends = (
filters.OrderingFilter,
search_fields = ["ip", "hostname", "username"]
filter_backends = [
AssetUserFilterBackend, AssetUserSearchBackend,
)
AssetUserLatestFilterBackend,
]
def allow_bulk_destroy(self, qs, filtered):
return False
def get_queryset(self):
# 尽可能先返回更少的数据
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
node_id = self.request.GET.get('node_id')
admin_user_id = self.request.GET.get("admin_user_id")
system_user_id = self.request.GET.get("system_user_id")
def get_object(self):
pk = self.kwargs.get("pk")
if pk is None:
return
queryset = self.get_queryset()
obj = queryset.get(id=pk)
return obj
kwargs = {}
assets = None
def get_exception_handler(self):
def handler(e, context):
logger.error(e, exc_info=True)
return Response({"error": str(e)}, status=400)
return handler
def perform_destroy(self, instance):
manager = AssetUserManager()
if system_user_id:
system_user = get_object_or_404(SystemUser, id=system_user_id)
assets = system_user.get_all_assets()
username = system_user.username
elif admin_user_id:
admin_user = get_object_or_404(AdminUser, id=admin_user_id)
assets = admin_user.assets.all()
username = admin_user.username
manager.prefer('admin_user')
manager.delete(instance)
if asset_id:
asset = get_object_or_404(Asset, id=asset_id)
assets = [asset]
elif node_id:
node = get_object_or_404(Node, id=node_id)
assets = node.get_all_assets()
if username:
kwargs['username'] = username
if assets is not None:
kwargs['assets'] = assets
queryset = manager.filter(**kwargs)
def get_queryset(self):
manager = AssetUserManager()
queryset = manager.all()
return queryset
class AssetUserExportViewSet(AssetUserViewSet):
serializer_class = serializers.AssetUserExportSerializer
http_method_names = ['get']
class AssetUserAuthInfoViewSet(AssetUserViewSet):
serializer_classes = {"default": serializers.AssetUserAuthInfoSerializer}
http_method_names = ['get', 'post']
permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self):
@@ -119,66 +127,31 @@ class AssetUserExportViewSet(AssetUserViewSet):
return super().get_permissions()
class AssetUserAuthInfoApi(generics.RetrieveAPIView):
serializer_class = serializers.AssetUserAuthInfoSerializer
permission_classes = [IsOrgAdminOrAppUser]
def get_permissions(self):
if settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
return super().get_permissions()
def get_object(self):
query_params = self.request.query_params
username = query_params.get('username')
asset_id = query_params.get('asset_id')
prefer = query_params.get("prefer")
asset = get_object_or_none(Asset, pk=asset_id)
try:
manger = AssetUserManager()
instance = manger.get(username, asset, prefer=prefer)
except Exception as e:
raise Http404("Not found")
else:
return instance
class AssetUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Test asset users connective
"""
class AssetUserTaskCreateAPI(generics.CreateAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.TaskIDSerializer
serializer_class = serializers.AssetUserTaskSerializer
filter_backends = AssetUserViewSet.filter_backends
filter_fields = AssetUserViewSet.filter_fields
def get_asset_users(self):
username = self.request.GET.get('username')
asset_id = self.request.GET.get('asset_id')
prefer = self.request.GET.get("prefer")
asset = get_object_or_none(Asset, pk=asset_id)
manager = AssetUserManager()
asset_users = manager.filter(username=username, assets=[asset], prefer=prefer)
return asset_users
queryset = manager.all()
for cls in self.filter_backends:
queryset = cls().filter_queryset(self.request, queryset, self)
return list(queryset)
def retrieve(self, request, *args, **kwargs):
def perform_create(self, serializer):
asset_users = self.get_asset_users()
prefer = self.request.GET.get("prefer")
kwargs = {}
if prefer == "admin_user":
kwargs["run_as_admin"] = True
task = test_asset_users_connectivity_manual.delay(asset_users, **kwargs)
return Response({"task": task.id})
# action = serializer.validated_data["action"]
# only this
# if action == "test":
task = test_asset_users_connectivity_manual.delay(asset_users)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
return task
class AssetUserPushApi(generics.CreateAPIView):
"""
Test asset users connective
"""
serializer_class = serializers.AssetUserPushSerializer
permission_classes = (IsOrgAdminOrAppUser,)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
asset = serializer.validated_data["asset"]
username = serializer.validated_data["username"]
pass
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler

View File

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

View File

@@ -1,24 +1,11 @@
# ~*~ coding: utf-8 ~*~
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
#
# Licensed under the GNU General Public License v2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.gnu.org/licenses/gpl-2.0.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import namedtuple
from rest_framework import status
from rest_framework.serializers import ValidationError
from rest_framework.views import APIView
from rest_framework.response import Response
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, Http404
from common.utils import get_logger, get_object_or_none
from common.tree import TreeNodeSerializer
@@ -27,7 +14,8 @@ from orgs.mixins import generics
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import (
update_assets_hardware_info_util, test_asset_connectivity_util
update_node_assets_hardware_info_manual,
test_node_assets_connectivity_manual,
)
from .. import serializers
@@ -36,9 +24,9 @@ logger = get_logger(__file__)
__all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
'NodeAddAssetsApi', 'NodeRemoveAssetsApi', 'NodeReplaceAssetsApi',
'NodeAddChildrenApi', 'RefreshNodeHardwareInfoApi',
'TestNodeConnectiveApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi', 'RefreshNodesCacheApi',
'NodeAddChildrenApi', 'NodeListAsTreeApi',
'NodeChildrenAsTreeApi',
'NodeTaskCreateApi',
]
@@ -64,9 +52,9 @@ class NodeViewSet(OrgModelViewSet):
def destroy(self, request, *args, **kwargs):
node = self.get_object()
if node.has_children_or_contains_assets():
msg = _("Deletion failed and the node contains children or assets")
return Response(data={'msg': msg}, status=status.HTTP_403_FORBIDDEN)
if node.has_children_or_has_assets():
error = _("Deletion failed and the node contains children or assets")
return Response(data={'error': error}, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs)
@@ -261,41 +249,41 @@ class NodeReplaceAssetsApi(generics.UpdateAPIView):
asset.nodes.set([instance])
class RefreshNodeHardwareInfoApi(APIView):
class NodeTaskCreateApi(generics.CreateAPIView):
model = Node
serializer_class = serializers.NodeTaskSerializer
permission_classes = (IsOrgAdmin,)
def get(self, request, *args, **kwargs):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.get_all_assets()
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
task_name = _("Update node asset hardware information: {}").format(node.name)
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
return Response({"task": task.id})
def get_object(self):
node_id = self.kwargs.get('pk')
node = get_object_or_none(self.model, id=node_id)
return node
@staticmethod
def set_serializer_data(s, task):
data = getattr(s, '_data', {})
data["task"] = task.id
setattr(s, '_data', data)
class TestNodeConnectiveApi(APIView):
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.get_all_assets()
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectivity_util.delay(assets, task_name=task_name)
return Response({"task": task.id})
class RefreshNodesCacheApi(APIView):
permission_classes = (IsOrgAdmin,)
def get(self, request, *args, **kwargs):
@staticmethod
def refresh_nodes_cache():
Node.refresh_nodes()
return Response("Ok")
Task = namedtuple('Task', ['id'])
task = Task(id="0")
return task
def perform_create(self, serializer):
action = serializer.validated_data["action"]
node = self.get_object()
if action == "refresh_cache" and node is None:
task = self.refresh_nodes_cache()
self.set_serializer_data(serializer, task)
return
if node is None:
raise Http404()
if action == "refresh":
task = update_node_assets_hardware_info_manual.delay(node)
else:
task = test_node_assets_connectivity_manual.delay(node)
self.set_serializer_data(serializer, task)
def delete(self, *args, **kwargs):
self.get(*args, **kwargs)
return Response(status=204)

View File

@@ -1,42 +1,25 @@
# ~*~ coding: utf-8 ~*~
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
#
# Licensed under the GNU General Public License v2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.gnu.org/licenses/gpl-2.0.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from django.db.models import Count
from common.serializers import CeleryTaskSerializer
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsAppUser
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.utils import tmp_to_org
from ..models import SystemUser, Asset
from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer
from ..tasks import (
push_system_user_to_assets_manual, test_system_user_connectivity_manual,
push_system_user_a_asset_manual, test_system_user_connectivity_a_asset,
push_system_user_a_asset_manual,
)
logger = get_logger(__file__)
__all__ = [
'SystemUserViewSet', 'SystemUserAuthInfoApi', 'SystemUserAssetAuthInfoApi',
'SystemUserPushApi', 'SystemUserTestConnectiveApi',
'SystemUserAssetsListView', 'SystemUserPushToAssetApi',
'SystemUserTestAssetConnectivityApi', 'SystemUserCommandFilterRuleListApi',
'SystemUserCommandFilterRuleListApi', 'SystemUserTaskApi',
]
@@ -45,16 +28,15 @@ class SystemUserViewSet(OrgBulkModelViewSet):
System user api set, for add,delete,update,list,retrieve resource
"""
model = SystemUser
filter_fields = ("name", "username")
filter_fields = ("name", "username", "protocol")
search_fields = filter_fields
serializer_class = serializers.SystemUserSerializer
serializer_classes = {
'default': serializers.SystemUserSerializer,
'list': serializers.SystemUserListSerializer,
}
permission_classes = (IsOrgAdminOrAppUser,)
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets'))
return queryset
class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
"""
@@ -62,7 +44,7 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.SystemUserAuthSerializer
serializer_class = SystemUserWithAuthInfoSerializer
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
@@ -75,88 +57,61 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
Get system user with asset auth info
"""
model = SystemUser
permission_classes = (IsAppUser,)
serializer_class = serializers.SystemUserAuthSerializer
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler
def get_object(self):
instance = super().get_object()
aid = self.kwargs.get('aid')
asset = get_object_or_404(Asset, pk=aid)
instance.load_specific_asset_auth(asset)
return instance
username = instance.username
if instance.username_same_with_user:
username = self.request.query_params.get("username")
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, pk=asset_id)
with tmp_to_org(asset.org_id):
instance.load_asset_special_auth(asset=asset, username=username)
return instance
class SystemUserPushApi(generics.RetrieveAPIView):
"""
Push system user to cluster assets api
"""
model = SystemUser
class SystemUserTaskApi(generics.CreateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = CeleryTaskSerializer
serializer_class = serializers.SystemUserTaskSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
nodes = system_user.nodes.all()
for node in nodes:
system_user.assets.add(*tuple(node.get_all_assets()))
task = push_system_user_to_assets_manual.delay(system_user)
return Response({"task": task.id})
def do_push(self, system_user, asset=None):
if asset is None:
task = push_system_user_to_assets_manual.delay(system_user)
else:
username = self.request.query_params.get('username')
task = push_system_user_a_asset_manual.delay(
system_user, asset, username=username
)
return task
class SystemUserTestConnectiveApi(generics.RetrieveAPIView):
"""
Push system user to cluster assets api
"""
model = SystemUser
permission_classes = (IsOrgAdmin,)
serializer_class = CeleryTaskSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
@staticmethod
def do_test(system_user, asset=None):
task = test_system_user_connectivity_manual.delay(system_user)
return Response({"task": task.id})
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filter_fields = ("hostname", "ip")
http_method_names = ['get']
search_fields = filter_fields
return task
def get_object(self):
pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=pk)
def get_queryset(self):
def perform_create(self, serializer):
action = serializer.validated_data["action"]
asset = serializer.validated_data.get('asset')
system_user = self.get_object()
return system_user.assets.all()
class SystemUserPushToAssetApi(generics.RetrieveAPIView):
model = SystemUser
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = push_system_user_a_asset_manual.delay(system_user, asset)
return Response({"task": task.id})
class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView):
model = SystemUser
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.TaskIDSerializer
def retrieve(self, request, *args, **kwargs):
system_user = self.get_object()
asset_id = self.kwargs.get('aid')
asset = get_object_or_404(Asset, id=asset_id)
task = test_system_user_connectivity_a_asset.delay(system_user, asset)
return Response({"task": task.id})
if action == 'push':
task = self.do_push(system_user, asset)
else:
task = self.do_test(system_user, asset)
data = getattr(serializer, '_data', {})
data["task"] = task.id
setattr(serializer, '_data', data)
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from django.db.models import F, Value
from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat
from common.permissions import IsOrgAdmin
@@ -8,10 +10,13 @@ from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org
from .. import models, serializers
__all__ = ['SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet']
__all__ = [
'SystemUserAssetRelationViewSet', 'SystemUserNodeRelationViewSet',
'SystemUserUserRelationViewSet',
]
class RelationMixin(OrgBulkModelViewSet):
class RelationMixin:
def get_queryset(self):
queryset = self.model.objects.all()
org_id = current_org.org_id()
@@ -23,12 +28,44 @@ class RelationMixin(OrgBulkModelViewSet):
))
return queryset
def send_post_add_signal(self, instance):
if not isinstance(instance, list):
instance = [instance]
class SystemUserAssetRelationViewSet(RelationMixin):
system_users_objects_map = defaultdict(list)
model, object_field = self.get_objects_attr()
for i in instance:
_id = getattr(i, object_field).id
system_users_objects_map[i.systemuser].append(_id)
sender = self.get_sender()
for system_user, objects in system_users_objects_map.items():
m2m_changed.send(
sender=sender, instance=system_user, action='post_add',
reverse=False, model=model, pk_set=objects
)
def get_sender(self):
return self.model
def get_objects_attr(self):
return models.Asset, 'asset'
def perform_create(self, serializer):
instance = serializer.save()
self.send_post_add_signal(instance)
class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
pass
class SystemUserAssetRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
filter_fields = [
'id', 'asset', 'systemuser',
]
search_fields = [
@@ -36,6 +73,9 @@ class SystemUserAssetRelationViewSet(RelationMixin):
"systemuser__name", "systemuser__username"
]
def get_objects_attr(self):
return models.Asset, 'asset'
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(
@@ -47,19 +87,50 @@ class SystemUserAssetRelationViewSet(RelationMixin):
return queryset
class SystemUserNodeRelationViewSet(RelationMixin):
class SystemUserNodeRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
filter_fields = [
'id', 'node', 'systemuser',
]
search_fields = [
"node__value", "systemuser__name", "systemuser_username"
]
def get_objects_attr(self):
return models.Node, 'node'
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset \
.annotate(node_key=F('node__key'))
return queryset
class SystemUserUserRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserUserRelationSerializer
model = models.SystemUser.users.through
permission_classes = (IsOrgAdmin,)
filter_fields = [
'id', 'user', 'systemuser',
]
search_fields = [
"user__username", "user__name",
"systemuser__name", "systemuser__username",
]
def get_objects_attr(self):
from users.models import User
return User, 'user'
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.annotate(
user_display=Concat(
F('user__name'), Value('('),
F('user__username'), Value(')')
)
)
return queryset

View File

@@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
#
from ..models import AdminUser
from .asset_user import AssetUserBackend
class AdminUserBackend(AssetUserBackend):
model = AdminUser
backend = 'AdminUser'

View File

@@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
#
from collections import defaultdict
from .base import BaseBackend
class AssetUserBackend(BaseBackend):
model = None
backend = "AssetUser"
@classmethod
def filter_queryset_more(cls, queryset):
return queryset
@classmethod
def filter(cls, username=None, assets=None, **kwargs):
queryset = cls.model.objects.all()
prefer_id = kwargs.get('prefer_id')
if prefer_id:
queryset = queryset.filter(id=prefer_id)
instances = cls.construct_authbook_objects(queryset, assets)
return instances
if username:
queryset = queryset.filter(username=username)
if assets:
queryset = queryset.filter(assets__in=assets).distinct()
queryset = cls.filter_queryset_more(queryset)
instances = cls.construct_authbook_objects(queryset, assets)
return instances
@classmethod
def construct_authbook_objects(cls, asset_users, assets):
instances = []
assets_user_assets_map = defaultdict(set)
if isinstance(asset_users, list):
assets_user_assets_map = {
asset_user.id: asset_user.assets.values_list('id', flat=True)
for asset_user in asset_users
}
else:
assets_user_assets = asset_users.values_list('id', 'assets')
for i, asset_id in assets_user_assets:
assets_user_assets_map[i].add(asset_id)
for asset_user in asset_users:
if not assets:
related_assets = asset_user.assets.all()
else:
assets_map = {a.id: a for a in assets}
related_assets = [
assets_map.get(i) for i in assets_user_assets_map.get(asset_user.id) if i in assets_map
]
for asset in related_assets:
instance = asset_user.construct_to_authbook(asset)
instance.backend = cls.backend
instances.append(instance)
return instances

View File

@@ -1,94 +1,48 @@
# -*- coding: utf-8 -*-
#
import uuid
from abc import abstractmethod
from ..models import Asset
class BaseBackend:
@classmethod
@abstractmethod
def filter(cls, username=None, assets=None, latest=True, prefer=None, prefer_id=None):
"""
:param username: 用户名
:param assets: <Asset>对象
:param latest: 是否是最新记录
:param prefer: 优先使用
:param prefer_id: 使用id
:return: 元素为<AuthBook>的可迭代对象(<list> or <QuerySet>)
"""
def all(self):
pass
@abstractmethod
def filter(self, username=None, hostname=None, ip=None, assets=None,
node=None, prefer_id=None, **kwargs):
pass
class AssetUserQuerySet(list):
def order_by(self, *ordering):
_ordering = []
reverse = False
for i in ordering:
if i[0] == '-':
reverse = True
i = i[1:]
_ordering.append(i)
self.sort(key=lambda obj: [getattr(obj, j) for j in _ordering], reverse=reverse)
return self
@abstractmethod
def search(self, item):
pass
def filter_in(self, kwargs):
in_kwargs = {}
queryset = []
for k, v in kwargs.items():
if len(v) == 0:
return self
if k.find("__in") >= 0:
_k = k.split('__')[0]
in_kwargs[_k] = v
else:
in_kwargs[k] = v
for k in in_kwargs:
kwargs.pop(k, None)
@abstractmethod
def get_queryset(self):
pass
if len(in_kwargs) == 0:
return self
for i in self:
matched = False
for k, v in in_kwargs.items():
attr = getattr(i, k, None)
# 如果属性或者value中是uuid,则转换成string
if isinstance(v[0], uuid.UUID):
v = [str(i) for i in v]
if isinstance(attr, uuid.UUID):
attr = str(attr)
if attr in v:
matched = True
if matched:
queryset.append(i)
return AssetUserQuerySet(queryset)
@abstractmethod
def delete(self, union_id):
pass
def filter_equal(self, kwargs):
def filter_it(obj):
wanted = []
real = []
for k, v in kwargs.items():
wanted.append(v)
value = getattr(obj, k, None)
if isinstance(value, uuid.UUID):
value = str(value)
real.append(value)
return wanted == real
kwargs = {k: v for k, v in kwargs.items() if k.find('__in') == -1}
if len(kwargs) > 0:
queryset = AssetUserQuerySet([i for i in self if filter_it(i)])
else:
queryset = self
return queryset
@staticmethod
def qs_to_values(qs):
values = qs.values(
'hostname', 'ip', "asset_id",
'username', 'password', 'private_key', 'public_key',
'score', 'version',
"asset_username", "union_id",
'date_created', 'date_updated',
'org_id', 'backend',
)
return values
def filter(self, **kwargs):
queryset = self.filter_in(kwargs).filter_equal(kwargs)
return queryset
def distinct(self):
items = list(set(self))
self[:] = items
return self
def __or__(self, other):
self.extend(other)
return self
@staticmethod
def make_assets_as_id(assets):
if not assets:
return []
if isinstance(assets[0], Asset):
assets = [a.id for a in assets]
return assets

View File

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

View File

@@ -1,110 +1,162 @@
# -*- coding: utf-8 -*-
#
from itertools import chain, groupby
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from .base import AssetUserQuerySet
from .db import AuthBookBackend
from .system_user import SystemUserBackend
from .admin_user import AdminUserBackend
from orgs.utils import current_org
from common.utils import get_logger, lazyproperty
from common.struct import QuerySetChain
from ..models import AssetUser, AuthBook
from .db import (
AuthbookBackend, SystemUserBackend, AdminUserBackend,
DynamicSystemUserBackend
)
logger = get_logger(__name__)
class NotSupportError(Exception):
pass
class AssetUserManager:
"""
资产用户管理器
"""
class AssetUserQueryset:
ObjectDoesNotExist = ObjectDoesNotExist
MultipleObjectsReturned = MultipleObjectsReturned
NotSupportError = NotSupportError
MSG_NOT_EXIST = '{} Object matching query does not exist'
MSG_MULTIPLE = '{} get() returned more than one object ' \
'-- it returned {}!'
backends = (
('db', AuthBookBackend),
def __init__(self, backends=()):
self.backends = backends
self._distinct_queryset = None
def backends_queryset(self):
return [b.get_queryset() for b in self.backends]
@lazyproperty
def backends_counts(self):
return [b.count() for b in self.backends]
def filter(self, hostname=None, ip=None, username=None,
assets=None, asset=None, node=None,
id=None, prefer_id=None, prefer=None, id__in=None):
if not assets and asset:
assets = [asset]
kwargs = dict(
hostname=hostname, ip=ip, username=username,
assets=assets, node=node, prefer=prefer, prefer_id=prefer_id,
id__in=id__in, union_id=id,
)
logger.debug("Filter: {}".format(kwargs))
backends = []
for backend in self.backends:
clone = backend.filter(**kwargs)
backends.append(clone)
return self._clone(backends)
def _clone(self, backends=None):
if backends is None:
backends = self.backends
return self.__class__(backends)
def search(self, item):
backends = []
for backend in self.backends:
new = backend.search(item)
backends.append(new)
return self._clone(backends)
def distinct(self):
logger.debug("Distinct asset user queryset")
queryset_chain = chain(*(backend.get_queryset() for backend in self.backends))
queryset_sorted = sorted(
queryset_chain,
key=lambda item: (item["asset_username"], item["score"]),
reverse=True,
)
results = groupby(queryset_sorted, key=lambda item: item["asset_username"])
final = [next(result[1]) for result in results]
self._distinct_queryset = final
return self
def get(self, latest=False, **kwargs):
queryset = self.filter(**kwargs)
if latest:
queryset = queryset.distinct()
queryset = list(queryset)
count = len(queryset)
if count == 1:
data = queryset[0]
return data
elif count > 1:
msg = 'Should return 1 record, but get {}'.format(count)
raise MultipleObjectsReturned(msg)
else:
msg = 'No record found(org is {})'.format(current_org.name)
raise ObjectDoesNotExist(msg)
def get_latest(self, **kwargs):
return self.get(latest=True, **kwargs)
@staticmethod
def to_asset_user(data):
obj = AssetUser()
for k, v in data.items():
setattr(obj, k, v)
return obj
@property
def queryset(self):
if self._distinct_queryset is not None:
return self._distinct_queryset
return QuerySetChain(self.backends_queryset())
def count(self):
if self._distinct_queryset is not None:
return len(self._distinct_queryset)
else:
return sum(self.backends_counts)
def __getitem__(self, ndx):
return self.queryset.__getitem__(ndx)
def __iter__(self):
self._data = iter(self.queryset)
return self
def __next__(self):
return self.to_asset_user(next(self._data))
class AssetUserManager:
support_backends = (
('db', AuthbookBackend),
('system_user', SystemUserBackend),
('admin_user', AdminUserBackend),
('system_user_dynamic', DynamicSystemUserBackend),
)
_prefer = "system_user"
def __init__(self):
self.backends = [backend() for name, backend in self.support_backends]
self._queryset = AssetUserQueryset(self.backends)
def filter(self, username=None, assets=None, latest=True, prefer=None, prefer_id=None):
if assets is not None and not assets:
return AssetUserQuerySet([])
def all(self):
return self._queryset
if prefer:
self._prefer = prefer
instances_map = {}
instances = []
for name, backend in self.backends:
# if name != "db":
# continue
_instances = backend.filter(
username=username, assets=assets, latest=latest,
prefer=self._prefer, prefer_id=prefer_id,
)
instances_map[name] = _instances
# 如果不是获取最新版本就不再merge
if not latest:
for _instances in instances_map.values():
instances.extend(_instances)
return AssetUserQuerySet(instances)
# merge的顺序
ordering = ["db"]
if self._prefer == "system_user":
ordering.extend(["system_user", "admin_user"])
def delete(self, obj):
name_backends_map = dict(self.support_backends)
backend_name = obj.backend
backend_cls = name_backends_map.get(backend_name)
union_id = obj.union_id
if backend_cls:
backend_cls().delete(union_id)
else:
ordering.extend(["admin_user", "system_user"])
# 根据prefer决定优先使用系统用户或管理用户谁的
ordering_instances = [instances_map.get(i, []) for i in ordering]
instances = self._merge_instances(*ordering_instances)
return AssetUserQuerySet(instances)
def get(self, username, asset, **kwargs):
instances = self.filter(username, assets=[asset], **kwargs)
if len(instances) == 1:
return instances[0]
elif len(instances) == 0:
self.raise_does_not_exist(self.__class__.__name__)
else:
self.raise_multiple_return(self.__class__.__name__, len(instances))
def raise_does_not_exist(self, name):
raise self.ObjectDoesNotExist(self.MSG_NOT_EXIST.format(name))
def raise_multiple_return(self, name, length):
raise self.MultipleObjectsReturned(self.MSG_MULTIPLE.format(name, length))
raise ObjectDoesNotExist("Not backend found")
@staticmethod
def create(**kwargs):
instance = AuthBookBackend.create(**kwargs)
return instance
# 使用create方法创建AuthBook对象解决并发创建问题添加锁机制
authbook = AuthBook.create(**kwargs)
return authbook
def all(self):
return self.filter()
def prefer(self, s):
self._prefer = s
return self
@staticmethod
def none():
return AssetUserQuerySet()
@staticmethod
def _merge_instances(*args):
instances = list(args[0])
keywords = [obj.keyword for obj in instances]
for _instances in args[1:]:
need_merge_instances = [obj for obj in _instances if obj.keyword not in keywords]
need_merge_keywords = [obj.keyword for obj in need_merge_instances]
instances.extend(need_merge_instances)
keywords.extend(need_merge_keywords)
return instances
def __getattr__(self, item):
return getattr(self._queryset, item)

View File

@@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
#
import itertools
from assets.models import SystemUser
from .asset_user import AssetUserBackend
class SystemUserBackend(AssetUserBackend):
model = SystemUser
backend = 'SystemUser'
@classmethod
def filter_queryset_more(cls, queryset):
queryset = cls._distinct_system_users_by_username(queryset)
return queryset
@classmethod
def _distinct_system_users_by_username(cls, system_users):
system_users = sorted(
system_users,
key=lambda su: (su.username, su.priority, su.date_updated),
reverse=True,
)
results = itertools.groupby(system_users, key=lambda su: su.username)
system_users = [next(result[1]) for result in results]
return system_users

View File

@@ -3,14 +3,5 @@
# from django.conf import settings
from .db import AuthBookBackend
# from .vault import VaultBackend
def get_backend():
default_backend = AuthBookBackend
# if settings.BACKEND_ASSET_USER_AUTH_VAULT:
# return VaultBackend
return default_backend

View File

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

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
import coreapi
from rest_framework.compat import coreapi, coreschema
from rest_framework import filters
from django.db.models import Q
@@ -58,12 +58,14 @@ class AssetByNodeFilterBackend(filters.BaseFilterBackend):
if query_all:
pattern = node.get_all_children_pattern(with_self=True)
else:
pattern = node.get_children_key_pattern(with_self=True)
# pattern = node.get_children_key_pattern(with_self=True)
# 只显示当前节点下资产
pattern = r"^{}$".format(node.key)
return self.perform_query(pattern, queryset)
class LabelFilterBackend(filters.BaseFilterBackend):
sep = '#'
sep = ':'
query_arg = 'label'
def get_schema_fields(self, view):
@@ -82,6 +84,8 @@ class LabelFilterBackend(filters.BaseFilterBackend):
q = None
for kv in labels_query:
if '#' in kv:
self.sep = '#'
if self.sep not in kv:
continue
key, value = kv.strip().split(self.sep)[:2]
@@ -113,3 +117,23 @@ class AssetRelatedByNodeFilterBackend(AssetByNodeFilterBackend):
def perform_query(pattern, queryset):
return queryset.filter(asset__nodes__key__regex=pattern).distinct()
class IpInFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
ips = request.query_params.get('ips')
if not ips:
return queryset
ip_list = [i.strip() for i in ips.split(',')]
queryset = queryset.filter(ip__in=ip_list)
return queryset
def get_schema_fields(self, view):
return [
coreapi.Field(
name='ips', location='query', required=False, type='string',
schema=coreschema.String(
title='ips',
description='ip in filter'
)
)
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,107 +0,0 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.utils.translation import gettext_lazy as _
from common.utils import validate_ssh_private_key, ssh_pubkey_gen, get_logger
from orgs.mixins.forms import OrgModelForm
from ..models import AdminUser, SystemUser
logger = get_logger(__file__)
__all__ = [
'FileForm', 'SystemUserForm', 'AdminUserForm', 'PasswordAndKeyAuthForm',
]
class FileForm(forms.Form):
file = forms.FileField()
class PasswordAndKeyAuthForm(forms.ModelForm):
# Form field name can not start with `_`, so redefine it,
password = forms.CharField(
widget=forms.PasswordInput, max_length=128,
strip=True, required=False,
help_text=_('Password or private key passphrase'),
label=_("Password"),
)
# Need use upload private key file except paste private key content
private_key = forms.FileField(required=False, label=_("Private key"))
def clean_private_key(self):
private_key_f = self.cleaned_data['private_key']
password = self.cleaned_data['password']
if private_key_f:
key_string = private_key_f.read()
private_key_f.seek(0)
key_string = key_string.decode()
if not validate_ssh_private_key(key_string, password):
msg = _('Invalid private key, Only support '
'RSA/DSA format key')
raise forms.ValidationError(msg)
return private_key_f
def validate_password_key(self):
password = self.cleaned_data['password']
private_key_f = self.cleaned_data.get('private_key', '')
if not password and not private_key_f:
raise forms.ValidationError(_(
'Password and private key file must be input one'
))
def gen_keys(self):
password = self.cleaned_data.get('password', '') or None
private_key_f = self.cleaned_data['private_key']
public_key = private_key = None
if private_key_f:
private_key = private_key_f.read().strip().decode('utf-8')
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
return private_key, public_key
class AdminUserForm(PasswordAndKeyAuthForm):
def save(self, commit=True):
raise forms.ValidationError("Use api to save")
class Meta:
model = AdminUser
fields = ['name', 'username', 'password', 'private_key', 'comment']
widgets = {
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
}
class SystemUserForm(OrgModelForm, PasswordAndKeyAuthForm):
# Admin user assets define, let user select, save it in form not in view
auto_generate_key = forms.BooleanField(initial=True, required=False)
def save(self, commit=True):
raise forms.ValidationError("Use api to save")
class Meta:
model = SystemUser
fields = [
'name', 'username', 'protocol', 'auto_generate_key',
'password', 'private_key', 'auto_push', 'sudo',
'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 = {
'auto_push': _('Auto push system user to asset'),
'priority': _('1-100, High level will be using login asset as default, '
'if user was granted more than 2 system user'),
'login_mode': _('If you choose manual login mode, you do not '
'need to fill in the username and password.'),
'sudo': _("Use comma split multi command, ex: /bin/whoami,/bin/ifconfig")
}

View File

@@ -6,7 +6,7 @@
Other module of this app shouldn't connect with other app.
:copyright: (c) 2014-2018 by Jumpserver Team.
:copyright: (c) 2014-2018 by JumpServer Team.
:license: GPL v2, see LICENSE for more details.
"""

View File

@@ -0,0 +1,24 @@
# Generated by Django 2.2.7 on 2020-01-06 07:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0046_auto_20191218_1705'),
]
operations = [
migrations.CreateModel(
name='AssetUser',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('assets.authbook',),
),
]

View File

@@ -0,0 +1,35 @@
# Generated by Django 2.2.7 on 2019-12-30 07:12
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0047_assetuser'),
]
operations = [
migrations.RemoveField(
model_name='authbook',
name='is_active',
),
migrations.AddField(
model_name='systemuser',
name='username_same_with_user',
field=models.BooleanField(default=False, verbose_name='Username same with user'),
),
migrations.AddField(
model_name='systemuser',
name='users',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Users'),
),
migrations.AddField(
model_name='systemuser',
name='groups',
field=models.ManyToManyField(blank=True, to='users.UserGroup',
verbose_name='User groups'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2020-01-19 07:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0048_auto_20191230_1512'),
]
operations = [
migrations.AddField(
model_name='systemuser',
name='sftp_root',
field=models.CharField(default='tmp', max_length=128, verbose_name='SFTP Root'),
),
]

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _
from common.fields.model import JsonDictTextField
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager
from .base import ConnectivityMixin
from .utils import Connectivity
__all__ = ['Asset', 'ProtocolsMixin', 'Platform']
@@ -220,7 +221,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
hostname_raw = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Hostname raw'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
@@ -243,6 +244,13 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def platform_base(self):
return self.platform.base
@lazyproperty
def admin_user_username(self):
"""求可连接性时直接用用户名去取避免再查一次admin user
serializer 中直接通过annotate方式返回了这个
"""
return self.admin_user.username
def is_windows(self):
return self.platform.is_windows()
@@ -275,9 +283,11 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
def connectivity(self):
if self._connectivity:
return self._connectivity
if not self.admin_user:
if not self.admin_user_username:
return Connectivity.unknown()
connectivity = self.admin_user.get_asset_connectivity(self)
connectivity = ConnectivityMixin.get_asset_username_connectivity(
self, self.admin_user_username
)
return connectivity
@connectivity.setter
@@ -290,7 +300,7 @@ class Asset(ProtocolsMixin, NodesRelationMixin, OrgModelMixin):
if not self.admin_user:
return {}
self.admin_user.load_specific_asset_auth(self)
self.admin_user.load_asset_special_auth(self)
info = {
'username': self.admin_user.username,
'password': self.admin_user.password,

View File

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

View File

@@ -1,30 +1,31 @@
# -*- coding: utf-8 -*-
#
from django.db import models
from django.db import models, transaction
from django.db.models import Max
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgManager
from .base import AssetUser
from .base import BaseUser
__all__ = ['AuthBook']
class AuthBookQuerySet(models.QuerySet):
def latest_version(self):
return self.filter(is_latest=True).filter(is_active=True)
def delete(self):
if self.count() > 1:
raise PermissionError(_("Bulk delete deny"))
return super().delete()
class AuthBookManager(OrgManager):
pass
class AuthBook(AssetUser):
class AuthBook(BaseUser):
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, verbose_name=_('Asset'))
is_latest = models.BooleanField(default=False, verbose_name=_('Latest version'))
version = models.IntegerField(default=1, verbose_name=_('Version'))
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
objects = AuthBookManager.from_queryset(AuthBookQuerySet)()
backend = "db"
@@ -35,37 +36,40 @@ class AuthBook(AssetUser):
class Meta:
verbose_name = _('AuthBook')
def set_to_latest(self):
self.remove_pre_latest()
self.is_latest = True
self.save()
def get_pre_latest(self):
pre_obj = self.__class__.objects.filter(
username=self.username, asset=self.asset
).latest_version().first()
return pre_obj
def remove_pre_latest(self):
pre_obj = self.get_pre_latest()
if pre_obj:
pre_obj.is_latest = False
pre_obj.save()
def set_version(self):
pre_obj = self.get_pre_latest()
if pre_obj:
self.version = pre_obj.version + 1
else:
self.version = 1
self.save()
def get_related_assets(self):
return [self.asset]
def generate_id_with_asset(self, asset):
return self.id
@classmethod
def get_max_version(cls, username, asset):
version_max = cls.objects.filter(username=username, asset=asset) \
.aggregate(Max('version'))
version_max = version_max['version__max'] or 0
return version_max
@classmethod
def create(cls, **kwargs):
"""
使用并发锁机制创建AuthBook对象, (主要针对并发创建 username, asset 相同的对象时)
并更新其他对象的 is_latest=False (其他对象: 与当前对象的 username, asset 相同)
同时设置自己的 is_latest=True, version=max_version + 1
"""
username = kwargs['username']
asset = kwargs['asset']
with transaction.atomic():
# 使用select_for_update限制并发创建相同的username、asset条目
instances = cls.objects.select_for_update().filter(username=username, asset=asset)
instances.filter(is_latest=True).update(is_latest=False)
max_version = cls.get_max_version(username, asset)
kwargs.update({
'version': max_version + 1,
'is_latest': True
})
obj = cls.objects.create(**kwargs)
return obj
@property
def connectivity(self):
return self.get_asset_connectivity(self.asset)

View File

@@ -12,98 +12,29 @@ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.utils import (
signer, ssh_key_string_to_obj, ssh_key_gen, get_logger
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
)
from common.validators import alphanumeric
from common import fields
from orgs.mixins.models import OrgModelMixin
from .utils import private_key_validator, Connectivity
from .utils import Connectivity
logger = get_logger(__file__)
class AssetUser(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
class ConnectivityMixin:
CONNECTIVITY_ASSET_CACHE_KEY = "ASSET_USER_{}_{}_ASSET_CONNECTIVITY"
CONNECTIVITY_AMOUNT_CACHE_KEY = "ASSET_USER_{}_{}_CONNECTIVITY_AMOUNT"
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
ASSET_USER_CACHE_TIME = 3600 * 24
_prefer = "system_user"
_assets_amount = None
@property
def private_key_obj(self):
if self.private_key:
return ssh_key_string_to_obj(self.private_key, password=self.password)
else:
return None
@property
def private_key_file(self):
if not self.private_key_obj:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name)
if not os.path.exists(key_path):
self.private_key_obj.write_private_key_file(key_path)
os.chmod(key_path, 0o400)
return key_path
@property
def public_key_obj(self):
if self.public_key:
try:
return sshpubkeys.SSHKey(self.public_key)
except TabError:
pass
return None
id = ''
username = ''
@property
def part_id(self):
i = '-'.join(str(self.id).split('-')[:3])
return i
def get_private_key(self):
if not self.private_key_obj:
return None
string_io = io.StringIO()
self.private_key_obj.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key
def get_related_assets(self):
assets = self.assets.all()
return assets
def set_auth(self, password=None, private_key=None, public_key=None):
update_fields = []
if password:
self.password = password
update_fields.append('password')
if private_key:
self.private_key = private_key
update_fields.append('private_key')
if public_key:
self.public_key = public_key
update_fields.append('public_key')
if update_fields:
self.save(update_fields=update_fields)
def set_connectivity(self, summary):
unreachable = summary.get('dark', {}).keys()
reachable = summary.get('contacted', {}).keys()
@@ -150,20 +81,10 @@ class AssetUser(OrgModelMixin):
cache.set(cache_key, amount, self.ASSET_USER_CACHE_TIME)
return amount
@property
def assets_amount(self):
if self._assets_amount is not None:
return self._assets_amount
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cached = cache.get(cache_key)
if not cached:
cached = self.get_related_assets().count()
cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME)
return cached
def expire_assets_amount(self):
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cache.delete(cache_key)
@classmethod
def get_asset_username_connectivity(cls, asset, username):
key = cls.CONNECTIVITY_ASSET_CACHE_KEY.format(username, asset.id)
return Connectivity.get(key)
def get_asset_connectivity(self, asset):
key = self.get_asset_connectivity_key(asset)
@@ -176,28 +97,103 @@ class AssetUser(OrgModelMixin):
key = self.get_asset_connectivity_key(asset)
Connectivity.set(key, c)
def get_asset_user(self, asset):
class AuthMixin:
private_key = ''
password = ''
public_key = ''
username = ''
_prefer = 'system_user'
@property
def private_key_obj(self):
if self.private_key:
key_obj = ssh_key_string_to_obj(self.private_key, password=self.password)
return key_obj
else:
return None
@property
def private_key_file(self):
if not self.private_key_obj:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name)
if not os.path.exists(key_path):
self.private_key_obj.write_private_key_file(key_path)
os.chmod(key_path, 0o400)
return key_path
def get_private_key(self):
if not self.private_key_obj:
return None
string_io = io.StringIO()
self.private_key_obj.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key
@property
def public_key_obj(self):
if self.public_key:
try:
return sshpubkeys.SSHKey(self.public_key)
except TabError:
pass
return None
def set_auth(self, password=None, private_key=None, public_key=None):
update_fields = []
if password:
self.password = password
update_fields.append('password')
if private_key:
self.private_key = private_key
update_fields.append('private_key')
if public_key:
self.public_key = public_key
update_fields.append('public_key')
if update_fields:
self.save(update_fields=update_fields)
def has_special_auth(self, asset=None):
from .authbook import AuthBook
queryset = AuthBook.objects.filter(username=self.username)
if asset:
queryset = queryset.filter(asset=asset)
return queryset.exists()
def get_asset_user(self, asset, username=None):
from ..backends import AssetUserManager
if username is None:
username = self.username
try:
manager = AssetUserManager().prefer(self._prefer)
other = manager.get(username=self.username, asset=asset, prefer_id=self.id)
manager = AssetUserManager()
other = manager.get_latest(
username=username, asset=asset,
prefer_id=self.id, prefer=self._prefer,
)
return other
except Exception as e:
logger.error(e, exc_info=True)
return None
def load_specific_asset_auth(self, asset):
instance = self.get_asset_user(asset)
def load_asset_special_auth(self, asset=None, username=None):
if not asset:
return self
instance = self.get_asset_user(asset, username=username)
if instance:
self._merge_auth(instance)
def _merge_auth(self, other):
if other.password:
self.password = other.password
if other.public_key:
self.public_key = other.public_key
if other.private_key:
if other.public_key or other.private_key:
self.private_key = other.private_key
self.public_key = other.public_key
def clear_auth(self):
self.password = ''
@@ -216,19 +212,57 @@ class AssetUser(OrgModelMixin):
)
return private_key, public_key
def auto_gen_auth(self):
password = str(uuid.uuid4())
private_key, public_key = ssh_key_gen(
username=self.username
)
def auto_gen_auth(self, password=True, key=True):
_password = None
_private_key = None
_public_key = None
if password:
_password = self.gen_password()
if key:
_private_key, _public_key = self.gen_key(self.username)
self.set_auth(
password=password, private_key=private_key,
public_key=public_key
password=_password, private_key=_private_key,
public_key=_public_key
)
def auto_gen_auth_password(self):
password = str(uuid.uuid4())
self.set_auth(password=password)
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, verbose_name=_('Name'))
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
ASSETS_AMOUNT_CACHE_KEY = "ASSET_USER_{}_ASSETS_AMOUNT"
ASSET_USER_CACHE_TIME = 600
_prefer = "system_user"
def get_related_assets(self):
assets = self.assets.filter(org_id=self.org_id)
return assets
def get_username(self):
return self.username
@lazyproperty
def assets_amount(self):
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cached = cache.get(cache_key)
if not cached:
cached = self.get_related_assets().count()
cache.set(cache_key, cached, self.ASSET_USER_CACHE_TIME)
return cached
def expire_assets_amount(self):
cache_key = self.ASSETS_AMOUNT_CACHE_KEY.format(self.id)
cache.delete(cache_key)
def _to_secret_json(self):
"""Push system user use it"""
@@ -240,26 +274,6 @@ class AssetUser(OrgModelMixin):
'private_key': self.private_key_file,
}
def generate_id_with_asset(self, asset):
user_id = [self.part_id]
asset_id = str(asset.id).split('-')[3:]
ids = user_id + asset_id
return '-'.join(ids)
def construct_to_authbook(self, asset):
from . import AuthBook
fields = [
'name', 'username', 'comment', 'org_id',
'password', 'private_key', 'public_key',
'date_created', 'date_updated', 'created_by'
]
i = self.generate_id_with_asset(asset)
obj = AuthBook(id=i, asset=asset, version=0, is_latest=True)
for field in fields:
value = getattr(self, field)
setattr(obj, field, value)
return obj
class Meta:
abstract = True

View File

@@ -18,7 +18,7 @@ __all__ = [
class CommandFilter(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=64, unique=True, verbose_name=_("Name"))
name = models.CharField(max_length=64, verbose_name=_("Name"))
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
@@ -29,6 +29,7 @@ class CommandFilter(OrgModelMixin):
return self.name
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Command filter")

View File

@@ -10,20 +10,21 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.models import OrgModelMixin
from .base import AssetUser
from .base import BaseUser
__all__ = ['Domain', 'Gateway']
class Domain(OrgModelMixin):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
name = models.CharField(max_length=128, verbose_name=_('Name'))
comment = models.TextField(blank=True, verbose_name=_('Comment'))
date_created = models.DateTimeField(auto_now_add=True, null=True,
verbose_name=_('Date created'))
class Meta:
verbose_name = _("Domain")
unique_together = [('org_id', 'name')]
def __str__(self):
return self.name
@@ -39,7 +40,7 @@ class Domain(OrgModelMixin):
return random.choice(self.gateways)
class Gateway(AssetUser):
class Gateway(BaseUser):
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_CHOICES = (

View File

@@ -11,9 +11,9 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext
from django.core.cache import cache
from common.utils import get_logger, timeit, lazyproperty
from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import set_current_org, get_current_org, tmp_to_org
from orgs.utils import get_current_org, tmp_to_org, current_org
from orgs.models import Organization
@@ -26,63 +26,108 @@ class NodeQuerySet(models.QuerySet):
raise PermissionError("Bulk delete node deny")
class TreeCache:
updated_time_cache_key = 'NODE_TREE_UPDATED_AT_{}'
cache_time = 3600
assets_updated_time_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT_{}'
def __init__(self, tree, org_id):
now = time.time()
self.created_time = now
self.assets_created_time = now
self.tree = tree
self.org_id = org_id
def _has_changed(self, tp="tree"):
if tp == "assets":
key = self.assets_updated_time_cache_key.format(self.org_id)
else:
key = self.updated_time_cache_key.format(self.org_id)
updated_time = cache.get(key, 0)
if updated_time > self.created_time:
return True
else:
return False
@classmethod
def set_changed(cls, tp="tree", t=None, org_id=None):
if org_id is None:
org_id = current_org.id
if tp == "assets":
key = cls.assets_updated_time_cache_key.format(org_id)
else:
key = cls.updated_time_cache_key.format(org_id)
ttl = cls.cache_time
if not t:
t = time.time()
cache.set(key, t, ttl)
def tree_has_changed(self):
return self._has_changed("tree")
def set_tree_changed(self, t=None):
logger.debug("Set tree tree changed")
self.__class__.set_changed(t=t, tp="tree")
def assets_has_changed(self):
return self._has_changed("assets")
def set_tree_assets_changed(self, t=None):
logger.debug("Set tree assets changed")
self.__class__.set_changed(t=t, tp="assets")
def get(self):
if self.tree_has_changed():
self.renew()
return self.tree
if self.assets_has_changed():
self.tree.init_assets()
return self.tree
def renew(self):
new_obj = self.__class__.new(self.org_id)
self.tree = new_obj.tree
self.created_time = new_obj.created_time
self.assets_created_time = new_obj.assets_created_time
@classmethod
def new(cls, org_id=None):
from ..utils import TreeService
logger.debug("Create node tree")
if not org_id:
org_id = current_org.id
with tmp_to_org(org_id):
tree = TreeService.new()
obj = cls(tree, org_id)
obj.tree = tree
return obj
class TreeMixin:
tree_created_time = None
tree_updated_time_cache_key = 'NODE_TREE_UPDATED_AT'
tree_cache_time = 3600
tree_assets_cache_key = 'NODE_TREE_ASSETS_UPDATED_AT'
tree_assets_created_time = None
_tree_service = None
_org_tree_map = {}
@classmethod
def tree(cls):
from ..utils import TreeService
tree_updated_time = cache.get(cls.tree_updated_time_cache_key, 0)
now = time.time()
# 什么时候重新初始化 _tree_service
if not cls.tree_created_time or \
tree_updated_time > cls.tree_created_time:
logger.debug("Create node tree")
tree = TreeService.new()
cls.tree_created_time = now
cls.tree_assets_created_time = now
cls._tree_service = tree
return tree
# 是否要重新初始化节点资产
node_assets_updated_time = cache.get(cls.tree_assets_cache_key, 0)
if not cls.tree_assets_created_time or \
node_assets_updated_time > cls.tree_assets_created_time:
cls._tree_service.init_assets()
cls.tree_assets_created_time = now
logger.debug("Refresh node tree assets")
return cls._tree_service
org_id = current_org.org_id()
t = cls.get_local_tree_cache(org_id)
if t is None:
t = TreeCache.new()
cls._org_tree_map[org_id] = t
return t.get()
@classmethod
def get_local_tree_cache(cls, org_id=None):
t = cls._org_tree_map.get(org_id)
return t
@classmethod
def refresh_tree(cls, t=None):
logger.debug("Refresh node tree")
key = cls.tree_updated_time_cache_key
ttl = cls.tree_cache_time
if not t:
t = time.time()
cache.set(key, t, ttl)
TreeCache.set_changed(tp="tree", t=t, org_id=current_org.id)
@classmethod
def refresh_node_assets(cls, t=None):
logger.debug("Refresh node assets")
key = cls.tree_assets_cache_key
ttl = cls.tree_cache_time
if not t:
t = time.time()
cache.set(key, t, ttl)
@staticmethod
def refresh_user_tree_cache():
"""
当节点-节点关系,节点-资产关系发生变化时,应该刷新用户授权树缓存
:return:
"""
from perms.utils.asset_permission import AssetPermissionUtilV2
AssetPermissionUtilV2.expire_all_user_tree_cache()
TreeCache.set_changed(tp="assets", t=t, org_id=current_org.id)
class FamilyMixin:
@@ -154,6 +199,20 @@ class FamilyMixin:
)
return child
def get_or_create_child(self, value, _id=None):
"""
:return: Node, bool (created)
"""
children = self.get_children()
exist = children.filter(value=value).exists()
if exist:
child = children.filter(value=value).first()
created = False
else:
child = self.create_child(value, _id)
created = True
return child, created
def get_next_child_key(self):
mark = self.child_mark
self.child_mark += 1
@@ -376,15 +435,6 @@ class SomeNodesMixin:
)
return obj
@classmethod
def empty_node(cls):
with tmp_to_org(Organization.system()):
defaults = {'value': cls.empty_value}
obj, created = cls.objects.get_or_create(
defaults=defaults, key=cls.empty_key
)
return obj
@classmethod
def default_node(cls):
with tmp_to_org(Organization.default()):
@@ -413,7 +463,6 @@ class SomeNodesMixin:
@classmethod
def initial_some_nodes(cls):
cls.default_node()
cls.empty_node()
cls.ungrouped_node()
cls.favorite_node()
@@ -523,13 +572,13 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
tree_node = TreeNode(**data)
return tree_node
def has_children_or_contains_assets(self):
if self.children or self.get_assets():
def has_children_or_has_assets(self):
if self.children or self.get_assets().exists():
return True
return False
def delete(self, using=None, keep_parents=False):
if self.has_children_or_contains_assets():
if self.has_children_or_has_assets():
return
return super().delete(using=using, keep_parents=keep_parents)
@@ -539,14 +588,13 @@ class Node(OrgModelMixin, SomeNodesMixin, TreeMixin, FamilyMixin, FullValueMixin
org = get_current_org()
if not org or not org.is_real():
Organization.default().change_to()
i = 0
while i < count:
nodes = list(cls.objects.all())
if count > 100:
length = 100
else:
length = count
nodes = list(cls.objects.all())
if count > 100:
length = 100
else:
length = count
for i in range(length):
node = random.choice(nodes)
node.create_child('Node {}'.format(i))
for i in range(length):
node = random.choice(nodes)
child = node.create_child('Node {}'.format(i))
print("{}. {}".format(i, child))

View File

@@ -4,14 +4,12 @@
import logging
from functools import reduce
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from common.utils import signer
from .base import AssetUser
from .base import BaseUser
from .asset import Asset
@@ -19,7 +17,7 @@ __all__ = ['AdminUser', 'SystemUser']
logger = logging.getLogger(__name__)
class AdminUser(AssetUser):
class AdminUser(BaseUser):
"""
A privileged user that ansible can use it to push system user and so on
"""
@@ -87,7 +85,7 @@ class AdminUser(AssetUser):
continue
class SystemUser(AssetUser):
class SystemUser(BaseUser):
PROTOCOL_SSH = 'ssh'
PROTOCOL_RDP = 'rdp'
PROTOCOL_TELNET = 'telnet'
@@ -107,9 +105,11 @@ class SystemUser(AssetUser):
(LOGIN_AUTO, _('Automatic login')),
(LOGIN_MANUAL, _('Manually login'))
)
username_same_with_user = models.BooleanField(default=False, verbose_name=_("Username same with user"))
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
users = models.ManyToManyField('users.User', blank=True, verbose_name=_("Users"))
groups = models.ManyToManyField('users.UserGroup', blank=True, verbose_name=_("User groups"))
priority = models.IntegerField(default=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'))
@@ -117,9 +117,20 @@ class SystemUser(AssetUser):
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
_prefer = 'system_user'
def __str__(self):
return '{0.name}({0.username})'.format(self)
username = self.username
if self.username_same_with_user:
username = 'dynamic'
return '{0.name}({1})'.format(self, username)
def get_username(self):
if self.username_same_with_user:
return list(self.users.values_list('username', flat=True))
else:
return self.username
@property
def nodes_amount(self):
@@ -147,6 +158,11 @@ class SystemUser(AssetUser):
def can_perm_to_asset(self):
return self.protocol not in [self.PROTOCOL_MYSQL]
def _merge_auth(self, other):
super()._merge_auth(other)
if self.username_same_with_user:
self.username = other.username
@property
def cmd_filter_rules(self):
from .cmd_filter import CommandFilterRule

View File

@@ -77,7 +77,7 @@ class Connectivity:
return cls(cls.UNKNOWN, timezone.now())
@classmethod
def set(cls, key, value, ttl=0):
def set(cls, key, value, ttl=None):
cache.set(key, value, ttl)
@classmethod

View File

@@ -55,3 +55,11 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer):
class TaskIDSerializer(serializers.Serializer):
task = serializers.CharField(read_only=True)
class AssetUserTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
task = serializers.CharField(read_only=True)

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
#
import re
from rest_framework import serializers
from django.db.models import Prefetch
from django.db.models import Prefetch, F, Count
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -12,8 +12,9 @@ from .base import ConnectivitySerializer
__all__ = [
'AssetSerializer', 'AssetSimpleSerializer',
'AssetDisplaySerializer',
'ProtocolsField', 'PlatformSerializer',
'AssetDetailSerializer',
'AssetDetailSerializer', 'AssetTaskSerializer',
]
@@ -66,7 +67,8 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
slug_field='name', queryset=Platform.objects.all(), label=_("Platform")
)
protocols = ProtocolsField(label=_('Protocols'), required=False)
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
domain_display = serializers.ReadOnlyField(source='domain.name')
admin_user_display = serializers.ReadOnlyField(source='admin_user.name')
"""
资产的数据结构
@@ -74,21 +76,35 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = Asset
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'ip', 'hostname', 'protocol', 'port',
'protocols', 'platform', 'is_active', 'public_ip', 'domain',
'admin_user', 'nodes', 'labels', 'number', 'vendor', 'model', 'sn',
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory',
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch',
'hostname_raw', 'comment', 'created_by', 'date_created',
'hardware_info', 'connectivity',
fields_mini = ['id', 'hostname', 'ip']
fields_small = fields_mini + [
'protocol', 'port', 'protocols', 'is_active', 'public_ip',
'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw', 'comment',
'created_by', 'date_created', 'hardware_info',
]
read_only_fields = (
fields_fk = [
'admin_user', 'admin_user_display', 'domain', 'domain_display', 'platform'
]
fk_only_fields = {
'platform': ['name']
}
fields_m2m = [
'nodes', 'labels',
]
annotates_fields = {
# 'admin_user_display': 'admin_user__name'
}
fields_as = list(annotates_fields.keys())
fields = fields_small + fields_fk + fields_m2m + fields_as
read_only_fields = [
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw',
'created_by', 'date_created',
)
] + fields_as
extra_kwargs = {
'protocol': {'write_only': True},
'port': {'write_only': True},
@@ -99,10 +115,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related(
Prefetch('nodes', queryset=Node.objects.all().only('id')),
Prefetch('labels', queryset=Label.objects.all().only('id')),
).select_related('admin_user', 'domain', 'platform')
queryset = queryset.select_related('admin_user', 'domain', 'platform')
return queryset
def compatible_with_old_protocol(self, validated_data):
@@ -130,6 +143,22 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
return super().update(instance, validated_data)
class AssetDisplaySerializer(AssetSerializer):
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
class Meta(AssetSerializer.Meta):
fields = AssetSerializer.Meta.fields + [
'connectivity',
]
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset\
.annotate(admin_user_username=F('admin_user__username'))
return queryset
class PlatformSerializer(serializers.ModelSerializer):
meta = serializers.DictField(required=False, allow_null=True)
@@ -151,3 +180,12 @@ class AssetSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'connectivity', 'port']
class AssetTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('refresh', 'refresh'),
('test', 'test'),
)
task = serializers.CharField(read_only=True)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)

View File

@@ -8,39 +8,23 @@ from common.serializers import AdaptedBulkListSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import AuthBook, Asset
from ..backends import AssetUserManager
from .base import ConnectivitySerializer, AuthSerializerMixin
__all__ = [
'AssetUserSerializer', 'AssetUserAuthInfoSerializer',
'AssetUserExportSerializer', 'AssetUserPushSerializer',
'AssetUserWriteSerializer', 'AssetUserReadSerializer',
'AssetUserAuthInfoSerializer', 'AssetUserPushSerializer',
]
class BasicAssetSerializer(serializers.ModelSerializer):
class Meta:
model = Asset
fields = ['hostname', 'ip']
class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
ip = serializers.CharField(read_only=True, label=_("IP"))
connectivity = ConnectivitySerializer(read_only=True, label=_("Connectivity"))
backend = serializers.CharField(read_only=True, label=_("Backend"))
class AssetUserWriteSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class Meta:
model = AuthBook
list_serializer_class = AdaptedBulkListSerializer
read_only_fields = (
'date_created', 'date_updated', 'created_by',
'is_latest', 'version', 'connectivity',
)
fields = [
"id", "hostname", "ip", "username", "password", "asset", "version",
"is_latest", "connectivity", "backend",
"date_created", "date_updated", "private_key", "public_key",
'id', 'username', 'password', 'private_key', "public_key",
'asset', 'comment',
]
extra_kwargs = {
'username': {'required': True},
@@ -53,11 +37,35 @@ class AssetUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
if not validated_data.get("name") and validated_data.get("username"):
validated_data["name"] = validated_data["username"]
instance = AssetUserManager.create(**validated_data)
instance.set_to_latest()
return instance
class AssetUserExportSerializer(AssetUserSerializer):
class AssetUserReadSerializer(AssetUserWriteSerializer):
id = serializers.CharField(read_only=True, source='union_id', label=_("ID"))
hostname = serializers.CharField(read_only=True, label=_("Hostname"))
ip = serializers.CharField(read_only=True, label=_("IP"))
asset = serializers.CharField(source='asset_id', label=_('Asset'))
backend = serializers.CharField(read_only=True, label=_("Backend"))
class Meta(AssetUserWriteSerializer.Meta):
read_only_fields = (
'date_created', 'date_updated',
'created_by', 'version',
)
fields = [
'id', 'username', 'password', 'private_key', "public_key",
'asset', 'hostname', 'ip', 'backend', 'version',
'date_created', "date_updated", 'comment',
]
extra_kwargs = {
'username': {'required': True},
'password': {'write_only': True},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
}
class AssetUserAuthInfoSerializer(AssetUserReadSerializer):
password = serializers.CharField(
max_length=256, allow_blank=True, allow_null=True,
required=False, label=_('Password')
@@ -72,12 +80,6 @@ class AssetUserExportSerializer(AssetUserSerializer):
)
class AssetUserAuthInfoSerializer(serializers.ModelSerializer):
class Meta:
model = AuthBook
fields = ['password', 'private_key', 'public_key']
class AssetUserPushSerializer(serializers.Serializer):
asset = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, label=_("Asset"))
username = serializers.CharField(max_length=1024)

View File

@@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _
from rest_framework import serializers
from common.utils import ssh_pubkey_gen, validate_ssh_private_key
from ..models import AssetUser
class AuthSerializer(serializers.ModelSerializer):
@@ -60,9 +61,6 @@ class AuthSerializerMixin:
if not value:
validated_data.pop(field, None)
# print(validated_data)
# raise serializers.ValidationError(">>>>>>")
def create(self, validated_data):
self.clean_auth_fields(validated_data)
return super().create(validated_data)
@@ -70,3 +68,15 @@ class AuthSerializerMixin:
def update(self, instance, validated_data):
self.clean_auth_fields(validated_data)
return super().update(instance, validated_data)
class AuthInfoSerializer(serializers.ModelSerializer):
private_key = serializers.ReadOnlyField(source='get_private_key')
class Meta:
model = AssetUser
fields = [
'username', 'password',
'private_key', 'public_key',
'date_updated',
]

View File

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

View File

@@ -15,11 +15,18 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = Domain
fields = [
'id', 'name', 'asset_count', 'gateway_count', 'comment', 'assets',
'date_created'
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'comment', 'date_created'
]
read_only_fields = ( 'asset_count', 'gateway_count', 'date_created')
fields_m2m = [
'asset_count', 'assets', 'gateway_count',
]
fields = fields_small + fields_m2m
read_only_fields = ('asset_count', 'gateway_count', 'date_created')
extra_kwargs = {
'assets': {'required': False}
}
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
@@ -41,6 +48,16 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'date_updated', 'created_by', 'comment',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.protocol_limit_to_ssh()
def protocol_limit_to_ssh(self):
protocol_field = self.fields['protocol']
choices = protocol_field.choices
choices.pop('rdp')
protocol_field._choices = choices
class GatewayWithAuthSerializer(GatewaySerializer):
def get_field_names(self, declared_fields, info):
@@ -51,6 +68,8 @@ class GatewayWithAuthSerializer(GatewaySerializer):
return fields
class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer):
gateways = GatewayWithAuthSerializer(many=True, read_only=True)

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ from ..models import Asset, Node
__all__ = [
'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer",
"NodeAssetsSerializer", "NodeTaskSerializer",
]
@@ -51,3 +51,12 @@ class NodeAssetsSerializer(BulkOrgResourceModelSerializer):
class NodeAddChildrenSerializer(serializers.Serializer):
nodes = serializers.ListField()
class NodeTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('refresh', 'refresh'),
('test', 'test'),
('refresh_cache', 'refresh_cache'),
)
task = serializers.CharField(read_only=True)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)

View File

@@ -1,20 +1,20 @@
import re
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from common.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.models import Node
from ..models import SystemUser
from .base import AuthSerializer, AuthSerializerMixin
from ..models import SystemUser, Asset
from .base import AuthSerializerMixin
__all__ = [
'SystemUserSerializer', 'SystemUserAuthSerializer',
'SystemUserSerializer', 'SystemUserListSerializer',
'SystemUserSimpleSerializer', 'SystemUserAssetRelationSerializer',
'SystemUserNodeRelationSerializer',
'SystemUserNodeRelationSerializer', 'SystemUserTaskSerializer',
'SystemUserUserRelationSerializer', 'SystemUserWithAuthInfoSerializer',
]
@@ -28,10 +28,13 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
model = SystemUser
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'username', 'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display', 'priority', 'protocol',
'id', 'name', 'username', 'protocol',
'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display',
'priority', 'username_same_with_user',
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
'assets_amount', 'nodes_amount', 'auto_generate_key'
'auto_generate_key', 'sftp_root',
'assets_amount', 'date_created', 'created_by'
]
extra_kwargs = {
'password': {"write_only": True},
@@ -67,17 +70,43 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
value = False
return value
def validate_username_same_with_user(self, username_same_with_user):
if not username_same_with_user:
return username_same_with_user
protocol = self.initial_data.get("protocol", "ssh")
queryset = SystemUser.objects.filter(
protocol=protocol, username_same_with_user=True
)
if self.instance:
queryset = queryset.exclude(id=self.instance.id)
exists = queryset.exists()
if not exists:
return username_same_with_user
error = _("Username same with user with protocol {} only allow 1").format(protocol)
raise serializers.ValidationError(error)
def validate_username(self, username):
if username:
return username
login_mode = self.initial_data.get("login_mode")
protocol = self.initial_data.get("protocol")
username_same_with_user = self.initial_data.get("username_same_with_user")
if username_same_with_user:
return ''
if login_mode == SystemUser.LOGIN_AUTO and \
protocol != SystemUser.PROTOCOL_VNC:
msg = _('* Automatic login mode must fill in the username.')
raise serializers.ValidationError(msg)
return username
def validate_sftp_root(self, value):
if value in ['home', 'tmp']:
return value
if not value.startswith('/'):
error = _("Path should starts with /")
raise serializers.ValidationError(error)
return value
def validate_password(self, password):
super().validate_password(password)
auto_gen_key = self.initial_data.get("auto_generate_key", False)
@@ -112,29 +141,42 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
attrs["public_key"] = public_key
return attrs
class SystemUserListSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
fields = [
'id', 'name', 'username', 'protocol',
'login_mode', 'login_mode_display',
'priority', "username_same_with_user",
'auto_push', 'sudo', 'shell', 'comment',
"assets_amount",
'auto_generate_key',
'sftp_root',
]
@classmethod
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related('cmd_filters', 'nodes')
queryset = queryset.annotate(assets_amount=Count("assets"))
return queryset
class SystemUserAuthSerializer(AuthSerializer):
"""
系统用户认证信息
"""
private_key = serializers.SerializerMethodField()
class Meta:
model = SystemUser
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
fields = [
"id", "name", "username", "protocol",
"login_mode", "password", "private_key",
'id', 'name', 'username', 'protocol',
'password', 'public_key', 'private_key',
'login_mode', 'login_mode_display',
'priority', 'username_same_with_user',
'auto_push', 'sudo', 'shell', 'comment',
'auto_generate_key', 'sftp_root',
]
@staticmethod
def get_private_key(obj):
return obj.get_private_key()
extra_kwargs = {
'nodes_amount': {'label': _('Node')},
'assets_amount': {'label': _('Asset')},
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
}
class SystemUserSimpleSerializer(serializers.ModelSerializer):
@@ -186,3 +228,25 @@ class SystemUserNodeRelationSerializer(RelationMixin, serializers.ModelSerialize
return self.tree.get_node_full_tag(obj.node_key)
else:
return obj.node.full_value
class SystemUserUserRelationSerializer(RelationMixin, serializers.ModelSerializer):
user_display = serializers.ReadOnlyField()
class Meta(RelationMixin.Meta):
model = SystemUser.users.through
fields = [
'id', "user", "user_display",
]
class SystemUserTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
("test", "test"),
("push", "push"),
)
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
asset = serializers.PrimaryKeyRelatedField(
queryset=Asset.objects, allow_null=True, required=False, write_only=True
)
task = serializers.CharField(read_only=True)

View File

@@ -7,13 +7,15 @@ from django.db.models.signals import (
from django.db.models.aggregates import Count
from django.dispatch import receiver
from common.utils import get_logger, timeit
from common.utils import get_logger
from common.decorator import on_transaction_commit
from orgs.utils import tmp_to_root_org
from .models import Asset, SystemUser, Node, AuthBook
from .utils import TreeService
from .tasks import (
update_assets_hardware_info_util,
test_asset_connectivity_util,
push_system_user_to_assets_manual,
push_system_user_to_assets,
add_nodes_assets_to_system_users
)
@@ -94,6 +96,25 @@ def on_system_user_assets_change(sender, instance=None, action='', model=None, p
push_system_user_to_assets.delay(system_user, assets)
@receiver(m2m_changed, sender=SystemUser.users.through)
def on_system_user_users_change(sender, instance=None, action='', model=None, pk_set=None, **kwargs):
"""
当系统用户和用户关系发生变化时,应该重新推送系统用户资产中
"""
if action != "post_add":
return
if not instance.username_same_with_user:
return
logger.debug("System user users change signal recv: {}".format(instance))
queryset = model.objects.filter(pk__in=pk_set)
if model == SystemUser:
system_users = queryset
else:
system_users = [instance]
for s in system_users:
push_system_user_to_assets_manual.delay(s)
@receiver(m2m_changed, sender=SystemUser.nodes.through)
def on_system_user_nodes_change(sender, instance=None, action=None, model=None, pk_set=None, **kwargs):
"""
@@ -113,6 +134,20 @@ def on_system_user_nodes_change(sender, instance=None, action=None, model=None,
add_nodes_assets_to_system_users.delay(nodes_keys, system_users)
@receiver(m2m_changed, sender=SystemUser.groups.through)
def on_system_user_groups_change(sender, instance=None, action=None, model=None,
pk_set=None, reverse=False, **kwargs):
"""
当系统用户和用户组关系发生变化时,应该将组下用户关联到新的系统用户上
"""
if action != "post_add" or reverse:
return
logger.info("System user groups update signal recv: {}".format(instance))
groups = model.objects.filter(pk__in=pk_set).annotate(users_count=Count("users"))
users = groups.filter(users_count__gt=0).values_list('users', flat=True)
instance.users.add(*tuple(users))
@receiver(m2m_changed, sender=Asset.nodes.through)
def on_asset_nodes_change(sender, instance=None, action='', **kwargs):
"""
@@ -121,6 +156,8 @@ def on_asset_nodes_change(sender, instance=None, action='', **kwargs):
if action.startswith('post'):
logger.debug("Asset nodes change signal recv: {}".format(instance))
Node.refresh_assets()
with tmp_to_root_org():
Node.refresh_assets()
@receiver(m2m_changed, sender=Asset.nodes.through)
@@ -148,7 +185,9 @@ def on_asset_nodes_add(sender, instance=None, action='', model=None, pk_set=None
system_users_assets = defaultdict(set)
for system_user in system_users:
system_users_assets[system_user].update(set(assets))
assets_has_set = system_user.assets.all().filter(id__in=assets).values_list('id', flat=True)
assets_remain = set(assets) - set(assets_has_set)
system_users_assets[system_user].update(assets_remain)
for system_user, _assets in system_users_assets.items():
system_user.assets.add(*tuple(_assets))
@@ -195,9 +234,5 @@ def on_asset_nodes_remove(sender, instance=None, action='', model=None,
def on_node_update_or_created(sender, **kwargs):
# 刷新节点
Node.refresh_nodes()
@receiver(post_save, sender=AuthBook)
def on_authbook_created(sender, instance=None, created=True, **kwargs):
if created and instance:
instance.set_version()
with tmp_to_root_org():
Node.refresh_nodes()

View File

@@ -4,11 +4,12 @@ from celery import shared_task
from django.utils.translation import ugettext as _
from django.core.cache import cache
from orgs.utils import tmp_to_root_org, org_aware_func
from common.utils import get_logger
from ops.celery.decorator import register_as_period_task
from ..models import AdminUser
from .utils import clean_hosts
from .utils import clean_ansible_task_hosts
from .asset_connectivity import test_asset_connectivity_util
from . import const
@@ -20,7 +21,7 @@ __all__ = [
]
@shared_task(queue="ansible")
@org_aware_func("admin_user")
def test_admin_user_connectivity_util(admin_user, task_name):
"""
Test asset admin user can connect or not. Using ansible api do that
@@ -29,7 +30,7 @@ def test_admin_user_connectivity_util(admin_user, task_name):
:return:
"""
assets = admin_user.get_related_assets()
hosts = clean_hosts(assets)
hosts = clean_ansible_task_hosts(assets)
if not hosts:
return {}
summary = test_asset_connectivity_util(hosts, task_name)
@@ -51,10 +52,13 @@ def test_admin_user_connectivity_period():
logger.debug("Test admin user connectivity, less than 40 minutes, skip")
return
cache.set(key, 1, 60*40)
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
task_name = _("Test admin user connectivity period: {}").format(admin_user.name)
test_admin_user_connectivity_util(admin_user, task_name)
with tmp_to_root_org():
admin_users = AdminUser.objects.all()
for admin_user in admin_users:
task_name = _("Test admin user connectivity period: {}").format(
admin_user.name
)
test_admin_user_connectivity_util(admin_user, task_name)
cache.set(key, 1, 60*40)

View File

@@ -1,55 +1,55 @@
# ~*~ coding: utf-8 ~*~
from itertools import groupby
from collections import defaultdict
from celery import shared_task
from django.utils.translation import ugettext as _
from common.utils import get_logger
from orgs.utils import org_aware_func
from ..models.utils import Connectivity
from . import const
from .utils import clean_hosts
from .utils import clean_ansible_task_hosts, group_asset_by_platform
logger = get_logger(__file__)
__all__ = ['test_asset_connectivity_util', 'test_asset_connectivity_manual']
__all__ = [
'test_asset_connectivity_util', 'test_asset_connectivity_manual',
'test_node_assets_connectivity_manual',
]
@shared_task(queue="ansible")
@org_aware_func("assets")
def test_asset_connectivity_util(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = _("Test assets connectivity")
hosts = clean_hosts(assets)
hosts = clean_ansible_task_hosts(assets)
if not hosts:
return {}
platform_hosts_map = {}
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
for i in platform_hosts:
platform_hosts_map[i[0]] = list(i[1])
hosts_category = {
'linux': {
'hosts': [],
'tasks': const.TEST_ADMIN_USER_CONN_TASKS
},
'windows': {
'hosts': [],
'tasks': const.TEST_WINDOWS_ADMIN_USER_CONN_TASKS
}
platform_tasks_map = {
"unixlike": const.PING_UNIXLIKE_TASKS,
"windows": const.PING_WINDOWS_TASKS
}
for host in hosts:
hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \
else hosts_category['linux']['hosts']
hosts_list.append(host)
results_summary = dict(
contacted=defaultdict(dict), dark=defaultdict(dict), success=True
)
created_by = assets[0].org_id
for k, value in hosts_category.items():
if not value['hosts']:
for platform, _hosts in platform_hosts_map.items():
if not _hosts:
continue
logger.debug("System user not has special auth")
tasks = platform_tasks_map.get(platform)
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=value['hosts'], tasks=value['tasks'],
task_name=task_name, hosts=_hosts, tasks=tasks,
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
created_by=created_by,
)
raw, summary = task.run()
success = summary.get('success', False)
@@ -59,6 +59,7 @@ def test_asset_connectivity_util(assets, task_name=None):
results_summary['success'] &= success
results_summary['contacted'].update(contacted)
results_summary['dark'].update(dark)
continue
for asset in assets:
if asset.hostname in results_summary.get('dark', {}).keys():
@@ -79,3 +80,12 @@ def test_asset_connectivity_manual(asset):
return False, summary['dark']
else:
return True, ""
@shared_task(queue="ansible")
def test_node_assets_connectivity_manual(node):
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
assets = node.get_all_assets()
result = test_asset_connectivity_util(assets, task_name=task_name)
return result

View File

@@ -3,7 +3,9 @@
from celery import shared_task
from django.utils.translation import ugettext as _
from common.utils import get_logger
from common.utils import get_logger, get_object_or_none
from orgs.utils import org_aware_func
from ..models import Asset
from . import const
from .utils import check_asset_can_run_ansible
@@ -13,15 +15,16 @@ logger = get_logger(__file__)
__all__ = [
'test_asset_user_connectivity_util', 'test_asset_users_connectivity_manual',
'get_test_asset_user_connectivity_tasks',
'get_test_asset_user_connectivity_tasks', 'test_user_connectivity',
'run_adhoc',
]
def get_test_asset_user_connectivity_tasks(asset):
if asset.is_unixlike():
tasks = const.TEST_ASSET_USER_CONN_TASKS
tasks = const.PING_UNIXLIKE_TASKS
elif asset.is_windows():
tasks = const.TEST_WINDOWS_ASSET_USER_CONN_TASKS
tasks = const.PING_WINDOWS_TASKS
else:
msg = _(
"The asset {} system platform {} does not "
@@ -32,46 +35,98 @@ def get_test_asset_user_connectivity_tasks(asset):
return tasks
@shared_task(queue="ansible")
def test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=False):
def run_adhoc(task_name, tasks, inventory):
"""
:param task_name
:param tasks
:param inventory
"""
from ops.ansible.runner import AdHocRunner
runner = AdHocRunner(inventory, options=const.TASK_OPTIONS)
result = runner.run(tasks, 'all', task_name)
return result.results_raw, result.results_summary
def test_user_connectivity(task_name, asset, username, password=None, private_key=None):
"""
:param task_name
:param asset
:param username
:param password
:param private_key
"""
from ops.inventory import JMSCustomInventory
tasks = get_test_asset_user_connectivity_tasks(asset)
if not tasks:
logger.debug("No tasks ")
return {}, {}
inventory = JMSCustomInventory(
assets=[asset], username=username, password=password,
private_key=private_key
)
raw, summary = run_adhoc(
task_name=task_name, tasks=tasks, inventory=inventory
)
return raw, summary
@org_aware_func("asset_user")
def test_asset_user_connectivity_util(asset_user, task_name):
"""
:param asset_user: <AuthBook>对象
:param task_name:
:param run_as_admin:
:return:
"""
from ops.utils import update_or_create_ansible_task
if not check_asset_can_run_ansible(asset_user.asset):
return
tasks = get_test_asset_user_connectivity_tasks(asset_user.asset)
if not tasks:
logger.debug("No tasks ")
try:
raw, summary = test_user_connectivity(
task_name=task_name, asset=asset_user.asset,
username=asset_user.username, password=asset_user.password,
private_key=asset_user.private_key_file
)
except Exception as e:
logger.warn("Failed run adhoc {}, {}".format(task_name, e))
return
args = (task_name,)
kwargs = {
'hosts': [asset_user.asset], 'tasks': tasks,
'pattern': 'all', 'options': const.TASK_OPTIONS,
'created_by': asset_user.org_id,
}
if run_as_admin:
kwargs["run_as_admin"] = True
else:
kwargs["run_as"] = asset_user.username
task, created = update_or_create_ansible_task(*args, **kwargs)
raw, summary = task.run()
asset_user.set_connectivity(summary)
@shared_task(queue="ansible")
def test_asset_users_connectivity_manual(asset_users, run_as_admin=False):
def test_asset_users_connectivity_manual(asset_users):
"""
:param asset_users: <AuthBook>对象
"""
for asset_user in asset_users:
task_name = _("Test asset user connectivity: {}").format(asset_user)
test_asset_user_connectivity_util(asset_user, task_name, run_as_admin=run_as_admin)
test_asset_user_connectivity_util(asset_user, task_name)
@shared_task(queue="ansible")
def push_asset_user_util(asset_user):
"""
:param asset_user: <Asset user>对象
"""
from .push_system_user import push_system_user_util
if not asset_user.backend.startswith('system_user'):
logger.error("Asset user is not from system user")
return
union_id = asset_user.union_id
union_id_list = union_id.split('_')
if len(union_id_list) < 2:
logger.error("Asset user union id length less than 2")
return
system_user_id = union_id_list[0]
asset_id = union_id_list[1]
asset = get_object_or_none(Asset, pk=asset_id)
system_user = None
if not asset:
return
hosts = check_asset_can_run_ansible([asset])
if asset.is_unixlike:
pass

View File

@@ -18,27 +18,10 @@ UPDATE_ASSETS_HARDWARE_TASKS = [
}
]
TEST_ADMIN_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "ping",
}
}
]
TEST_WINDOWS_ADMIN_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "win_ping",
}
}
]
ASSET_ADMIN_CONN_CACHE_KEY = "ASSET_ADMIN_USER_CONN_{}"
SYSTEM_USER_CONN_CACHE_KEY = "SYSTEM_USER_CONN_{}"
TEST_SYSTEM_USER_CONN_TASKS = [
PING_UNIXLIKE_TASKS = [
{
"name": "ping",
"action": {
@@ -46,7 +29,7 @@ TEST_SYSTEM_USER_CONN_TASKS = [
}
}
]
TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [
PING_WINDOWS_TASKS = [
{
"name": "ping",
"action": {
@@ -55,24 +38,6 @@ TEST_WINDOWS_SYSTEM_USER_CONN_TASKS = [
}
]
TEST_ASSET_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "ping",
}
}
]
TEST_WINDOWS_ASSET_USER_CONN_TASKS = [
{
"name": "ping",
"action": {
"module": "win_ping",
}
}
]
TASK_OPTIONS = {
'timeout': 10,
'forks': 10,
@@ -98,7 +63,9 @@ GATHER_ASSET_USERS_TASKS = [
"name": "get last login",
"action": {
"module": "shell",
"args": "users=$(getent passwd | grep -v 'nologin' | grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -F $i -1 | head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
"args": "users=$(getent passwd | grep -v 'nologin' | "
"grep -v 'shudown' | awk -F: '{ print $1 }');for i in $users;do last -w -F $i -1 | "
"head -1 | grep -v '^$' | awk '{ print $1\"@\"$3\"@\"$5,$6,$7,$8 }';done"
}
}
]

View File

@@ -9,15 +9,16 @@ from django.utils.translation import ugettext as _
from common.utils import (
capacity_convert, sum_capacity, get_logger
)
from orgs.utils import org_aware_func
from . import const
from .utils import clean_hosts
from .utils import clean_ansible_task_hosts
logger = get_logger(__file__)
disk_pattern = re.compile(r'^hd|sd|xvd|vd|nv')
__all__ = [
'update_assets_hardware_info_util', 'update_asset_hardware_info_manual',
'update_assets_hardware_info_period',
'update_assets_hardware_info_period', 'update_node_assets_hardware_info_manual',
]
@@ -82,6 +83,7 @@ def set_assets_hardware_info(assets, result, **kwargs):
@shared_task
@org_aware_func("assets")
def update_assets_hardware_info_util(assets, task_name=None):
"""
Using ansible api to update asset hardware info
@@ -93,13 +95,13 @@ def update_assets_hardware_info_util(assets, task_name=None):
if task_name is None:
task_name = _("Update some assets hardware info")
tasks = const.UPDATE_ASSETS_HARDWARE_TASKS
hosts = clean_hosts(assets)
hosts = clean_ansible_task_hosts(assets)
if not hosts:
return {}
created_by = str(assets[0].org_id)
task, created = update_or_create_ansible_task(
task_name, hosts=hosts, tasks=tasks, created_by=created_by,
pattern='all', options=const.TASK_OPTIONS, run_as_admin=True,
task_name, hosts=hosts, tasks=tasks,
pattern='all', options=const.TASK_OPTIONS,
run_as_admin=True,
)
result = task.run()
set_assets_hardware_info(assets, result)
@@ -109,9 +111,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task(queue="ansible")
def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info: {}").format(asset.hostname)
update_assets_hardware_info_util(
[asset], task_name=task_name
)
update_assets_hardware_info_util([asset], task_name=task_name)
@shared_task(queue="ansible")
@@ -123,3 +123,11 @@ def update_assets_hardware_info_period():
if not const.PERIOD_TASK_ENABLED:
logger.debug("Period task disabled, update assets hardware info pass")
return
@shared_task(queue="ansible")
def update_node_assets_hardware_info_manual(node):
task_name = _("Update node asset hardware information: {}").format(node.name)
assets = node.get_all_assets()
result = update_assets_hardware_info_util.delay(assets, task_name=task_name)
return result

View File

@@ -7,10 +7,10 @@ from celery import shared_task
from django.utils.translation import ugettext as _
from django.utils import timezone
from orgs.utils import tmp_to_org
from orgs.utils import tmp_to_org, org_aware_func
from common.utils import get_logger
from ..models import GatheredUser, Node
from .utils import clean_hosts
from .utils import clean_ansible_task_hosts
from . import const
__all__ = ['gather_asset_users', 'gather_nodes_asset_users']
@@ -101,11 +101,12 @@ def add_asset_users(assets, results):
@shared_task(queue="ansible")
@org_aware_func("assets")
def gather_asset_users(assets, task_name=None):
from ops.utils import update_or_create_ansible_task
if task_name is None:
task_name = _("Gather assets users")
assets = clean_hosts(assets)
assets = clean_ansible_task_hosts(assets)
if not assets:
return
hosts_category = {
@@ -131,7 +132,7 @@ def gather_asset_users(assets, task_name=None):
task, created = update_or_create_ansible_task(
task_name=_task_name, hosts=value['hosts'], tasks=value['tasks'],
pattern='all', options=const.TASK_OPTIONS,
run_as_admin=True, created_by=value['hosts'][0].org_id,
run_as_admin=True,
)
raw, summary = task.run()
results[k].update(raw['ok'])

View File

@@ -1,11 +1,13 @@
# ~*~ coding: utf-8 ~*~
from itertools import groupby
from celery import shared_task
from django.utils.translation import ugettext as _
from common.utils import encrypt_password, get_logger
from orgs.utils import tmp_to_org, org_aware_func
from . import const
from .utils import clean_hosts_by_protocol, clean_hosts
from .utils import clean_ansible_task_hosts, group_asset_by_platform
logger = get_logger(__file__)
@@ -15,31 +17,34 @@ __all__ = [
]
def get_push_linux_system_user_tasks(system_user):
def get_push_unixlike_system_user_tasks(system_user, username=None):
if username is None:
username = system_user.username
password = system_user.password
public_key = system_user.public_key
tasks = [
{
'name': 'Add user {}'.format(system_user.username),
'name': 'Add user {}'.format(username),
'action': {
'module': 'user',
'args': 'name={} shell={} state=present'.format(
system_user.username, system_user.shell,
username, system_user.shell or '/bin/bash',
),
}
},
{
'name': 'Add group {}'.format(system_user.username),
'name': 'Add group {}'.format(username),
'action': {
'module': 'group',
'args': 'name={} state=present'.format(
system_user.username,
),
'args': 'name={} state=present'.format(username),
}
},
{
'name': 'Check home dir exists',
'action': {
'module': 'stat',
'args': 'path=/home/{}'.format(system_user.username)
'args': 'path=/home/{}'.format(username)
},
'register': 'home_existed'
},
@@ -47,29 +52,29 @@ def get_push_linux_system_user_tasks(system_user):
'name': "Set home dir permission",
'action': {
'module': 'file',
'args': "path=/home/{0} owner={0} group={0} mode=700".format(system_user.username)
'args': "path=/home/{0} owner={0} group={0} mode=700".format(username)
},
'when': 'home_existed.stat.exists == true'
}
]
if system_user.password:
if password:
tasks.append({
'name': 'Set {} password'.format(system_user.username),
'name': 'Set {} password'.format(username),
'action': {
'module': 'user',
'args': 'name={} shell={} state=present password={}'.format(
system_user.username, system_user.shell,
encrypt_password(system_user.password, salt="K3mIlKK"),
username, system_user.shell,
encrypt_password(password, salt="K3mIlKK"),
),
}
})
if system_user.public_key:
if public_key:
tasks.append({
'name': 'Set {} authorized key'.format(system_user.username),
'name': 'Set {} authorized key'.format(username),
'action': {
'module': 'authorized_key',
'args': "user={} state=present key='{}'".format(
system_user.username, system_user.public_key
username, public_key
)
}
})
@@ -81,26 +86,27 @@ def get_push_linux_system_user_tasks(system_user):
sudo_tmp.append(s.strip(','))
sudo = ','.join(sudo_tmp)
tasks.append({
'name': 'Set {} sudo setting'.format(system_user.username),
'name': 'Set {} sudo setting'.format(username),
'action': {
'module': 'lineinfile',
'args': "dest=/etc/sudoers state=present regexp='^{0} ALL=' "
"line='{0} ALL=(ALL) NOPASSWD: {1}' "
"validate='visudo -cf %s'".format(
system_user.username, sudo,
)
"validate='visudo -cf %s'".format(username, sudo)
}
})
return tasks
def get_push_windows_system_user_tasks(system_user):
def get_push_windows_system_user_tasks(system_user, username=None):
if username is None:
username = system_user.username
password = system_user.password
tasks = []
if not system_user.password:
if not password:
return tasks
tasks.append({
'name': 'Add user {}'.format(system_user.username),
task = {
'name': 'Add user {}'.format(username),
'action': {
'module': 'win_user',
'args': 'fullname={} '
@@ -112,84 +118,100 @@ def get_push_windows_system_user_tasks(system_user):
'password_never_expires=yes '
'groups="Users,Remote Desktop Users" '
'groups_action=add '
''.format(system_user.name,
system_user.username,
system_user.password),
''.format(username, username, password),
}
})
}
tasks.append(task)
return tasks
def get_push_system_user_tasks(host, system_user):
if host.is_unixlike():
tasks = get_push_linux_system_user_tasks(system_user)
elif host.is_windows():
tasks = get_push_windows_system_user_tasks(system_user)
else:
msg = _(
"The asset {} system platform {} does not "
"support run Ansible tasks".format(host.hostname, host.platform)
)
logger.info(msg)
tasks = []
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
"""
:param system_user:
:param platform:
:param username: 当动态时,近推送某个
:return:
"""
get_task_map = {
"unixlike": get_push_unixlike_system_user_tasks,
"windows": get_push_windows_system_user_tasks,
}
get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
if not system_user.username_same_with_user:
return get_tasks(system_user)
tasks = []
# 仅推送这个username
if username is not None:
tasks.extend(get_tasks(system_user, username))
return tasks
users = system_user.users.all().values_list('username', flat=True)
print(_("System user is dynamic: {}").format(list(users)))
for _username in users:
tasks.extend(get_tasks(system_user, _username))
return tasks
@shared_task(queue="ansible")
def push_system_user_util(system_user, assets, task_name):
@org_aware_func("system_user")
def push_system_user_util(system_user, assets, task_name, username=None):
from ops.utils import update_or_create_ansible_task
if not system_user.is_need_push():
msg = _("Push system user task skip, auto push not enable or "
"protocol is not ssh or rdp: {}").format(system_user.name)
logger.info(msg)
return {}
# Set root as system user is dangerous
if system_user.username.lower() in ["root", "administrator"]:
msg = _("For security, do not push user {}".format(system_user.username))
logger.info(msg)
return {}
hosts = clean_hosts(assets)
hosts = clean_ansible_task_hosts(assets, system_user=system_user)
if not hosts:
return {}
hosts = clean_hosts_by_protocol(system_user, hosts)
if not hosts:
return {}
platform_hosts_map = {}
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
for i in platform_hosts:
platform_hosts_map[i[0]] = list(i[1])
for host in hosts:
system_user.load_specific_asset_auth(host)
tasks = get_push_system_user_tasks(host, system_user)
if not tasks:
continue
def run_task(_tasks, _hosts):
if not _tasks:
return
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=[host], tasks=tasks, pattern='all',
task_name=task_name, hosts=_hosts, tasks=_tasks, pattern='all',
options=const.TASK_OPTIONS, run_as_admin=True,
created_by=system_user.org_id,
)
task.run()
for platform, _hosts in platform_hosts_map.items():
if not _hosts:
continue
print(_("Start push system user for platform: [{}]").format(platform))
print(_("Hosts count: {}").format(len(_hosts)))
if not system_user.has_special_auth():
logger.debug("System user not has special auth")
tasks = get_push_system_user_tasks(system_user, platform, username=username)
run_task(tasks, _hosts)
continue
for _host in _hosts:
system_user.load_asset_special_auth(_host)
tasks = get_push_system_user_tasks(system_user, platform, username=username)
run_task(tasks, [_host])
@shared_task(queue="ansible")
def push_system_user_to_assets_manual(system_user):
assets = system_user.get_all_assets()
def push_system_user_to_assets_manual(system_user, username=None):
assets = system_user.get_related_assets()
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name=task_name)
return push_system_user_util(system_user, assets, task_name=task_name, username=username)
@shared_task(queue="ansible")
def push_system_user_a_asset_manual(system_user, asset):
task_name = _("Push system users to asset: {} => {}").format(
system_user.name, asset
def push_system_user_a_asset_manual(system_user, asset, username=None):
if username is None:
username = system_user.username
task_name = _("Push system users to asset: {}({}) => {}").format(
system_user.name, username, asset
)
return push_system_user_util(system_user, [asset], task_name=task_name)
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
@shared_task(queue="ansible")
def push_system_user_to_assets(system_user, assets):
def push_system_user_to_assets(system_user, assets, username=None):
task_name = _("Push system users to assets: {}").format(system_user.name)
return push_system_user_util(system_user, assets, task_name)
return push_system_user_util(system_user, assets, task_name, username=username)
@@ -199,4 +221,4 @@ def push_system_user_to_assets(system_user, assets):
# @after_app_shutdown_clean_periodic
# def push_system_user_period():
# for system_user in SystemUser.objects.all():
# push_system_user_related_nodes(system_user)
# push_system_user_related_nodes(system_user)

View File

@@ -1,13 +1,17 @@
from itertools import groupby
from collections import defaultdict
from celery import shared_task
from django.utils.translation import ugettext as _
from common.utils import get_logger
from orgs.utils import tmp_to_org, org_aware_func
from ..models import SystemUser
from . import const
from .utils import clean_hosts, clean_hosts_by_protocol
from .utils import (
clean_ansible_task_hosts, group_asset_by_platform
)
logger = get_logger(__name__)
__all__ = [
@@ -16,7 +20,7 @@ __all__ = [
]
@shared_task(queue="ansible")
@org_aware_func("system_user")
def test_system_user_connectivity_util(system_user, assets, task_name):
"""
Test system cant connect his assets or not.
@@ -27,41 +31,37 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
"""
from ops.utils import update_or_create_ansible_task
hosts = clean_hosts(assets)
# hosts = clean_ansible_task_hosts(assets, system_user=system_user)
# TODO: 这里不传递系统用户因为clean_ansible_task_hosts会通过system_user来判断是否可以推送
# 不符合测试可连接性逻辑, 后面需要优化此逻辑
hosts = clean_ansible_task_hosts(assets)
if not hosts:
return {}
platform_hosts_map = {}
hosts_sorted = sorted(hosts, key=group_asset_by_platform)
platform_hosts = groupby(hosts_sorted, key=group_asset_by_platform)
for i in platform_hosts:
platform_hosts_map[i[0]] = list(i[1])
hosts = clean_hosts_by_protocol(system_user, hosts)
if not hosts:
return {}
hosts_category = {
'linux': {
'hosts': [],
'tasks': const.TEST_SYSTEM_USER_CONN_TASKS
},
'windows': {
'hosts': [],
'tasks': const.TEST_WINDOWS_SYSTEM_USER_CONN_TASKS
}
platform_tasks_map = {
"unixlike": const.PING_UNIXLIKE_TASKS,
"windows": const.PING_WINDOWS_TASKS
}
for host in hosts:
hosts_list = hosts_category['windows']['hosts'] if host.is_windows() \
else hosts_category['linux']['hosts']
hosts_list.append(host)
results_summary = dict(
contacted=defaultdict(dict), dark=defaultdict(dict), success=True
)
for k, value in hosts_category.items():
if not value['hosts']:
continue
task, created = update_or_create_ansible_task(
task_name=task_name, hosts=value['hosts'], tasks=value['tasks'],
def run_task(_tasks, _hosts, _username):
old_name = "{}".format(system_user)
new_name = "{}({})".format(system_user.name, _username)
_task_name = task_name.replace(old_name, new_name)
_task, created = update_or_create_ansible_task(
task_name=_task_name, hosts=_hosts, tasks=_tasks,
pattern='all', options=const.TASK_OPTIONS,
run_as=system_user.username, created_by=system_user.org_id,
run_as=_username,
)
raw, summary = task.run()
raw, summary = _task.run()
success = summary.get('success', False)
contacted = summary.get('contacted', {})
dark = summary.get('dark', {})
@@ -70,23 +70,45 @@ def test_system_user_connectivity_util(system_user, assets, task_name):
results_summary['contacted'].update(contacted)
results_summary['dark'].update(dark)
for platform, _hosts in platform_hosts_map.items():
if not _hosts:
continue
if platform not in ["unixlike", "windows"]:
continue
tasks = platform_tasks_map[platform]
print(_("Start test system user connectivity for platform: [{}]").format(platform))
print(_("Hosts count: {}").format(len(_hosts)))
# 用户名不是动态的,用户名则是一个
if not system_user.username_same_with_user:
logger.debug("System user not has special auth")
run_task(tasks, _hosts, system_user.username)
# 否则需要多个任务
else:
users = system_user.users.all().values_list('username', flat=True)
print(_("System user is dynamic: {}").format(list(users)))
for username in users:
run_task(tasks, _hosts, username)
system_user.set_connectivity(results_summary)
return results_summary
@shared_task(queue="ansible")
@org_aware_func("system_user")
def test_system_user_connectivity_manual(system_user):
task_name = _("Test system user connectivity: {}").format(system_user)
assets = system_user.get_all_assets()
return test_system_user_connectivity_util(system_user, assets, task_name)
assets = system_user.get_related_assets()
test_system_user_connectivity_util(system_user, assets, task_name)
@shared_task(queue="ansible")
@org_aware_func("system_user")
def test_system_user_connectivity_a_asset(system_user, asset):
task_name = _("Test system user connectivity: {} => {}").format(
system_user, asset
)
return test_system_user_connectivity_util(system_user, [asset], task_name)
test_system_user_connectivity_util(system_user, [asset], task_name)
@shared_task(queue="ansible")
@@ -94,8 +116,9 @@ def test_system_user_connectivity_period():
if not const.PERIOD_TASK_ENABLED:
logger.debug("Period task disabled, test system user connectivity pass")
return
system_users = SystemUser.objects.all()
for system_user in system_users:
queryset_map = SystemUser.objects.all_group_by_org()
for org, system_user in queryset_map.items():
task_name = _("Test system user connectivity period: {}").format(system_user)
assets = system_user.get_all_assets()
test_system_user_connectivity_util(system_user, assets, task_name)
with tmp_to_org(org):
assets = system_user.get_related_assets()
test_system_user_connectivity_util(system_user, assets, task_name)

View File

@@ -7,7 +7,8 @@ from common.utils import get_logger
logger = get_logger(__file__)
__all__ = [
'check_asset_can_run_ansible', 'clean_hosts', 'clean_hosts_by_protocol'
'check_asset_can_run_ansible', 'clean_ansible_task_hosts',
'group_asset_by_platform',
]
@@ -23,23 +24,43 @@ def check_asset_can_run_ansible(asset):
return True
def clean_hosts(assets):
clean_assets = []
def check_system_user_can_run_ansible(system_user):
if not system_user.is_need_push():
msg = _("Push system user task skip, auto push not enable or "
"protocol is not ssh or rdp: {}").format(system_user.name)
logger.info(msg)
return False
# Push root as system user is dangerous
if system_user.username.lower() in ["root", "administrator"]:
msg = _("For security, do not push user {}".format(system_user.username))
logger.info(msg)
return False
# if system_user.protocol != "ssh":
# msg = _("System user protocol not ssh: {}".format(system_user))
# logger.info(msg)
# return False
return True
def clean_ansible_task_hosts(assets, system_user=None):
if system_user and not check_system_user_can_run_ansible(system_user):
return []
cleaned_assets = []
for asset in assets:
if not check_asset_can_run_ansible(asset):
continue
clean_assets.append(asset)
if not clean_assets:
cleaned_assets.append(asset)
if not cleaned_assets:
logger.info(_("No assets matched, stop task"))
return clean_assets
return cleaned_assets
def clean_hosts_by_protocol(system_user, assets):
hosts = [
asset for asset in assets
if asset.has_protocol(system_user.protocol)
]
if not hosts:
msg = _("No assets matched related system user protocol, stop task")
logger.info(msg)
return hosts
def group_asset_by_platform(asset):
if asset.is_unixlike():
return 'unixlike'
elif asset.is_windows():
return 'windows'
else:
return 'other'

View File

@@ -1,41 +0,0 @@
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}asset_group_bulk_update_modal{% endblock %}
{% block modal_class %}modal-lg{% endblock %}
{% block modal_title%}{% trans "Update asset group" %}{% endblock %}
{% block modal_body %}
{% load bootstrap3 %}
<p class="text-success text-center">{% trans "Hint: only change the field you want to update." %}</p>
<form method="post" class="form-horizontal" action="" id="fm_asset_group_bulk_update">
<div class="form-group">
<label for="assets" class="col-sm-2 control-label">{% trans 'Assets' %}</label>
<div class="col-sm-9" id="select2-container">
<select name="assets" id="select2_groups" data-placeholder="{% trans 'Select Asset' %}" class="select2 form-control m-b" multiple>
{% for asset in assets %}
<option value="{{ asset.id }}">{{ asset.ip }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label for="system_users" class="col-sm-2 control-label">{% trans 'System users' %}</label>
<div class="col-sm-9" id="select2-container">
<select name="system_users" id="select2_groups" data-placeholder="{% trans 'Select System Users' %}" class="select2 form-control m-b" multiple>
{% for system_user in system_users %}
<option value="{{ system_user.id }}">{{ system_user.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-9 col-lg-9 col-sm-offset-2">
<div class="checkbox checkbox-success">
<input type="checkbox" name="enable_mfa" checked id="id_enable_mfa"><label for="id_enable_mfa">{% trans 'Enable-MFA' %}</label>
</div>
</div>
</div>
</form>
{% endblock %}
{% block modal_confirm_id %}btn_asset_group_bulk_update{% endblock %}

View File

@@ -1,224 +0,0 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
{% block modal_class %}modal-lg{% endblock %}
{% block modal_id %}asset_list_modal{% endblock %}
{% block modal_title%}{% trans "Asset list" %}{% endblock %}
{% block modal_body %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.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>
.inmodal .modal-header {
padding: 10px 10px;
text-align: center;
}
#asset_modal_tree.ztree * {
background-color: white;
}
#asset_modal_tree.ztree {
background-color: white;
}
</style>
<div class="wrapper wrapper-content">
<div class="row">
<div class="col-sm-3" id="split-left" style="padding-left: 3px;overflow: auto;max-height: 500px">
<div class="ibox float-e-margins">
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager ">
<div id="asset_modal_tree" class="ztree">
{% trans 'Loading' %} ...
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div class="col-sm-9 animated fadeInRight" id="split-right">
<div class="mail-box-header">
<table class="table table-striped table-bordered table-hover " id="asset_list_modal_table" style="width: 100%">
<thead>
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Hostname' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
function syncTableSelectedAssetToSelect2(table) {
var assets = table.selected;
var options = [];
var select2Id = assetModalOption.select2Id;
$(select2Id + ' option').each(function (i, v) {
options.push(v.value)
});
table.selected_rows.forEach(function (i) {
var name = i.hostname + '(' + i.ip + ')';
var option = new Option(name, i.id, false, true);
if (options.indexOf(i.id) === -1) {
$(select2Id).append(option).trigger('change');
}
});
$(select2Id).val(assets).trigger('change');
}
// 解决input框中的资产和弹出表格中资产的显示不一致
function syncSelectedAssetsToModalTable(assetModalTable) {
var select2Id = assetModalOption.select2Id;
var inputAssets = $(select2Id).val();
var selectedAssets = assetModalTable.selected.concat();
// input assets无table assets选中则取消勾选(再次click)
if (selectedAssets.length !== 0) {
$.each(selectedAssets, function (index, assetId) {
if ($.inArray(assetId, inputAssets) === -1) {
$('#' + assetId).trigger('click'); // 取消勾选
}
});
}
// input assets有table assets没选则选中(click)
if (inputAssets) {
assetModalTable.selected = inputAssets;
$.each(inputAssets, function (index, assetId) {
var dom = document.getElementById(assetId);
if (dom !== null) {
var selected = dom.parentElement.parentElement.className.indexOf('selected')
}
if (selected === -1) {
$('#' + assetId).trigger('click');
}
});
}
}
defaultOnAssetModalConfirm = syncTableSelectedAssetToSelect2;
defaultOnModalTableDone = syncSelectedAssetsToModalTable;
var assetModalOption = {
selectStyle: 'multi',
select2Id: '#id_assets',
onModalTableDone: defaultOnModalTableDone,
onModalTreeDone: null,
onModalConfirm: defaultOnAssetModalConfirm,
};
var assetModalTable, assetModalTree = null;
function initAssetModalTable() {
if(assetModalTable){
return
}
if (assetModalOption.selectStyle === 'single') {
$('.ipt_check_all').addClass('hidden')
}
var options = {
ele: $('#asset_list_modal_table'),
ajax_url: '{% url "api-assets:asset-list" %}?show_current_asset=1',
columns: [
{data: "id"}, {data: "hostname" }, {data: "ip" }
],
lengthMenu: [[10, 25, 50], [10, 25, 50]],
pageLength: 10,
select_style: assetModalOption.selectStyle,
paging_numbers_length: 3
};
assetModalTable = jumpserver.initServerSideDataTable(options);
if (assetModalOption.onModalTableDone) {
assetModalOption.onModalTableDone(assetModalTable);
}
return assetModalTable
}
function onModalTreeNodeSelected(event, treeNode) {
var url = assetModalTable.ajax.url();
url = setUrlParam(url, "node_id", treeNode.meta.node.id);
url = setUrlParam(url, "show_current_asset", "");
assetModalTable.ajax.url(url);
assetModalTable.ajax.reload();
}
function initModalTree() {
var url = '{% url 'api-assets:node-children-tree' %}?assets=0';
var setting = {
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
async: {
enable: true,
url: url,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
callback: {
onSelected: onModalTreeNodeSelected
}
};
$.get(url, function(data, status){
$.fn.zTree.init($("#asset_modal_tree"), setting);
assetModalTree = $.fn.zTree.getZTreeObj("assetTree2");
if (assetModalOption.onModalTreeDone) {
assetModalOption.onModalTreeDone(assetModalTree);
}
return assetModalTree;
});
}
function setAssetModalOptions(options) {
assetModalOption = options;
}
function initAssetTreeModel(selector) {
$(selector).parent().find(".select2-selection").on('click', function (e) {
if ($(e.target).attr('class') !== 'select2-selection__choice__remove'){
e.preventDefault();
e.stopPropagation();
$("#asset_list_modal").modal();
}
})
}
$(document).ready(function(){
}).on('show.bs.modal', function () {
initAssetModalTable();
initModalTree();
}).on('click', '#btn_asset_modal_confirm', function () {
if (assetModalOption.onModalConfirm) {
assetModalOption.onModalConfirm(assetModalTable, assetModalTree);
}
$("#asset_list_modal").modal('hide');
})
</script>
{% endblock %}
{% block modal_button %}
{{ block.super }}
{% endblock %}
{% block modal_confirm_id %}btn_asset_modal_confirm{% endblock %}

View File

@@ -1,87 +0,0 @@
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}asset_user_auth_update_modal{% endblock %}
{% block modal_title%}{% trans "Update asset user auth" %}{% endblock %}
{% block modal_body %}
<form class="form-horizontal" role="form" onkeydown="if(event.keyCode==13){ $('#btn_asset_user_auth_update_modal_confirm').trigger('click'); return false;}">
{% csrf_token %}
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Hostname" %}</label>
<div class="col-sm-10">
<p class="form-control-static" id="id_hostname_p"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Username" %}</label>
<div class="col-sm-10">
<p class="form-control-static" id="id_username_p"></p>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Password" %}</label>
<div class="col-sm-10">
<input class="form-control" id="id_password_auth" type="password" name="password" placeholder="{% trans 'Please input password' %}"/>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">{% trans "Private key" %}</label>
<div class="col-sm-10">
<div class="row bootstrap3-multi-input">
<div class="col-xs-12">
<input id="id_private_key" type="file" name="private_key"/>
</div>
</div>
</div>
</div>
</form>
<script>
var authHostname, authUsername, authAssetId = null;
$(document).ready(function () {
}).on("show.bs.modal", "#asset_user_auth_update_modal", function () {
$('#id_hostname_p').html(authHostname);
$('#id_username_p').html(authUsername);
$('#id_password_auth').parent().removeClass('has-error');
$('#id_password_auth').val('');
}).on('click', '#btn_asset_user_auth_update_modal_confirm', function(){
var password = $('#id_password_auth').val();
var privateKey = $('#id_private_key').prop('files');
var hasPrivateKey = privateKey.length > 0;
if (!password && !hasPrivateKey) {
$('#id_password_auth').parent().addClass('has-error');
return
}
var data = {
'asset': authAssetId,
'username': authUsername
};
if (password) {
data["password"] = password
}
var props = {
data: data,
url: "{% url 'api-assets:asset-user-list' %}",
form: $("form"),
method: 'POST',
success: function () {
toastr.success("{% trans 'Update successfully!' %}");
$("#asset_user_auth_update_modal").modal('hide');
}
};
if (hasPrivateKey) {
var reader = new FileReader();//新建一个FileReader
reader.readAsText(privateKey[0], "UTF-8");//读取文件
reader.onload = function(evt){ //读取完文件之后会回来这里
data["private_key"] = evt.target.result;
formSubmit(props);
}
}
if (!hasPrivateKey) {
formSubmit(props);
}
})
</script>
{% endblock %}
{% block modal_confirm_id %}btn_asset_user_auth_update_modal_confirm{% endblock %}

View File

@@ -1,98 +0,0 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
{% block modal_id %}asset_user_auth_view{% endblock %}
{% block modal_title%}{% trans "Asset user auth" %}{% endblock %}
{% block modal_body %}
<style>
.inmodal .modal-body {
background: #fff;
}
</style>
<form class="form-horizontal" action="" style="padding-top: 20px">
<div class="auth-field">
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Hostname' %}</label>
<div class="col-sm-8">
<p class="form-control-static" id="id_hostname_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Username' %}</label>
<div class="col-sm-8" >
<p class="form-control-static" id="id_username_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Password' %}</label>
<div class="col-sm-8">
<input id="id_password_view" type="password" class="form-control" value="" readonly style="border: none;padding-left: 0;background-color: #fff;width: 100%">
</div>
<div class="col-sm-2" style="padding-left: 2px">
<a class="btn btn-white btn-sm btn-show-password"><i class="fa fa-eye"></i></a>
<a class="btn btn-white btn-sm btn-copy-password"><i class="fa fa-copy"></i></a>
</div>
</div>
</div>
</form>
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script>
var showPassword = false;
var authAssetId = "";
var authHostname = "";
var authUsername = "";
var mfaFor = "";
function initClipboard() {
var clipboard = new Clipboard('.btn-copy-password', {
text: function (trigger) {
return $("#id_password_view").val()
}
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
function showAuth() {
var url = "{% url "api-assets:asset-user-auth-info" %}?asset_id=" + authAssetId + "&username=" + authUsername;
if (prefer) {
url = setUrlParam(url, 'prefer', prefer)
}
$("#id_username_view").html(authUsername);
$("#id_hostname_view").html(authHostname);
var success = function (data) {
var password = data.password;
$("#id_password_view").val(password);
};
var error = function() {
var msg = "{% trans 'Get auth info error' %}";
toastr.error(msg)
};
requestApi({
url: url,
method: "GET",
success: success,
flash_message: false,
error: error
})
}
$(document).ready(function () {
initClipboard();
}).on("click", ".btn-show-password", function () {
showPassword = !showPassword;
if (showPassword) {
$("#id_password_view").attr("type", "text")
} else {
$("#id_password_view").attr("type", "password")
}
}).on("show.bs.modal", "#asset_user_auth_view", function () {
showAuth();
})
</script>
{% endblock %}
{% block modal_button %}
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
{% endblock %}

View File

@@ -1,161 +0,0 @@
{% load i18n %}
<style>
.btn-group>.btn+.dropdown-toggle {
padding-right: 4px;
padding-left: 4px;
}
table.dataTable tbody tr.selected a {
color: rgb(103, 106, 108);;
}
</style>
<table class="table table-striped table-bordered table-hover" id="asset_user_list_table" style="width: 100%">
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" class="ipt_check_all" >
</th>
<th class="text-center">{% trans 'Hostname' %}</th>
<th class="text-center">{% trans 'IP' %}</th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Version' %}</th>
<th class="text-center">{% trans 'Connectivity'%}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% include 'assets/_asset_user_auth_update_modal.html' %}
{% include 'assets/_asset_user_auth_view_modal.html' %}
{% include 'authentication/_mfa_confirm_modal.html' %}
<script>
var assetUserListUrl = "{% url "api-assets:asset-user-list" %}";
var assetUserTable;
var needPush = false;
var prefer = null;
var lastMFATime = "{{ request.session.MFA_VERIFY_TIME }}";
var testDatetime = "{% trans 'Test datetime: ' %}";
var mfaVerifyTTL = "{{ SECURITY_MFA_VERIFY_TTL }}";
var mfaNeedCheck = "{{ SECURITY_VIEW_AUTH_NEED_MFA }}" === "True";
function initAssetUserTable() {
var options = {
ele: $('#asset_user_list_table'),
toggle: true,
columnDefs: [
{
targets: 5, createdCell: function (td, cellData) {
var innerHtml = "";
if (cellData.status == 1) {
innerHtml = '<i class="fa fa-circle text-navy"></i>'
} else if (cellData.status == 0) {
innerHtml = '<i class="fa fa-circle text-danger"></i>'
} else {
innerHtml = '<i class="fa fa-circle text-warning"></i>'
}
var dateManual = toSafeLocalDateStr(cellData.datetime);
var dataContent = testDatetime + dateManual;
innerHtml = "<a data-toggle='popover' data-content='" + dataContent + "'" + 'data-placement="auto bottom"' + ">" + innerHtml + "</a>";
$(td).html(innerHtml);
}
},
{
targets: 6, createdCell: function (td, cellData) {
var data = toSafeLocalDateStr(cellData);
$(td).html(data);
},
},
{
targets: 7, createdCell: function (td, cellData, rowData) {
var view_btn = '<button class="btn btn-xs btn-primary m-l-xs btn-view-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans "View" %}</button>'
var update_btn = '<li><a class="btn-update-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Update' %}</a></li>';
var test_btn = '<li><a class="btn-test-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Test' %}</a></li>';
var push_btn = '<li><a class="btn-push-auth" data-user="username123" data-hostname="hostname123" data-asset="asset123">{% trans 'Push' %}</a></li>';
if (needPush) {
test_btn += push_btn;
}
var actions = '<div class="btn-group">' + view_btn +
' <button data-toggle="dropdown" class="btn btn-primary btn-xs dropdown-toggle">' +
' <span class="caret"></span>' +
' </button>' +
' <ul class="dropdown-menu">' +
update_btn + test_btn +
' </ul>' +
' </div>';
actions = actions.replaceAll("username123", rowData.username)
.replaceAll("hostname123", rowData.hostname)
.replaceAll("asset123", rowData.asset);
$(td).html(actions);
},
width: '70px'
}
],
ajax_url: assetUserListUrl,
columns: [
{data: "id"}, {data: "hostname"}, {data: "ip"},
{data: "username"}, {data: "version", orderable: false},
{data: "connectivity"},
{data: "date_created", orderable: false},
{data: "asset", orderable: false}
],
op_html: $('#actions').html()
};
table = jumpserver.initServerSideDataTable(options);
return table
}
$(document).ready(function(){
})
.on('click', '.btn-view-auth', function () {
authAssetId = $(this).data("asset") ;
authHostname = $(this).data("hostname");
authUsername = $(this).data('user');
if (!mfaNeedCheck){
$("#asset_user_auth_view").modal('show');
return
}
var now = new Date();
var nowTime = now.getTime() / 1000;
if ( !lastMFATime || nowTime - lastMFATime > mfaVerifyTTL ) {
mfaFor = "viewAuth";
$("#mfa_auth_confirm").modal("show");
} else {
$("#asset_user_auth_view").modal('show');
}
})
.on("success", '#mfa_auth_confirm', function () {
if (mfaFor !== "viewAuth") {
return
}
$("#asset_user_auth_view").modal("show");
})
.on('click', '.btn-update-auth', function() {
authUsername = $(this).data("user") ;
authHostname = $(this).data("hostname");
authAssetId = $(this).data("asset");
$("#asset_user_auth_update_modal").modal('show');
})
.on("click", '.btn-test-auth', function () {
authUsername = $(this).data("user") ;
authAssetId = $(this).data("asset");
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id=" + authAssetId + "&username=" + authUsername;
if (prefer) {
the_url = setUrlParam(the_url, "prefer", prefer)
}
var success = function (data) {
var task_id = data.task;
showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
</script>

View File

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

View File

@@ -1,68 +0,0 @@
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
<style>
.modal-body {
background-color: white !important;
}
</style>
{% block modal_id %}node_detail_modal{% endblock %}
{% block modal_title %}{% trans "Node detail" %}{% endblock %}
{% block modal_body %}
<form class="form-horizontal" action="" style="padding-top: 20px">
<div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'ID' %}</label>
<div class="col-sm-8">
<p class="form-control-static" id="id_node_detail_id_view"></p>
</div>
<div class="col-sm-2" style="padding-left: 2px">
<a class="btn btn-white btn-sm btn-node-detail-copy-id"><i class="fa fa-copy"></i></a>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Name' %}</label>
<div class="col-sm-8" >
<p class="form-control-static" id="id_node_detail_name_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Full name' %}</label>
<div class="col-sm-8" >
<p class="form-control-static" id="id_node_detail_full_name_view"></p>
</div>
</div>
<div class="form-group">
<label for="" class="col-sm-2 control-label">{% trans 'Key' %}</label>
<div class="col-sm-8">
<p class="form-control-static" id="id_node_detail_key_view"></p>
</div>
</div>
</div>
</form>
<script src="{% static "js/plugins/clipboard/clipboard.min.js" %}"></script>
<script>
function initClipboard() {
var clipboard = new Clipboard('.btn-node-detail-copy-id', {
text: function (trigger) {
return $("#id_node_detail_id_view").html()
}
});
clipboard.on("success", function (e) {
toastr.success("{% trans "Copy success" %}")
})
}
$(document).ready(function () {
initClipboard();
})
</script>
{% endblock %}
{% block modal_button %}
<button data-dismiss="modal" class="btn btn-white" type="button">{% trans "Close" %}</button>
{% endblock %}

View File

@@ -1,344 +0,0 @@
{% load static %}
{% load i18n %}
<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>
<style type="text/css">
div#rMenu {
position: absolute;
visibility: hidden;
text-align: left;
{#top: 100%;#}
top: 0;
left: 0;
z-index: 999;
{#float: left;#}
padding: 0 0;
margin: 2px 0 0;
list-style: none;
background-clip: padding-box;
}
.dataTables_wrapper .dataTables_processing {
opacity: .9;
border: none;
}
div#rMenu li{
margin: 1px 0;
cursor: pointer;
list-style: none outside none;
}
.dropdown a:hover {
background-color: #f1f1f1
}
</style>
<div class="ibox treebox float-e-margins" style="overflow:auto;">
<div class="ibox-content mailbox-content" style="padding-top: 0;padding-left: 1px">
<div class="file-manager" id="tree-node-id">
<div id="{% block treeID %}nodeTree{% endblock %}" class="ztree">
{% trans 'Loading' %} ...
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
<div id="rMenu">
<ul class="dropdown-menu menu-actions">
<li class="divider"></li>
<li id="m_create" tabindex="-1" onclick="addTreeNode();"><a><i class="fa fa-plus-square-o"></i> {% trans 'Add node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="editTreeNode();"><a><i class="fa fa-pencil-square-o"></i> {% trans 'Rename node' %}</a></li>
<li id="m_del" tabindex="-1" onclick="removeTreeNode();"><a><i class="fa fa-minus-square"></i> {% trans 'Delete node' %}</a></li>
</ul>
</div>
<script>
var zTree, rMenu = null;
var current_node_id = null;
var current_node = null;
var showMenu = false;
var treeUrl = '{% url 'api-assets:node-children-tree' %}?assets=0';
// options:
// {
// "onSelected": func,
// "showAssets": false,
// "beforeAsync": func()
// "showMenu": false,
// "otherMenu": "",
// "showAssets": false,
// }
var inited = false;
function initNodeTree(options) {
if (options.showAssets) {
treeUrl = setUrlParam(treeUrl, 'assets', '1')
}
var setting = {
view: {
dblClickExpand: false,
showLine: true
},
data: {
simpleData: {
enable: true
}
},
async: {
enable: true,
url: treeUrl,
autoParam: ["id=key", "name=n", "level=lv"],
type: 'get'
},
edit: {
enable: true,
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
}
},
callback: {
onRightClick: OnRightClick,
beforeClick: beforeClick,
onRename: onRename,
onSelected: options.onSelected || defaultCallback("On selected"),
beforeDrag: beforeDrag,
onDrag: onDrag,
beforeDrop: beforeDrop,
onDrop: onDrop,
beforeAsync: options.beforeAsync || defaultCallback("Before async")
}
};
$.get(treeUrl, function (data, status) {
zTree = $.fn.zTree.init($("#nodeTree"), setting, data);
rootNodeAddDom(zTree, function () {
const url = '{% url 'api-assets:refresh-nodes-cache' %}';
requestApi({
url: url,
method: 'GET',
flash_message: false,
success: function () {
initNodeTree(options);
}
});
});
inited = true;
});
if (inited) {
return
}
if (options.showMenu) {
showMenu = true;
rMenu = $("#rMenu");
}
if (options.otherMenu) {
$(".menu-actions").append(options.otherMenu)
}
return zTree
}
function addTreeNode() {
hideRMenu();
var parentNode = zTree.getSelectedNodes()[0];
if (!parentNode){
return
}
var url = "{% url 'api-assets:node-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", parentNode.meta.node.id);
$.post(url, {}, function (data, status){
if (status === "success") {
var newNode = {
id: data["key"],
name: data["value"],
pId: parentNode.id,
meta: {
"node": data
}
};
newNode.checked = zTree.getSelectedNodes()[0].checked;
zTree.addNodes(parentNode, 0, newNode);
var node = zTree.getNodeByParam('id', newNode.id, parentNode);
zTree.editName(node);
} else {
alert("{% trans 'Create node failed' %}")
}
});
}
function removeTreeNode() {
hideRMenu();
var current_node = zTree.getSelectedNodes()[0];
if (!current_node){
return
}
if (current_node.children && current_node.children.length > 0) {
toastr.error("{% trans 'Have child node, cancel' %}");
} else if (current_node.meta.node.assets_amount !== 0) {
toastr.error("{% trans 'Have assets, cancel' %}");
} else {
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", current_node_id);
$.ajax({
url: url,
method: "DELETE",
success: function () {
zTree.removeNode(current_node);
}
});
}
}
function editTreeNode() {
hideRMenu();
var current_node = zTree.getSelectedNodes()[0];
if (!current_node){
return
}
if (current_node) {
current_node.name = current_node.meta.node.value;
}
zTree.editName(current_node);
}
function OnRightClick(event, treeId, treeNode) {
if (!showMenu) {
return
}
if (!treeNode && event.target.tagName.toLowerCase() !== "button" && $(event.target).parents("a").length === 0) {
zTree.cancelSelectedNode();
showRMenu("root", event.clientX, event.clientY);
} else if (treeNode && !treeNode.noR) {
zTree.selectNode(treeNode);
showRMenu("node", event.clientX, event.clientY);
}
}
function showRMenu(type, x, y) {
var offset = $("#tree-node-id").offset();
var scrollTop = document.querySelector('.treebox').scrollTop;
x -= offset.left;
y -= offset.top + scrollTop;
x += document.body.scrollLeft;
y += document.body.scrollTop + document.documentElement.scrollTop;
rMenu.css({"top":y+"px", "left":x+"px", "visibility":"visible"});
$("#rMenu ul").show();
$("body").bind("mousedown", onBodyMouseDown);
}
function beforeClick(treeId, treeNode, clickFlag) {
return true;
}
function hideRMenu() {
if (rMenu) rMenu.css({"visibility": "hidden"});
$("body").unbind("mousedown", onBodyMouseDown);
}
function onBodyMouseDown(event){
if (!(event.target.id === "rMenu" || $(event.target).parents("#rMenu").length>0)) {
rMenu.css({"visibility" : "hidden"});
}
}
function onRename(event, treeId, treeNode, isCancel){
var url = "{% url 'api-assets:node-detail' pk=DEFAULT_PK %}"
.replace("{{ DEFAULT_PK }}", current_node_id);
var data = {"value": treeNode.name};
if (isCancel){
return
}
requestApi({
url: url,
body: JSON.stringify(data),
method: "PATCH",
success_message: "{% trans 'Rename success' %}",
success: function () {
var assets_amount = treeNode.meta.node.assets_amount;
if (!assets_amount) {
assets_amount = 0;
}
treeNode.name = treeNode.name + ' (' + assets_amount + ')';
zTree.updateNode(treeNode);
},
})
}
function beforeDrag() {
return true
}
function beforeDrop(treeId, treeNodes, targetNode, moveType) {
var treeNodesNames = [];
$.each(treeNodes, function (index, value) {
treeNodesNames.push(value.name);
});
var msg = "你想移动节点: `" + treeNodesNames.join(",") + "` 到 `" + targetNode.name + "` 下吗?";
return confirm(msg);
}
function onDrag(event, treeId, treeNodes) {
}
function onDrop(event, treeId, treeNodes, targetNode, moveType) {
var treeNodesIds = [];
$.each(treeNodes, function (index, value) {
treeNodesIds.push(value.meta.node.id);
});
var the_url = "{% url 'api-assets:node-add-children' pk=DEFAULT_PK %}".replace("{{ DEFAULT_PK }}", targetNode.meta.node.id);
var body = {nodes: treeNodesIds};
requestApi({
url: the_url,
method: "PUT",
body: JSON.stringify(body)
})
}
function defaultCallback(action) {
function logging() {
console.log(action)
}
return logging
}
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;
}
}
$(document).ready(function () {
$('.treebox').css('height', window.innerHeight - 60);
})
.on('click', '.btn-show-current-asset', function(){
hideRMenu();
$(this).css('display', 'none');
$('#show_all_asset').css('display', 'inline-block');
setCookie('show_current_asset', '1');
location.reload()
})
.on('click', '.btn-show-all-asset', function(){
hideRMenu();
$(this).css('display', 'none');
$('#show_current_asset').css('display', 'inline-block');
setCookie('show_current_asset', '');
location.reload();
}).on('click', '.tree-toggle-btn', function (e) {
e.preventDefault();
toggle();
})
</script>

View File

@@ -1,267 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% 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 %}
<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 %}
<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>
<div class="col-sm-8">
{{ form.auto_generate_key}}
</div>
</div>
</div>
<div class="auth-fields">
{% bootstrap_field form.password layout="horizontal" %}
{% bootstrap_field form.private_key layout="horizontal" %}
</div>
<div class="form-group">
<label for="{{ form.auto_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
<div class="col-sm-8">
{{ form.auto_push}}
</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" %}
{% bootstrap_field form.comment 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 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.id_for_label }}';
var auto_push_id = '#' + '{{ form.auto_push.id_for_label }}';
var command_filter_block_id = '#command-filter-block';
var sudo_id = '#' + '{{ form.sudo.id_for_label }}';
var shell_id = '#' + '{{ form.shell.id_for_label }}';
function autoLoginModeProtocol() {
// 协议+自动登录模式字段控制
$('#auth_title_id').removeClass('hidden');
var protocol = $(protocol_id + " option:selected").text();
if (['rdp'].indexOf(protocol) !== -1) {
authFieldsDisplay();
$(auto_generate_key).closest('.form-group').removeClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').removeClass('hidden');
$(auto_push_id).closest('.form-group').removeClass('hidden');
$(command_filter_block_id).addClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else if (protocol === 'vnc') {
$('.auth-fields').removeClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').removeClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).addClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else if (protocol === 'mysql'){
$('.auth-fields').removeClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').removeClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).removeClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else if (protocol === 'telnet') {
$('.auth-fields').removeClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').removeClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).removeClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else {
authFieldsDisplay();
$(auto_generate_key).closest('.form-group').removeClass('hidden');
$(private_key_id).closest('.form-group').removeClass('hidden');
$(password_id).closest('.form-group').removeClass('hidden');
$(auto_push_id).closest('.form-group').removeClass('hidden');
$(command_filter_block_id).removeClass('hidden');
$(sudo_id).closest('.form-group').removeClass('hidden');
$(shell_id).closest('.form-group').removeClass('hidden');
}
}
function manualLoginModeProtocol() {
// 协议+手动登录模式字段控制
$('#auth_title_id').addClass('hidden');
var protocol = $(protocol_id + " option:selected").text();
if (['rdp'].indexOf(protocol) !== -1) {
$('.auth-fields').addClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).addClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else if (protocol === 'vnc'){
$('.auth-fields').addClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).addClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else if (protocol === 'mysql'){
$('.auth-fields').addClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).removeClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else if (protocol === 'telnet') {
$('.auth-fields').addClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).removeClass('hidden');
$(sudo_id).closest('.form-group').addClass('hidden');
$(shell_id).closest('.form-group').addClass('hidden');
}
else {
$('.auth-fields').addClass('hidden');
$(auto_generate_key).closest('.form-group').addClass('hidden');
$(password_id).closest('.form-group').addClass('hidden');
$(private_key_id).closest('.form-group').addClass('hidden');
$(auto_push_id).closest('.form-group').addClass('hidden');
$(command_filter_block_id).removeClass('hidden');
$(sudo_id).closest('.form-group').removeClass('hidden');
$(shell_id).closest('.form-group').removeClass('hidden');
}
}
function authFieldsDisplay() {
if ($(auto_generate_key).prop('checked')) {
$('.auth-fields').addClass('hidden');
} else {
$('.auth-fields').removeClass('hidden');
}
}
function fieldDisplay(){
var login_mode = $(login_mode_id).val();
if (login_mode === 'manual'){
manualLoginModeProtocol();
}
else if(login_mode === 'auto'){
autoLoginModeProtocol();
}
}
$(document).ready(function () {
$('.select2').select2();
authFieldsDisplay();
fieldDisplay();
})
.on('change', auto_generate_key, function(){
authFieldsDisplay();
})
.on('change', login_mode_id, function(){
fieldDisplay();
})
.on('change', protocol_id, function(){
fieldDisplay();
}).on("submit", "form", function (evt) {
evt.preventDefault();
{% block formUrl %}
var the_url = '{% url 'api-assets:system-user-list' %}';
var redirect_to = '{% url "assets:system-user-list" %}';
var method = "POST";
{% endblock %}
var form = $("form");
var data = form.serializeObject();
objectAttrsIsList(data, ['cmd_filters']);
objectAttrsIsBool(data, ["auto_generate_key", "auto_push"]);
data["private_key"] = $("#id_private_key").data('file');
var props = {
url: the_url,
data: data,
method: method,
form: form,
redirect_to: redirect_to
};
formSubmit(props);
}).on('change', '#id_private_key', function () {
readFile($(this)).on("onload", function (evt, data) {
$(this).data("file", data)
})
})
</script>
{% endblock %}

View File

@@ -1,24 +0,0 @@
{% 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

@@ -1,93 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li>
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li class="active">
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.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">
{% include 'assets/_asset_user_list.html' %}
</div>
</div>
</div>
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
<tr class="no-borders-tr">
<td width="50%">{% trans 'Test connective' %}:</td>
<td>
<span style="float: right">
<button type="button" class="btn btn-primary btn-xs btn-test-connective" style="width: 54px">{% trans 'Test' %}</button>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
assetUserListUrl = setUrlParam(assetUserListUrl, "admin_user_id", "{{ admin_user.id }}");
prefer = "admin_user";
initAssetUserTable();
})
.on('click', '.btn-test-connective', function () {
var the_url = "{% url 'api-assets:admin-user-connective' pk=admin_user.id %}";
var success = function (data) {
var task_id = data.task;
showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
</script>
{% endblock %}

View File

@@ -1,86 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% load static %}
{% load bootstrap3 %}
{% 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">
{% if form.non_field_errors %}
<div class="alert alert-danger">
{{ form.non_field_errors }}
</div>
{% endif %}
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
{% csrf_token %}
{% bootstrap_field form.name layout="horizontal" %}
{% bootstrap_field form.username layout="horizontal" %}
{% bootstrap_field form.password layout="horizontal" %}
{% bootstrap_field form.private_key layout="horizontal" %}
{% bootstrap_field form.comment 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>
$(document).ready(function () {
$('.select2').select2();
})
.on("submit", "form", function (evt) {
evt.preventDefault();
var the_url = '{% url 'api-assets:admin-user-list' %}';
var redirect_to = '{% url "assets:admin-user-list" %}';
var method = "POST";
{% if type == "update" %}
the_url = '{% url 'api-assets:admin-user-detail' pk=object.id %}';
redirect_to = '{% url "assets:admin-user-list" %}';
method = "PUT";
{% endif %}
var form = $("form");
var data = form.serializeObject();
data["private_key"] = $("#id_private_key").data('file');
var props = {
url: the_url,
data: data,
method: method,
form: form,
redirect_to: redirect_to
};
formSubmit(props);
})
.on('change', '#id_private_key', function () {
readFile($(this)).on("onload", function (evt, data) {
$(this).data("file", data)
})
})
</script>
{% endblock %}

View File

@@ -1,166 +0,0 @@
{% extends 'base.html' %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li class="active">
<a href="{% url 'assets:admin-user-detail' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
</li>
<li>
<a href="{% url 'assets:admin-user-assets' pk=admin_user.pk %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Assets list' %} </a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'assets:admin-user-update' pk=admin_user.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-danger btn-delete-admin-user">
<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>{{ admin_user.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>{{ admin_user.name }}</b></td>
</tr>
<tr>
<td>{% trans 'Username' %}:</td>
<td><b>{{ admin_user.username }}</b></td>
</tr>
<tr>
<td>{% trans 'Date created' %}:</td>
<td><b>{{ admin_user.date_created }}</b></td>
</tr>
<tr>
<td>{% trans 'Created by' %}:</td>
<td><b>{{ asset_group.created_by }}</b></td>
</tr>
<tr>
<td>{% trans 'Comment' %}:</td>
<td><b>{{ admin_user.comment }}</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 'Replace node assets admin user with this' %}
</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 'Select nodes' %}" id="nodes_selected" class="nodes-select2" style="width: 100%" multiple="" tabindex="4">
{% for node in nodes %}
<option value="{{ node.id }}" id="opt_{{ node.id }}" >{{ node.full_value }}</option>
{% endfor %}
</select>
</td>
</tr>
<tr>
<td colspan="2" class="no-borders">
<button type="button" class="btn btn-primary btn-sm" id="btn-change-admin-user">{% trans 'Confirm' %}</button>
</td>
</tr>
</form>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
function replaceNodeAssetsAdminUser(nodes) {
var the_url = "{% url 'api-assets:replace-nodes-admin-user' pk=admin_user.id %}";
var body = {
nodes: nodes
};
var success = function(data) {
// remove all the selected groups from select > option and rendered ul element;
$('.select2-selection__rendered').empty();
$('#nodes_selected').val('');
$.map(jumpserver.nodes_selected, function(value, index) {
$('#opt_' + index).remove();
});
// clear jumpserver.groups_selected
jumpserver.nodes_selected = {};
};
requestApi({
url: the_url,
body: JSON.stringify(body),
success: success
});
}
jumpserver.nodes_selected = {};
$(document).ready(function () {
nodesSelect2Init(".nodes-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-delete-admin-user', function () {
var $this = $(this);
var name = "{{ admin_user.name }}";
var uid = "{{ admin_user.id }}";
var the_url = '{% url "api-assets:admin-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
var redirect_url = "{% url 'assets:admin-user-list' %}";
objectDelete($this, name, the_url, redirect_url);
})
.on('click', '#btn-change-admin-user', function () {
if (Object.keys(jumpserver.nodes_selected).length === 0) {
return false;
}
var nodes = [];
$.map(jumpserver.nodes_selected, function(value, index) {
nodes.push(index);
});
replaceNodeAssetsAdminUser(nodes);
})
</script>
{% endblock %}

View File

@@ -1,77 +0,0 @@
{% extends '_base_list.html' %}
{% load i18n static %}
{% block help_message %}
{% 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. '%}
{% endblock %}
{% block table_search %}
{% include '_csv_import_export.html' %}
{% endblock %}
{% block table_container %}
<div class="uc pull-left m-r-5">
<a href="{% url "assets:admin-user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create admin user" %} </a>
</div>
<table class="table table-striped table-bordered table-hover " id="admin_user_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 'Username' %}</th>
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'Comment' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
var admin_user_table = 0;
function initTable() {
var options = {
ele: $('#admin_user_list_table'),
columnDefs: [
{targets: 1, render: function (cellData, tp, rowData, meta) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:admin-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
return detail_btn.replace('{{ DEFAULT_PK }}', rowData.id);
}},
{targets: 5, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:admin-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
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:admin-user-list" %}',
columns: [
{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount", orderable: false},
{#{data: "connectivity_amount"}, {data: "connectivity_amount"}, {data: "connectivity_amount"},#}
{data: "comment"}, {data: "id", orderable: false, width: "120px"}
]
};
return jumpserver.initServerSideDataTable(options);
}
$(document).ready(function(){
admin_user_table = initTable();
initCsvImportExport(admin_user_table, "{% trans "Admin user" %}")
})
.on('click', '.btn_admin_user_delete', function () {
var $this = $(this);
var $data_table = $("#admin_user_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:admin-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid);
objectDelete($this, name, the_url);
setTimeout( function () {
$data_table.ajax.reload();
}, 3000);
})
</script>
{% endblock %}

View File

@@ -1,94 +0,0 @@
{% extends 'base.html' %}
{% load common_tags %}
{% load static %}
{% load i18n %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="panel-options">
<ul class="nav nav-tabs">
<li>
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %}</a>
</li>
<li class="active">
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %}</a>
</li>
</ul>
</div>
<div class="tab-content">
<div class="col-sm-8" style="padding-left: 0;">
<div class="ibox float-e-margins">
<div class="ibox-title">
<span style="float: left">{% trans 'Asset users of' %} <b>{{ asset.hostname }} </b></span>
<div class="ibox-tools">
<a class="collapse-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<ul class="dropdown-menu dropdown-user">
</ul>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
{% include 'assets/_asset_user_list.html' %}
</div>
</div>
</div>
<div class="col-sm-4" style="padding-left: 0;padding-right: 0">
<div class="panel panel-primary">
<div class="panel-heading">
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
</div>
<div class="panel-body">
<table class="table">
<tbody>
{% if asset.is_support_ansible %}
<tr class="no-borders-tr">
<td>{% trans 'Test connective' %}:</td>
<td>
<span class="pull-right">
<button type="button" class="btn btn-primary btn-xs" id="btn-bulk-test-connective" style="width: 54px">{% trans 'Test' %}</button>
</span>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
assetUserListUrl = setUrlParam(assetUserListUrl, "asset_id", "{{ asset.id }}");
initAssetUserTable()
})
.on('click', '#btn-bulk-test-connective', function () {
var the_url = "{% url 'api-assets:asset-user-connective' %}" + "?asset_id={{ asset.id }}";
var success = function (data) {
var task_id = data.task;
showCeleryTaskLog(task_id);
};
requestApi({
url: the_url,
method: 'GET',
success: success,
flash_message: false
});
})
</script>
{% endblock %}

View File

@@ -1,71 +0,0 @@
{% extends '_base_create_update.html' %}
{% load static %}
{% load bootstrap3 %}
{% load i18n %}
{% block form %}
<div class="ydxbd" id="formlists" style="display: block;">
<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">{% 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>
{% endif %}
{% endfor %}
</div>
</div>
<form method="post" class="form-horizontal" id="add_form">
{% csrf_token %}
{% bootstrap_form form layout="horizontal" %}
<div class="form-group abc">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
<button class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
</div>
</div>
</form>
{% include 'assets/_asset_list_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
<script>
$(document).ready(function () {
$('.select2').select2();
initAssetTreeModel("#id_assets");
}).on('click', '.field-tag', function() {
changeField(this);
}).on('click', '#change_all', function () {
var tag_fields = $('.field-tag');
var $this = $(this);
var active = '1';
if ($this.attr('value') == '0'){
active = '0';
$this.attr('value', '1').addClass('label-primary')
} else {
active = '1';
$this.attr('value', '0').removeClass('label-primary')
}
$.each(tag_fields, function (k, v) {
changeField(v, active)
})
});
function changeField(obj, active) {
var $this = $(obj);
var field_id = $this.data('id');
if (!active) {
active = $this.attr('value');
}
if (active == '0') {
$this.attr('value', '1').addClass('label-primary');
var form_groups = $('#add_form .form-group:not(.abc)');
form_groups.filter(':has(#' + field_id + ')').show().find('select,input').prop('disabled', false)
} else {
$this.attr('value', '0').removeClass('label-primary');
var form_groups = $('#add_form .form-group:not(.abc)');
form_groups.filter(':has(#' + field_id + ')').hide().find('select,input').prop('disabled', true)
}
}
</script>
{% endblock %}

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