Compare commits

...

310 Commits

Author SHA1 Message Date
Jiangjie.Bai
9bd9d443b4 fix: 修复windows执行ansible显示sudo失败的问题 2022-05-05 11:40:37 +08:00
feng626
e56fc93a6e fix: 组织管理员 添加 view platform perm 2022-04-28 19:10:15 +08:00
fit2bot
eb17183d97 fix: workbench_orgs 去重 (#8151)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-25 11:39:51 +08:00
feng626
b0057ecb9d perf: client download 2022-04-24 15:09:50 +08:00
ibuler
a141a8d2c2 fix: 修复社区版跳转问题 2022-04-21 22:48:34 +08:00
Jiangjie.Bai
a4be0ff2f3 Merge pull request #8131 from jumpserver/dev
v2.21.0
2022-04-21 18:11:21 +08:00
Jiangjie.Bai
a6d61721dd fix: 修改csrftoken获取问题 2022-04-21 16:17:33 +08:00
fit2bot
c3de7b78c2 fix: 远程应用授权时 有些资产已经不存在了 导致授权失败 (#8127)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-21 15:36:40 +08:00
Jiangjie.Bai
e83d676712 Merge pull request #8119 from jumpserver/dev
v2.21.0-rc6
2022-04-20 20:25:43 +08:00
Jiangjie.Bai
63ee2dd8fb fix: 修复获取权限树权限控制 2022-04-20 20:14:44 +08:00
feng626
74f88d842d fix: 修改replay download perm 2022-04-20 19:14:42 +08:00
fit2bot
e61bae5ee4 perf: 优化权限位 (#8110)
* perf: 优化权限位

* perf: 优化返回的组织

* perf: 保证结果是 ok

* perf: 去掉 distinct

* perf: tree count

Co-authored-by: ibuler <ibuler@qq.com>
2022-04-20 18:50:53 +08:00
fit2bot
b0b379e5a9 fix: del org check ldap (#8114)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-20 16:38:42 +08:00
fit2bot
415521a003 fix: 删除组织时检测ldap同步组织 (#8112)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-20 16:32:33 +08:00
Jiangjie.Bai
c29d133776 feat: 修改LDAP导入定时任务interval/crontab优先级
Signed-off-by: Jiangjie.Bai <bugatti_it@163.com>
2022-04-20 16:31:56 +08:00
fit2bot
d2dd487e2c feat: 修改LDAP导入组织问题 (#8111)
Signed-off-by: Jiangjie.Bai <bugatti_it@163.com>

Co-authored-by: BaiJiangJie <bugatti_it@163.com>
2022-04-20 16:05:33 +08:00
ibuler
f1bd4ea91f perf: 修改 系统级别用户角色的 perms 2022-04-20 11:50:11 +08:00
fit2bot
7647438792 perf: 账号备份log (#8106)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-20 11:18:50 +08:00
Jiangjie.Bai
015ff4b119 Merge pull request #8105 from jumpserver/dev
v2.21.0-rc5
2022-04-20 10:46:27 +08:00
fit2bot
af9248ef7c fix: 还原connection token 逻辑 (#8101)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-20 10:24:08 +08:00
Jiangjie.Bai
c04ab1aab9 Merge pull request #8100 from jumpserver/dev
v2.21.0-rc5
2022-04-19 21:52:51 +08:00
feng626
611a00a5fa fix: 修复super user perm bug 2022-04-19 21:52:01 +08:00
Jiangjie.Bai
57969a4e23 fix: 修改获取smart endpoint的逻辑 2022-04-19 19:50:18 +08:00
Jiangjie.Bai
5f370c1c04 perf: 优化内置系统用户角色权限 2022-04-19 19:19:47 +08:00
Jiangjie.Bai
f026b86a20 fix: 修复获取组织用户 2022-04-19 18:09:07 +08:00
ibuler
0addba7c14 perf: 修改 command 命令执行 2022-04-19 17:21:27 +08:00
ibuler
e4b0ab6a45 perf: 修改命令执行区分组织 2022-04-19 17:21:27 +08:00
Jiangjie.Bai
b4ac24ad6d fix: 修改endpoint/rule权限树位置 2022-04-19 17:21:09 +08:00
halo
500477fad1 fix: ftp日志清理bug 2022-04-19 17:13:17 +08:00
Jiangjie.Bai
3b9cb2a99c fix: 修改翻译临时密码 2022-04-19 16:26:33 +08:00
Jiangjie.Bai
f8fade4cf2 feat: 添加配置项 KoKo SSH Client 方式 2022-04-19 16:16:55 +08:00
Jiangjie.Bai
be2708f83d fix: 修复ajax请求携带csrftoken问题 2022-04-19 14:29:23 +08:00
ibuler
516cb05d69 perf: 修改翻译 2022-04-19 13:33:28 +08:00
老广
714b6b1233 Merge pull request #8085 from jumpserver/dev
v2.21.0-rc5
2022-04-19 13:15:16 +08:00
ibuler
3e3835dc28 perf: 修改用户权限 2022-04-19 10:42:36 +08:00
ibuler
f4ed4e1176 perf: 添加 temp token 排序 2022-04-18 19:55:16 +08:00
feng626
7b2d51f343 fix: 修复角色过滤失败 2022-04-18 19:54:44 +08:00
feng626
fe47e40588 fix: es6 create index fail 2022-04-18 19:44:54 +08:00
fit2bot
4362f8d5af perf: 优化组织 (#8080)
* perf: 优化用户的orgs

* perf: 优化组织

Co-authored-by: ibuler <ibuler@qq.com>
2022-04-18 17:17:23 +08:00
Jiangjie.Bai
6f49d240af Merge pull request #8079 from jumpserver/dev
v2.21.0-rc4
2022-04-18 15:31:02 +08:00
Jiangjie.Bai
3eab621b28 feat: 优化Endpoint迁移逻辑,增加XRDP规则和Endpoint
fix: 修改Endpoint迁移文件
2022-04-18 15:29:40 +08:00
Jiangjie.Bai
afcbe60531 Merge pull request #8076 from jumpserver/dev
v2.21.0-rc3
2022-04-18 11:43:40 +08:00
jiangweidong
548a374c6d fix: 修复部署在没有密码的redis上时,站内信数量不更新问题 2022-04-18 11:39:20 +08:00
feng626
10c146b07d fix: 修复远程应用无资产下载xrdp file 500 问题 2022-04-18 11:38:31 +08:00
fit2bot
a647e73c02 feat: 设置SessionCookieNamePrefix (#8071)
* feat: 设置SessionCookieNamePrefix

* feat: 设置SessionCookieNamePrefix

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-04-15 21:33:15 +08:00
Jiangjie.Bai
7b02777f1e fix: 修改endpoint smart API允许有效用户访问 2022-04-15 16:33:41 +08:00
Jiangjie.Bai
97e59384e0 fix: connection token API 返回有效时间 2022-04-15 16:33:20 +08:00
jiangweidong
70a07539af perf: 优化部分云厂商的redis连接的问题 2022-04-15 10:00:49 +08:00
Jiangjie.Bai
f98c170b8c Merge pull request #8061 from jumpserver/dev
v2.21.0-rc2
2022-04-14 19:51:29 +08:00
fit2bot
0b94d7414a feat: download (#8062)
Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
2022-04-14 19:51:10 +08:00
feng626
7aa0c9bf19 feat: download 2022-04-14 15:00:59 +08:00
Jiangjie.Bai
6d8e8856ac fix: 修改命令timestamp_display只读 2022-04-14 14:45:19 +08:00
Jiangjie.Bai
c240a471dc fix: Public Setting 添加 Magnus 2022-04-14 14:16:02 +08:00
Jiangjie.Bai
ea478fc801 fix: Public Setting 添加 Magnus 2022-04-14 12:55:18 +08:00
fit2bot
5127214375 feat: 站内信一键已读 (#8057)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-14 12:18:11 +08:00
Jiangjie.Bai
21c41a6334 Merge pull request #8054 from jumpserver/dev
v2.21.0-rc1
2022-04-13 20:25:47 +08:00
fit2bot
b610d71e11 feat: 添加 临时 password (#8035)
* perf: 添加 template password

* perf: 修改id

* perf: 修改 翻译

* perf: 修改 tmp token

* perf: 修改 token

Co-authored-by: ibuler <ibuler@qq.com>
2022-04-13 20:24:56 +08:00
Jiangjie.Bai
10b033010e feat: 优化命令导出时间戳可读性 2022-04-13 19:50:17 +08:00
fit2bot
c630b11bd5 fix: port str (#8055)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-13 19:48:31 +08:00
ibuler
b0f7c114fc perf: 修改 csrf token domain 2022-04-13 16:08:07 +08:00
fit2bot
72608146cc chore: lgtm (#8048)
* chore: lgtm

* perf: add lgtm

Co-authored-by: ibuler <ibuler@qq.com>
2022-04-12 18:25:28 +08:00
ibuler
3213fe0984 chore: 添加action lgtm 2022-04-12 18:06:42 +08:00
fit2bot
f481463c64 feat: 添加Endpoint (#8041)
* feat: add Endpoint EndpointRule EndpointProtocol model

* feat: add Endpoint EndpointRule EndpointProtocol API

* feat: modify protocols field

* feat: 修改序列类

* feat: 获取connect-url连接地址

* feat: 获取connect-url连接地址

* feat: 优化后台获取smart-endpoint逻辑

* feat: 优化后台获取smart-endpoint逻辑

* feat: 删除配置KOKO、XRDP、MAGNUS

* feat: 删除配置KOKO、XRDP、MAGNUS

* feat: 修改翻译

* feat: 修改smart endpoint

* feat: 修改翻译

* feat: smart API 添加token解析

* feat: 删除 smart serializer

* feat: 修改迁移逻辑

* feat: 解决冲突

* feat: 修改匹配 endpoint

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-04-12 17:45:10 +08:00
ibuler
4cf90df17c perf: 默认角色添加 created by 2022-04-12 16:20:46 +08:00
ibuler
ffd98c6e3f fix: 修改 import 2022-04-12 16:20:46 +08:00
fit2bot
1f8ded49fa feat: 工作台区分组织 (#8040)
* perf: 工作台受组织角色控制

* perf: workspace => workbench

* perf: 修改 workspace codename

Co-authored-by: ibuler <ibuler@qq.com>
2022-04-12 14:25:49 +08:00
fit2bot
7c7d7d52b2 perf: asset number 扩容 (#8045)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-12 14:01:57 +08:00
feng626
f769d5a9bb fix: 修复用户数据不同步问题 2022-04-08 15:47:14 +08:00
feng626
c8758f417d feat: ldap一键导入及设置用户组织 2022-04-06 17:13:34 +08:00
Eric
ef36b2e662 perf: 完善 setting 的动态配置 2022-04-06 16:45:22 +08:00
jiangweidong
fe8527fd07 feat: 修改翻译 2022-04-02 16:04:13 +08:00
feng626
2cb08b4785 fix: user is common user 2022-04-02 16:02:57 +08:00
fit2bot
a936092020 perf: es相关代码格式优化 (#8020)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-02 13:26:18 +08:00
fit2bot
e602bc0341 fix: 修复网关翻译 (#8016)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-01 16:52:50 +08:00
Jiangjie.Bai
3121b4e3ff feat: 更新翻译 2022-03-31 13:05:55 +08:00
Jiangjie.Bai
eff562505e feat: 更新翻译 2022-03-31 13:05:55 +08:00
Jiangjie.Bai
73cb5e10b4 fix: 添加用户不能自更新字段逻辑 & 修复用户is_active创建失败的问题
fix: 添加用户不能自更新字段逻辑 & 修复用户is_active创建失败的问题

fix: 添加用户不能自更新字段逻辑 & 修复用户is_active创建失败的问题
2022-03-30 19:52:45 +08:00
fit2bot
c58d245636 fix: 修复koko setting (#8005)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-30 19:51:56 +08:00
fit2bot
e7af037513 perf: 修改命令command input 长度问题 (#7996)
* perf: 修改命令command input max_length 1024

* perf: 修改命令command input 长度问题

* perf: 修改命令command input 长度问题

* perf: 修改命令command input 长度问题

* perf: 修改命令command input 长度问题

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-03-30 19:07:49 +08:00
Jiangjie.Bai
54d1996507 feat: 支持续期Connection Token 2022-03-30 11:20:56 +08:00
dependabot[bot]
71f8b40e21 build(deps): bump paramiko from 2.7.2 to 2.10.1 in /requirements
Bumps [paramiko](https://github.com/paramiko/paramiko) from 2.7.2 to 2.10.1.
- [Release notes](https://github.com/paramiko/paramiko/releases)
- [Changelog](https://github.com/paramiko/paramiko/blob/main/NEWS)
- [Commits](https://github.com/paramiko/paramiko/compare/2.7.2...2.10.1)

---
updated-dependencies:
- dependency-name: paramiko
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-30 11:06:49 +08:00
ibuler
59342a88c0 perf: 优化各种翻译 2022-03-29 19:33:38 +08:00
fit2bot
b8e6bc932b perf: 添加 mariadb port (#7989)
* perf: 添加 mariadb port

* perf: 优化 mariadb 树上天津更多信息

* perf: remove mixin

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-29 17:22:59 +08:00
Jiangjie.Bai
cddff9fd19 feat: 改密计划支持su切换用户执行
feat: 改密计划支持su切换用户执行

feat: 改密计划支持su切换用户执行

feat: 改密计划支持su切换用户执行

feat: 改密计划支持su切换用户执行

feat: 改密计划支持su切换用户执行

feat: 改密计划支持su切换用户执行
2022-03-29 15:32:33 +08:00
feng626
d856f1364a feat: 拉起ssh api 2022-03-29 13:23:58 +08:00
fit2bot
52709d2efa feat: 企业微信、钉钉 工作台免密登录(飞书已实现) (#7855)
* feat: 添加oauth接口

* feat: 企业微信支持OAuth认证,工作台免密登录

* feat: 钉钉支持OAuth认证,工作台免密登录

* fix: 修复参数错误

Co-authored-by: halo <wuyihuangw@gmail.com>
2022-03-29 13:19:13 +08:00
ibuler
a20de3df16 perf: app tree 添加attrs,luna 使用 2022-03-29 12:44:36 +08:00
ibuler
e303b4f571 perf: 修复 settings patch 问题 2022-03-28 13:40:36 +08:00
jiangweidong
03fdaa03e4 feat: 支持日语 2022-03-28 10:06:54 +08:00
fit2bot
b7b1d81ea0 fix: magenus bug (#7977)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-25 16:46:36 +08:00
fit2bot
e0fdfa52b9 feat: 支持 magnus (#7965)
* feat: 支持 magnus

* perf: 添加 setting 到 api

* perf: 放出 mongodb

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-25 14:45:08 +08:00
ibuler
8718dc6751 pref: 优雅一发 2022-03-25 14:44:33 +08:00
ibuler
9e284f96e5 perf: 修改依赖写法,避免 github 认错 2022-03-25 14:44:33 +08:00
Eric
fc06295d04 perf: 优化 windows rdp 窗口显示 2022-03-25 10:50:34 +08:00
fit2bot
9b73727bbc fix: 修复系统组件绑定角色bug (#7962)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-23 10:56:23 +08:00
Jiangjie.Bai
6bde31cdd0 fix: 修复获取远程应用认证信息问题 2022-03-22 18:54:25 +08:00
ibuler
2721793b8f fix: 修复权限 view 没有 Model 的问题 2022-03-22 16:59:01 +08:00
feng626
2ec0cb8a2c fix: 修复用户绑定角色重大bug 2022-03-22 16:55:53 +08:00
feng626
d01d44b48d fix: api docs 2022-03-22 13:05:44 +08:00
老广
0ef7a9571c Merge pull request #7800 from jumpserver/pr@dev@feat_core_redis_support_ssl
feat: JumpServer支持部署在使用了ssl的redis上
2022-03-22 10:34:32 +08:00
ibuler
54fd1fb0c8 perf: 移动到信号中 2022-03-21 19:27:23 +08:00
ibuler
87c6eec619 perf: 优化 role bingding,优化 is_superuser 2022-03-21 19:27:23 +08:00
jiangweidong
e35fbfc7e9 Update session.py 2022-03-21 19:02:30 +08:00
fit2bot
3345456dc2 fix: 修复wateway api (#7947)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-21 17:53:16 +08:00
Jiangjie.Bai
9ae74120ed fix: 修复用户API权限 2022-03-21 12:00:15 +08:00
Jiangjie.Bai
9e5c132485 fix: 修复用户API权限 2022-03-21 11:58:29 +08:00
fit2bot
5cc2fdae4f fix: 修复api docs打不开的问题 (#7938)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-03-21 11:14:49 +08:00
jiangweidong
e993f31b6d feat: 支持纳管百度云资产 (#7921)
Co-authored-by: ibuler <ibuler@qq.com>
2022-03-21 10:39:47 +08:00
Jiangjie.Bai
60edbb36a1 fix: 应用树隐藏mongodb节点 2022-03-18 18:02:15 +08:00
fit2bot
5da1ec55a7 perf: org del ticket perm (#7932)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-18 17:44:20 +08:00
Jiangjie.Bai
b8c083af7e fix: 工单权限位放到sys角色中 2022-03-18 17:36:22 +08:00
Jiangjie.Bai
996621f303 fix: 移除权限dashboard 2022-03-18 17:10:34 +08:00
fit2bot
ec9e5da653 fix: 修复apikey perm (#7918)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-18 15:53:34 +08:00
Jiangjie.Bai
d4e4015d91 fix: 修复去除rolebiding change 权限 2022-03-18 15:38:33 +08:00
Jiangjie.Bai
005dd27701 Merge pull request #7917 from jumpserver/dev
v2.20.0
2022-03-17 19:22:22 +08:00
Jiangjie.Bai
ac6052546a fix: 修改翻译 2022-03-17 19:20:39 +08:00
Jiangjie.Bai
0265adcc72 fix: 修改翻译 2022-03-17 19:20:39 +08:00
Jiangjie.Bai
9654083662 fix: 修改站内信权限位 2022-03-17 19:06:44 +08:00
fit2bot
08ff8fa285 fix: login confirm bug (#7914)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-17 18:50:37 +08:00
Jiangjie.Bai
f82f7eba2b fix: 修改一些权限 2022-03-17 17:51:02 +08:00
Jiangjie.Bai
a8cee26874 fix: 修复资产详情授权用户tab页的权限位 2022-03-17 17:25:20 +08:00
Jiangjie.Bai
8080d36d90 Merge pull request #7911 from jumpserver/dev
v2.20.0-rc6
2022-03-17 17:07:22 +08:00
ibuler
3ed7477057 perf: 修改 default role 2022-03-17 16:53:29 +08:00
ibuler
a3cddd5d34 fix: 修复批量命令的bug 2022-03-17 16:26:22 +08:00
Jiangjie.Bai
26b3c60e5c fix: 删除一些perms相关的权限位 2022-03-17 16:26:00 +08:00
ibuler
b5dea38164 perf: 修改bug 2022-03-17 16:09:51 +08:00
ibuler
7addb881f6 fix: 修复系统用户Perms 2022-03-17 16:09:51 +08:00
fit2bot
9c395b674f fix: command log search (#7906)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-17 15:39:05 +08:00
Jiangjie.Bai
b297ebe973 fix: 修改应用category 2022-03-17 15:37:57 +08:00
fit2bot
5c7bfcff1c perf: 修改 i18n (#7904)
* perf: some perm to xpack

* perf: 修改 i18n

* perf: 修改 i18n

* perf: 修改 i18n

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-03-17 15:23:22 +08:00
fit2bot
76796f249d fix: user match (#7903)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-17 14:47:24 +08:00
Jiangjie.Bai
55a63477ed fix: 去除系统用户资产相关权限 2022-03-17 14:38:19 +08:00
ibuler
5942037d81 perf: some perm to xpack 2022-03-17 14:24:52 +08:00
Jiangjie.Bai
5882b8a682 fix: 去除application secret change perm 2022-03-17 14:24:25 +08:00
fit2bot
34e75099a3 perf: 设置默认的角色,系统用户角色添加权限 (#7898)
* perf: 修改 role handler

* perf: 设置默认的角色,系统用户角色添加权限

* perf: authentication 还是放到系统中吧

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-03-17 14:08:16 +08:00
feng626
8fe84345e4 fix: 工单默认权限 2022-03-17 14:06:34 +08:00
Jiangjie.Bai
a31c3ccc30 fix: 去除application account delete perm 2022-03-17 14:05:08 +08:00
Jiangjie.Bai
e13e34098a fix: 站内信命令告警中的会话地址 2022-03-17 14:05:08 +08:00
feng626
e8653c74cd fix: ticket URL type 2022-03-17 12:35:08 +08:00
feng626
1433c35ff9 fix: test gateway 2022-03-17 12:33:59 +08:00
Jiangjie.Bai
a237b5a63d fix: 去掉terminal task add/delete 权限位 2022-03-17 11:40:54 +08:00
feng626
2587c8693e fix: account_change_pwd 2022-03-17 11:36:48 +08:00
feng626
dfe5e2bce3 fix: 修改rbac permission 2022-03-17 11:36:48 +08:00
Jiangjie.Bai
91a34d1a88 Merge pull request #7888 from jumpserver/dev
v2.20.0-rc5
2022-03-16 20:49:53 +08:00
Jiangjie.Bai
1a05a942c2 fix: 更新资产详情授权用户权限 2022-03-16 20:49:29 +08:00
Jiangjie.Bai
30556023d1 fix: 修复会话录像播放权限 2022-03-16 20:42:38 +08:00
fit2bot
aa022a02c1 fix: 修复用户认证失败的详细信息显示 (#7886)
* fix: 修复用户认证失败的详细信息显示

* fix: 更新授权树翻译

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-03-16 20:41:54 +08:00
fit2bot
433d829c29 fix: 添加profile myorgs (#7887)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-16 20:36:12 +08:00
fit2bot
3b507dc795 perf: 优化英文下树的显示 (#7883)
* perf: 优化英文下树的显示

* perf: 修改翻译

* perf: 修改翻译

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-16 20:27:38 +08:00
Jiangjie.Bai
8233c69038 fix: 修复adhoc权限 2022-03-16 19:26:40 +08:00
ibuler
0fbc548c02 fix: 修复访问 flower 错误 2022-03-16 19:05:06 +08:00
Jiangjie.Bai
aa9ae14e46 fix: 修复view action获取 2022-03-16 18:33:55 +08:00
Jiangjie.Bai
04b35ba520 fix: 修复批量命令权限位 2022-03-16 18:33:13 +08:00
ibuler
580d2cd80b perf: 修改 org admin 2022-03-16 18:26:16 +08:00
ibuler
da9136f7af perf: 修改 org role 2022-03-16 18:26:16 +08:00
ibuler
1ce2706f20 perf: 修改翻译 2022-03-16 18:26:16 +08:00
ibuler
bbdeba3659 perf: 修复 org admin all 2022-03-16 18:26:16 +08:00
feng626
3a26b9d102 fix: 修复工单相应bug 2022-03-16 17:40:51 +08:00
ibuler
ee757e261d perf: 修改写法 2022-03-16 17:14:42 +08:00
ibuler
f41e6db007 perf: sso token 只能超级管理员 2022-03-16 17:14:42 +08:00
ibuler
7eed7b32cc perf: 修复 org role binding 在root组织下看到的可能不对 2022-03-16 17:14:42 +08:00
Jiangjie.Bai
efb26132f6 fix: 修复sso用户登录失败的问题 2022-03-16 17:11:22 +08:00
Jiangjie.Bai
572c5b6925 fix: 修改工单管理权限位 2022-03-16 14:54:50 +08:00
ibuler
8a1cd7e2a9 perf: fix some bug 2022-03-16 14:18:01 +08:00
Jiangjie.Bai
c065f82d30 fix: 排除收集用户执行的更新、删除权限位 2022-03-16 14:17:35 +08:00
ibuler
995c9a6c19 perf: 修复一些bug, rolebingding 找到合适的 2022-03-16 14:08:54 +08:00
Jiangjie.Bai
5ec970fab4 fix: 全局组织受权限位控制 2022-03-16 14:07:00 +08:00
Jiangjie.Bai
166745baf6 Merge pull request #7866 from jumpserver/dev
v2.20.0 rc4
2022-03-15 20:54:40 +08:00
Jiangjie.Bai
d320443c9f fix: 去除change_setting权限位 2022-03-15 20:25:18 +08:00
ibuler
4bfa88f01f perf: 修改 app tree 2022-03-15 20:09:30 +08:00
Jiangjie.Bai
aedd8ba589 fix: 修复查看我的应用翻译 2022-03-15 19:51:59 +08:00
Jiangjie.Bai
172b492bc3 fix: 修复存储测试权限位 2022-03-15 19:41:25 +08:00
Jiangjie.Bai
c4280d259a fix: 修复翻译/权限设置 2022-03-15 19:18:01 +08:00
Jiangjie.Bai
b25ec559bb fix: 修复翻译/权限设置 2022-03-15 19:18:01 +08:00
ibuler
cd46c8c78e perf: 发送验证码,不再提示 2022-03-15 18:47:12 +08:00
ibuler
8839e6293b perf: 修改 mfa 地址 2022-03-15 18:47:12 +08:00
ibuler
4f887b1b11 perf: 优化 tree node icon 2022-03-15 18:46:11 +08:00
fit2bot
90840a4417 fix: 工单创建bug (#7858)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-15 18:22:13 +08:00
fit2bot
a18b9bad0a fix:  修复ticketsession (#7857)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-15 17:32:54 +08:00
feng626
e1a238b778 fix: 删除资产授权缓存接口 2022-03-15 16:55:45 +08:00
吴小白
ee44ae2e12 perf: 优化构建速度 2022-03-15 16:55:10 +08:00
fit2bot
d77e84e6f8 fix: ticket comment bug (#7854)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-15 16:50:51 +08:00
Jiangjie.Bai
2042c7a6e5 fix: 云同步权限显示创建执行 2022-03-15 14:18:37 +08:00
Jiangjie.Bai
40aca26155 fix: 暂时排除会话分享的权限位 2022-03-15 13:13:16 +08:00
Jiangjie.Bai
e18e76002c fix: 暂时排除会话分享的权限位 2022-03-15 13:00:56 +08:00
Jiangjie.Bai
c77f02b295 Merge pull request #7844 from jumpserver/dev
v2.20.0-rc3
2022-03-15 11:37:30 +08:00
Jiangjie.Bai
3924ff0114 fix: 修改查看会话录像权限位 2022-03-15 11:37:09 +08:00
Jiangjie.Bai
6a0264ad3b fix: 删除权限位connect_myasset/myapp 2022-03-15 11:11:37 +08:00
Jiangjie.Bai
2d7349d596 fix: 删除权限位connect_myasset/myapp 2022-03-15 11:11:37 +08:00
feng626
c41a81c8d0 fix: 修改授权权限 2022-03-15 11:06:48 +08:00
feng626
7ba19ab1a1 fix: exclude adhoc change delete 2022-03-14 20:36:02 +08:00
Jiangjie.Bai
72247d1df3 fix: 修复批量命令权限 2022-03-14 20:35:43 +08:00
feng626
faf82d7cfb fix: del connect my asset 2022-03-14 20:34:35 +08:00
feng626
4e8defc647 fix: 修复上传bug 2022-03-14 19:15:12 +08:00
Jiangjie.Bai
2f18208874 fix: 修复资产改密计划权限控制 2022-03-14 19:01:26 +08:00
Jiangjie.Bai
b37e8cdc3f fix: 修复更新资产账号权限控制 2022-03-14 17:51:03 +08:00
Jiangjie.Bai
5b960fc46b fix: 修复测试资产账号权限 2022-03-14 17:51:03 +08:00
fit2bot
df51c82cfd perf: 优化Migration,删掉原来的 content type (#7835)
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-03-14 17:10:34 +08:00
feng626
e9deb6fc7a fix: app relation api 2022-03-14 17:05:47 +08:00
Jiangjie.Bai
cca49fa9cd fix: 修复授权树显示 2022-03-14 16:47:29 +08:00
Jiangjie.Bai
cfed849175 Merge pull request #7834 from jumpserver/dev
fix: 修复setting perm
2022-03-14 15:53:11 +08:00
feng626
a7cc457f54 fix: 修复setting perm 2022-03-14 15:50:53 +08:00
Jiangjie.Bai
5996cedcd6 Merge pull request #7832 from jumpserver/dev
fix: 修复权限问题
2022-03-14 15:16:51 +08:00
feng626
567c1b0124 fix: node test refresh perm 2022-03-14 14:37:59 +08:00
fit2bot
2da541c127 fix: 修改授权树(账号备份) (#7830)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-03-14 14:23:13 +08:00
jiangweidong
794139782f feat: JumpServer支持部署在使用了ssl的redis上,可使用证书连接 2022-03-14 11:35:14 +08:00
Jiangjie.Bai
307b739a03 fix: 修改授权树(收集用户) 2022-03-14 11:34:14 +08:00
feng626
ca5708988a fix: 修复saml2 auth 2022-03-14 11:17:15 +08:00
ibuler
90d84f4d69 perf: 修改消息订阅 2022-03-14 11:16:54 +08:00
feng626
758f418f63 perf: 删除多余权限 2022-03-14 10:48:04 +08:00
Jiangjie.Bai
a64ec8a1d2 Merge pull request #7825 from jumpserver/dev
v2.20.0-rc2
2022-03-14 10:38:35 +08:00
ibuler
60564d1b4f perf: 修改 bug 2022-03-14 10:01:51 +08:00
fit2bot
017710c056 perf: 修改perms (#7822)
* perf: 修改 perm tree

* perf: 修改perms

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-11 21:24:07 +08:00
Jiangjie.Bai
a876a82a76 fix: 从组织移除用户 2022-03-11 21:20:44 +08:00
Jiangjie.Bai
8423ae602f fix: 移除TICKET_ENABLED配置;系统设置API限制权限 2022-03-11 21:02:26 +08:00
Jiangjie.Bai
8e2471c1eb fix: 移除TICKET_ENABLED配置;系统设置API限制权限 2022-03-11 21:02:26 +08:00
Jiangjie.Bai
224a9fbdb3 fix: 修复创建授权时actions为空保存时报错的问题 2022-03-11 21:00:37 +08:00
ibuler
797b184c7f perf: 修改 perm tree 2022-03-11 20:31:38 +08:00
fit2bot
b3632f6531 fix: dingtalk auth perm (#7814)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-11 19:32:23 +08:00
fit2bot
e3bc54e764 fix: 修复用户组添加用户 (#7809)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-11 19:31:55 +08:00
fit2bot
f0325c48df fix: 工单权限 (#7808)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-11 19:31:29 +08:00
fit2bot
416d4bd0c3 fix: 修复tree (#7802)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-11 17:24:28 +08:00
fit2bot
10c877c120 fix: 修复工单ticket exclude perm (#7799)
* fix: 修复工单ticket exclude perm

* fix: 修复perm tree

Co-authored-by: feng626 <1304903146@qq.com>
2022-03-11 13:30:14 +08:00
jiangweidong
f04378eaf8 feat: JumpServer支持部署在使用了ssl的redis上 2022-03-11 12:56:22 +08:00
fit2bot
b644c47173 perf: 排除工单流权限 (#7798)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-11 11:04:21 +08:00
老广
45331dc9e8 Merge pull request #7796 from jumpserver/dev
v2.20.0-rc1
2022-03-10 20:34:18 +08:00
ibuler
8a565b9eef perf: 修改设置 2022-03-10 20:30:41 +08:00
ibuler
4eb7b50b52 perf: 修改设置 2022-03-10 20:30:41 +08:00
Jiangjie.Bai
fd64bd03b4 fix: 修改LDAP配置API权限 2022-03-10 19:03:51 +08:00
Jiangjie.Bai
9c75147179 fix: 修改LDAP配置API权限 2022-03-10 19:03:51 +08:00
fit2bot
147e4cce94 perf: 修改 migrations (#7794)
* perf: 优化 auditor 权限

* perf: 修改 migrations

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-10 18:55:53 +08:00
fit2bot
d1e25e1fef fix: 删除应用/授权应用相关权限 (#7792)
* fix: 删除应用/授权应用相关权限

* fix: 删rbac清除code的一些迁移文件;增加到utils目录下

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-03-10 18:51:45 +08:00
fit2bot
af2ba07338 fix: exclude (#7791)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-10 18:11:33 +08:00
fit2bot
29b9adb684 perf: 修复一把数据 (#7789)
* fix: 优化 perm exclude

* perf: 修复一把数据

perf: 修复一些数据

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-10 17:40:31 +08:00
fit2bot
64e0860d24 fix: 修复app suggestion perm bug (#7788)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-10 17:24:24 +08:00
ibuler
9934007397 fix: 优化 perm exclude 2022-03-10 16:56:32 +08:00
Jiangjie.Bai
4044a71aea revert: 回滚权限树 2022-03-10 15:27:15 +08:00
fit2bot
9725f0c963 fix: 修复工单流500bug (#7784)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-10 11:26:34 +08:00
Jiangjie.Bai
b017e68a56 Perf: 优化RBAC权限树 (#7782)
* fix: 优化权限树(1)

* fix: 优化权限树(2)

* fix: 优化权限树(3)

* fix: 优化权限树(4)

* fix: 优化权限树(5)

* fix: 优化权限树(添加迁移文件)

* fix: 优化权限树(6)

* fix: 优化权限树(7)

* fix: 优化权限树(8)

* fix: 优化权限树(9)
2022-03-10 11:25:33 +08:00
fit2bot
9ca0eaf7ce fix: 修改校对资产数量permbug (#7777)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-09 15:01:44 +08:00
fit2bot
94e60e180e fix: 修复新建组织用户列表获取到是全局用户bug (#7776)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-09 11:17:28 +08:00
fit2bot
8ed221ea5a fix: org role perm (#7775)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-08 20:22:10 +08:00
fit2bot
42ebb1f82f fix: 删除组织角色时 判断全局绑定用户数量 (#7774)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-08 20:00:19 +08:00
ibuler
9492518773 perf: 修改 redis conn 2022-03-08 18:49:50 +08:00
feng626
1cca9c10fb perf: 修改perm判断逻辑 2022-03-08 18:41:50 +08:00
feng626
c4a6715eb8 fix: 命令存储 or 命令权限 2022-03-08 17:43:26 +08:00
ibuler
4c31b5ec0f perf: 去掉一些 perm 2022-03-08 17:33:06 +08:00
fit2bot
9fd7fa9339 fix: add auditor perm (#7768)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-08 15:53:02 +08:00
feng626
a930f3aab3 fix: 修复创建更新用户给定默认权限 2022-03-08 14:07:02 +08:00
ibuler
5081fb5fe7 perf: 优化perm tree, 并添加缓存 2022-03-08 13:46:25 +08:00
fit2bot
cb072123d6 fix: 修复org admin ticket flow perm (#7765)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-08 12:42:46 +08:00
fit2bot
761265dec5 fix: 修复index api perm (#7761)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-08 10:43:01 +08:00
fit2bot
89de111acc fix: 修复特殊用户过滤 (#7759)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-07 20:09:52 +08:00
ibuler
14327ee398 perf: 优化账号备份api path 2022-03-07 19:56:27 +08:00
fit2bot
1b007c8c5c perf: 修改权限树 (#7757)
* perf: 修改 rbac tree

* perf: 修改权限树

* perf:  修改用户默认权限

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-07 19:02:37 +08:00
fit2bot
3222687aaa fix: 修复应用授权权限bug (#7756)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-07 18:42:22 +08:00
feng626
79994f5ddc fix: 资产授权权限bug 2022-03-07 18:25:06 +08:00
fit2bot
8271492ec1 fix: 修复查看资产密钥bug (#7754)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-07 17:55:35 +08:00
fit2bot
27560793f8 fix: 修复系统用户任务权限bug (#7753)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-07 16:01:02 +08:00
Jiangjie.Bai
615929dd43 fix: 修复可以删除已关联用户角色的问题 2022-03-07 15:05:58 +08:00
feng626
a1c1b128e9 fix: 修复relation_systemuser_perm问题 2022-03-07 14:53:38 +08:00
ibuler
fa2c70c6be perf: 修改 perm node title 2022-03-07 14:39:07 +08:00
Jiangjie.Bai
46e119db1f fix: 后台不限制roles的必填项 2022-03-07 13:56:13 +08:00
ibuler
0afff45bae fix: validate app perm error 2022-03-07 11:42:39 +08:00
fit2bot
31d219524b fix: 修复创建用户roles必填问题 (#7745)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-03-07 11:21:25 +08:00
fit2bot
a20884e2ad perf: 修改 rbac tree (#7743)
* perf: 修改 rbac tree

* perf: 修改verbose name

* fix: 修复系统用户

* fix: 还原 xpack

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-07 11:19:03 +08:00
feng626
eb6bddc599 fix: 拆分角色权限树router 2022-03-04 18:13:59 +08:00
Jiangjie.Bai
8a8ed90eef fix: 保留之前的jms_oidc_rp包 2022-03-04 18:12:46 +08:00
Jiangjie.Bai
75825f5baa fix: 删除jms_oidc_rp包中的表jms_oidc_rp_oidcuser 2022-03-04 18:12:46 +08:00
fit2bot
0141fce27d fix: 审计员add工作台 (#7738)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-04 10:46:45 +08:00
老广
3f9f9351f3 Fix rbac (#7737)
* perf: 修改 rbac role bingding

* fix: suggestion perm

* perf: 修改 requirements

* perf: 修改 rbac

* fix: auditor_perms

Co-authored-by: feng626 <1304903146@qq.com>
2022-03-04 10:16:21 +08:00
ibuler
390b8693df perf: 修改 signal handler 2022-03-03 10:03:06 +08:00
fit2bot
dafc416783 Fix rbac (#7728)
* perf: 重命名 signal handlers

* fix: 修复 ticket processor 问题

* perf: 修改 ticket 处理人api

* fix: 修复创建系统账号bug

* fix: 升级celery_beat==2.2.1和flower==1.0.0;修改celery进程启动参数先后顺序

* perf: 修改 authentication token

* fix: 修复上传权限bug

* fix: 登录页面增加i18n切换;

* fix: 系统角色删除限制

* perf: 修改一下 permissions tree

* perf: 生成 i18n

* perf: 修改一点点

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-03-02 20:48:43 +08:00
halo
04e46e4b1c fix: 修复节点创建不能指定id问题,兼容api 2022-03-02 16:24:03 +08:00
fit2bot
ab1024fbf4 perf: 优化 super ticket 的状态 (#7714)
* fix: token 系统用户增加 protocol

* fix: 修复清除orphan session时同时清除对应的 session_task

* perf: 修改 connection token api

* fix: 修复无法获取系统角色绑定的问题

* perf: 增加 db terminal 及 magnus 组件

* perf: 修改 migrations

* fix: 修复AUTHENTICATION_BACKENDS相关的逻辑

* fix: 修改判断backend认证逻辑

* fix: 修复资产账号查看密码跳过mfa

* fix: 修复用户组授权权限错误

* feat: 支持COS对象存储

* feat: 升级依赖 jms_storage==0.0.42

* fix: 修复 koko api 问题

* feat: 修改存储翻译信息

* perf: 修改 ticket 权限

* fix: 修复获取资产授权系统用户 get_queryset

* perf: 抽取 ticket

* perf: 修改 cmd filter 的权限

* fix: 修改 ticket perm

* perf: 修改工单的api

Co-authored-by: Eric <xplzv@126.com>
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: 小冯 <xiaofeng@xiaofengdeMacBook-Pro.local>
Co-authored-by: feng626 <1304903146@qq.com>
2022-02-28 20:22:14 +08:00
Jiangjie.Bai
03afa4f974 Fix rbac (#7713)
* fix: token 系统用户增加 protocol

* fix: 修复清除orphan session时同时清除对应的 session_task

* perf: 修改 connection token api

* fix: 修复无法获取系统角色绑定的问题

* perf: 增加 db terminal 及 magnus 组件

* perf: 修改 migrations

* fix: 修复AUTHENTICATION_BACKENDS相关的逻辑

* fix: 修改判断backend认证逻辑

* fix: 修复资产账号查看密码跳过mfa

* fix: 修复用户组授权权限错误

* feat: 支持COS对象存储

* feat: 升级依赖 jms_storage==0.0.42

* fix: 修复 koko api 问题

* feat: 修改存储翻译信息

* perf: 修改 ticket 权限

* fix: 修复获取资产授权系统用户 get_queryset

* perf: 抽取 ticket

* perf: 修改 cmd filter 的权限

* fix: 修改 ticket perm

* fix: 修复oidc依赖问题

Co-authored-by: Eric <xplzv@126.com>
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: 小冯 <xiaofeng@xiaofengdeMacBook-Pro.local>
Co-authored-by: feng626 <1304903146@qq.com>
2022-02-28 19:28:58 +08:00
Jiangjie.Bai
edfca5eb24 Fix rbac (#7699)
* perf: 优化 suggesstion

* perf: 修改 migrations

* feat: 添加OIDC认证逻辑

* perf: 修改 backend

* perf: 优化认证backends

* perf: 优化认证backends

* perf: 优化CAS认证, 用户多域名进行访问时回调到各自域名

Co-authored-by: ibuler <ibuler@qq.com>
2022-02-25 19:23:59 +08:00
老广
02ca473492 Fix rbac (#7690)
* perf: 优化 suggesstion

* perf: 修改 migrations
2022-02-24 12:03:40 +08:00
老广
484b75bb53 Merge pull request #7687 from jumpserver/fix_rbac
fix: rbac
2022-02-24 10:06:47 +08:00
xinwen
9c3fd59ef4 fix: 删除用户最近登录的资产接口 2022-02-23 18:35:53 +08:00
feng626
bbf3250161 fix: 优化用户列表接口性能 2022-02-23 18:35:10 +08:00
feng626
8604b9019f fix: 添加迁移文件 2022-02-23 18:32:30 +08:00
Jiangjie.Bai
966b4250b8 Merge branch 'fix_rbac' of github.com:jumpserver/jumpserver into fix_rbac 2022-02-23 18:28:47 +08:00
Jiangjie.Bai
291f2b0e13 fix: 修复用户列表出现服务账号的问题 2022-02-23 18:28:23 +08:00
Eric
a1d15ef206 fix: 修复管理员终断会话API 2022-02-23 17:10:02 +08:00
ibuler
e76eec530f perf: 添加migrations 2022-02-23 17:06:07 +08:00
ibuler
add4d8d2cd fix: 修改上传录像 api 权限 2022-02-23 15:47:12 +08:00
ibuler
c6ece550a9 perf: 修改翻译 2022-02-22 19:53:00 +08:00
xinwen
e3b620089a feat: 个人页面接口 2022-02-22 18:29:49 +08:00
Jiangjie.Bai
64f721875b fix: 修复用户页面点击资产树节点提示对象找不到的问题 2022-02-22 18:29:49 +08:00
ibuler
02d3747c70 perf: 添加监测密码的脚本 2022-02-22 18:29:49 +08:00
feng626
09494193ab fix: 修复批量操作权限bug 2022-02-22 18:29:49 +08:00
feng626
702111f578 fix: 修复系统用户sudo tab权限 2022-02-22 18:29:49 +08:00
feng626
e08db7423f fix: 修复资产账号测试可连接性权限问题 2022-02-22 18:29:49 +08:00
ibuler
0c95faac04 fix: 修复 cache set 2022-02-22 18:29:49 +08:00
fit2bot
534cbf1281 perf: 修改依赖版本 (#7666)
* perf: 修复一些错误权限位

* Pr@fix rbac@fix rbac permissions (#7648)

* fix: 确保每次 migrate 执行更新 role permissions

* perf: 修改 choices

* feat: 兼容apple m1

* perf: 修改 migrations role permissions

* perf: pymysql 导入

* perf: admin 判断

* perf: 修改依赖版本

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Aaron3S <chenyang@fit2cloud.com>
2022-02-22 18:29:49 +08:00
fit2bot
abe5fa9036 perf: 修改依赖库版本 (#7661)
* perf: 修复一些错误权限位

* Pr@fix rbac@fix rbac permissions (#7648)

* fix: 确保每次 migrate 执行更新 role permissions

* perf: 修改 choices

* feat: 兼容apple m1

* perf: 修改 migrations role permissions

* perf: pymysql 导入

* perf: admin 判断

* perf: 修改依赖库版本

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Aaron3S <chenyang@fit2cloud.com>
2022-02-22 18:29:49 +08:00
feng626
2a2f05e51c fix: 删除组织关联删除工单 工单流 2022-02-22 18:29:49 +08:00
xinwen
f460916e84 fix: swagger 2022-02-22 18:29:49 +08:00
jiangweidong
ad2cb233d7 feat: 支持 MongoDB 数据库的纳管 (#7631) 2022-02-22 10:09:48 +08:00
halo
0dbf035146 perf: 优化代码 2022-02-21 16:52:21 +08:00
halo
ea124fd0db fix: 修复keycloak配置转换为openid不生效问题,持久化到数据库 2022-02-21 16:52:21 +08:00
Jiangjie.Bai
83ff8dbf26 fix: rbac 合并 (#7658)
* perf: 修复一些错误权限位

* Pr@fix rbac@fix rbac permissions (#7648)

* fix: 确保每次 migrate 执行更新 role permissions

* perf: 修改 choices

* feat: 兼容apple m1

* perf: 修改 migrations role permissions

* perf: pymysql 导入

* perf: admin 判断

* fix: 修复消息订阅权限

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Aaron3S <chenyang@fit2cloud.com>
Co-authored-by: feng626 <1304903146@qq.com>
2022-02-21 16:24:03 +08:00
Jiangjie.Bai
783c163324 fix: 修复获取命令过滤规则时按照有组织的资源进行组织过滤 2022-02-19 11:44:03 +08:00
Jiangjie.Bai
3deced4ade fix: 修复登录系统用户temp_auth保存时构建的key和获取时不一致的问题 2022-02-18 20:07:19 +08:00
fit2bot
63de4e1806 perf: 添加 is_org_admin (#7644)
* fix: 修复 org members 的问题

* perf: 修改 org member

* perf: 修改 is sa

* perf: 修改 active

* perf: 修复写法

* perf: is_sa to is_service_account

Co-authored-by: ibuler <ibuler@qq.com>
2022-02-18 16:25:54 +08:00
ibuler
48d0c7b6cc fix: 修复 org members 的问题 2022-02-18 14:37:38 +08:00
ibuler
20cc8a124f perf: 修改connection token domain
perf: 添加 org_id
2022-02-18 14:10:22 +08:00
ibuler
db050e405d fix: 修改 migrations 2022-02-17 23:02:32 +08:00
fit2bot
e259d2a9e9 fix: fix rbac to dev (#7636)
* feat: 添加 RBAC 应用模块

* feat: 添加 RBAC Model、API

* feat: 添加 RBAC Model、API 2

* feat: 添加 RBAC Model、API 3

* feat: 添加 RBAC Model、API 4

* feat: RBAC

* feat: RBAC

* feat: RBAC

* feat: RBAC

* feat: RBAC

* feat: RBAC 整理权限位

* feat: RBAC 整理权限位2

* feat: RBAC 整理权限位2

* feat: RBAC 整理权限位

* feat: RBAC 添加默认角色

* feat: RBAC 添加迁移文件;迁移用户角色->用户角色绑定

* feat: RBAC 添加迁移文件;迁移用户角色->用户角色绑定

* feat: RBAC 修改用户模块API

* feat: RBAC 添加组织模块迁移文件 & 修改组织模块API

* feat: RBAC 添加组织模块迁移文件 & 修改组织模块API

* feat: RBAC 修改用户角色属性的使用

* feat: RBAC No.1

* xxx

* perf: 暂存

* perf: ...

* perf(rbac): 添加 perms 到 profile serializer 中

* stash

* perf: 使用init

* perf: 修改migrations

* perf: rbac

* stash

* stash

* pref: 修改rbac

* stash it

* stash: 先去修复其他bug

* perf: 修改 role 添加 users

* pref: 修改 RBAC Model

* feat: 添加权限的 tree api

* stash: 暂存一下

* stash: 暂存一下

* perf: 修改 model verbose name

* feat: 添加model各种 verbose name

* perf: 生成 migrations

* perf: 优化权限位

* perf: 添加迁移脚本

* feat: 添加组织角色迁移

* perf: 添加迁移脚本

* stash

* perf: 添加migrateion

* perf: 暂存一下

* perf: 修改rbac

* perf: stash it

* fix: 迁移冲突

* fix: 迁移冲突

* perf: 暂存一下

* perf: 修改 rbac 逻辑

* stash: 暂存一下

* perf: 修改内置角色

* perf: 解决 root 组织的问题

* perf: stash it

* perf: 优化 rbac

* perf: 优化 rolebinding 处理

* perf: 完成用户离开组织的问题

* perf: 暂存一下

* perf: 修改翻译

* perf: 去掉了 IsSuperUser

* perf: IsAppUser 去掉完成

* perf: 修改 connection token 的权限

* perf: 去掉导入的问题

* perf: perms define 格式,修改 app 用户 的全新啊

* perf: 修改 permission

* perf: 去掉一些 org admin

* perf: 去掉部分 org admin

* perf: 再去掉点 org admin role

* perf: 再去掉部分 org admin

* perf: user 角色搜索

* perf: 去掉很多 js

* perf: 添加权限位

* perf: 修改权限

* perf: 去掉一个 todo

* merge: with dev

* fix: 修复冲突

Co-authored-by: Bai <bugatti_it@163.com>
Co-authored-by: Michael Bai <baijiangjie@gmail.com>
Co-authored-by: ibuler <ibuler@qq.com>
2022-02-17 20:13:31 +08:00
489 changed files with 16394 additions and 66686 deletions

18
.github/workflows/lgtm.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
name: Send LGTM reaction
on:
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1.0.0
- uses: micnncim/action-lgtm-reaction@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
trigger: '["^.?lgtm$"]'

View File

@@ -8,7 +8,6 @@ WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
# 构建运行时环境
FROM python:3.8-slim
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
@@ -17,38 +16,66 @@ ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
WORKDIR /opt/jumpserver
COPY ./requirements/deb_requirements.txt ./requirements/deb_requirements.txt
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
sshpass"
ARG TOOLS=" \
curl \
default-mysql-client \
iproute2 \
iputils-ping \
locales \
procps \
redis-tools \
telnet \
vim \
wget"
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \
&& apt -y install telnet iproute2 redis-tools default-mysql-client vim wget curl locales procps \
&& apt -y install $(cat requirements/deb_requirements.txt) \
&& rm -rf /var/lib/apt/lists/* \
&& apt -y install ${BUILD_DEPENDENCIES} \
&& apt -y install ${DEPENDENCIES} \
&& apt -y install ${TOOLS} \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc
COPY ./requirements/requirements.txt ./requirements/requirements.txt
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN mkdir -p /root/.ssh/ \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \
&& rm -rf /var/lib/apt/lists/* \
&& mv /bin/sh /bin/sh.bak \
&& ln -s /bin/bash /bin/sh
RUN mkdir -p /opt/jumpserver/oracle/ \
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar > /dev/null \
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
RUN echo > config.yml
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN echo > config.yml \
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs

View File

@@ -1,20 +1,14 @@
from common.permissions import IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember
from common.drf.api import JMSBulkModelViewSet
from ..models import LoginACL
from .. import serializers
from ..filters import LoginAclFilter
__all__ = ['LoginACLViewSet', ]
__all__ = ['LoginACLViewSet']
class LoginACLViewSet(JMSBulkModelViewSet):
queryset = LoginACL.objects.all()
filterset_class = LoginAclFilter
search_fields = ('name',)
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LoginACLSerializer
def get_permissions(self):
if self.action in ["retrieve", "list"]:
self.permission_classes = (IsOrgAdmin, HasQueryParamsUserAndIsCurrentOrgMember)
return super().get_permissions()

View File

@@ -1,5 +1,4 @@
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin
from .. import models, serializers
@@ -10,5 +9,4 @@ class LoginAssetACLViewSet(OrgBulkModelViewSet):
model = models.LoginAssetACL
filterset_fields = ('name', )
search_fields = filterset_fields
permission_classes = (IsOrgAdmin, )
serializer_class = serializers.LoginAssetACLSerializer

View File

@@ -1,19 +1,23 @@
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from common.permissions import IsAppUser
from common.utils import reverse, lazyproperty
from orgs.utils import tmp_to_org
from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..models import LoginAssetACL
from .. import serializers
__all__ = ['LoginAssetCheckAPI', 'LoginAssetConfirmStatusAPI']
__all__ = ['LoginAssetCheckAPI']
class LoginAssetCheckAPI(CreateAPIView):
permission_classes = (IsAppUser,)
serializer_class = serializers.LoginAssetCheckSerializer
model = LoginAssetACL
rbac_perms = {
'POST': 'tickets.add_superticket'
}
def get_queryset(self):
return LoginAssetACL.objects.all()
def create(self, request, *args, **kwargs):
is_need_confirm, response_data = self.check_if_need_confirm()
@@ -46,7 +50,7 @@ class LoginAssetCheckAPI(CreateAPIView):
org_id=self.serializer.org.id
)
confirm_status_url = reverse(
view_name='api-acls:login-asset-confirm-status',
view_name='api-tickets:super-ticket-status',
kwargs={'pk': str(ticket.id)}
)
ticket_detail_url = reverse(
@@ -71,6 +75,3 @@ class LoginAssetCheckAPI(CreateAPIView):
serializer.is_valid(raise_exception=True)
return serializer
class LoginAssetConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass

View File

@@ -1,5 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class AclsConfig(AppConfig):
name = 'acls'
verbose_name = _('Acls')

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.1.13 on 2021-11-30 02:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('acls', '0002_auto_20210926_1047'),
]
operations = [
migrations.AlterModelOptions(
name='loginacl',
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login acl'},
),
migrations.AlterModelOptions(
name='loginassetacl',
options={'ordering': ('priority', '-date_updated', 'name'), 'verbose_name': 'Login asset acl'},
),
]

View File

@@ -12,7 +12,6 @@ router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl'
urlpatterns = [
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),
path('login-asset-confirm/<uuid:pk>/status/', api.LoginAssetConfirmStatusAPI.as_view(), name='login-asset-confirm-status')
]
urlpatterns += router.urls

View File

@@ -1,4 +1,3 @@
from .application import *
from .account import *
from .mixin import *
from .remote_app import *

View File

@@ -6,8 +6,10 @@ from django.db.models import F, Q
from common.drf.filters import BaseFilterSet
from common.drf.api import JMSBulkModelViewSet
from rbac.permissions import RBACPermission
from assets.models import SystemUser
from ..models import Account
from ..hands import IsOrgAdminOrAppUser, IsOrgAdmin, NeedMFAVerify
from ..hands import NeedMFAVerify
from .. import serializers
@@ -31,7 +33,8 @@ class AccountFilterSet(BaseFilterSet):
username = self.get_query_param('username')
if not username:
return qs
qs = qs.filter(Q(username=username) | Q(systemuser__username=username)).distinct()
q = Q(username=username) | Q(systemuser__username=username)
qs = qs.filter(q).distinct()
return qs
@@ -41,14 +44,21 @@ class ApplicationAccountViewSet(JMSBulkModelViewSet):
filterset_class = AccountFilterSet
filterset_fields = ['username', 'app_display', 'type', 'category', 'app']
serializer_class = serializers.AppAccountSerializer
permission_classes = (IsOrgAdmin,)
def get_queryset(self):
queryset = Account.get_queryset()
return queryset
class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
perm_model = SystemUser
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [IsOrgAdminOrAppUser, NeedMFAVerify]
permission_classes = [RBACPermission, NeedMFAVerify]
http_method_names = ['get', 'options']
rbac_perms = {
'retrieve': 'applications.view_applicationaccountsecret',
'list': 'applications.view_applicationaccountsecret',
}

View File

@@ -7,7 +7,6 @@ from rest_framework.response import Response
from common.tree import TreeNodeSerializer
from common.mixins.api import SuggestionMixin
from ..hands import IsOrgAdminOrAppUser
from .. import serializers
from ..models import Application
@@ -18,16 +17,19 @@ class ApplicationViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Application
filterset_fields = {
'name': ['exact'],
'category': ['exact'],
'category': ['exact', 'in'],
'type': ['exact', 'in'],
}
search_fields = ('name', 'type', 'category')
permission_classes = (IsOrgAdminOrAppUser,)
serializer_classes = {
'default': serializers.AppSerializer,
'get_tree': TreeNodeSerializer,
'suggestion': serializers.MiniAppSerializer
}
rbac_perms = {
'get_tree': 'applications.view_application',
'match': 'applications.match_application'
}
@action(methods=['GET'], detail=False, url_path='tree')
def get_tree(self, request, *args, **kwargs):

View File

@@ -2,10 +2,8 @@
#
from orgs.mixins import generics
from ..hands import IsAppUser
from .. import models
from ..serializers import RemoteAppConnectionInfoSerializer
from ..permissions import IsRemoteApp
__all__ = [
@@ -15,5 +13,4 @@ __all__ = [
class RemoteAppConnectionInfoApi(generics.RetrieveAPIView):
model = models.Application
permission_classes = (IsAppUser, IsRemoteApp)
serializer_class = RemoteAppConnectionInfoSerializer

View File

@@ -1,7 +1,9 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.apps import AppConfig
class ApplicationsConfig(AppConfig):
name = 'applications'
verbose_name = _('Applications')

View File

@@ -1,10 +1,10 @@
# coding: utf-8
#
from django.db.models import TextChoices
from django.db import models
from django.utils.translation import ugettext_lazy as _
class AppCategory(TextChoices):
class AppCategory(models.TextChoices):
db = 'db', _('Database')
remote_app = 'remote_app', _('Remote app')
cloud = 'cloud', 'Cloud'
@@ -13,15 +13,20 @@ class AppCategory(TextChoices):
def get_label(cls, category):
return dict(cls.choices).get(category, '')
@classmethod
def is_xpack(cls, category):
return category in ['remote_app']
class AppType(TextChoices):
class AppType(models.TextChoices):
# db category
mysql = 'mysql', 'MySQL'
redis = 'redis', 'Redis'
mariadb = 'mariadb', 'MariaDB'
oracle = 'oracle', 'Oracle'
pgsql = 'postgresql', 'PostgreSQL'
mariadb = 'mariadb', 'MariaDB'
sqlserver = 'sqlserver', 'SQLServer'
redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB'
# remote-app category
chrome = 'chrome', 'Chrome'
@@ -36,9 +41,13 @@ class AppType(TextChoices):
def category_types_mapper(cls):
return {
AppCategory.db: [
cls.mysql, cls.oracle, cls.redis, cls.pgsql, cls.mariadb, cls.sqlserver
cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
cls.sqlserver, cls.redis, cls.mongodb
],
AppCategory.remote_app: [
cls.chrome, cls.mysql_workbench,
cls.vmware_client, cls.custom
],
AppCategory.remote_app: [cls.chrome, cls.mysql_workbench, cls.vmware_client, cls.custom],
AppCategory.cloud: [cls.k8s]
}
@@ -65,3 +74,12 @@ class AppType(TextChoices):
@classmethod
def cloud_types(cls):
return [tp.value for tp in cls.category_types_mapper()[AppCategory.cloud]]
@classmethod
def is_xpack(cls, tp):
tp_category_mapper = cls.type_category_mapper()
category = tp_category_mapper[tp]
if AppCategory.is_xpack(category):
return True
return tp in ['oracle', 'postgresql', 'sqlserver']

View File

@@ -11,5 +11,5 @@
"""
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser, NeedMFAVerify
from common.permissions import NeedMFAVerify
from users.models import User, UserGroup

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.13 on 2022-02-17 13:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('applications', '0016_auto_20220118_1455'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'permissions': [('view_applicationaccountsecret', 'Can view application account secret'), ('change_appplicationaccountsecret', 'Can change application account secret')], 'verbose_name': 'Application account'},
),
migrations.AlterModelOptions(
name='applicationuser',
options={'verbose_name': 'Application user'},
),
migrations.AlterModelOptions(
name='historicalaccount',
options={'get_latest_by': 'history_date', 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Application account'},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.13 on 2022-02-23 07:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0017_auto_20220217_2135'),
]
operations = [
migrations.AlterField(
model_name='application',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.14 on 2022-03-10 10:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('applications', '0018_auto_20220223_1539'),
]
operations = [
migrations.AlterModelOptions(
name='application',
options={'ordering': ('name',), 'permissions': [('match_application', 'Can match application')], 'verbose_name': 'Application'},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2022-03-16 12:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0019_auto_20220310_1853'),
]
operations = [
migrations.AlterField(
model_name='application',
name='type',
field=models.CharField(choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type'),
),
]

View File

@@ -20,8 +20,12 @@ class Account(BaseUser):
auth_attrs = ['username', 'password', 'private_key', 'public_key']
class Meta:
verbose_name = _('Account')
verbose_name = _('Application account')
unique_together = [('username', 'app', 'systemuser')]
permissions = [
('view_applicationaccountsecret', _('Can view application account secret')),
('change_appplicationaccountsecret', _('Can change application account secret')),
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -3,10 +3,12 @@ from urllib.parse import urlencode, parse_qsl
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from common.tree import TreeNode
from common.utils import is_uuid
from assets.models import Asset, SystemUser
from ..utils import KubernetesTree
@@ -18,6 +20,7 @@ class ApplicationTreeNodeMixin:
name: str
type: str
category: str
attrs: dict
@staticmethod
def create_tree_id(pid, type, v):
@@ -79,6 +82,8 @@ class ApplicationTreeNodeMixin:
nodes = []
categories = const.AppType.category_types_mapper().keys()
for category in categories:
if not settings.XPACK_ENABLED and const.AppCategory.is_xpack(category):
continue
i = cls.create_tree_id(pid, 'category', category.value)
node = cls.create_choice_node(
category, i, pid=pid, tp='category',
@@ -96,7 +101,10 @@ class ApplicationTreeNodeMixin:
temp_pid = pid
type_category_mapper = const.AppType.type_category_mapper()
types = const.AppType.type_category_mapper().keys()
for tp in types:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
continue
category = type_category_mapper.get(tp)
pid = cls.create_tree_id(pid, 'category', category.value)
i = cls.create_tree_id(pid, 'type', tp.value)
@@ -137,7 +145,6 @@ class ApplicationTreeNodeMixin:
pid, counts, show_empty=show_empty,
show_count=show_count
)
return tree_nodes
@classmethod
@@ -155,6 +162,8 @@ class ApplicationTreeNodeMixin:
# 应用的节点
for app in queryset:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(app.type):
continue
node = app.as_tree_node(root_node.id)
tree_nodes.append(node)
return tree_nodes
@@ -164,13 +173,18 @@ class ApplicationTreeNodeMixin:
pid = self.create_tree_id(pid, 'type', self.type)
return pid
def as_tree_node(self, pid, is_luna=False):
if is_luna and self.type == const.AppType.k8s:
def as_tree_node(self, pid, k8s_as_tree=False):
if self.type == const.AppType.k8s and k8s_as_tree:
node = KubernetesTree(pid).as_tree_node(self)
else:
node = self._as_tree_node(pid)
return node
def _attrs_to_tree(self):
if self.category == const.AppCategory.db:
return self.attrs
return {}
def _as_tree_node(self, pid):
icon_skin_category_mapper = {
'remote_app': 'chrome',
@@ -192,6 +206,7 @@ class ApplicationTreeNodeMixin:
'data': {
'category': self.category,
'type': self.type,
'attrs': self._attrs_to_tree()
}
}
})
@@ -219,6 +234,9 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
verbose_name = _('Application')
unique_together = [('org_id', 'name')]
ordering = ('name',)
permissions = [
('match_application', _('Can match application')),
]
def __str__(self):
category_display = self.get_category_display()
@@ -229,6 +247,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_remote_app(self):
return self.category == const.AppCategory.remote_app.value
@property
def category_cloud(self):
return self.category == const.AppCategory.cloud.value
@property
def category_db(self):
return self.category == const.AppCategory.db.value
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app:
@@ -254,14 +280,26 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
'parameters': parameters
}
def get_remote_app_asset(self):
def get_remote_app_asset(self, raise_exception=True):
asset_id = self.attrs.get('asset')
if not asset_id:
if is_uuid(asset_id):
return Asset.objects.filter(id=asset_id).first()
if raise_exception:
raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first()
return asset
def get_target_ip(self):
target_ip = ''
if self.category_remote_app:
asset = self.get_remote_app_asset()
target_ip = asset.ip if asset else target_ip
elif self.category_cloud:
target_ip = self.attrs.get('cluster')
elif self.category_db:
target_ip = self.attrs.get('host')
return target_ip
class ApplicationUser(SystemUser):
class Meta:
proxy = True
verbose_name = _('Application user')

View File

@@ -119,7 +119,8 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
'username': {'default': '', 'required': False},
'password': {'write_only': True},
'app_display': {'label': _('Application display')},
'systemuser_display': {'label': _('System User')}
'systemuser_display': {'label': _('System User')},
'account': {'label': _('account')}
}
use_model_bulk_create = True
model_bulk_create_kwargs = {

View File

@@ -1,10 +1,11 @@
from .mysql import *
from .redis import *
from .mariadb import *
from .oracle import *
from .pgsql import *
from .sqlserver import *
from .redis import *
from .mongodb import *
from .chrome import *
from .mysql_workbench import *

View File

@@ -0,0 +1,11 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['MongoDBSerializer']
class MongoDBSerializer(DBSerializer):
port = serializers.IntegerField(default=27017, label=_('Port'), allow_null=True)

View File

@@ -25,11 +25,12 @@ category_serializer_classes_mapping = {
type_serializer_classes_mapping = {
# db
const.AppType.mysql.value: application_type.MySQLSerializer,
const.AppType.redis.value: application_type.RedisSerializer,
const.AppType.mariadb.value: application_type.MariaDBSerializer,
const.AppType.oracle.value: application_type.OracleSerializer,
const.AppType.pgsql.value: application_type.PostgreSerializer,
const.AppType.sqlserver.value: application_type.SQLServerSerializer,
const.AppType.redis.value: application_type.RedisSerializer,
const.AppType.mongodb.value: application_type.MongoDBSerializer,
# cloud
const.AppType.k8s.value: application_type.K8SSerializer
}

View File

@@ -11,6 +11,7 @@ app_name = 'applications'
router = BulkRouter()
router.register(r'applications', api.ApplicationViewSet, 'application')
router.register(r'accounts', api.ApplicationAccountViewSet, 'application-account')
router.register(r'system-users-apps-relations', api.SystemUserAppRelationViewSet, 'system-users-apps-relation')
router.register(r'account-secrets', api.ApplicationAccountSecretViewSet, 'application-account-secret')

View File

@@ -10,4 +10,4 @@ from .domain import *
from .cmd_filter import *
from .gathered_user import *
from .favorite_asset import *
from .backup import *
from .account_backup import *

View File

@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
#
from rest_framework import status, mixins, viewsets
from rest_framework import status, viewsets
from rest_framework.response import Response
from common.permissions import IsOrgAdmin
from orgs.mixins.api import OrgBulkModelViewSet
from .. import serializers
from ..tasks import execute_account_backup_plan
from ..models import (
@@ -24,17 +22,13 @@ class AccountBackupPlanViewSet(OrgBulkModelViewSet):
ordering_fields = ('name',)
ordering = ('name',)
serializer_class = serializers.AccountBackupPlanSerializer
permission_classes = (IsOrgAdmin,)
class AccountBackupPlanExecutionViewSet(
mixins.CreateModelMixin, mixins.ListModelMixin,
mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AccountBackupPlanExecutionSerializer
search_fields = ('trigger',)
filterset_fields = ('trigger', 'plan_id')
permission_classes = (IsOrgAdmin,)
http_method_names = ['get', 'post', 'options']
def get_queryset(self):
queryset = AccountBackupPlanExecution.objects.all()

View File

@@ -1,13 +1,14 @@
from django.db.models import F, Q
from rest_framework.decorators import action
from django_filters import rest_framework as filters
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
from orgs.mixins.api import OrgBulkModelViewSet
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, NeedMFAVerify
from rbac.permissions import RBACPermission
from common.drf.filters import BaseFilterSet
from common.permissions import NeedMFAVerify
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node
from .. import serializers
@@ -62,7 +63,10 @@ class AccountViewSet(OrgBulkModelViewSet):
'default': serializers.AccountSerializer,
'verify_account': serializers.AssetTaskSerializer
}
permission_classes = (IsOrgAdmin,)
rbac_perms = {
'verify_account': 'assets.test_authbook',
'partial_update': 'assets.change_assetaccountsecret',
}
def get_queryset(self):
queryset = AuthBook.get_queryset()
@@ -82,17 +86,23 @@ class AccountSecretsViewSet(AccountViewSet):
serializer_classes = {
'default': serializers.AccountSecretSerializer
}
permission_classes = (IsOrgAdmin, NeedMFAVerify)
http_method_names = ['get']
permission_classes = [RBACPermission, NeedMFAVerify]
rbac_perms = {
'list': 'assets.view_assetaccountsecret',
'retrieve': 'assets.view_assetaccountsecret',
}
class AccountTaskCreateAPI(CreateAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.AccountTaskSerializer
filterset_fields = AccountViewSet.filterset_fields
search_fields = AccountViewSet.search_fields
filterset_class = AccountViewSet.filterset_class
def check_permissions(self, request):
return request.user.has_perm('assets.test_assetconnectivity')
def get_accounts(self):
queryset = AuthBook.objects.all()
queryset = self.filter_queryset(queryset)
@@ -109,5 +119,4 @@ class AccountTaskCreateAPI(CreateAPIView):
def get_exception_handler(self):
def handler(e, context):
return Response({"error": str(e)}, status=400)
return handler

View File

@@ -2,9 +2,9 @@ from django.db.models import Count
from orgs.mixins.api import OrgBulkModelViewSet
from common.utils import get_logger
from ..hands import IsOrgAdmin
from ..models import SystemUser
from .. import serializers
from rbac.permissions import RBACPermission
logger = get_logger(__file__)
@@ -20,7 +20,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
filterset_fields = ("name", "username")
search_fields = filterset_fields
serializer_class = serializers.AdminUserSerializer
permission_classes = (IsOrgAdmin,)
permission_classes = (RBACPermission,)
ordering_fields = ('name',)
ordering = ('name', )

View File

@@ -1,13 +1,11 @@
# -*- coding: utf-8 -*-
#
from assets.api import FilterAssetByNodeMixin
from rest_framework.viewsets import ModelViewSet
from rest_framework.generics import RetrieveAPIView, ListAPIView
from django.shortcuts import get_object_or_404
from django.db.models import Q
from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsSuperUser
from common.mixins.api import SuggestionMixin
from users.models import User, UserGroup
from users.serializers import UserSerializer, UserGroupSerializer
@@ -17,7 +15,8 @@ from perms.serializers import AssetPermissionSerializer
from perms.filters import AssetPermissionFilter
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from ..models import Asset, Node, Platform
from assets.api import FilterAssetByNodeMixin
from ..models import Asset, Node, Platform, Gateway
from .. import serializers
from ..tasks import (
update_assets_hardware_info_manual, test_assets_connectivity_manual,
@@ -55,7 +54,9 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
'default': serializers.AssetSerializer,
'suggestion': serializers.MiniAssetSerializer
}
permission_classes = (IsOrgAdminOrAppUser,)
rbac_perms = {
'match': 'assets.match_asset'
}
extra_filter_backends = [FilterAssetByNodeFilterBackend, LabelFilterBackend, IpInFilterBackend]
def set_assets_node(self, assets):
@@ -76,8 +77,10 @@ class AssetViewSet(SuggestionMixin, FilterAssetByNodeMixin, OrgBulkModelViewSet)
class AssetPlatformRetrieveApi(RetrieveAPIView):
queryset = Platform.objects.all()
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.PlatformSerializer
rbac_perms = {
'retrieve': 'assets.view_gateway'
}
def get_object(self):
asset_pk = self.kwargs.get('pk')
@@ -87,16 +90,10 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
class AssetPlatformViewSet(ModelViewSet):
queryset = Platform.objects.all()
permission_classes = (IsSuperUser,)
serializer_class = serializers.PlatformSerializer
filterset_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:
self.permission_denied(
@@ -131,7 +128,6 @@ class AssetsTaskMixin:
class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetTaskSerializer
permission_classes = (IsOrgAdmin,)
def create(self, request, *args, **kwargs):
pk = self.kwargs.get('pk')
@@ -139,11 +135,26 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
request.data['assets'] = [pk]
return super().create(request, *args, **kwargs)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo',
'push_system_user': 'assets.push_assetsystemuser',
'test': 'assets.test_assetconnectivity',
'test_system_user': 'assets.test_assetconnectivity'
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
def perform_asset_task(self, serializer):
data = serializer.validated_data
action = data['action']
if action not in ['push_system_user', 'test_system_user']:
return
asset = data['asset']
system_users = data.get('system_users')
if not system_users:
@@ -166,24 +177,37 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
model = Asset
serializer_class = serializers.AssetsTaskSerializer
permission_classes = (IsOrgAdmin,)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo',
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
class AssetGatewayListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewayWithAuthSerializer
rbac_perms = {
'list': 'assets.view_gateway'
}
def get_queryset(self):
asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
if not asset.domain:
return []
return Gateway.objects.none()
queryset = asset.domain.gateways.filter(protocol='ssh')
return queryset
class BaseAssetPermUserOrUserGroupListApi(ListAPIView):
permission_classes = (IsOrgAdmin,)
rbac_perms = {
'GET': 'perms.view_assetpermission'
}
def get_object(self):
asset_id = self.kwargs.get('pk')
@@ -201,6 +225,9 @@ class AssetPermUserListApi(BaseAssetPermUserOrUserGroupListApi):
filterset_class = UserFilter
search_fields = ('username', 'email', 'name', 'id', 'source', 'role')
serializer_class = UserSerializer
rbac_perms = {
'GET': 'perms.view_assetpermission'
}
def get_queryset(self):
perms = self.get_asset_related_perms()
@@ -220,11 +247,13 @@ class AssetPermUserGroupListApi(BaseAssetPermUserOrUserGroupListApi):
class BaseAssetPermUserOrUserGroupPermissionsListApiMixin(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
model = AssetPermission
serializer_class = AssetPermissionSerializer
filterset_class = AssetPermissionFilter
search_fields = ('name',)
rbac_perms = {
'list': 'perms.view_assetpermission'
}
def get_object(self):
asset_id = self.kwargs.get('pk')

View File

@@ -8,14 +8,11 @@ from django.shortcuts import get_object_or_404
from common.utils import reverse
from common.utils import lazyproperty
from orgs.mixins.api import OrgBulkModelViewSet
from tickets.api import GenericTicketStatusRetrieveCloseAPI
from ..hands import IsOrgAdmin, IsAppUser
from ..models import CommandFilter, CommandFilterRule
from .. import serializers
__all__ = [
'CommandFilterViewSet', 'CommandFilterRuleViewSet', 'CommandConfirmAPI',
'CommandConfirmStatusAPI'
]
@@ -23,7 +20,6 @@ class CommandFilterViewSet(OrgBulkModelViewSet):
model = CommandFilter
filterset_fields = ("name",)
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterSerializer
@@ -31,7 +27,6 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
model = CommandFilterRule
filterset_fields = ('content',)
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.CommandFilterRuleSerializer
def get_queryset(self):
@@ -43,8 +38,10 @@ class CommandFilterRuleViewSet(OrgBulkModelViewSet):
class CommandConfirmAPI(CreateAPIView):
permission_classes = (IsAppUser,)
serializer_class = serializers.CommandConfirmSerializer
rbac_perms = {
'POST': 'tickets.add_superticket'
}
def create(self, request, *args, **kwargs):
ticket = self.create_command_confirm_ticket()
@@ -56,14 +53,14 @@ class CommandConfirmAPI(CreateAPIView):
run_command=self.serializer.data.get('run_command'),
session=self.serializer.session,
cmd_filter_rule=self.serializer.cmd_filter_rule,
org_id=self.serializer.org.id
org_id=self.serializer.org.id,
)
return ticket
@staticmethod
def get_response_data(ticket):
confirm_status_url = reverse(
view_name='api-assets:command-confirm-status',
view_name='api-tickets:super-ticket-status',
kwargs={'pk': str(ticket.id)}
)
ticket_detail_url = reverse(
@@ -86,6 +83,3 @@ class CommandConfirmAPI(CreateAPIView):
serializer.is_valid(raise_exception=True)
return serializer
class CommandConfirmStatusAPI(GenericTicketStatusRetrieveCloseAPI):
pass

View File

@@ -6,7 +6,6 @@ from rest_framework.views import APIView, Response
from rest_framework.serializers import ValidationError
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from orgs.mixins.api import OrgBulkModelViewSet
from ..models import Domain, Gateway
from .. import serializers
@@ -20,7 +19,6 @@ class DomainViewSet(OrgBulkModelViewSet):
model = Domain
filterset_fields = ("name", )
search_fields = filterset_fields
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.DomainSerializer
ordering_fields = ('name',)
ordering = ('name', )
@@ -35,13 +33,15 @@ class GatewayViewSet(OrgBulkModelViewSet):
model = Gateway
filterset_fields = ("domain__name", "name", "username", "ip", "domain")
search_fields = ("domain__name", "name", "username", "ip")
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = serializers.GatewaySerializer
class GatewayTestConnectionApi(SingleObjectMixin, APIView):
permission_classes = (IsOrgAdmin,)
queryset = Gateway.objects.all()
object = None
rbac_perms = {
'POST': 'assets.test_gateway'
}
def post(self, request, *args, **kwargs):
self.object = self.get_object(Gateway.objects.all())

View File

@@ -3,7 +3,6 @@
from orgs.mixins.api import OrgModelViewSet
from assets.models import GatheredUser
from common.permissions import IsOrgAdmin
from ..serializers import GatheredUserSerializer
from ..filters import AssetRelatedByNodeFilterBackend
@@ -15,7 +14,6 @@ __all__ = ['GatheredUserViewSet']
class GatheredUserViewSet(OrgModelViewSet):
model = GatheredUser
serializer_class = GatheredUserSerializer
permission_classes = [IsOrgAdmin]
extra_filter_backends = [AssetRelatedByNodeFilterBackend]
filterset_fields = ['asset', 'username', 'present', 'asset__ip', 'asset__hostname', 'asset_id']

View File

@@ -17,7 +17,6 @@ from django.db.models import Count
from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdmin
from ..models import Label
from .. import serializers
@@ -30,7 +29,6 @@ class LabelViewSet(OrgBulkModelViewSet):
model = Label
filterset_fields = ("name", "value")
search_fields = filterset_fields
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.LabelSerializer
def list(self, request, *args, **kwargs):

View File

@@ -20,7 +20,6 @@ from common.tree import TreeNodeSerializer
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from orgs.utils import current_org
from ..hands import IsOrgAdmin
from ..models import Node
from ..tasks import (
update_node_assets_hardware_info_manual,
@@ -31,7 +30,6 @@ from .. import serializers
from .mixin import SerializeToTreeNodeMixin
from assets.locks import NodeAddChildrenLock
logger = get_logger(__file__)
__all__ = [
'NodeViewSet', 'NodeChildrenApi', 'NodeAssetsApi',
@@ -45,9 +43,12 @@ __all__ = [
class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Node
filterset_fields = ('value', 'key', 'id')
search_fields = ('value', )
permission_classes = (IsOrgAdmin,)
search_fields = ('value',)
serializer_class = serializers.NodeSerializer
rbac_perms = {
'match': 'assets.match_node',
'check_assets_amount_task': 'assets.change_node'
}
@action(methods=[POST], detail=False, url_path='check_assets_amount_task')
def check_assets_amount_task(self, request):
@@ -85,7 +86,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
]
"""
model = Node
permission_classes = (IsOrgAdmin,)
serializer_class = TreeNodeSerializer
@staticmethod
@@ -100,7 +100,6 @@ class NodeListAsTreeApi(generics.ListAPIView):
class NodeChildrenApi(generics.ListCreateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
instance = None
is_initial = False
@@ -199,7 +198,6 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
class NodeAssetsApi(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSerializer
def get_queryset(self):
@@ -214,7 +212,6 @@ class NodeAssetsApi(generics.ListAPIView):
class NodeAddChildrenApi(generics.UpdateAPIView):
model = Node
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeAddChildrenSerializer
instance = None
@@ -231,7 +228,6 @@ class NodeAddChildrenApi(generics.UpdateAPIView):
class NodeAddAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@@ -243,7 +239,6 @@ class NodeAddAssetsApi(generics.UpdateAPIView):
class NodeRemoveAssetsApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@@ -262,7 +257,6 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
class MoveAssetsToNodeApi(generics.UpdateAPIView):
model = Node
serializer_class = serializers.NodeAssetsSerializer
permission_classes = (IsOrgAdmin,)
instance = None
def perform_update(self, serializer):
@@ -303,9 +297,21 @@ class MoveAssetsToNodeApi(generics.UpdateAPIView):
class NodeTaskCreateApi(generics.CreateAPIView):
perm_model = Asset
model = Node
serializer_class = serializers.NodeTaskSerializer
permission_classes = (IsOrgAdmin,)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo',
'test': 'assets.test_assetconnectivity'
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
def get_object(self):
node_id = self.kwargs.get('pk')
@@ -338,4 +344,3 @@ class NodeTaskCreateApi(generics.CreateAPIView):
else:
task = test_node_assets_connectivity_manual.delay(node)
self.set_serializer_data(serializer, task)

View File

@@ -1,16 +1,15 @@
# ~*~ coding: utf-8 ~*~
from django.shortcuts import get_object_or_404
from django.middleware import csrf
from rest_framework.response import Response
from rest_framework.decorators import action
from common.utils import get_logger, get_object_or_none
from common.utils.crypto import get_aes_crypto
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser, IsValidUser
from common.permissions import IsValidUser
from common.mixins.api import SuggestionMixin
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from common.mixins.api import SuggestionMixin
from orgs.utils import tmp_to_root_org
from rest_framework.decorators import action
from ..models import SystemUser, CommandFilterRule
from .. import serializers
from ..serializers import SystemUserWithAuthInfoSerializer, SystemUserTempAuthSerializer
@@ -46,7 +45,11 @@ class SystemUserViewSet(SuggestionMixin, OrgBulkModelViewSet):
}
ordering_fields = ('name', 'protocol', 'login_mode')
ordering = ('name', )
permission_classes = (IsOrgAdminOrAppUser,)
rbac_perms = {
'su_from': 'assets.view_systemuser',
'su_to': 'assets.view_systemuser',
'match': 'assets.match_systemuser'
}
@action(methods=['get'], detail=False, url_path='su-from')
def su_from(self, request, *args, **kwargs):
@@ -80,8 +83,13 @@ class SystemUserAuthInfoApi(generics.RetrieveUpdateDestroyAPIView):
Get system user auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
rbac_perms = {
'retrieve': 'assets.view_systemusersecret',
'list': 'assets.view_systemusersecret',
'change': 'assets.change_systemuser',
'destroy': 'assets.change_systemuser',
}
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
@@ -114,7 +122,7 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(instance_id, self.request.user, data)
instance.set_temp_auth(instance_id, self.request.user.id, data)
return Response(serializer.data, status=201)
@@ -123,7 +131,6 @@ class SystemUserAssetAuthInfoApi(generics.RetrieveAPIView):
Get system user with asset auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
def get_object(self):
@@ -140,8 +147,10 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
Get system user with asset auth info
"""
model = SystemUser
permission_classes = (IsOrgAdminOrAppUser,)
serializer_class = SystemUserWithAuthInfoSerializer
rbac_perms = {
'retrieve': 'assets.view_systemusersecret',
}
def get_object(self):
instance = super().get_object()
@@ -153,7 +162,6 @@ class SystemUserAppAuthInfoApi(generics.RetrieveAPIView):
class SystemUserTaskApi(generics.CreateAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.SystemUserTaskSerializer
def do_push(self, system_user, asset_ids=None):
@@ -175,6 +183,18 @@ class SystemUserTaskApi(generics.CreateAPIView):
pk = self.kwargs.get('pk')
return get_object_or_404(SystemUser, pk=pk)
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'push': 'assets.push_assetsystemuser',
'test': 'assets.test_assetconnectivity'
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
if not has:
self.permission_denied(request)
def perform_create(self, serializer):
action = serializer.validated_data["action"]
asset = serializer.validated_data.get('asset')
@@ -198,7 +218,9 @@ class SystemUserTaskApi(generics.CreateAPIView):
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
permission_classes = (IsOrgAdminOrAppUser,)
rbac_perms = {
'list': 'assets.view_commandfilterule'
}
def get_serializer_class(self):
from ..serializers import CommandFilterRuleSerializer
@@ -224,10 +246,12 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
class SystemUserAssetsListView(generics.ListAPIView):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.AssetSimpleSerializer
filterset_fields = ("hostname", "ip")
search_fields = filterset_fields
rbac_perms = {
'list': 'assets.view_asset'
}
def get_object(self):
pk = self.kwargs.get('pk')

View File

@@ -5,7 +5,6 @@ from django.db.models import F, Value, Model
from django.db.models.signals import m2m_changed
from django.db.models.functions import Concat
from common.permissions import IsOrgAdmin
from common.utils import get_logger
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import current_org
@@ -65,13 +64,13 @@ class RelationMixin:
class BaseRelationViewSet(RelationMixin, OrgBulkModelViewSet):
pass
perm_model = models.SystemUser
class SystemUserAssetRelationViewSet(BaseRelationViewSet):
perm_model = models.AuthBook
serializer_class = serializers.SystemUserAssetRelationSerializer
model = models.SystemUser.assets.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'asset', 'systemuser',
]
@@ -97,7 +96,6 @@ class SystemUserAssetRelationViewSet(BaseRelationViewSet):
class SystemUserNodeRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserNodeRelationSerializer
model = models.SystemUser.nodes.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'node', 'systemuser',
]
@@ -118,7 +116,6 @@ class SystemUserNodeRelationViewSet(BaseRelationViewSet):
class SystemUserUserRelationViewSet(BaseRelationViewSet):
serializer_class = serializers.SystemUserUserRelationSerializer
model = models.SystemUser.users.through
permission_classes = (IsOrgAdmin,)
filterset_fields = [
'id', 'user', 'systemuser',
]
@@ -140,4 +137,3 @@ class SystemUserUserRelationViewSet(BaseRelationViewSet):
)
)
return queryset

View File

@@ -1,11 +1,16 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from django.apps import AppConfig
class AssetsConfig(AppConfig):
name = 'assets'
verbose_name = _('App assets')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def ready(self):
super().ready()
from . import signals_handler
from . import signal_handlers

View File

@@ -11,5 +11,4 @@
"""
from common.permissions import IsAppUser, IsOrgAdmin, IsValidUser, IsOrgAdminOrAppUser
from users.models import User, UserGroup

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.13 on 2022-02-17 13:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0085_commandfilterrule_ignore_case'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname'], 'permissions': [('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset')], 'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='authbook',
options={'permissions': [('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
),
migrations.AlterModelOptions(
name='label',
options={'verbose_name': 'Label'},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.13 on 2022-02-23 07:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0086_auto_20220217_2135'),
]
operations = [
migrations.AlterField(
model_name='systemuser',
name='protocol',
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
),
]

View File

@@ -0,0 +1,25 @@
# Generated by Django 3.1.14 on 2022-03-03 08:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0087_auto_20220223_1539'),
]
operations = [
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset')], 'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='node',
options={'ordering': ['parent_key', 'value'], 'permissions': [('match_node', 'Can match node')], 'verbose_name': 'Node'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'permissions': [('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.14 on 2022-03-09 22:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0088_auto_20220303_1612'),
]
operations = [
migrations.AlterModelOptions(
name='authbook',
options={'permissions': [('test_authbook', 'Can test asset account connectivity'), ('view_assetaccountsecret', 'Can view asset account secret'), ('change_assetaccountsecret', 'Can change asset account secret')], 'verbose_name': 'AuthBook'},
),
migrations.AlterModelOptions(
name='systemuser',
options={'ordering': ['name'], 'permissions': [('match_systemuser', 'Can match system user')], 'verbose_name': 'System user'},
),
migrations.AlterModelOptions(
name='asset',
options={'ordering': ['hostname'], 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), ('test_assetconnectivity', 'Can test asset connectivity'), ('push_assetsystemuser', 'Can push system user to asset'), ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'},
),
migrations.AlterModelOptions(
name='gateway',
options={'permissions': [('test_gateway', 'Test gateway')], 'verbose_name': 'Gateway'},
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2022-04-12 03:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0089_auto_20220310_0616'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='number',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
),
]

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#
import uuid
import logging
@@ -8,7 +8,6 @@ from functools import reduce
from collections import OrderedDict
from django.db import models
from common.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
@@ -59,7 +58,7 @@ class AssetQuerySet(models.QuerySet):
class ProtocolsMixin:
protocols = ''
class Protocol(TextChoices):
class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
@@ -224,7 +223,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
# Some information
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
number = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Asset number'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
@@ -236,6 +235,9 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
def __str__(self):
return '{0.hostname}({0.ip})'.format(self)
def get_target_ip(self):
return self.ip
def set_admin_user_relation(self):
from .authbook import AuthBook
if not self.admin_user:
@@ -281,16 +283,44 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
def is_support_ansible(self):
return self.has_protocol('ssh') and self.platform_base not in ("Other",)
def get_auth_info(self):
def get_auth_info(self, with_become=False):
if not self.admin_user:
return {}
self.admin_user.load_asset_special_auth(self)
if self.is_unixlike() and self.admin_user.su_enabled and self.admin_user.su_from:
auth_user = self.admin_user.su_from
become_user = self.admin_user
else:
auth_user = self.admin_user
become_user = None
auth_user.load_asset_special_auth(self)
info = {
'username': self.admin_user.username,
'password': self.admin_user.password,
'private_key': self.admin_user.private_key_file,
'username': auth_user.username,
'password': auth_user.password,
'private_key': auth_user.private_key_file
}
if not with_become or self.is_windows():
return info
if become_user:
become_user.load_asset_special_auth(self)
become_method = 'su'
become_username = become_user.username
become_pass = become_user.password
else:
become_method = 'sudo'
become_username = 'root'
become_pass = auth_user.password
become_info = {
'become': {
'method': become_method,
'username': become_username,
'pass': become_pass
}
}
info.update(become_info)
return info
def nodes_display(self):
@@ -355,3 +385,11 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
unique_together = [('org_id', 'hostname')]
verbose_name = _("Asset")
ordering = ["hostname", ]
permissions = [
('refresh_assethardwareinfo', _('Can refresh asset hardware info')),
('test_assetconnectivity', _('Can test asset connectivity')),
('push_assetsystemuser', _('Can push system user to asset')),
('match_asset', _('Can match asset')),
('add_assettonode', _('Add asset to node')),
('move_assettonode', _('Move asset to node')),
]

View File

@@ -26,6 +26,11 @@ class AuthBook(BaseUser, AbsConnectivity):
class Meta:
verbose_name = _('AuthBook')
unique_together = [('username', 'asset', 'systemuser')]
permissions = [
('test_authbook', _('Can test asset account connectivity')),
('view_assetaccountsecret', _('Can view asset account secret')),
('change_assetaccountsecret', _('Can change asset account secret'))
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -186,13 +186,15 @@ class CommandFilterRule(OrgModelMixin):
return ticket
@classmethod
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, asset_id=None, application_id=None):
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None,
asset_id=None, application_id=None, org_id=None):
user_groups = []
user = get_object_or_none(User, pk=user_id)
if user:
user_groups.extend(list(user.groups.all()))
user_group = get_object_or_none(UserGroup, pk=user_group_id)
if user_group:
org_id = user_group.org_id
user_groups.append(user_group)
system_user = get_object_or_none(SystemUser, pk=system_user_id)
asset = get_object_or_none(Asset, pk=asset_id)
@@ -203,13 +205,18 @@ class CommandFilterRule(OrgModelMixin):
if user_groups:
q |= Q(user_groups__in=set(user_groups))
if system_user:
org_id = system_user.org_id
q |= Q(system_users=system_user)
if asset:
org_id = asset.org_id
q |= Q(assets=asset)
if application:
org_id = application.org_id
q |= Q(applications=application)
if q:
cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True)
if org_id:
cmd_filters = cmd_filters.filter(org_id=org_id)
rule_ids = cmd_filters.values_list('rules', flat=True)
rules = cls.objects.filter(id__in=rule_ids)
else:

View File

@@ -7,7 +7,6 @@ import random
from django.core.cache import cache
import paramiko
from django.db import models
from django.db.models import TextChoices
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
@@ -55,7 +54,7 @@ class Gateway(BaseUser):
UNCONNECTIVE_SILENCE_PERIOD_KEY_TMPL = 'asset_unconnective_gateway_silence_period_{}'
UNCONNECTIVE_SILENCE_PERIOD_BEGIN_VALUE = 60 * 5
class Protocol(TextChoices):
class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH'
ip = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True)
@@ -71,6 +70,9 @@ class Gateway(BaseUser):
class Meta:
unique_together = [('name', 'org_id')]
verbose_name = _("Gateway")
permissions = [
('test_gateway', _('Test gateway'))
]
def set_unconnective(self):
unconnective_key = self.UNCONNECTIVE_KEY_TMPL.format(self.id)

View File

@@ -37,3 +37,4 @@ class Label(OrgModelMixin):
class Meta:
db_table = "assets_label"
unique_together = [('name', 'value', 'org_id')]
verbose_name = _('Label')

View File

@@ -558,6 +558,9 @@ class Node(OrgModelMixin, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
class Meta:
verbose_name = _("Node")
ordering = ['parent_key', 'value']
permissions = [
('match_node', _('Can match node')),
]
def __str__(self):
return self.full_value

View File

@@ -5,13 +5,11 @@
import logging
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 django.core.cache import cache
from common.utils import signer, get_object_or_none
from common.db.models import TextChoices
from .base import BaseUser
from .asset import Asset
from .authbook import AuthBook
@@ -24,17 +22,18 @@ logger = logging.getLogger(__name__)
class ProtocolMixin:
protocol: str
class Protocol(TextChoices):
class Protocol(models.TextChoices):
ssh = 'ssh', 'SSH'
rdp = 'rdp', 'RDP'
telnet = 'telnet', 'Telnet'
vnc = 'vnc', 'VNC'
mysql = 'mysql', 'MySQL'
redis = 'redis', 'Redis'
oracle = 'oracle', 'Oracle'
mariadb = 'mariadb', 'MariaDB'
postgresql = 'postgresql', 'PostgreSQL'
sqlserver = 'sqlserver', 'SQLServer'
redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB'
k8s = 'k8s', 'K8S'
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
@@ -46,8 +45,9 @@ class ProtocolMixin:
Protocol.rdp
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
Protocol.mysql, Protocol.redis, Protocol.oracle,
Protocol.mariadb, Protocol.postgresql, Protocol.sqlserver
Protocol.mysql, Protocol.mariadb, Protocol.oracle,
Protocol.postgresql, Protocol.sqlserver,
Protocol.redis, Protocol.mongodb
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
Protocol.k8s
@@ -133,6 +133,14 @@ class AuthMixin:
self.password = password
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
from applications.models import Application
app = get_object_or_none(Application, pk=app_id)
if app and app.category_remote_app:
# Remote app
self._load_remoteapp_more_auth(app, username, user_id)
return
# Other app
self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
@@ -148,6 +156,11 @@ class AuthMixin:
_username = username
self.username = _username
def _load_remoteapp_more_auth(self, app, username, user_id):
asset = app.get_remote_app_asset(raise_exception=False)
if asset:
self.load_asset_more_auth(asset_id=asset.id, username=username, user_id=user_id)
def load_asset_special_auth(self, asset, username=''):
"""
AuthBook 的数据状态
@@ -217,7 +230,7 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
(LOGIN_MANUAL, _('Manually input'))
)
class Type(TextChoices):
class Type(models.TextChoices):
common = 'common', _('Common user')
admin = 'admin', _('Admin user')
@@ -323,9 +336,12 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
ordering = ['name']
unique_together = [('name', 'org_id')]
verbose_name = _("System user")
permissions = [
('match_systemuser', _('Can match system user')),
]
# Todo: 准备废弃
# Deprecated: 准备废弃
class AdminUser(BaseUser):
"""
A privileged user that ansible can use it to push system user and so on

View File

@@ -15,6 +15,7 @@ class AdminUserSerializer(SuS):
SuS.Meta.fields_m2m + \
[
'type', 'protocol', "priority", 'sftp_root', 'ssh_key_fingerprint',
'su_enabled', 'su_from',
'date_created', 'date_updated', 'comment', 'created_by',
]

View File

@@ -12,13 +12,11 @@ from terminal.models import Session
class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class Meta:
model = CommandFilter
fields_mini = ['id', 'name']
fields_small = fields_mini + [
'org_id', 'org_name',
'is_active',
'org_id', 'org_name', 'is_active',
'date_created', 'date_updated',
'comment', 'created_by',
]
@@ -26,25 +24,32 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications']
fields = fields_small + fields_fk + fields_m2m
extra_kwargs = {
'rules': {'read_only': True}
'rules': {'read_only': True},
'date_created': {'label': _("Date created")},
'date_updated': {'label': _("Date updated")},
}
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
type_display = serializers.ReadOnlyField(source='get_type_display')
action_display = serializers.ReadOnlyField(source='get_action_display')
type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
action_display = serializers.ReadOnlyField(source='get_action_display', label=_("Action display"))
class Meta:
model = CommandFilterRule
fields_mini = ['id']
fields_small = fields_mini + [
'type', 'type_display', 'content', 'ignore_case', 'pattern', 'priority',
'action', 'action_display', 'reviewers',
'date_created', 'date_updated',
'comment', 'created_by',
'type', 'type_display', 'content', 'ignore_case', 'pattern',
'priority', 'action', 'action_display', 'reviewers',
'date_created', 'date_updated', 'comment', 'created_by',
]
fields_fk = ['filter']
fields = fields_small + fields_fk
extra_kwargs = {
'date_created': {'label': _("Date created")},
'date_updated': {'label': _("Date updated")},
'action_display': {'label': _("Action display")},
'pattern': {'label': _("Pattern")}
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

View File

@@ -43,7 +43,7 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
is_connective = serializers.BooleanField(required=False)
is_connective = serializers.BooleanField(required=False, label=_('Connectivity'))
class Meta:
model = Gateway

View File

@@ -27,7 +27,7 @@ class LabelSerializer(BulkOrgResourceModelSerializer):
'category', 'date_created', 'asset_count',
)
extra_kwargs = {
'assets': {'required': False}
'assets': {'required': False, 'label': _('Asset')}
}
@staticmethod

View File

@@ -5,7 +5,6 @@ from django.utils.translation import ugettext as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import Asset, Node
__all__ = [
'NodeSerializer', "NodeAddChildrenSerializer",
"NodeAssetsSerializer", "NodeTaskSerializer",
@@ -45,7 +44,6 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
def create(self, validated_data):
full_value = validated_data.get('full_value')
value = validated_data.get('value')
# 直接多层级创建
if full_value:
@@ -53,7 +51,8 @@ class NodeSerializer(BulkOrgResourceModelSerializer):
# 根据 value 在 root 下创建
else:
key = Node.org_root().get_next_child_key()
node = Node.objects.create(key=key, value=value)
validated_data['key'] = key
node = Node.objects.create(**validated_data)
return node

View File

@@ -88,7 +88,7 @@ class AssetAccountHandler(BaseAccountHandler):
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
logger.info('\n\033[33m- 共收集{}条资产账号\033[0m'.format(accounts.count()))
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
return df_dict
@@ -156,10 +156,7 @@ class AccountBackupHandler:
logger.info('步骤完成: 用时 {}s'.format(timedelta))
return files
def send_backup_mail(self, files):
recipients = self.execution.plan_snapshot.get('recipients')
if not recipients:
return
def send_backup_mail(self, files, recipients):
if not files:
return
recipients = User.objects.filter(id__in=list(recipients))
@@ -198,8 +195,16 @@ class AccountBackupHandler:
is_success = False
error = '-'
try:
files = self.create_excel()
self.send_backup_mail(files)
recipients = self.execution.plan_snapshot.get('recipients')
if not recipients:
logger.info(
'\n'
'\033[32m>>> 该备份任务未分配收件人\033[0m'
''
)
else:
files = self.create_excel()
self.send_backup_mail(files, recipients)
except Exception as e:
self.is_frozen = True
logger.error('任务执行被异常中断')

View File

@@ -26,8 +26,8 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation')
router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation')
router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation')
router.register(r'backup', api.AccountBackupPlanViewSet, 'backup')
router.register(r'backup-execution', api.AccountBackupPlanExecutionViewSet, 'backup-execution')
router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup')
router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution')
cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter')
cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule')
@@ -68,7 +68,6 @@ urlpatterns = [
path('gateways/<uuid:pk>/test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'),
path('cmd-filters/command-confirm/', api.CommandConfirmAPI.as_view(), name='command-confirm'),
path('cmd-filters/command-confirm/<uuid:pk>/status/', api.CommandConfirmStatusAPI.as_view(), name='command-confirm-status')
]

View File

@@ -3,8 +3,10 @@
from rest_framework.mixins import ListModelMixin, CreateModelMixin
from django.db.models import F, Value
from django.db.models.functions import Concat
from rest_framework.permissions import IsAuthenticated
from rest_framework import generics
from common.permissions import IsOrgAdminOrAppUser, IsOrgAuditor, IsOrgAdmin
from common.drf.api import JMSReadOnlyModelViewSet
from common.drf.filters import DatetimeRangeFilter
from common.api import CommonGenericViewSet
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
@@ -20,7 +22,6 @@ class FTPLogViewSet(CreateModelMixin,
OrgGenericViewSet):
model = FTPLog
serializer_class = FTPLogSerializer
permission_classes = (IsOrgAdminOrAppUser | IsOrgAuditor,)
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
@@ -30,9 +31,8 @@ class FTPLogViewSet(CreateModelMixin,
ordering = ['-date_start']
class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
class UserLoginCommonMixin:
queryset = UserLoginLog.objects.all()
permission_classes = [IsOrgAdmin | IsOrgAuditor]
serializer_class = UserLoginLogSerializer
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
@@ -41,6 +41,9 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
filterset_fields = ['username', 'ip', 'city', 'type', 'status', 'mfa']
search_fields = ['username', 'ip', 'city']
class UserLoginLogViewSet(UserLoginCommonMixin, ListModelMixin, CommonGenericViewSet):
@staticmethod
def get_org_members():
users = current_org.get_members().values_list('username', flat=True)
@@ -55,10 +58,18 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
return queryset
class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView):
permission_classes = [IsAuthenticated]
def get_queryset(self):
qs = super().get_queryset()
qs = qs.filter(username=self.request.user.username)
return qs
class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
model = OperateLog
serializer_class = OperateLogSerializer
permission_classes = [IsOrgAdmin | IsOrgAuditor]
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('datetime', ('date_from', 'date_to'))
@@ -70,7 +81,6 @@ class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
queryset = PasswordChangeLog.objects.all()
permission_classes = [IsOrgAdmin | IsOrgAuditor]
serializer_class = PasswordChangeLogSerializer
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
@@ -91,7 +101,6 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
model = CommandExecution
serializer_class = CommandExecutionSerializer
permission_classes = [IsOrgAdmin | IsOrgAuditor]
extra_filter_backends = [DatetimeRangeFilter]
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
@@ -117,12 +126,15 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
serializer_class = CommandExecutionHostsRelationSerializer
m2m_field = CommandExecution.hosts.field
permission_classes = [IsOrgAdmin | IsOrgAuditor]
filterset_fields = [
'id', 'asset', 'commandexecution'
]
search_fields = ('asset__hostname', )
http_method_names = ['options', 'get']
rbac_perms = {
'GET': 'ops.view_commandexecution',
'list': 'ops.view_commandexecution',
}
def get_queryset(self):
queryset = super().get_queryset()

View File

@@ -1,12 +1,14 @@
from django.apps import AppConfig
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from django.db.models.signals import post_save
class AuditsConfig(AppConfig):
name = 'audits'
verbose_name = _('Audits')
def ready(self):
from . import signals_handler
from . import signal_handlers
if settings.SYSLOG_ENABLE:
post_save.connect(signals_handler.on_audits_log_create)
post_save.connect(signal_handlers.on_audits_log_create)

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.13 on 2021-11-30 02:37
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('audits', '0012_auto_20210414_1443'),
]
operations = [
migrations.AlterModelOptions(
name='ftplog',
options={'verbose_name': 'File transfer log'},
),
migrations.AlterModelOptions(
name='operatelog',
options={'verbose_name': 'Operate log'},
),
migrations.AlterModelOptions(
name='passwordchangelog',
options={'verbose_name': 'Password change log'},
),
migrations.AlterModelOptions(
name='userloginlog',
options={'ordering': ['-datetime', 'username'], 'verbose_name': 'User login log'},
),
]

View File

@@ -43,6 +43,9 @@ class FTPLog(OrgModelMixin):
is_success = models.BooleanField(default=True, verbose_name=_("Success"))
date_start = models.DateTimeField(auto_now_add=True, verbose_name=_('Date start'))
class Meta:
verbose_name = _("File transfer log")
class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create'
@@ -73,6 +76,9 @@ class OperateLog(OrgModelMixin):
self.org_id = Organization.ROOT_ID
return super(OperateLog, self).save(*args, **kwargs)
class Meta:
verbose_name = _("Operate log")
class PasswordChangeLog(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
@@ -84,6 +90,9 @@ class PasswordChangeLog(models.Model):
def __str__(self):
return "{} change {}'s password".format(self.change_by, self.user)
class Meta:
verbose_name = _('Password change log')
class UserLoginLog(models.Model):
LOGIN_TYPE_CHOICE = (
@@ -155,3 +164,4 @@ class UserLoginLog(models.Model):
class Meta:
ordering = ['-datetime', 'username']
verbose_name = _('User login log')

View File

@@ -70,6 +70,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
return backend_label_mapping
def _setup(self):
@@ -102,11 +103,6 @@ def create_operate_log(action, sender, resource):
M2M_NEED_RECORD = {
'OrganizationMember': (
_('User and Organization'),
_('{User} JOINED {Organization}'),
_('{User} LEFT {Organization}')
),
User.groups.through._meta.object_name: (
_('User and Group'),
_('{User} JOINED {UserGroup}'),

View File

@@ -7,7 +7,7 @@ from celery import shared_task
from ops.celery.decorator import (
register_as_period_task
)
from .models import UserLoginLog, OperateLog
from .models import UserLoginLog, OperateLog, FTPLog
from common.utils import get_log_keep_day
@@ -29,7 +29,7 @@ def clean_ftp_log_period():
now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days)
OperateLog.objects.filter(datetime__lt=expired_day).delete()
FTPLog.objects.filter(datetime__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)

View File

@@ -1,7 +1,7 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
from django.urls.conf import re_path
from django.urls.conf import re_path, path
from rest_framework.routers import DefaultRouter
from common import api as capi
@@ -20,6 +20,7 @@ router.register(r'command-executions-hosts-relations', api.CommandExecutionHostR
urlpatterns = [
path('my-login-logs/', api.MyLoginLogAPIView.as_view(), name='my-login-log'),
]
old_version_urlpatterns = [

View File

@@ -11,3 +11,4 @@ from .wecom import *
from .dingtalk import *
from .feishu import *
from .password import *
from .temp_token import *

View File

@@ -2,15 +2,14 @@
#
from rest_framework.viewsets import ModelViewSet
from common.permissions import IsValidUser
from .. import serializers
from rbac.permissions import RBACPermission
class AccessKeyViewSet(ModelViewSet):
permission_classes = (IsValidUser,)
serializer_class = serializers.AccessKeySerializer
search_fields = ['^id', '^secret']
permission_classes = [RBACPermission]
def get_queryset(self):
return self.request.user.access_keys.all()

View File

@@ -24,27 +24,45 @@ from applications.models import Application
from authentication.signals import post_auth_failed
from common.utils import get_logger, random_string
from common.mixins.api import SerializerMixin
from common.permissions import IsSuperUserOrAppUser, IsValidUser, IsSuperUser
from common.utils.common import get_file_by_arch
from orgs.mixins.api import RootOrgViewMixin
from common.http import is_true
from perms.models.base import Action
from perms.utils.application.permission import validate_permission as app_validate_permission
from perms.utils.application.permission import get_application_actions
from perms.utils.asset.permission import get_asset_actions
from common.const.http import PATCH
from terminal.models import EndpointRule
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
)
logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet']
__all__ = ['UserConnectionTokenViewSet', 'TokenCacheMixin']
class ClientProtocolMixin:
"""
下载客户端支持的连接文件,里面包含了 token和 其他连接信息
- [x] RDP
- [ ] KoKo
本质上,这里还是暴露出 token 来,进行使用
"""
request: Request
get_serializer: Callable
create_token: Callable
get_serializer_context: Callable
def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset:
target_ip = asset.get_target_ip()
elif application:
target_ip = application.get_target_ip()
else:
target_ip = ''
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
return endpoint
def get_request_resource(self, serializer):
asset = serializer.validated_data.get('asset')
@@ -86,7 +104,7 @@ class ClientProtocolMixin:
'autoreconnection enabled:i': '1',
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'smart sizing:i': '0',
'smart sizing:i': '1',
#'drivestoredirect:s': '*',
# 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench',
@@ -99,7 +117,7 @@ class ClientProtocolMixin:
width = self.request.query_params.get('width')
full_screen = is_true(self.request.query_params.get('full_screen'))
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token = self.create_token(user, asset, application, system_user)
token, secret = self.create_token(user, asset, application, system_user)
# 设置磁盘挂载
if drives_redirect:
@@ -116,10 +134,10 @@ class ClientProtocolMixin:
options['screen mode id:i'] = '2' if full_screen else '1'
# RDP Server 地址
address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389':
address = self.request.get_host().split(':')[0] + ':3389'
options['full address:s'] = address
endpoint = self.get_smart_endpoint(
protocol='rdp', asset=asset, application=application
)
options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 用户名
options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain:
@@ -128,8 +146,7 @@ class ClientProtocolMixin:
if width and height:
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
else:
options['smart sizing:i'] = '1'
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
@@ -154,6 +171,28 @@ class ClientProtocolMixin:
content += f'{k}:{v}\n'
return name, content
def get_ssh_token(self, serializer):
asset, application, system_user, user = self.get_request_resource(serializer)
token, secret = self.create_token(user, asset, application, system_user)
if asset:
name = asset.hostname
elif application:
name = application.name
else:
name = '*'
endpoint = self.get_smart_endpoint(
protocol='ssh', asset=asset, application=application
)
content = {
'ip': endpoint.host,
'port': str(endpoint.ssh_port),
'username': f'JMS-{token}',
'password': secret
}
token = json.dumps(content)
return name, token
def get_encrypt_cmdline(self, app: Application):
parameters = app.get_rdp_remote_app_setting()['parameters']
parameters = parameters.encode('ascii')
@@ -167,7 +206,7 @@ class ClientProtocolMixin:
rst = rst.decode('ascii')
return rst
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file', permission_classes=[IsValidUser])
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs):
if self.request.method == 'GET':
data = self.request.query_params
@@ -195,13 +234,11 @@ class ClientProtocolMixin:
asset, application, system_user, user = self.get_request_resource(serializer)
protocol = system_user.protocol
username = user.username
config, token = '', ''
if protocol == 'rdp':
name, config = self.get_rdp_file_content(serializer)
elif protocol == 'ssh':
# Todo:
name = ''
config = 'ssh://system_user@asset@user@jumpserver-ssh'
name, token = self.get_ssh_token(serializer)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
@@ -210,11 +247,12 @@ class ClientProtocolMixin:
"filename": filename,
"protocol": system_user.protocol,
"username": username,
"token": token,
"config": config
}
return data
@action(methods=['POST', 'GET'], detail=False, url_path='client-url', permission_classes=[IsValidUser])
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
serializer = self.get_valid_serializer()
try:
@@ -255,6 +293,7 @@ class SecretDetailMixin:
'asset': asset,
'application': application,
'gateway': gateway,
'domain': domain,
'remote_app': remote_app,
}
@@ -267,12 +306,19 @@ class SecretDetailMixin:
return {
'asset': asset,
'application': None,
'domain': asset.domain,
'gateway': gateway,
'remote_app': None,
}
@action(methods=['POST'], detail=False, permission_classes=[IsSuperUserOrAppUser], url_path='secret-info/detail')
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
perm_required = 'authentication.view_connectiontokensecret'
# 非常重要的 api再逻辑层再判断一下双重保险
if not request.user.has_perm(perm_required):
raise PermissionDenied('Not allow to view secret')
token = request.data.get('token', '')
try:
value, user, system_user, asset, app, expired_at, actions = self.valid_token(token)
@@ -288,31 +334,85 @@ class SecretDetailMixin:
user=user, system_user=system_user,
expired_at=expired_at, actions=actions
)
cmd_filter_kwargs = {
'system_user_id': system_user.id,
'user_id': user.id,
}
if asset:
asset_detail = self._get_asset_secret_detail(asset)
system_user.load_asset_more_auth(asset.id, user.username, user.id)
data['type'] = 'asset'
data.update(asset_detail)
cmd_filter_kwargs['asset_id'] = asset.id
else:
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.username, user.id)
data['type'] = 'application'
data.update(app_detail)
cmd_filter_kwargs['application_id'] = app.id
from assets.models import CommandFilterRule
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
data['cmd_filter_rules'] = cmd_filter_rules
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)
class TokenCacheMixin:
""" endpoint smart view 用到此类来解析token中的资产、应用 """
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
def get_token_cache_key(self, token):
return self.CACHE_KEY_PREFIX.format(token)
def get_token_ttl(self, token):
key = self.get_token_cache_key(token)
return cache.ttl(key)
def set_token_to_cache(self, token, value, ttl=5*60):
key = self.get_token_cache_key(token)
cache.set(key, value, timeout=ttl)
def get_token_from_cache(self, token):
key = self.get_token_cache_key(token)
value = cache.get(key, None)
return value
def renewal_token(self, token, ttl=5*60):
value = self.get_token_from_cache(token)
if value:
pre_ttl = self.get_token_ttl(token)
self.set_token_to_cache(token, value, ttl)
post_ttl = self.get_token_ttl(token)
ok = True
msg = f'{pre_ttl}s is renewed to {post_ttl}s.'
else:
ok = False
msg = 'Token is not found.'
data = {
'ok': ok,
'msg': msg
}
return data
class UserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
SecretDetailMixin, GenericViewSet
SecretDetailMixin, TokenCacheMixin, GenericViewSet
):
permission_classes = (IsSuperUserOrAppUser,)
serializer_classes = {
'default': ConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
rbac_perms = {
'GET': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'renewal': 'authentication.add_superconnectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
@staticmethod
def check_resource_permission(user, asset, application, system_user):
@@ -329,9 +429,22 @@ class UserConnectionTokenViewSet(
raise PermissionDenied(error)
return True
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
if not self.request.user.is_superuser and user != self.request.user:
raise PermissionDenied('Only super user can create user token')
@action(methods=[PATCH], detail=False)
def renewal(self, request, *args, **kwargs):
""" 续期 Token """
perm_required = 'authentication.add_superconnectiontoken'
if not request.user.has_perm(perm_required):
raise PermissionDenied('No permissions for authentication.add_superconnectiontoken')
token = request.data.get('token', '')
data = self.renewal_token(token)
status_code = 200 if data.get('ok') else 404
return Response(data=data, status=status_code)
def create_token(self, user, asset, application, system_user, ttl=5*60):
# 再次强调一下权限
perm_required = 'authentication.add_superconnectiontoken'
if user != self.request.user and not self.request.user.has_perm(perm_required):
raise PermissionDenied('Only can create user token')
self.check_resource_permission(user, asset, application, system_user)
token = random_string(36)
secret = random_string(16)
@@ -359,17 +472,22 @@ class UserConnectionTokenViewSet(
'application_name': str(application)
})
key = self.CACHE_KEY_PREFIX.format(token)
cache.set(key, value, timeout=ttl)
return token
self.set_token_to_cache(token, value, ttl)
return token, secret
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
asset, application, system_user, user = self.get_request_resource(serializer)
token = self.create_token(user, asset, application, system_user)
return Response({"token": token}, status=201)
token, secret = self.create_token(user, asset, application, system_user)
tp = 'app' if application else 'asset'
data = {
"id": token, 'secret': secret,
'type': tp, 'protocol': system_user.protocol,
'expire_time': self.get_token_ttl(token),
}
return Response(data, status=201)
def valid_token(self, token):
from users.models import User
@@ -378,8 +496,7 @@ class UserConnectionTokenViewSet(
from perms.utils.asset.permission import validate_permission as asset_validate_permission
from perms.utils.application.permission import validate_permission as app_validate_permission
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
value = self.get_token_from_cache(token)
if not value:
raise serializers.ValidationError('Token not found')
@@ -403,19 +520,9 @@ class UserConnectionTokenViewSet(
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app, expired_at, actions
def get_permissions(self):
if self.action in ["create", "get_rdp_file"]:
if self.request.data.get('user', None):
self.permission_classes = (IsSuperUser,)
else:
self.permission_classes = (IsValidUser,)
return super().get_permissions()
def get(self, request):
token = request.query_params.get('token')
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
value = self.get_token_from_cache(token)
if not value:
return Response('', status=404)
return Response(value)

View File

@@ -5,7 +5,6 @@ from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication import errors
@@ -32,4 +31,4 @@ class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
user_id_url_kwarg = 'user_id'
permission_classes = (IsOrgAdmin,)

View File

@@ -5,7 +5,6 @@ from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication import errors
@@ -32,7 +31,6 @@ class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
user_id_url_kwarg = 'user_id'
permission_classes = (IsOrgAdmin,)
class FeiShuEventSubscriptionCallback(APIView):

View File

@@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
#
from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny
from django.shortcuts import get_object_or_404
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from .. import errors, mixins
__all__ = ['TicketStatusApi']

View File

@@ -39,14 +39,6 @@ class MFASendCodeApi(AuthMixin, CreateAPIView):
username = ''
ip = ''
def get_user_from_db(self, username):
try:
user = get_object_or_404(User, username=username)
return user
except Exception as e:
self.incr_mfa_failed_time(username, self.ip)
raise e
def get_user_from_db(self, username):
"""避免暴力测试用户名"""
ip = self.get_request_ip()

View File

@@ -13,7 +13,7 @@ from common.utils.timezone import utc_now
from common.const.http import POST, GET
from common.drf.api import JMSGenericViewSet
from common.drf.serializers import EmptySerializer
from common.permissions import IsSuperUser
from common.permissions import OnlySuperUser
from common.utils import reverse
from users.models import User
from ..serializers import SSOTokenSerializer
@@ -32,9 +32,8 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
'login_url': SSOTokenSerializer,
'login': EmptySerializer
}
permission_classes = (IsSuperUser,)
@action(methods=[POST], detail=False, permission_classes=[IsSuperUser], url_path='login-url')
@action(methods=[POST], detail=False, permission_classes=[OnlySuperUser], url_path='login-url')
def login_url(self, request, *args, **kwargs):
if not settings.AUTH_SSO:
raise SSOAuthClosed()
@@ -84,6 +83,6 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
return HttpResponseRedirect(next_url)
user = token.user
login(self.request, user, 'authentication.backends.api.SSOAuthentication')
login(self.request, user, settings.AUTH_BACKEND_SSO)
self.send_auth_signal(success=True, user=user)
return HttpResponseRedirect(next_url)

View File

@@ -0,0 +1,29 @@
from django.utils import timezone
from rest_framework.response import Response
from rest_framework.decorators import action
from common.drf.api import JMSModelViewSet
from ..models import TempToken
from ..serializers import TempTokenSerializer
from rbac.permissions import RBACPermission
class TempTokenViewSet(JMSModelViewSet):
serializer_class = TempTokenSerializer
permission_classes = [RBACPermission]
http_method_names = ['post', 'get', 'options', 'patch']
rbac_perms = {
'expire': 'authentication.change_temptoken',
}
def get_queryset(self):
username = self.request.user.username
return TempToken.objects.filter(username=username).order_by('-date_created')
@action(methods=['PATCH'], detail=True, url_path='expire')
def expire(self, *args, **kwargs):
instance = self.get_object()
instance.date_expired = timezone.now()
instance.save()
serializer = self.get_serializer(instance)
return Response(serializer.data)

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
from django.shortcuts import redirect
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView

View File

@@ -5,7 +5,6 @@ from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import IsOrgAdmin
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication import errors
@@ -32,4 +31,4 @@ class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
user_id_url_kwarg = 'user_id'
permission_classes = (IsOrgAdmin,)

View File

@@ -1,11 +1,13 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class AuthenticationConfig(AppConfig):
name = 'authentication'
verbose_name = _('Authentication')
def ready(self):
from . import signals_handlers
from . import signal_handlers
from . import notifications
super().ready()

View File

@@ -0,0 +1,57 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from users.models import User
from common.utils import get_logger
UserModel = get_user_model()
logger = get_logger(__file__)
class JMSBaseAuthBackend:
@staticmethod
def is_enabled():
return True
def has_perm(self, user_obj, perm, obj=None):
return False
def user_can_authenticate(self, user):
"""
Reject users with is_valid=False. Custom user models that don't have
that attribute are allowed.
"""
# 在 check_user_auth 中进行了校验,可以返回对应的错误信息
# is_valid = getattr(user, 'is_valid', None)
# return is_valid or is_valid is None
return True
# allow user to authenticate
def username_allow_authenticate(self, username):
return self.allow_authenticate(username=username)
def user_allow_authenticate(self, user):
return self.allow_authenticate(user=user)
def allow_authenticate(self, user=None, username=None):
if user:
allowed_backend_paths = user.get_allowed_auth_backend_paths()
else:
allowed_backend_paths = User.get_user_allowed_auth_backend_paths(username)
if allowed_backend_paths is None:
# 特殊值 None 表示没有限制
return True
backend_name = self.__class__.__name__
allowed_backend_names = [path.split('.')[-1] for path in allowed_backend_paths]
allow = backend_name in allowed_backend_names
if not allow:
info = 'User {} skip authentication backend {}, because it not in {}'
info = info.format(username, backend_name, ','.join(allowed_backend_names))
logger.debug(info)
return allow
class JMSModelBackend(JMSBaseAuthBackend, ModelBackend):
pass

View File

@@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
#
# 保证 utils 中的模块进行初始化
from . import utils
from .backends import *

View File

@@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
#
from django_cas_ng.backends import CASBackend as _CASBackend
from django.conf import settings
from ..base import JMSBaseAuthBackend
__all__ = ['CASBackend']
class CASBackend(_CASBackend):
def user_can_authenticate(self, user):
return True
class CASBackend(JMSBaseAuthBackend, _CASBackend):
@staticmethod
def is_enabled():
return settings.AUTH_CAS

View File

@@ -0,0 +1,32 @@
from django_cas_ng import utils
from django_cas_ng.utils import (
django_settings, get_protocol,
urllib_parse, REDIRECT_FIELD_NAME, get_redirect_url
)
def get_service_url(request, redirect_to=None):
"""
重写 get_service url 方法, CAS_ROOT_PROXIED_AS 为空时, 支持跳转回当前访问的域名地址
"""
"""Generates application django service URL for CAS"""
if getattr(django_settings, 'CAS_ROOT_PROXIED_AS', None):
service = django_settings.CAS_ROOT_PROXIED_AS + request.path
else:
protocol = get_protocol(request)
host = request.get_host()
service = urllib_parse.urlunparse(
(protocol, host, request.path, '', '', ''),
)
if not django_settings.CAS_STORE_NEXT:
if '?' in service:
service += '&'
else:
service += '?'
service += urllib_parse.urlencode({
REDIRECT_FIELD_NAME: redirect_to or get_redirect_url(request)
})
return service
utils.get_service_url = get_service_url

View File

@@ -8,13 +8,14 @@ from django.core.cache import cache
from django.utils.translation import ugettext as _
from six import text_type
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import authentication, exceptions
from common.auth import signature
from common.utils import get_object_or_none, make_signature, http_to_unixtime
from ..models import AccessKey, PrivateToken
from .base import JMSBaseAuthBackend, JMSModelBackend
UserModel = get_user_model()
@@ -28,18 +29,6 @@ def get_request_date_header(request):
return date
class JMSModelBackend(ModelBackend):
def user_can_authenticate(self, user):
return True
def get_user(self, user_id):
try:
user = UserModel._default_manager.get(pk=user_id)
except UserModel.DoesNotExist:
return None
return user if user.is_valid else None
class AccessKeyAuthentication(authentication.BaseAuthentication):
"""App使用Access key进行签名认证, 目前签名算法比较简单,
app注册或者手动建立后,会生成 access_key_id access_key_secret,
@@ -164,7 +153,7 @@ class AccessTokenAuthentication(authentication.BaseAuthentication):
return self.keyword
class PrivateTokenAuthentication(authentication.TokenAuthentication):
class PrivateTokenAuthentication(JMSBaseAuthBackend, authentication.TokenAuthentication):
model = PrivateToken
@@ -212,46 +201,3 @@ class SignatureAuthentication(signature.SignatureAuthentication):
except AccessKey.DoesNotExist:
return None, None
class SSOAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, sso_token=None, **kwargs):
pass
class WeComAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass
class DingTalkAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass
class FeiShuAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass
class AuthorizationTokenAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
def authenticate(self, request, **kwargs):
pass

View File

@@ -1,43 +1,38 @@
# coding:utf-8
#
import warnings
import ldap
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django_auth_ldap.backend import _LDAPUser, LDAPBackend, LDAPSettings
from django_auth_ldap.backend import _LDAPUser, LDAPBackend
from django_auth_ldap.config import _LDAPConfig, LDAPSearch, LDAPSearchUnion
from users.utils import construct_user_email
from common.const import LDAP_AD_ACCOUNT_DISABLE
from .base import JMSBaseAuthBackend
logger = _LDAPConfig.get_logger()
class LDAPAuthorizationBackend(LDAPBackend):
class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
"""
Override this class to override _LDAPUser to LDAPUser
"""
@staticmethod
def user_can_authenticate(user):
"""
Reject users with is_active=False. Custom user models that don't have
that attribute are allowed.
"""
is_valid = getattr(user, 'is_valid', None)
return is_valid or is_valid is None
def is_enabled():
return settings.AUTH_LDAP
def get_or_build_user(self, username, ldap_user):
"""
This must return a (User, built) 2-tuple for the given LDAP user.
This must return a (User, built) 2-tuple for the given LDAP user.
username is the Django-friendly username of the user. ldap_user.dn is
the user's DN and ldap_user.attrs contains all of their LDAP
attributes.
username is the Django-friendly username of the user. ldap_user.dn is
the user's DN and ldap_user.attrs contains all of their LDAP
attributes.
The returned User object may be an unsaved model instance.
The returned User object may be an unsaved model instance.
"""
"""
model = self.get_user_model()
if self.settings.USER_QUERY_FIELD:
@@ -58,7 +53,7 @@ class LDAPAuthorizationBackend(LDAPBackend):
else:
built = False
return (user, built)
return user, built
def pre_check(self, username, password):
if not settings.AUTH_LDAP:
@@ -80,6 +75,9 @@ class LDAPAuthorizationBackend(LDAPBackend):
def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend')
if username is None or password is None:
logger.info('No username or password')
return None
match, msg = self.pre_check(username, password)
if not match:
logger.info('Authenticate failed: {}'.format(msg))

View File

@@ -0,0 +1 @@
from .backends import *

View File

@@ -0,0 +1,290 @@
"""
OpenID Connect relying party (RP) authentication backends
=========================================================
This modules defines backends allowing to authenticate a user using a specific token endpoint
of an OpenID Connect provider (OP).
"""
import base64
import requests
from rest_framework.exceptions import ParseError
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.urls import reverse
from django.conf import settings
from common.utils import get_logger
from ..base import JMSBaseAuthBackend
from .utils import validate_and_return_id_token, build_absolute_uri
from .decorator import ssl_verification
from .signals import (
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
)
logger = get_logger(__file__)
__all__ = ['OIDCAuthCodeBackend', 'OIDCAuthPasswordBackend']
class UserMixin:
@transaction.atomic
def get_or_create_user_from_claims(self, request, claims):
log_prompt = "Get or Create user from claims [ActionForUser]: {}"
logger.debug(log_prompt.format('start'))
sub = claims['sub']
name = claims.get('name', sub)
username = claims.get('preferred_username', sub)
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
logger.debug(
log_prompt.format(
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
)
)
user, created = get_user_model().objects.get_or_create(
username=username, defaults={"name": name, "email": email}
)
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => openid create or update user"))
openid_create_or_update_user.send(
sender=self.__class__, request=request, user=user, created=created,
name=name, username=username, email=email
)
return user, created
class OIDCBaseBackend(UserMixin, JMSBaseAuthBackend, ModelBackend):
@staticmethod
def is_enabled():
return settings.AUTH_OPENID
class OIDCAuthCodeBackend(OIDCBaseBackend):
""" Allows to authenticate users using an OpenID Connect Provider (OP).
This authentication backend is able to authenticate users in the case of the OpenID Connect
Authorization Code flow. The ``authenticate`` method provided by this backend is likely to be
called when the callback URL is requested by the OpenID Connect Provider (OP). Thus it will
call the OIDC provider again in order to request a valid token using the authorization code that
should be available in the request parameters associated with the callback call.
"""
@ssl_verification
def authenticate(self, request, nonce=None, **kwargs):
""" Authenticates users in case of the OpenID Connect Authorization code flow. """
log_prompt = "Process authenticate [OIDCAuthCodeBackend]: {}"
logger.debug(log_prompt.format('start'))
# NOTE: the request object is mandatory to perform the authentication using an authorization
# code provided by the OIDC supplier.
if (nonce is None and settings.AUTH_OPENID_USE_NONCE) or request is None:
logger.debug(log_prompt.format('Request or nonce value is missing'))
return
# Fetches required GET parameters from the HTTP request object.
state = request.GET.get('state')
code = request.GET.get('code')
# Don't go further if the state value or the authorization code is not present in the GET
# parameters because we won't be able to get a valid token for the user in that case.
if (state is None and settings.AUTH_OPENID_USE_STATE) or code is None:
logger.debug(log_prompt.format('Authorization code or state value is missing'))
raise SuspiciousOperation('Authorization code or state value is missing')
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token payload'))
token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
# Prepares the token headers that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token headers'))
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET)
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())}
# Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint'))
token_response = requests.post(
settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, headers=headers
)
try:
token_response.raise_for_status()
token_response_data = token_response.json()
except Exception as e:
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
raw_id_token = token_response_data.get('id_token')
id_token = validate_and_return_id_token(raw_id_token, nonce)
if id_token is None:
logger.debug(log_prompt.format(
'ID Token is missing, raw id token is: {}'.format(raw_id_token))
)
return
# Retrieves the access token and refresh token.
access_token = token_response_data.get('access_token')
refresh_token = token_response_data.get('refresh_token')
# Stores the ID token, the related access token and the refresh token in the session.
request.session['oidc_auth_id_token'] = raw_id_token
request.session['oidc_auth_access_token'] = access_token
request.session['oidc_auth_refresh_token'] = refresh_token
# If the id_token contains userinfo scopes and claims we don't have to hit the userinfo
# endpoint.
# https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
if settings.AUTH_OPENID_ID_TOKEN_INCLUDE_CLAIMS:
logger.debug(log_prompt.format('ID Token in claims'))
claims = id_token
else:
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
claims_response = requests.get(
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
headers={'Authorization': 'Bearer {0}'.format(access_token)}
)
try:
claims_response.raise_for_status()
claims = claims_response.json()
except Exception as e:
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
raise ParseError(error)
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)
logger.debug(log_prompt.format('Update or create oidc user'))
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=user.username,
reason="User is invalid"
)
return None
class OIDCAuthPasswordBackend(OIDCBaseBackend):
@ssl_verification
def authenticate(self, request, username=None, password=None, **kwargs):
try:
return self._authenticate(request, username, password, **kwargs)
except Exception as e:
error = f'Authenticate exception: {e}'
logger.error(error, exc_info=True)
return
def _authenticate(self, request, username=None, password=None, **kwargs):
"""
https://oauth.net/2/
https://aaronparecki.com/oauth-2-simplified/#password
"""
log_prompt = "Process authenticate [OIDCAuthPasswordBackend]: {}"
logger.debug(log_prompt.format('start'))
request_timeout = 15
if not username or not password:
logger.debug(log_prompt.format('Username or password is missing'))
return
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token payload'))
token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'password',
'username': username,
'password': password,
}
# Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint'))
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload, timeout=request_timeout)
try:
token_response.raise_for_status()
token_response_data = token_response.json()
except Exception as e:
error = "Json token response error, token response " \
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
)
return
# Retrieves the access token
access_token = token_response_data.get('access_token')
# Fetches the claims (user information) from the userinfo endpoint provided by the OP.
logger.debug(log_prompt.format('Fetches the claims from the userinfo endpoint'))
claims_response = requests.get(
settings.AUTH_OPENID_PROVIDER_USERINFO_ENDPOINT,
headers={'Authorization': 'Bearer {0}'.format(access_token)},
timeout=request_timeout
)
try:
claims_response.raise_for_status()
claims = claims_response.json()
except Exception as e:
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
)
return
logger.debug(log_prompt.format('Get or create user from claims'))
user, created = self.get_or_create_user_from_claims(request, claims)
logger.debug(log_prompt.format('Update or create oidc user'))
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(
sender=self.__class__, request=request, user=user
)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid"
)
return None

View File

@@ -0,0 +1,60 @@
# coding: utf-8
#
import warnings
import contextlib
import requests
from django.conf import settings
from urllib3.exceptions import InsecureRequestWarning
from .utils import get_logger
__all__ = ['ssl_verification']
old_merge_environment_settings = requests.Session.merge_environment_settings
logger = get_logger(__file__)
@contextlib.contextmanager
def no_ssl_verification():
"""
https://stackoverflow.com/questions/15445981/
how-do-i-disable-the-security-certificate-check-in-python-requests
"""
opened_adapters = set()
def merge_environment_settings(self, url, proxies, stream, verify, cert):
# Verification happens only once per connection so we need to close
# all the opened adapters once we're done. Otherwise, the effects of
# verify=False persist beyond the end of this context manager.
opened_adapters.add(self.get_adapter(url))
_settings = old_merge_environment_settings(
self, url, proxies, stream, verify, cert
)
_settings['verify'] = False
return _settings
requests.Session.merge_environment_settings = merge_environment_settings
try:
with warnings.catch_warnings():
warnings.simplefilter('ignore', InsecureRequestWarning)
yield
finally:
requests.Session.merge_environment_settings = old_merge_environment_settings
for adapter in opened_adapters:
try:
adapter.close()
except:
pass
def ssl_verification(func):
def wrapper(*args, **kwargs):
if not settings.AUTH_OPENID_IGNORE_SSL_VERIFICATION:
return func(*args, **kwargs)
with no_ssl_verification():
return func(*args, **kwargs)
return wrapper

View File

@@ -1,10 +1,106 @@
from jms_oidc_rp.middleware import OIDCRefreshIDTokenMiddleware as _OIDCRefreshIDTokenMiddleware
import time
import requests
import requests.exceptions
from django.core.exceptions import MiddlewareNotUsed
from django.conf import settings
from django.contrib import auth
from common.utils import get_logger
from .utils import validate_and_return_id_token
from .decorator import ssl_verification
class OIDCRefreshIDTokenMiddleware(_OIDCRefreshIDTokenMiddleware):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
logger = get_logger(__file__)
class OIDCRefreshIDTokenMiddleware:
""" Allows to periodically refresh the ID token associated with the authenticated user. """
def __init__(self, get_response):
if not settings.AUTH_OPENID:
raise MiddlewareNotUsed
self.get_response = get_response
def __call__(self, request):
# Refreshes tokens only in the applicable cases.
if request.method == 'GET' and not request.is_ajax() and request.user.is_authenticated and settings.AUTH_OPENID:
self.refresh_token(request)
response = self.get_response(request)
return response
@ssl_verification
def refresh_token(self, request):
""" Refreshes the token of the current user. """
log_prompt = "Process refresh Token: {}"
# logger.debug(log_prompt.format('Start'))
# NOTE: SHARE_SESSION is False means that the user does not share sessions
# with other applications
if not settings.AUTH_OPENID_SHARE_SESSION:
logger.debug(log_prompt.format('Not share session'))
return
# NOTE: no refresh token in the session means that the user wasn't authentified using the
# OpenID Connect provider (OP).
refresh_token = request.session.get('oidc_auth_refresh_token')
if refresh_token is None:
logger.debug(log_prompt.format('Refresh token is missing'))
return
id_token_exp_timestamp = request.session.get('oidc_auth_id_token_exp_timestamp', None)
now_timestamp = time.time()
# Returns immediately if the token is still valid.
if id_token_exp_timestamp is not None and id_token_exp_timestamp > now_timestamp:
# logger.debug(log_prompt.format('Returns immediately because token is still valid'))
return
# Prepares the token payload that will be used to request a new token from the token
# endpoint.
refresh_token = request.session.pop('oidc_auth_refresh_token')
token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
'scope': settings.AUTH_OPENID_SCOPES,
}
# Calls the token endpoint.
logger.debug(log_prompt.format('Calls the token endpoint'))
token_response = requests.post(settings.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT, data=token_payload)
try:
token_response.raise_for_status()
except requests.exceptions.HTTPError as e:
logger.debug(log_prompt.format('Request exception http error: {}'.format(str(e))))
logger.debug(log_prompt.format('Logout'))
auth.logout(request)
return
token_response_data = token_response.json()
# Validates the token.
logger.debug(log_prompt.format('Validate ID Token'))
raw_id_token = token_response_data.get('id_token')
id_token = validate_and_return_id_token(raw_id_token, validate_nonce=False)
# If the token cannot be validated we have to log out the current user.
if id_token is None:
logger.debug(log_prompt.format('ID Token is None'))
auth.logout(request)
logger.debug(log_prompt.format('Logout'))
return
# Retrieves the access token and refresh token.
access_token = token_response_data.get('access_token')
refresh_token = token_response_data.get('refresh_token')
# Stores the ID token, the related access token and the refresh token in the session.
request.session['oidc_auth_id_token'] = raw_id_token
request.session['oidc_auth_access_token'] = access_token
request.session['oidc_auth_refresh_token'] = refresh_token
# Saves the new expiration timestamp.
request.session['oidc_auth_id_token_exp_timestamp'] = \
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE

View File

@@ -0,0 +1,18 @@
"""
OpenID Connect relying party (RP) signals
=========================================
This modules defines Django signals that can be triggered during the OpenID Connect
authentication process.
"""
from django.dispatch import Signal
openid_create_or_update_user = Signal(
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
)
openid_user_login_success = Signal(providing_args=['request', 'user'])
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])

View File

@@ -0,0 +1,20 @@
"""
OpenID Connect relying party (RP) URLs
======================================
This modules defines the URLs allowing to perform OpenID Connect flows on a Relying Party (RP).
It defines three main endpoints: the authentication request endpoint, the authentication
callback endpoint and the end session endpoint.
"""
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OIDCAuthRequestView.as_view(), name='login'),
path('callback/', views.OIDCAuthCallbackView.as_view(), name='login-callback'),
path('logout/', views.OIDCEndSessionView.as_view(), name='logout'),
]

View File

@@ -0,0 +1,126 @@
"""
OpenID Connect relying party (RP) utilities
===========================================
This modules defines utilities allowing to manipulate ID tokens and other common helpers.
"""
import datetime as dt
from calendar import timegm
from urllib.parse import urlparse, urljoin
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_bytes, smart_bytes
from jwkest import JWKESTException
from jwkest.jwk import KEYS
from jwkest.jws import JWS
from django.conf import settings
from common.utils import get_logger
logger = get_logger(__file__)
def validate_and_return_id_token(jws, nonce=None, validate_nonce=True):
""" Validates the id_token according to the OpenID Connect specification. """
log_prompt = "Validate ID Token: {}"
logger.debug(log_prompt.format('Get shared key'))
shared_key = settings.AUTH_OPENID_CLIENT_ID \
if settings.AUTH_OPENID_PROVIDER_SIGNATURE_ALG == 'HS256' \
else settings.AUTH_OPENID_PROVIDER_SIGNATURE_KEY # RS256
try:
# Decodes the JSON Web Token and raise an error if the signature is invalid.
logger.debug(log_prompt.format('Verify compact jwk'))
id_token = JWS().verify_compact(force_bytes(jws), _get_jwks_keys(shared_key))
except JWKESTException as e:
logger.debug(log_prompt.format('Verify compact jwkest exception: {}'.format(str(e))))
return
# Validates the claims embedded in the id_token.
logger.debug(log_prompt.format('Validate claims'))
_validate_claims(id_token, nonce=nonce, validate_nonce=validate_nonce)
return id_token
def _get_jwks_keys(shared_key):
""" Returns JWKS keys used to decrypt id_token values. """
# The OpenID Connect Provider (OP) uses RSA keys to sign/enrypt ID tokens and generate public
# keys allowing to decrypt them. These public keys are exposed through the 'jwks_uri' and should
# be used to decrypt the JWS - JSON Web Signature.
log_prompt = "Get jwks keys: {}"
logger.debug(log_prompt.format('Start'))
jwks_keys = KEYS()
logger.debug(log_prompt.format('Load from provider jwks endpoint'))
jwks_keys.load_from_url(settings.AUTH_OPENID_PROVIDER_JWKS_ENDPOINT)
# Adds the shared key (which can correspond to the client_secret) as an oct key so it can be
# used for HMAC signatures.
logger.debug(log_prompt.format('Add key'))
jwks_keys.add({'key': smart_bytes(shared_key), 'kty': 'oct'})
logger.debug(log_prompt.format('End'))
return jwks_keys
def _validate_claims(id_token, nonce=None, validate_nonce=True):
""" Validates the claims embedded in the JSON Web Token. """
log_prompt = "Validate claims: {}"
logger.debug(log_prompt.format('Start'))
iss_parsed_url = urlparse(id_token['iss'])
provider_parsed_url = urlparse(settings.AUTH_OPENID_PROVIDER_ENDPOINT)
if iss_parsed_url.netloc != provider_parsed_url.netloc:
logger.debug(log_prompt.format('Invalid issuer'))
raise SuspiciousOperation('Invalid issuer')
if isinstance(id_token['aud'], str):
id_token['aud'] = [id_token['aud']]
if settings.AUTH_OPENID_CLIENT_ID not in id_token['aud']:
logger.debug(log_prompt.format('Invalid audience'))
raise SuspiciousOperation('Invalid audience')
if len(id_token['aud']) > 1 and 'azp' not in id_token:
logger.debug(log_prompt.format('Incorrect id_token: azp'))
raise SuspiciousOperation('Incorrect id_token: azp')
if 'azp' in id_token and id_token['azp'] != settings.AUTH_OPENID_CLIENT_ID:
raise SuspiciousOperation('Incorrect id_token: azp')
utc_timestamp = timegm(dt.datetime.utcnow().utctimetuple())
if utc_timestamp > id_token['exp']:
logger.debug(log_prompt.format('Signature has expired'))
raise SuspiciousOperation('Signature has expired')
if 'nbf' in id_token and utc_timestamp < id_token['nbf']:
logger.debug(log_prompt.format('Incorrect id_token: nbf'))
raise SuspiciousOperation('Incorrect id_token: nbf')
# Verifies that the token was issued in the allowed timeframe.
if utc_timestamp > id_token['iat'] + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE:
logger.debug(log_prompt.format('Incorrect id_token: iat'))
raise SuspiciousOperation('Incorrect id_token: iat')
# Validate the nonce to ensure the request was not modified if applicable.
id_token_nonce = id_token.get('nonce', None)
if validate_nonce and settings.AUTH_OPENID_USE_NONCE and id_token_nonce != nonce:
logger.debug(log_prompt.format('Incorrect id_token: nonce'))
raise SuspiciousOperation('Incorrect id_token: nonce')
logger.debug(log_prompt.format('End'))
def build_absolute_uri(request, path=None):
"""
Build absolute redirect uri
"""
if path is None:
path = '/'
if settings.BASE_SITE_URL:
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
else:
redirect_uri = request.build_absolute_uri(path)
return redirect_uri

View File

@@ -0,0 +1,222 @@
"""
OpenID Connect relying party (RP) views
=======================================
This modules defines views allowing to start the authorization and authentication process in
order to authenticate a specific user. The most important views are: the "login" allowing to
authenticate the users using the OP and get an authorizartion code, the callback view allowing
to retrieve a valid token for the considered user and the logout view.
"""
import time
from django.conf import settings
from django.contrib import auth
from django.core.exceptions import SuspiciousOperation
from django.http import HttpResponseRedirect, QueryDict
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.http import is_safe_url, urlencode
from django.views.generic import View
from .utils import get_logger, build_absolute_uri
logger = get_logger(__file__)
class OIDCAuthRequestView(View):
""" Allows to start the authorization flow in order to authenticate the end-user.
This view acts as the main endpoint to trigger the authentication process involving the OIDC
provider (OP). It prepares an authentication request that will be sent to the authorization
server in order to authenticate the end-user.
"""
http_method_names = ['get', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OIDCAuthRequestView]: {}"
logger.debug(log_prompt.format('Start'))
# Defines common parameters used to bootstrap the authentication request.
logger.debug(log_prompt.format('Construct request params'))
authentication_request_params = request.GET.dict()
authentication_request_params.update({
'scope': settings.AUTH_OPENID_SCOPES,
'response_type': 'code',
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
})
# States should be used! They are recommended in order to maintain state between the
# authentication request and the callback.
if settings.AUTH_OPENID_USE_STATE:
logger.debug(log_prompt.format('Use state'))
state = get_random_string(settings.AUTH_OPENID_STATE_LENGTH)
authentication_request_params.update({'state': state})
request.session['oidc_auth_state'] = state
# Nonces should be used too! In that case the generated nonce is stored both in the
# authentication request parameters and in the user's session.
if settings.AUTH_OPENID_USE_NONCE:
logger.debug(log_prompt.format('Use nonce'))
nonce = get_random_string(settings.AUTH_OPENID_NONCE_LENGTH)
authentication_request_params.update({'nonce': nonce, })
request.session['oidc_auth_nonce'] = nonce
# Stores the "next" URL in the session if applicable.
logger.debug(log_prompt.format('Stores next url in the session'))
next_url = request.GET.get('next')
request.session['oidc_auth_next_url'] = next_url \
if is_safe_url(url=next_url, allowed_hosts=(request.get_host(), )) else None
# Redirects the user to authorization endpoint.
logger.debug(log_prompt.format('Construct redirect url'))
query = urlencode(authentication_request_params)
redirect_url = '{url}?{query}'.format(
url=settings.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT, query=query)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(redirect_url)
class OIDCAuthCallbackView(View):
""" Allows to complete the authentication process.
This view acts as the main endpoint to complete the authentication process involving the OIDC
provider (OP). It checks the request sent by the OIDC provider in order to determine whether the
considered was successfully authentified or not and authenticates the user at the current
application level if applicable.
"""
http_method_names = ['get', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OIDCAuthCallbackView]: {}"
logger.debug(log_prompt.format('Start'))
callback_params = request.GET
# Retrieve the state value that was previously generated. No state means that we cannot
# authenticate the user (so a failure should be returned).
state = request.session.get('oidc_auth_state', None)
# Retrieve the nonce that was previously generated and remove it from the current session.
# If no nonce is available (while the USE_NONCE setting is set to True) this means that the
# authentication cannot be performed and so we have redirect the user to a failure URL.
nonce = request.session.pop('oidc_auth_nonce', None)
# NOTE: a redirect to the failure page should be return if some required GET parameters are
# missing or if no state can be retrieved from the current session.
if (
((nonce and settings.AUTH_OPENID_USE_NONCE) or not settings.AUTH_OPENID_USE_NONCE)
and
(
(state and settings.AUTH_OPENID_USE_STATE and 'state' in callback_params)
or
(not settings.AUTH_OPENID_USE_STATE)
)
and
('code' in callback_params)
):
# Ensures that the passed state values is the same as the one that was previously
# generated when forging the authorization request. This is necessary to mitigate
# Cross-Site Request Forgery (CSRF, XSRF).
if settings.AUTH_OPENID_USE_STATE and callback_params['state'] != state:
logger.debug(log_prompt.format('Invalid OpenID Connect callback state value'))
raise SuspiciousOperation('Invalid OpenID Connect callback state value')
# Authenticates the end-user.
next_url = request.session.get('oidc_auth_next_url', None)
logger.debug(log_prompt.format('Process authenticate'))
user = auth.authenticate(nonce=nonce, request=request)
if user and user.is_valid:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)
# Stores an expiration timestamp in the user's session. This value will be used if
# the project is configured to periodically refresh user's token.
self.request.session['oidc_auth_id_token_exp_timestamp'] = \
time.time() + settings.AUTH_OPENID_ID_TOKEN_MAX_AGE
# Stores the "session_state" value that can be passed by the OpenID Connect provider
# in order to maintain a consistent session state across the OP and the related
# relying parties (RP).
self.request.session['oidc_auth_session_state'] = \
callback_params.get('session_state', None)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(
next_url or settings.AUTH_OPENID_AUTHENTICATION_REDIRECT_URI
)
if 'error' in callback_params:
logger.debug(
log_prompt.format('Error in callback params: {}'.format(callback_params['error']))
)
# If we receive an error in the callback GET parameters, this means that the
# authentication could not be performed at the OP level. In that case we have to logout
# the current user because we could've obtained this error after a prompt=none hit on
# OpenID Connect Provider authenticate endpoint.
logger.debug(log_prompt.format('Logout'))
auth.logout(request)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(settings.AUTH_OPENID_AUTHENTICATION_FAILURE_REDIRECT_URI)
class OIDCEndSessionView(View):
""" Allows to end the session of any user authenticated using OpenID Connect.
This view acts as the main endpoint to end the session of an end-user that was authenticated
using the OIDC provider (OP). It calls the "end-session" endpoint provided by the provider if
applicable.
"""
http_method_names = ['get', 'post', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OIDCEndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
return self.post(request)
def post(self, request):
""" Processes POST requests. """
log_prompt = "Process POST requests [OIDCEndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
# Log out the current user.
if request.user.is_authenticated:
logger.debug(log_prompt.format('Current user is authenticated'))
try:
logout_url = self.provider_end_session_url \
if settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT else logout_url
except KeyError: # pragma: no cover
logout_url = logout_url
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
auth.logout(request)
# Redirects the user to the appropriate URL.
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(logout_url)
@property
def provider_end_session_url(self):
""" Returns the end-session URL. """
q = QueryDict(mutable=True)
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
self.request.session['oidc_auth_id_token']
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())

View File

@@ -1,4 +0,0 @@
"""
使用下面的工程进行jumpserver 的 oidc 认证
https://github.com/BaiJiangJie/jumpserver-django-oidc-rp
"""

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