Compare commits

...

138 Commits

Author SHA1 Message Date
Jiangjie.Bai
b3cd9d50c9 fix: 修复登录失败日志的原因信息 2022-08-08 15:48:41 +08:00
feng626
07caaa5e46 fix: 修复用户自动登录bug 2022-08-08 11:43:04 +08:00
Jiangjie.Bai
9e9190aed5 fix: 修复更新资产账号不成功的问题(末尾:) 2022-08-02 16:42:04 +08:00
Jiangjie.Bai
3cddaef798 fix: 修复系统平台不能导入的问题 2022-08-02 14:56:03 +08:00
Jiangjie.Bai
99a6d7b0ab perf: 修改用户登录 ACL 翻译信息 2022-08-02 14:54:41 +08:00
Jiangjie.Bai
306f078a09 perf: 修改冲突 2022-07-29 11:37:38 +08:00
feng626
c1cb211724 fix: super ticket close bug 2022-07-26 18:34:15 +08:00
Jiangjie.Bai
f52a0ce960 Merge pull request #8645 from jumpserver/dev
v2.24.0
2022-07-21 15:40:57 +08:00
ibuler
9d17f27fb3 fix: 修复密码可能解密失败报错 2022-07-21 15:37:32 +08:00
feng626
36d0b8d085 fix: 组件角色绑定错误 2022-07-21 15:37:06 +08:00
fit2bot
046356728a perf: sso token 最小60秒 (#8642)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-21 13:51:15 +08:00
Jiangjie.Bai
d34c4fb7ec Merge pull request #8640 from jumpserver/dev
v2.24.0-rc5
2022-07-20 19:07:18 +08:00
Jiangjie.Bai
ca49029d8f fix: 锁定依赖 keystoneauth1==3.4.0 2022-07-20 17:22:22 +08:00
ibuler
12036f8c96 perf: 修改 django 版本 2022-07-20 17:01:06 +08:00
Jiangjie.Bai
60e455bea2 fix: 修改theme_info默认值为{} 2022-07-20 16:13:20 +08:00
fit2bot
e7dd731139 fix: 授权过期url 404 -> /console/ (#8634)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-20 15:44:00 +08:00
Jiangjie.Bai
88ae8ac67a fix: 会话列表修改翻译为终端ID 2022-07-20 13:54:43 +08:00
feng626
626b6da9c4 fix cmd为空时bug 2022-07-20 13:43:04 +08:00
fit2bot
cb8690dd63 fix: 处理组件获取connection token获取不到的问题 (#8629)
* fix: 处理组件获取connection token获取不到的问题

* fix: ViewSet 显示获取资源用户

* fix: ViewSet 显示获取资源用户

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-07-20 13:23:43 +08:00
ibuler
2b2aa8f072 perf: 优化 换行 2022-07-20 13:19:39 +08:00
ibuler
772e540527 perf: 修改 connect token 换行 2022-07-20 13:19:39 +08:00
ibuler
ca5f6f3c6f perf: 修改 windows ansible shell 2022-07-20 13:17:45 +08:00
Jiangjie.Bai
29656b1630 fix: 修改获取 rdp-file / client-url / smart-endpoint 时endpoint host后台处理为当前请求的host 2022-07-19 19:05:15 +08:00
吴小白
bdf59da0f6 Merge pull request #8625 from jumpserver/pr@dev@fix_perms_asset-user-permission
fix: 管理员与用户资产列表排序不统一
2022-07-19 18:06:40 +08:00
Jiangjie.Bai
7b6eeb2e3d fix: 清除 ftp 日志 2022-07-19 17:57:09 +08:00
huangzhiwen
fed0732c1e fix: 管理员与用户资产列表排序不统一 2022-07-19 17:28:10 +08:00
Jiangjie.Bai
c12efffcc9 Merge pull request #8622 from jumpserver/dev
v2.24.0-rc4
2022-07-19 16:25:32 +08:00
feng626
358460e7f0 fix: 如配置SECURITY_VIEW_AUTH_NEED_MFA 跳过校验 2022-07-19 16:24:55 +08:00
Jiangjie.Bai
6319be0ea3 Merge pull request #8620 from jumpserver/dev
v2.24.0-rc4
2022-07-19 16:12:08 +08:00
fit2bot
cc2b858769 fix: 修复获取令牌信息的remote app资产信息 (#8619)
* fix: 修复连接令牌只获取自己的令牌信息;修复连接令牌系统用户角色权限问题(普通用户看不到);

* fix: 修复获取令牌信息的remote app资产信息

* fix: 修复获取用户个人信息时使用连接令牌

* fix: 修复获取profile时的连接令牌问题

* fix: 修复连接令牌问题

* fix: 修复连接令牌问题

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-07-19 15:57:02 +08:00
fit2bot
585ddeb25b fix: 授权过期天数修改 (#8618)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-19 11:15:56 +08:00
fit2bot
0eab83f73b fix: 修改翻译 (#8616)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-19 10:49:15 +08:00
fit2bot
62d403bf21 fix: reverse console (#8615)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-18 19:24:17 +08:00
ibuler
bb9d32dc18 perf: 修改所有组织名称 2022-07-18 14:31:50 +08:00
fit2bot
e09383ecf4 fix: django 3.1.14 (#8613)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-18 13:24:31 +08:00
Jiangjie.Bai
4d7f8ffc71 Merge pull request #8610 from jumpserver/dev
v2.24.0-rc3
2022-07-18 12:02:23 +08:00
fit2bot
af5295d30e fix: django 还原 (#8609)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-18 11:53:47 +08:00
feng626
5055d140fd fix: 修复host为空情况 2022-07-18 11:32:38 +08:00
Eric
2ca72a4bff fix: 修复未选择用户,无法创建共享会话的问题 2022-07-18 11:32:06 +08:00
Eric
de61e780e3 fix: 修复错误提示的翻译问题 2022-07-18 11:31:02 +08:00
halo
e1b3851be3 perf: 优化资产节点搜索,查询全路径 2022-07-17 14:18:20 +08:00
Jiangjie.Bai
c665b0dbae Merge pull request #8603 from jumpserver/dev
v2.24.0-rc2
2022-07-15 18:07:09 +08:00
huangzhiwen
0eaca0c1cb fix: 解决mac m1 pip install pymssql报错问题 2022-07-15 18:06:34 +08:00
fit2cloud
8824b6b54e fix: 解决pip不能安装psycopg2-binary和pymssql问题 2022-07-15 18:06:34 +08:00
fit2bot
4fd82b9946 fix: 修改翻译 (#8602)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-15 17:17:56 +08:00
feng626
1b1b70e7bd fix: 处理应用账号脏数据 2022-07-15 17:03:26 +08:00
Jiangjie.Bai
41541a91b9 fix: 修复 public 和 smart API 权限包含 connection token 2022-07-15 15:01:20 +08:00
fit2bot
93537c07a1 fix: 修复工单授权组织问题 (#8599)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-15 11:38:50 +08:00
Jiangjie.Bai
a770a19252 Merge pull request #8595 from jumpserver/dev
v2.24.0-rc1
2022-07-14 17:44:33 +08:00
Jiangjie.Bai
395636296d fix: 修改连接token secret长度为16 2022-07-14 17:43:57 +08:00
fit2bot
9967d52416 perf: 暂时去掉历史账号权限 (#8594)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-14 15:37:00 +08:00
Jiangjie.Bai
717f97cd88 Merge pull request #8592 from jumpserver/dev
v2.24.0-rc1
2022-07-14 14:40:03 +08:00
Jiangjie.Bai
dec8e3459a feat: 添加 Oracle 数据库 version 迁移文件默认 12c 版本
feat: 添加 Oracle 数据库 version 迁移文件默认 12c 版本
2022-07-14 11:18:26 +08:00
fit2bot
4a3d7a8524 perf: history account model queryset (#8588)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-13 17:13:02 +08:00
fit2bot
f758414844 fix: 审批时 来着不同组织的资产校验 (#8586)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-13 16:54:44 +08:00
dependabot[bot]
af080fe38d build(deps): bump django from 3.2.13 to 3.2.14 in /requirements
Bumps [django](https://github.com/django/django) from 3.2.13 to 3.2.14.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.13...3.2.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-13 16:51:22 +08:00
ibuler
f0fbc73f73 perf: 工作台支持 root 2022-07-13 16:31:35 +08:00
fit2bot
ce2f6fdc84 feat: Endpoint 支持 oracle 版本 (#8585)
* feat: Endpoint 支持 oracle 版本

* feat: Endpoint 支持 oracle 版本

* feat: Endpoint 支持 oracle 版本

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-07-13 16:29:05 +08:00
fit2bot
2abca39597 fix: ticket bug (#8584)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-13 15:48:55 +08:00
fit2bot
11e538d417 fix: 工单三方审批不支持修改资产 (#8582)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-13 11:31:53 +08:00
fit2bot
5155b3c184 fix: 修复bluk_create root 组织下判断 (#8581)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-13 11:00:12 +08:00
feng626
e724cdf53d fix: OrgManager add bulk_create method 2022-07-13 10:47:12 +08:00
halo
191d37dd56 feat: 支持session存储方式可配置,可选cache或db 2022-07-13 10:21:20 +08:00
Jiangjie.Bai
602192696c feat: 添加翻译信息 2022-07-12 18:31:18 +08:00
Jiangjie.Bai
b262643f0a fix: 连接令牌添加 expire_time 和 is_valid 字段 2022-07-12 18:29:48 +08:00
fit2bot
cd119a2999 fix: 飞书登录登录日志不记录认证方式 (#8574)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-12 17:36:40 +08:00
fit2bot
d789810984 fix: condirm (#8572)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-12 16:02:41 +08:00
fit2bot
b5cfc6831b feat: 工单支持审批时修改资产 (#8549)
Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
2022-07-12 15:28:42 +08:00
Jiangjie.Bai
b64727e04c fix: 修改用户自更新失败的问题 2022-07-12 14:22:07 +08:00
fit2bot
c7c0374c78 perf: 修改主题 (#8569)
* perf: 优化登录 backend

* perf: 修改主题

Co-authored-by: ibuler <ibuler@qq.com>
2022-07-12 13:45:48 +08:00
Jiangjie.Bai
f3cf071362 feat: 修改connection token secret不显示 2022-07-11 19:37:55 +08:00
fit2bot
27cbbfbc79 refactor: 重构 Connection Token 模块 (完成获取 Super connection token API 逻辑) (#8559)
* refactor: 重构 Connection Token 模块 (完成 Model 设计和创建 Token 的API逻辑)

* refactor: 重构 Connection Token 模块 (完成获取 Token 详细信息的 API 逻辑)

* refactor: 重构 Connection Token 模块 (完成获取 RDP 文件 API 逻辑)

* refactor: 重构 Connection Token 模块 (完成获取 Client url API 逻辑)

* refactor: 重构 Connection Token 模块 (完成获取 Super connection token API 逻辑)

* refactor: 重构 Connection Token 模块 (完成删除原 Connection token 逻辑)

* refactor: 重构 Connection Token 模块 (完成删除原 Connection)

* refactor: 重构 Connection Token 模块 (完善序列类字段)

* refactor: 重构 Connection Token 模块 (完善expire API)

* refactor: 重构 Connection Token 模块 (完善迁移文件)

* refactor: 重构 Connection Token 模块 (完善翻译文件)

* refactor: 重构 Connection Token 模块 (拆分Connection ViewSet)

* refactor: 重构 Connection Token 模块 (修改翻译)

* refactor: 重构 Connection Token 模块 (优化)

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-07-11 18:09:06 +08:00
feng626
7047e445a3 feat: 下载页面添加离线播放器 2022-07-11 17:21:07 +08:00
fit2bot
06375110b9 fix: 修改mfa check 判断 (#8561)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-11 10:48:01 +08:00
Jiangjie.Bai
0e6dbb3e5d fix: 修复 ES 存储 config 被修改的问题 2022-07-08 11:00:22 +08:00
fit2bot
bf7c05f753 fix: 调整confirm (#8554)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-07 17:07:57 +08:00
Jiangjie.Bai
1b4d389f2b fix: 修复创建共享会话链接时 created_by 字段长度问题 2022-07-07 15:44:07 +08:00
fit2bot
0f11ca9c37 perf: 修改翻译 (#8543)
* perf: 修改翻译

* perf: 优化 flash msg page

* perf: 修改 i18n

* perf: 修改 i18n

Co-authored-by: ibuler <ibuler@qq.com>
2022-07-06 17:26:09 +08:00
ibuler
4537e30e4a perf: 修改颜色 2022-07-05 20:28:42 +08:00
dependabot[bot]
2f71ee71b9 build(deps): bump django from 3.2.12 to 3.2.13 in /requirements
Bumps [django](https://github.com/django/django) from 3.2.12 to 3.2.13.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.12...3.2.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-05 20:28:20 +08:00
ibuler
98644eeb61 perf: 修改 logo 2022-07-05 20:07:24 +08:00
fit2bot
001e5d857f pref: debug toolbar 太费时间 先禁用 (#8528)
* perf: 修改主题色

* pref: debug toolbar 太费时间 先禁用

* perf: 修改颜色

* perf: 优化 interface

* perf: 修改 avartar

* perf: css color

Co-authored-by: ibuler <ibuler@qq.com>
2022-07-05 14:43:56 +08:00
Jiangjie.Bai
bbcf992531 feat: 添加 OmniDB Enabled 控制
feat: 添加 OmniDB Enabled 控制
2022-07-05 11:12:37 +08:00
fit2bot
75aacd0da6 fix: 用户登录错误处理bug (#8531)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-05 11:08:35 +08:00
fit2bot
0aad0b7279 feat: 账号历史信息 (#8500)
* feat: 账号历史信息

* del app

Co-authored-by: feng626 <1304903146@qq.com>
2022-07-04 18:54:47 +08:00
fit2bot
8ebcb4b73a fix: translate (#8529)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-04 15:14:59 +08:00
fit2bot
88f60b58dd fix: 修复翻译 (#8527)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-04 14:57:45 +08:00
fit2bot
a6cc8a8b05 perf: 优化confirm接口 (#8451)
* perf: 优化confirm接口

* perf: 修改 校验

* perf: 优化 confirm API 逻辑

* Delete django.po

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
2022-07-04 11:29:39 +08:00
halo
ca19e45905 perf: 优化截取方法 2022-07-04 11:00:41 +08:00
halo
c5bf4075e7 perf: 优化截取方法 2022-07-04 11:00:41 +08:00
halo
04ceca1b83 perf: 修复命令表系统用户字段长度问题,截取成64字符 2022-07-04 11:00:41 +08:00
Jiangjie.Bai
90228e69e0 perf: 会话列表显示终端名称;修复启动 warning 问题 2022-07-01 19:21:13 +08:00
Jiangjie.Bai
62a2a74c27 perf: 会话列表显示终端名称;修复启动 warning 问题 2022-07-01 19:21:13 +08:00
fit2bot
927ae43af2 perf: 优化工单 (#8524)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-01 19:07:55 +08:00
feng626
272f64d743 fix: get_target_ip bug 2022-07-01 14:16:54 +08:00
feng626
af2d927c1f perf: del pandas 2022-06-30 20:20:09 +08:00
Jiangjie.Bai
011e9ffec4 fix: 修复导入导出文件时对于bool类型字段的判断问题 2022-06-30 18:28:07 +08:00
Jiangjie.Bai
8e65975cd7 fix: 修改会话共享可以指定用户的一些问题 2022-06-30 14:22:40 +08:00
fit2bot
9465138faf fix: 修复工单迁移文件 (#8513)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-30 11:44:54 +08:00
fit2bot
081089d636 fix: 修复工单命令复合迁移问题 (#8512)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-30 11:23:53 +08:00
jiangweidong
5d80933e7b feat: 会话分享可设置1、5分钟时限,且可分享给指定人 (#8227)
* perf: 完成会话分享可设置1、5分钟时限,且可分享给指定人

* perf: 完成会话分享可设置1、5分钟时限,且可分享给指定人

* perf: 完成会话分享可设置1、5分钟时限,且可分享给指定人

* feat: 完成会话分享可设置1、5分钟时限,且可分享给指定人
2022-06-30 11:21:26 +08:00
fit2bot
067a90ff9a fix: 修复工单数据库命令复合bug (#8511)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-30 11:17:12 +08:00
Jiangjie.Bai
05826abf9d feat: Endpoint 支持标签匹配
feat: Endpoint 支持标签匹配

feat: Endpoint 支持标签匹配

feat: Endpoint 支持标签匹配

feat: Endpoint 添加帮助信息

feat: Endpoint 添加帮助信息
2022-06-29 18:50:27 +08:00
Jiangjie.Bai
e8363ddff8 perf: 优化 BASE_SITE_URL OIDC 可以为空,实现多个不同端点访问时回调为当前访问的地址 2022-06-29 18:46:05 +08:00
fit2bot
de41747bb2 perf: 添加 debug tool bar (#8504)
* perf: 添加 debug tool bar

* perf: 修改 config name

Co-authored-by: ibuler <ibuler@qq.com>
2022-06-29 14:48:54 +08:00
ibuler
77067f18d5 stash tdsql
pref: 测试完成

perf: 修改支持 tdsql 5.7

revert: 欢迎之前的内容

revert: some

perf: 修改 tdsql

pref: 修改 。
2022-06-28 18:05:20 +08:00
fit2bot
3cbce63c54 perf: 拆分登录 View (#8502)
* perf: 拆分登录 View

* perf: 修改 code

Co-authored-by: ibuler <ibuler@qq.com>
2022-06-28 17:39:13 +08:00
fit2bot
c3c99cc5e8 perf: 优化 redis (#8484)
* perf: 优化 redis

* perf: 优化 redis 时间

* perf: 优化时间

* perf: 修改 ssl

* perf: 修改 ssl

* perf: 修改 ssl name

* perf: 修改名称

Co-authored-by: ibuler <ibuler@qq.com>
2022-06-28 17:23:20 +08:00
fit2bot
b33e376c90 fix: 解决一些工单已知问题 (#8501)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-28 17:19:33 +08:00
ibuler
b619ebf423 perf: 修改 jumpserver 版本号,避免缓存 2022-06-28 10:49:02 +08:00
ibuler
b784d8ba87 fix: 升级依赖库版本,解决生成 key 时的内存泄露 2022-06-27 19:11:43 +08:00
fit2bot
fd7f73a18e fix: 修复工单权限问题 (#8493)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-27 14:02:28 +08:00
fit2bot
8247f24d3f fix: 修复工单bug (#8488)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-27 10:15:29 +08:00
ibuler
3749a0c6a1 perf: 修复 middleware 导致的内存增长 2022-06-25 10:34:18 +08:00
Jiangjie.Bai
fd41fd78cf fix: 修改 private_key 的序列类长度为 16384 2022-06-24 19:24:21 +08:00
ibuler
8c31e8e634 perf: 修改 sdk 版本 2022-06-23 19:18:15 +08:00
feng626
648fabbe03 fix: 修复工单迁移文件 2022-06-23 19:15:57 +08:00
feng626
9388f37c39 fix: ticket bug 2022-06-23 18:35:53 +08:00
feng626
b264db3e7e fix: 修复工单迁移文件 2022-06-23 17:45:34 +08:00
ibuler
dbc5b7bdc3 perf: 升级 tencent sdk 2022-06-23 17:29:12 +08:00
ibuler
ac20bc05ba perf: 优化 css 2022-06-23 15:55:33 +08:00
fit2bot
7e2f81a418 perf: 重构 ticket (#8281)
* perf: 重构 ticket

* perf: 优化 tickets

* perf: 暂存

* perf: 建立 ticket model

* perf: 暂存一下

* perf: 修改 tickets

* perf: 修改 import

* perf: 修改model

* perf: 暂存一波

* perf: 修改...

* del process_map field

* 工单重构

* 资产 应用对接前端

* perf: 修改 ticket

* fix: bug

* 修改迁移文件

* 添加其他api

* 去掉process_map

* perf: 优化去掉 signal

* perf: 修改这里

* 修改一点

* perf: 修改工单

* perf: 修改状态

* perf: 修改工单流转

* step 状态切换

* perf: 修改 ticket open

* perf: 修改流程

* perf: stash it

* 改又改

* stash it

* perf: stash

* stash

* migrate

* perf migrate

* 调整一下

* 修复bug

* 修改一点

* 修改一点

* 优化一波

* perf: ticket migrations

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-23 13:52:28 +08:00
Jiangjie.Bai
2471787277 Merge pull request #8457 from jumpserver/pr@dev@perf_redis
perf: redis AND login page
2022-06-23 10:39:22 +08:00
ibuler
e6abdbdadc perf: 修改 req version 2022-06-22 18:17:57 +08:00
Jiangjie.Bai
5ed65ca2ff fix: 修复post方法调用AuthBook接口时500的问题 2022-06-22 17:11:35 +08:00
ibuler
ba6b1bf692 perf: 修改翻译 2022-06-22 14:30:52 +08:00
ibuler
1aa58e1486 perf: 修改 ignore 2022-06-21 19:25:56 +08:00
ibuler
fa51465485 perf: 修改去掉 导入 certs 2022-06-21 19:23:29 +08:00
ibuler
8f59bb2a48 perf: 优化登陆 2022-06-21 19:06:06 +08:00
ibuler
2366da1485 perf: redis AND login page 2022-06-21 18:43:48 +08:00
ibuler
f1a22575d3 perf: 优化登录页面 2022-06-21 16:18:13 +08:00
ibuler
7c1882bb53 perf: login 2022-06-21 10:08:14 +08:00
ibuler
97baeebb2a perf: 修改 redis scan counter 2022-06-20 19:40:07 +08:00
ibuler
8b819f3779 perf: 优化登录 2022-06-20 19:22:48 +08:00
Jiangjie.Bai
d1420de4c2 fix: 修复es类型的命令存储更新忽略证书字段不成功的问题 2022-06-20 14:47:30 +08:00
ibuler
379c7198da pref: 去掉 django-redis-cache 依赖 2022-06-20 14:11:59 +08:00
Eric
710cd0fb3b fix:修复es日期索引忽略证书的问题 2022-06-20 14:06:56 +08:00
Jiangjie.Bai
3fde31f2e0 fix: 修复工单自定义搜索时500的问题 2022-06-17 15:26:23 +08:00
238 changed files with 6614 additions and 5485 deletions

3
.gitignore vendored
View File

@@ -31,12 +31,13 @@ media
celerybeat.pid
django.db
celerybeat-schedule.db
data/static
docs/_build/
xpack
xpack.bak
logs/*
### Vagrant ###
.vagrant/
release/*
releashe
/apps/script.py
data/*

View File

@@ -47,7 +47,7 @@ class LoginAssetCheckAPI(CreateAPIView):
asset=self.serializer.asset,
system_user=self.serializer.system_user,
assignees=acl.reviewers.all(),
org_id=self.serializer.org.id
org_id=self.serializer.org.id,
)
confirm_status_url = reverse(
view_name='api-tickets:super-ticket-status',
@@ -59,7 +59,7 @@ class LoginAssetCheckAPI(CreateAPIView):
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
ticket_assignees = ticket.current_step.ticket_assignees.all()
data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@@ -44,87 +44,49 @@ class LoginACL(BaseACL):
def __str__(self):
return self.name
@property
def action_reject(self):
return self.action == self.ActionChoices.reject
@property
def action_allow(self):
return self.action == self.ActionChoices.allow
def is_action(self, action):
return self.action == action
@classmethod
def filter_acl(cls, user):
return user.login_acls.all().valid().distinct()
@staticmethod
def allow_user_confirm_if_need(user, ip):
acl = LoginACL.filter_acl(user).filter(
action=LoginACL.ActionChoices.confirm
).first()
acl = acl if acl and acl.reviewers.exists() else None
if not acl:
return False, acl
ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period')
is_contain_ip = contains_ip(ip, ip_group)
is_contain_time_period = contains_time_period(time_periods)
return is_contain_ip and is_contain_time_period, acl
def match(user, ip):
acls = LoginACL.filter_acl(user)
if not acls:
return
@staticmethod
def allow_user_to_login(user, ip):
acl = LoginACL.filter_acl(user).exclude(
action=LoginACL.ActionChoices.confirm
).first()
if not acl:
return True, ''
ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period')
is_contain_ip = contains_ip(ip, ip_group)
is_contain_time_period = contains_time_period(time_periods)
for acl in acls:
if acl.is_action(LoginACL.ActionChoices.confirm) and not acl.reviewers.exists():
continue
ip_group = acl.rules.get('ip_group')
time_periods = acl.rules.get('time_period')
is_contain_ip = contains_ip(ip, ip_group)
is_contain_time_period = contains_time_period(time_periods)
if is_contain_ip and is_contain_time_period:
# 满足条件,则返回
return acl
reject_type = ''
if is_contain_ip and is_contain_time_period:
# 满足条件
allow = acl.action_allow
if not allow:
reject_type = 'ip' if is_contain_ip else 'time'
else:
# 不满足条件
# 如果acl本身允许那就拒绝如果本身拒绝那就允许
allow = not acl.action_allow
if not allow:
reject_type = 'ip' if not is_contain_ip else 'time'
return allow, reject_type
@staticmethod
def construct_confirm_ticket_meta(request=None):
def create_confirm_ticket(self, request):
from tickets import const
from tickets.models import ApplyLoginTicket
from orgs.models import Organization
title = _('Login confirm') + ' {}'.format(self.user)
login_ip = get_request_ip(request) if request else ''
login_ip = login_ip or '0.0.0.0'
login_city = get_ip_city(login_ip)
login_datetime = local_now_display()
ticket_meta = {
'apply_login_ip': login_ip,
'apply_login_city': login_city,
'apply_login_datetime': login_datetime,
}
return ticket_meta
def create_confirm_ticket(self, request=None):
from tickets import const
from tickets.models import Ticket
from orgs.models import Organization
ticket_title = _('Login confirm') + ' {}'.format(self.user)
ticket_meta = self.construct_confirm_ticket_meta(request)
data = {
'title': ticket_title,
'type': const.TicketType.login_confirm.value,
'meta': ticket_meta,
'title': title,
'type': const.TicketType.login_confirm,
'applicant': self.user,
'apply_login_city': login_city,
'apply_login_ip': login_ip,
'apply_login_datetime': login_datetime,
'org_id': Organization.ROOT_ID,
}
ticket = Ticket.objects.create(**data)
applicant = self.user
ticket = ApplyLoginTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant)
ticket.open(applicant)
ticket.open_by_system(assignees)
return ticket

View File

@@ -85,19 +85,18 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
@classmethod
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
from tickets.const import TicketType
from tickets.models import Ticket
from tickets.models import ApplyLoginAssetTicket
title = _('Login asset confirm') + ' ({})'.format(user)
data = {
'title': _('Login asset confirm') + ' ({})'.format(user),
'title': title,
'type': TicketType.login_asset_confirm,
'meta': {
'apply_login_user': str(user),
'apply_login_asset': str(asset),
'apply_login_system_user': str(system_user),
},
'applicant': user,
'apply_login_user': user,
'apply_login_asset': asset,
'apply_login_system_user': system_user,
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
ticket.create_process_map_and_node(assignees, user)
ticket.open(applicant=user)
ticket = ApplyLoginAssetTicket.objects.create(**data)
ticket.open_by_system(assignees)
return ticket

View File

@@ -2,15 +2,16 @@
#
from django_filters import rest_framework as filters
from django.db.models import F, Q
from django.db.models import Q
from common.drf.filters import BaseFilterSet
from common.drf.api import JMSBulkModelViewSet
from common.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from authentication.const import ConfirmType
from rbac.permissions import RBACPermission
from assets.models import SystemUser
from ..models import Account
from ..hands import NeedMFAVerify
from .. import serializers
@@ -57,7 +58,7 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [RBACPermission, NeedMFAVerify]
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
http_method_names = ['get', 'options']
rbac_perms = {
'retrieve': 'applications.view_applicationaccountsecret',

View File

@@ -1,6 +1,5 @@
# coding: utf-8
#
from django.shortcuts import get_object_or_404
from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response

View File

@@ -83,3 +83,9 @@ class AppType(models.TextChoices):
if AppCategory.is_xpack(category):
return True
return tp in ['oracle', 'postgresql', 'sqlserver']
class OracleVersion(models.TextChoices):
version_11g = '11g', '11g'
version_12c = '12c', '12c'
version_other = 'other', _('Other')

View File

@@ -11,5 +11,4 @@
"""
from common.permissions import NeedMFAVerify
from users.models import User, UserGroup

View File

@@ -0,0 +1,22 @@
# Generated by Django 3.1.14 on 2022-06-29 10:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('applications', '0020_auto_20220316_2028'),
]
operations = [
migrations.AlterModelOptions(
name='historicalaccount',
options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Application account', 'verbose_name_plural': 'historical Application accounts'},
),
migrations.AlterField(
model_name='historicalaccount',
name='history_date',
field=models.DateTimeField(db_index=True),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2022-07-14 02:46
from django.db import migrations
def migrate_db_oracle_version_to_attrs(apps, schema_editor):
db_alias = schema_editor.connection.alias
model = apps.get_model("applications", "Application")
oracles = list(model.objects.using(db_alias).filter(type='oracle'))
for o in oracles:
o.attrs['version'] = '12c'
model.objects.using(db_alias).bulk_update(oracles, ['attrs'])
class Migration(migrations.Migration):
dependencies = [
('applications', '0021_auto_20220629_1826'),
]
operations = [
migrations.RunPython(migrate_db_oracle_version_to_attrs)
]

View File

@@ -0,0 +1,48 @@
# Generated by Django 3.1.14 on 2022-07-15 07:56
import time
from collections import defaultdict
from django.db import migrations
def migrate_account_dirty_data(apps, schema_editor):
db_alias = schema_editor.connection.alias
account_model = apps.get_model('applications', 'Account')
count = 0
bulk_size = 1000
while True:
accounts = account_model.objects.using(db_alias) \
.filter(org_id='')[count:count + bulk_size]
if not accounts:
break
accounts = list(accounts)
start = time.time()
for i in accounts:
if i.app:
org_id = i.app.org_id
elif i.systemuser:
org_id = i.systemuser.org_id
else:
org_id = ''
if org_id:
i.org_id = org_id
account_model.objects.bulk_update(accounts, ['org_id', ])
print("Update account org is empty: {}-{} using: {:.2f}s".format(
count, count + len(accounts), time.time() - start
))
count += len(accounts)
class Migration(migrations.Migration):
dependencies = [
('applications', '0022_auto_20220714_1046'),
]
operations = [
migrations.RunPython(migrate_account_dirty_data),
]

View File

@@ -10,6 +10,7 @@ from common.mixins import CommonModelMixin
from common.tree import TreeNode
from common.utils import is_uuid
from assets.models import Asset, SystemUser
from ..const import OracleVersion
from ..utils import KubernetesTree
from .. import const
@@ -214,6 +215,8 @@ class ApplicationTreeNodeMixin:
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
APP_TYPE = const.AppType
name = models.CharField(max_length=128, verbose_name=_('Name'))
category = models.CharField(
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
@@ -255,6 +258,9 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_db(self):
return self.category == const.AppCategory.db.value
def is_type(self, tp):
return self.type == tp
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app:
@@ -298,6 +304,15 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
target_ip = self.attrs.get('host')
return target_ip
def get_target_protocol_for_oracle(self):
""" Oracle 类型需要单独处理,因为要携带版本号 """
if not self.is_type(self.APP_TYPE.oracle):
return
version = self.attrs.get('version', OracleVersion.version_12c)
if version == OracleVersion.version_other:
return
return 'oracle_%s' % version
class ApplicationUser(SystemUser):
class Meta:

View File

@@ -31,7 +31,7 @@ class ExistAssetPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
class RemoteAppSerializer(serializers.Serializer):
asset_info = serializers.SerializerMethodField()
asset_info = serializers.SerializerMethodField(label=_('Asset Info'))
asset = ExistAssetPrimaryKeyRelatedField(
queryset=Asset.objects, required=True, label=_("Asset"), allow_null=True
)

View File

@@ -2,9 +2,15 @@ from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
from applications.const import OracleVersion
__all__ = ['OracleSerializer']
class OracleSerializer(DBSerializer):
version = serializers.ChoiceField(
choices=OracleVersion.choices, default=OracleVersion.version_12c,
allow_null=True, label=_('Version'),
help_text=_('Magnus currently supports only 11g and 12c connections')
)
port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True)

View File

@@ -11,3 +11,4 @@ from .cmd_filter import *
from .gathered_user import *
from .favorite_asset import *
from .account_backup import *
from .account_history import *

View File

@@ -0,0 +1,50 @@
from django.db.models import F
from assets.api.accounts import (
AccountFilterSet, AccountViewSet, AccountSecretsViewSet
)
from common.mixins import RecordViewLogMixin
from .. import serializers
from ..models import AuthBook
__all__ = ['AccountHistoryViewSet', 'AccountHistorySecretsViewSet']
class AccountHistoryFilterSet(AccountFilterSet):
class Meta:
model = AuthBook.history.model
fields = AccountFilterSet.Meta.fields
class AccountHistoryViewSet(AccountViewSet):
model = AuthBook.history.model
filterset_class = AccountHistoryFilterSet
serializer_classes = {
'default': serializers.AccountHistorySerializer,
}
rbac_perms = {
'list': 'assets.view_assethistoryaccount',
'retrieve': 'assets.view_assethistoryaccount',
}
http_method_names = ['get', 'options']
def get_queryset(self):
queryset = self.model.objects.all() \
.annotate(ip=F('asset__ip')) \
.annotate(hostname=F('asset__hostname')) \
.annotate(platform=F('asset__platform__name')) \
.annotate(protocols=F('asset__protocols'))
return queryset
class AccountHistorySecretsViewSet(RecordViewLogMixin, AccountHistoryViewSet):
serializer_classes = {
'default': serializers.AccountHistorySecretSerializer
}
http_method_names = ['get']
permission_classes = AccountSecretsViewSet.permission_classes
rbac_perms = {
'list': 'assets.view_assethistoryaccountsecret',
'retrieve': 'assets.view_assethistoryaccountsecret',
}

View File

@@ -1,4 +1,4 @@
from django.db.models import F, Q
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from rest_framework.decorators import action
@@ -9,12 +9,13 @@ from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
from common.drf.filters import BaseFilterSet
from common.mixins import RecordViewLogMixin
from common.permissions import NeedMFAVerify
from common.permissions import UserConfirmation
from authentication.const import ConfirmType
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node
from .. import serializers
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
__all__ = ['AccountFilterSet', 'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
class AccountFilterSet(BaseFilterSet):
@@ -88,7 +89,7 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
'default': serializers.AccountSecretSerializer
}
http_method_names = ['get']
permission_classes = [RBACPermission, NeedMFAVerify]
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
rbac_perms = {
'list': 'assets.view_assetaccountsecret',
'retrieve': 'assets.view_assetaccountsecret',

View File

@@ -6,7 +6,7 @@ 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.mixins.api import SuggestionMixin
from common.mixins.api import SuggestionMixin, RenderToJsonMixin
from users.models import User, UserGroup
from users.serializers import UserSerializer, UserGroupSerializer
from users.filters import UserFilter
@@ -88,7 +88,7 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
return asset.platform
class AssetPlatformViewSet(ModelViewSet):
class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']

View File

@@ -69,7 +69,7 @@ class CommandConfirmAPI(CreateAPIView):
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
ticket_assignees = ticket.current_step.ticket_assignees.all()
return {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@@ -43,7 +43,7 @@ __all__ = [
class NodeViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Node
filterset_fields = ('value', 'key', 'id')
search_fields = ('value',)
search_fields = ('full_value',)
serializer_class = serializers.NodeSerializer
rbac_perms = {
'match': 'assets.match_node',

View File

@@ -0,0 +1,26 @@
# Generated by Django 3.1.14 on 2022-06-29 10:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0090_auto_20220412_1145'),
]
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'), ('view_assethistoryaccount', 'Can view asset history account'), ('view_assethistoryaccountsecret', 'Can view asset history account secret')], 'verbose_name': 'AuthBook'},
),
migrations.AlterModelOptions(
name='historicalauthbook',
options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical AuthBook', 'verbose_name_plural': 'historical AuthBooks'},
),
migrations.AlterField(
model_name='historicalauthbook',
name='history_date',
field=models.DateTimeField(db_index=True),
),
]

View File

@@ -29,7 +29,9 @@ class AuthBook(BaseUser, AbsConnectivity):
permissions = [
('test_authbook', _('Can test asset account connectivity')),
('view_assetaccountsecret', _('Can view asset account secret')),
('change_assetaccountsecret', _('Can change asset account secret'))
('change_assetaccountsecret', _('Can change asset account secret')),
('view_assethistoryaccount', _('Can view asset history account')),
('view_assethistoryaccountsecret', _('Can view asset history account secret')),
]
def __init__(self, *args, **kwargs):

View File

@@ -125,6 +125,9 @@ class CommandFilterRule(OrgModelMixin):
regex.append(cmd)
continue
if not cmd:
continue
# 如果是单个字符
if cmd[-1].isalpha():
regex.append(r'\b{0}\b'.format(cmd))
@@ -165,26 +168,23 @@ class CommandFilterRule(OrgModelMixin):
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType
from tickets.models import Ticket
from tickets.models import ApplyCommandTicket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm,
'meta': {
'apply_run_user': session.user,
'apply_run_asset': session.asset,
'apply_run_system_user': session.system_user,
'apply_run_command': run_command,
'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
},
'applicant': session.user_obj,
'apply_run_user_id': session.user_id,
'apply_run_asset': str(session.asset),
'apply_run_system_user_id': session.system_user_id,
'apply_run_command': run_command[:4090],
'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
applicant = session.user_obj
ticket = ApplyCommandTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant)
ticket.open(applicant)
ticket.open_by_system(assignees)
return ticket
@classmethod

View File

@@ -9,7 +9,7 @@ import paramiko
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
from common.utils import get_logger, lazyproperty
from orgs.mixins.models import OrgModelMixin
from .base import BaseUser
@@ -36,7 +36,7 @@ class Domain(OrgModelMixin):
def has_gateway(self):
return self.gateway_set.filter(is_active=True).exists()
@property
@lazyproperty
def gateways(self):
return self.gateway_set.filter(is_active=True)
@@ -44,8 +44,9 @@ class Domain(OrgModelMixin):
gateways = [gw for gw in self.gateways if gw.is_connective]
if gateways:
return random.choice(gateways)
else:
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(self.gateways)}.')
if self.gateways:
return random.choice(self.gateways)

View File

@@ -11,4 +11,5 @@ from .cmd_filter import *
from .gathered_user import *
from .favorite_asset import *
from .account import *
from .account_history import *
from .backup import *

View File

@@ -53,7 +53,15 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return attrs
def get_protocols(self, v):
return v.protocols.replace(' ', ', ')
""" protocols 是 queryset 中返回的Post 创建成功后返回序列化时没有这个字段 """
if hasattr(v, 'protocols'):
protocols = v.protocols
elif hasattr(v, 'asset') and v.asset:
protocols = v.asset.protocols
else:
protocols = ''
protocols = protocols.replace(' ', ', ')
return protocols
@classmethod
def setup_eager_loading(cls, queryset):

View File

@@ -0,0 +1,38 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from assets.models import AuthBook
from common.drf.serializers import SecretReadableMixin
from .account import AccountSerializer, AccountSecretSerializer
class AccountHistorySerializer(AccountSerializer):
systemuser_display = serializers.SerializerMethodField(label=_('System user display'))
class Meta:
model = AuthBook.history.model
fields = AccountSerializer.Meta.fields_mini + \
AccountSerializer.Meta.fields_write_only + \
AccountSerializer.Meta.fields_fk + \
['history_id', 'date_created', 'date_updated']
read_only_fields = fields
ref_name = 'AccountHistorySerializer'
@staticmethod
def get_systemuser_display(instance):
if not instance.systemuser:
return ''
return str(instance.systemuser)
def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info)
fields = list(set(fields) - {'org_name'})
return fields
def to_representation(self, instance):
return super(AccountSerializer, self).to_representation(instance)
class AccountHistorySecretSerializer(SecretReadableMixin, AccountHistorySerializer):
class Meta(AccountHistorySerializer.Meta):
extra_kwargs = AccountSecretSerializer.Meta.extra_kwargs

View File

@@ -189,6 +189,9 @@ class PlatformSerializer(serializers.ModelSerializer):
'id', 'name', 'base', 'charset',
'internal', 'meta', 'comment'
]
extra_kwargs = {
'internal': {'read_only': True},
}
class AssetSimpleSerializer(serializers.ModelSerializer):

View File

@@ -13,7 +13,7 @@ from .utils import validate_password_for_ansible
class AuthSerializer(serializers.ModelSerializer):
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096, label=_('Private key'))
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key'))
def gen_keys(self, private_key=None, password=None):
if private_key is None:
@@ -38,7 +38,7 @@ class AuthSerializerMixin(serializers.ModelSerializer):
validators=[validate_password_for_ansible]
)
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=4096
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384
)
passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512,

View File

@@ -1,6 +1,6 @@
import os
import time
import pandas as pd
from openpyxl import Workbook
from collections import defaultdict, OrderedDict
from django.conf import settings
@@ -48,7 +48,7 @@ class BaseAccountHandler:
_fields = cls.get_header_fields(v)
header_fields.update(_fields)
else:
header_fields[field] = v.label
header_fields[field] = str(v.label)
return header_fields
@classmethod
@@ -59,7 +59,7 @@ class BaseAccountHandler:
data = cls.unpack_data(serializer.data)
row_dict = {}
for field, header_name in header_fields.items():
row_dict[header_name] = data[field]
row_dict[header_name] = str(data[field])
return row_dict
@@ -72,24 +72,24 @@ class AssetAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_df(cls):
df_dict = defaultdict(list)
def create_data_map(cls):
data_map = defaultdict(list)
sheet_name = AuthBook._meta.verbose_name
accounts = AuthBook.get_queryset().select_related('systemuser')
if not accounts.first():
return df_dict
return data_map
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
for account in accounts:
account.load_auth()
row = cls.create_row(account, AccountSecretSerializer, header_fields)
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
if sheet_name not in data_map:
data_map[sheet_name].append(list(row.keys()))
data_map[sheet_name].append(list(row.values()))
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
return df_dict
return data_map
class AppAccountHandler(BaseAccountHandler):
@@ -101,19 +101,19 @@ class AppAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_df(cls):
df_dict = defaultdict(list)
def create_data_map(cls):
data_map = defaultdict(list)
accounts = Account.get_queryset().select_related('systemuser')
for account in accounts:
account.load_auth()
app_type = account.type
sheet_name = AppType.get_label(app_type)
row = cls.create_row(account, AppAccountSecretSerializer)
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
if sheet_name not in data_map:
data_map[sheet_name].append(list(row.keys()))
data_map[sheet_name].append(list(row.values()))
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
return df_dict
return data_map
handler_map = {
@@ -142,15 +142,18 @@ class AccountBackupHandler:
if not handler:
continue
df_dict = handler.create_df()
if not df_dict:
data_map = handler.create_data_map()
if not data_map:
continue
filename = handler.get_filename(self.plan_name)
with pd.ExcelWriter(filename) as w:
for sheet, df in df_dict.items():
sheet = sheet.replace(' ', '-')
getattr(df, 'to_excel')(w, sheet_name=sheet, index=False)
wb = Workbook(filename)
for sheet, data in data_map.items():
ws = wb.create_sheet(str(sheet))
for row in data:
ws.append(row)
wb.save(filename)
files.append(filename)
timedelta = round((time.time() - time_start), 2)
logger.info('步骤完成: 用时 {}s'.format(timedelta))

View File

@@ -13,6 +13,8 @@ router = BulkRouter()
router.register(r'assets', api.AssetViewSet, 'asset')
router.register(r'accounts', api.AccountViewSet, 'account')
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
router.register(r'accounts-history', api.AccountHistoryViewSet, 'account-history')
router.register(r'account-history-secrets', api.AccountHistorySecretsViewSet, 'account-history-secret')
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
router.register(r'system-users', api.SystemUserViewSet, 'system-user')
router.register(r'admin-users', api.AdminUserViewSet, 'admin-user')

View File

@@ -51,6 +51,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _('FeiShu')
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
return backend_label_mapping
@@ -277,7 +278,6 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
check_different_city_login_if_need(user, request)
data = generate_data(user.username, request, login_type=login_type)
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
request.session["MFA_VERIFY_TIME"] = int(time.time())
data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log(**data)

View File

@@ -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)
FTPLog.objects.filter(datetime__lt=expired_day).delete()
FTPLog.objects.filter(date_start__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)

View File

@@ -1,85 +1,64 @@
# -*- coding: utf-8 -*-
#
import time
from datetime import datetime
from django.utils import timezone
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from rest_framework.generics import ListCreateAPIView
from rest_framework.generics import RetrieveAPIView, CreateAPIView
from rest_framework.response import Response
from rest_framework import status
from common.permissions import IsValidUser
from ..mfa import MFAOtp
from common.permissions import IsValidUser, UserConfirmation
from ..const import ConfirmType
from ..mixins import authenticate
from ..serializers import ConfirmSerializer
class ConfirmViewSet(ListCreateAPIView):
class ConfirmBindORUNBindOAuth(RetrieveAPIView):
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
def retrieve(self, request, *args, **kwargs):
return Response('ok')
class ConfirmApi(RetrieveAPIView, CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = ConfirmSerializer
def check(self, confirm_type: str):
if confirm_type == ConfirmType.MFA:
return self.user.mfa_enabled
def get_confirm_backend(self, confirm_type):
backend_classes = ConfirmType.get_can_confirm_backend_classes(confirm_type)
if not backend_classes:
return
for backend_cls in backend_classes:
backend = backend_cls(self.request.user, self.request)
if not backend.check():
continue
return backend
if confirm_type == ConfirmType.PASSWORD:
return self.user.is_password_authenticate()
def retrieve(self, request, *args, **kwargs):
confirm_type = request.query_params.get('confirm_type')
backend = self.get_confirm_backend(confirm_type)
if backend is None:
msg = _('This action require verify your MFA')
return Response(data={'error': msg}, status=status.HTTP_404_NOT_FOUND)
if confirm_type == ConfirmType.RELOGIN:
return not self.user.is_password_authenticate()
def authenticate(self, confirm_type, secret_key):
if confirm_type == ConfirmType.MFA:
ok, msg = MFAOtp(self.user).check_code(secret_key)
return ok, msg
if confirm_type == ConfirmType.PASSWORD:
ok = authenticate(self.request, username=self.user.username, password=secret_key)
msg = '' if ok else _('Authentication failed password incorrect')
return ok, msg
if confirm_type == ConfirmType.RELOGIN:
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
login_time = self.request.session.get('login_time')
SPECIFIED_TIME = 5
msg = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
if not login_time:
return False, msg
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
return False, msg
return True, ''
@property
def user(self):
return self.request.user
def list(self, request, *args, **kwargs):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
return Response('ok')
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
return Response('ok')
data = []
for i, confirm_type in enumerate(ConfirmType.values, 1):
if self.check(confirm_type):
data.append({'name': confirm_type, 'level': i})
msg = _('This action require verify your MFA')
return Response({'error': msg, 'backends': data}, status=400)
data = {
'confirm_type': backend.name,
'content': backend.content,
}
return Response(data=data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
confirm_type = validated_data.get('confirm_type')
mfa_type = validated_data.get('mfa_type')
secret_key = validated_data.get('secret_key')
ok, msg = self.authenticate(confirm_type, secret_key)
backend = self.get_confirm_backend(confirm_type)
ok, msg = backend.authenticate(secret_key, mfa_type)
if ok:
request.session["MFA_VERIFY_TIME"] = int(time.time())
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index(confirm_type) + 1
request.session['CONFIRM_TIME'] = int(time.time())
return Response('ok')
return Response({'error': msg}, status=400)

View File

@@ -1,58 +1,65 @@
# -*- coding: utf-8 -*-
#
import urllib.parse
import json
from typing import Callable
import abc
import os
import json
import base64
import ctypes
from django.core.cache import cache
from django.shortcuts import get_object_or_404
import urllib.parse
from django.http import HttpResponse
from django.utils import timezone
from django.utils.translation import ugettext as _
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from django.shortcuts import get_object_or_404
from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers
from django.conf import settings
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
from rest_framework.request import Request
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.utils.common import get_file_by_arch
from orgs.mixins.api import RootOrgViewMixin
from common.drf.api import JMSModelViewSet
from common.http import is_true
from orgs.mixins.api import RootOrgViewMixin
from perms.models.base import Action
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, SuperConnectionTokenSerializer
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
)
logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', 'TokenCacheMixin']
from ..models import ConnectionToken
class ClientProtocolMixin:
"""
下载客户端支持的连接文件,里面包含了 token和 其他连接信息
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
- [x] RDP
- [ ] KoKo
本质上,这里还是暴露出 token 来,进行使用
"""
class ConnectionTokenMixin:
request: Request
get_serializer: Callable
create_token: Callable
get_serializer_context: Callable
@staticmethod
def check_token_valid(token: ConnectionToken):
is_valid, error = token.check_valid()
if not is_valid:
raise PermissionDenied(error)
@abc.abstractmethod
def get_request_resource_user(self, serializer):
raise NotImplementedError
def get_request_resources(self, serializer):
user = self.get_request_resource_user(serializer)
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data.get('system_user')
return user, asset, application, system_user
@staticmethod
def check_user_has_resource_permission(user, asset, application, system_user):
from perms.utils.asset import has_asset_system_permission
from perms.utils.application import has_application_system_permission
if asset and not has_asset_system_permission(user, asset, system_user):
error = f'User not has this asset and system user permission: ' \
f'user={user.id} system_user={system_user.id} asset={asset.id}'
raise PermissionDenied(error)
if application and not has_application_system_permission(user, application, system_user):
error = f'User not has this application and system user permission: ' \
f'user={user.id} system_user={system_user.id} application={application.id}'
raise PermissionDenied(error)
def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset:
@@ -64,21 +71,32 @@ class ClientProtocolMixin:
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
return endpoint
def get_request_resource(self, serializer):
asset = serializer.validated_data.get('asset')
application = serializer.validated_data.get('application')
system_user = serializer.validated_data['system_user']
user = serializer.validated_data.get('user')
user = user if user else self.request.user
return asset, application, system_user, user
@staticmethod
def parse_env_bool(env_key, env_default, true_value, false_value):
return true_value if is_true(os.getenv(env_key, env_default)) else false_value
def get_rdp_file_content(self, serializer):
options = {
def get_client_protocol_data(self, token: ConnectionToken):
from assets.models import SystemUser
protocol = token.system_user.protocol
username = token.user.username
rdp_config = ssh_token = ''
if protocol == SystemUser.Protocol.rdp:
filename, rdp_config = self.get_rdp_file_info(token)
elif protocol == SystemUser.Protocol.ssh:
filename, ssh_token = self.get_ssh_token(token)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
return {
"filename": filename,
"protocol": protocol,
"username": username,
"token": ssh_token,
"config": rdp_config
}
def get_rdp_file_info(self, token: ConnectionToken):
rdp_options = {
'full address:s': '',
'username:s': '',
# 'screen mode id:i': '1',
@@ -111,412 +129,177 @@ class ClientProtocolMixin:
# 'remoteapplicationcmdline:s': '',
}
asset, application, system_user, user = self.get_request_resource(serializer)
# 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect:
actions = Action.choices_to_value(token.actions)
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
rdp_options['drivestoredirect:s'] = '*'
# 设置全屏
full_screen = is_true(self.request.query_params.get('full_screen'))
rdp_options['screen mode id:i'] = '2' if full_screen else '1'
# 设置 RDP Server 地址
endpoint = self.get_smart_endpoint(
protocol='rdp', asset=token.asset, application=token.application
)
rdp_options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 设置用户名
rdp_options['username:s'] = '{}|{}'.format(token.user.username, str(token.id))
if token.system_user.ad_domain:
rdp_options['domain:s'] = token.system_user.ad_domain
# 设置宽高
height = self.request.query_params.get('height')
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, secret = self.create_token(user, asset, application, system_user)
# 设置磁盘挂载
if drives_redirect:
actions = 0
if asset:
actions = get_asset_actions(user, asset, system_user)
elif application:
actions = get_application_actions(user, application, system_user)
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
options['drivestoredirect:s'] = '*'
# 全屏
options['screen mode id:i'] = '2' if full_screen else '1'
# RDP Server 地址
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:
options['domain:s'] = system_user.ad_domain
# 宽高
if width and height:
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_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')
# 设置其他选项
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
if asset:
name = asset.hostname
elif application:
name = application.name
application.get_rdp_remote_app_setting()
app = f'||jmservisor'
options['remoteapplicationmode:i'] = '1'
options['alternate shell:s'] = app
options['remoteapplicationprogram:s'] = app
options['remoteapplicationname:s'] = name
if token.asset:
name = token.asset.hostname
elif token.application and token.application.category_remote_app:
app = '||jmservisor'
name = token.application.name
rdp_options['remoteapplicationmode:i'] = '1'
rdp_options['alternate shell:s'] = app
rdp_options['remoteapplicationprogram:s'] = app
rdp_options['remoteapplicationname:s'] = name
else:
name = '*'
filename = "{}-{}-jumpserver".format(token.user.username, name)
filename = urllib.parse.quote(filename)
content = ''
for k, v in options.items():
for k, v in rdp_options.items():
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
return filename, content
def get_ssh_token(self, token: ConnectionToken):
if token.asset:
name = token.asset.hostname
elif token.application:
name = token.application.name
else:
name = '*'
filename = f'{token.user.username}-{name}-jumpserver'
endpoint = self.get_smart_endpoint(
protocol='ssh', asset=asset, application=application
protocol='ssh', asset=token.asset, application=token.application
)
content = {
data = {
'ip': endpoint.host,
'port': str(endpoint.ssh_port),
'username': f'JMS-{token}',
'password': secret
'username': 'JMS-{}'.format(str(token.id)),
'password': token.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')
lib_path = get_file_by_arch('xpack/libs', 'librailencrypt.so')
lib = ctypes.CDLL(lib_path)
lib.encrypt.argtypes = [ctypes.c_char_p, ctypes.c_int]
lib.encrypt.restype = ctypes.c_char_p
rst = lib.encrypt(parameters, len(parameters))
rst = rst.decode('ascii')
return rst
def get_valid_serializer(self):
if self.request.method == 'GET':
data = self.request.query_params
else:
data = self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
return serializer
def get_client_protocol_data(self, serializer):
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':
name, token = self.get_ssh_token(serializer)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
filename = "{}-{}-jumpserver".format(username, name)
data = {
"filename": filename,
"protocol": system_user.protocol,
"username": username,
"token": token,
"config": config
}
return data
@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
else:
data = self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
name, data = self.get_rdp_file_content(serializer)
response = HttpResponse(data, content_type='application/octet-stream')
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
filename = urllib.parse.quote(filename)
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@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:
protocol_data = self.get_client_protocol_data(serializer)
except ValueError as e:
return Response({'error': str(e)}, status=401)
protocol_data = json.dumps(protocol_data).encode()
protocol_data = base64.b64encode(protocol_data).decode()
data = {
'url': 'jms://{}'.format(protocol_data),
}
return Response(data=data)
token = json.dumps(data)
return filename, token
class SecretDetailMixin:
valid_token: Callable
request: Request
get_serializer: Callable
@staticmethod
def _get_application_secret_detail(application):
gateway = None
remote_app = None
asset = None
if application.category_remote_app:
remote_app = application.get_rdp_remote_app_setting()
asset = application.get_remote_app_asset()
domain = asset.domain
else:
domain = application.domain
if domain and domain.has_gateway():
gateway = domain.random_gateway()
return {
'asset': asset,
'application': application,
'gateway': gateway,
'domain': domain,
'remote_app': remote_app,
}
@staticmethod
def _get_asset_secret_detail(asset):
gateway = None
if asset and asset.domain and asset.domain.has_gateway():
gateway = asset.domain.random_gateway()
return {
'asset': asset,
'application': None,
'domain': asset.domain,
'gateway': gateway,
'remote_app': None,
}
@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)
except serializers.ValidationError as e:
post_auth_failed.send(
sender=self.__class__, username='', request=self.request,
reason=_('Invalid token')
)
raise e
data = dict(
id=token, secret=value.get('secret', ''),
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 renewal_token(self, token, ttl=None):
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
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=None):
key = self.get_token_cache_key(token)
ttl = ttl or settings.CONNECTION_TOKEN_EXPIRATION
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 get_token_cache_key(self, token):
return self.CACHE_KEY_PREFIX.format(token)
class BaseUserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
TokenCacheMixin, GenericViewSet
):
@staticmethod
def check_resource_permission(user, asset, application, system_user):
from perms.utils.asset import has_asset_system_permission
from perms.utils.application import has_application_system_permission
if asset and not has_asset_system_permission(user, asset, system_user):
error = f'User not has this asset and system user permission: ' \
f'user={user.id} system_user={system_user.id} asset={asset.id}'
raise PermissionDenied(error)
if application and not has_application_system_permission(user, application, system_user):
error = f'User not has this application and system user permission: ' \
f'user={user.id} system_user={system_user.id} application={application.id}'
raise PermissionDenied(error)
return True
def create_token(self, user, asset, application, system_user, ttl=None):
self.check_resource_permission(user, asset, application, system_user)
token = random_string(36)
secret = random_string(16)
value = {
'id': token,
'secret': secret,
'user': str(user.id),
'username': user.username,
'system_user': str(system_user.id),
'system_user_name': system_user.name,
'created_by': str(self.request.user),
'date_created': str(timezone.now())
}
if asset:
value.update({
'type': 'asset',
'asset': str(asset.id),
'hostname': asset.hostname,
})
elif application:
value.update({
'type': 'application',
'application': application.id,
'application_name': str(application)
})
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, 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)
class UserConnectionTokenViewSet(BaseUserConnectionTokenViewSet, SecretDetailMixin):
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
'type', 'user_display', 'system_user_display',
'application_display', 'asset_display'
)
search_fields = filterset_fields
serializer_classes = {
'default': ConnectionTokenSerializer,
'list': ConnectionTokenDisplaySerializer,
'retrieve': ConnectionTokenDisplaySerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'GET': 'authentication.view_connectiontoken',
'retrieve': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'expire': 'authentication.add_connectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
def valid_token(self, token):
from users.models import User
from assets.models import SystemUser, Asset
from applications.models import Application
from perms.utils.asset.permission import validate_permission as asset_validate_permission
from perms.utils.application.permission import validate_permission as app_validate_permission
def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user)
value = self.get_token_from_cache(token)
if not value:
raise serializers.ValidationError('Token not found')
def get_request_resource_user(self, serializer):
return self.request.user
user = get_object_or_404(User, id=value.get('user'))
if not user.is_valid:
raise serializers.ValidationError("User not valid, disabled or expired")
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
asset = None
app = None
if value.get('type') == 'asset':
asset = get_object_or_404(Asset, id=value.get('asset'))
if not asset.is_active:
raise serializers.ValidationError("Asset disabled")
has_perm, actions, expired_at = asset_validate_permission(user, asset, system_user)
def get_object(self):
if self.request.user.is_service_account:
# TODO: 组件获取 token 详情,将来放在 Super-connection-token API 中
obj = get_object_or_404(ConnectionToken, pk=self.kwargs.get('pk'))
else:
app = get_object_or_404(Application, id=value.get('application'))
has_perm, actions, expired_at = app_validate_permission(user, app, system_user)
obj = super(ConnectionTokenViewSet, self).get_object()
return obj
if not has_perm:
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app, expired_at, actions
def create_connection_token(self):
data = self.request.query_params if self.request.method == 'GET' else self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
token: ConnectionToken = serializer.instance
return token
def get(self, request):
token = request.query_params.get('token')
value = self.get_token_from_cache(token)
if not value:
return Response('', status=404)
return Response(value)
def perform_create(self, serializer):
user, asset, application, system_user = self.get_request_resources(serializer)
self.check_user_has_resource_permission(user, asset, application, system_user)
return super(ConnectionTokenViewSet, self).perform_create(serializer)
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
# 非常重要的 api在逻辑层再判断一下双重保险
perm_required = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(perm_required):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
self.check_token_valid(token)
token.load_system_user_auth()
serializer = self.get_serializer(instance=token)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_valid(token)
filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename)
response = HttpResponse(content, content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_valid(token)
try:
protocol_data = self.get_client_protocol_data(token)
except ValueError as e:
return Response(data={'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
protocol_data = json.dumps(protocol_data).encode()
protocol_data = base64.b64encode(protocol_data).decode()
data = {
'url': 'jms://{}'.format(protocol_data)
}
return Response(data=data)
@action(methods=['PATCH'], detail=True)
def expire(self, request, *args, **kwargs):
instance = self.get_object()
instance.expire()
return Response(status=status.HTTP_204_NO_CONTENT)
class UserSuperConnectionTokenViewSet(
BaseUserConnectionTokenViewSet, TokenCacheMixin, GenericViewSet
):
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
serializer_classes = {
'default': SuperConnectionTokenSerializer,
}
@@ -525,10 +308,22 @@ class UserSuperConnectionTokenViewSet(
'renewal': 'authentication.add_superconnectiontoken'
}
@action(methods=[PATCH], detail=False)
def get_request_resource_user(self, serializer):
return serializer.validated_data.get('user')
@action(methods=['PATCH'], detail=False)
def renewal(self, request, *args, **kwargs):
""" 续期 Token """
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)
from common.utils.timezone import as_current_tz
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
date_expired = as_current_tz(token.date_expired)
if token.is_expired:
raise PermissionDenied('Token is expired at: {}'.format(date_expired))
token.renewal()
data = {
'ok': True,
'msg': f'Token is renewed, date expired: {date_expired}'
}
return Response(data=data, status=status.HTTP_200_OK)

View File

@@ -2,10 +2,11 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import UserConfirmation
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication.const import ConfirmType
from authentication import errors
logger = get_logger(__file__)
@@ -26,7 +27,7 @@ class DingTalkQRUnBindBase(APIView):
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
permission_classes = (IsAuthConfirmTimeValid,)
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):

View File

@@ -2,10 +2,11 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import UserConfirmation
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication.const import ConfirmType
from authentication import errors
logger = get_logger(__file__)
@@ -26,7 +27,7 @@ class FeiShuQRUnBindBase(APIView):
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
permission_classes = (IsAuthConfirmTimeValid,)
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):

View File

@@ -25,5 +25,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
ticket = self.get_ticket()
if ticket:
request.session.pop('auth_ticket_id', '')
ticket.close(processor=self.get_user_from_session())
ticket.close()
return Response('', status=200)

View File

@@ -10,22 +10,17 @@ from rest_framework.generics import CreateAPIView
from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from common.permissions import IsValidUser, NeedMFAVerify
from common.utils import get_logger
from common.exceptions import UnexpectError
from users.models.user import User
from ..serializers import OtpVerifySerializer
from .. import serializers
from .. import errors
from ..mfa.otp import MFAOtp
from ..mixins import AuthMixin
logger = get_logger(__name__)
__all__ = [
'MFAChallengeVerifyApi', 'UserOtpVerifyApi',
'MFASendCodeApi'
'MFAChallengeVerifyApi', 'MFASendCodeApi'
]
@@ -88,30 +83,3 @@ class MFAChallengeVerifyApi(AuthMixin, CreateAPIView):
raise ValidationError(data)
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)
class UserOtpVerifyApi(CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = OtpVerifySerializer
def get(self, request, *args, **kwargs):
return Response({'code': 'valid', 'msg': 'verified'})
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
code = serializer.validated_data["code"]
otp = MFAOtp(request.user)
ok, error = otp.check_code(code)
if ok:
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response({"ok": "1"})
else:
return Response({"error": _("Code is invalid, {}").format(error)}, status=400)
def get_permissions(self):
if self.request.method.lower() == 'get' \
and settings.SECURITY_VIEW_AUTH_NEED_MFA:
self.permission_classes = [NeedMFAVerify]
return super().get_permissions()

View File

@@ -2,10 +2,11 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.permissions import UserConfirmation
from common.mixins.api import RoleUserMixin, RoleAdminMixin
from authentication.const import ConfirmType
from authentication import errors
logger = get_logger(__file__)
@@ -26,7 +27,7 @@ class WeComQRUnBindBase(APIView):
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
permission_classes = (IsAuthConfirmTimeValid,)
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):

View File

@@ -0,0 +1,5 @@
from .mfa import ConfirmMFA
from .password import ConfirmPassword
from .relogin import ConfirmReLogin
CONFIRM_BACKENDS = [ConfirmReLogin, ConfirmPassword, ConfirmMFA]

View File

@@ -0,0 +1,30 @@
import abc
class BaseConfirm(abc.ABC):
def __init__(self, user, request):
self.user = user
self.request = request
@property
@abc.abstractmethod
def name(self) -> str:
return ''
@property
@abc.abstractmethod
def display_name(self) -> str:
return ''
@abc.abstractmethod
def check(self) -> bool:
return False
@property
def content(self):
return ''
@abc.abstractmethod
def authenticate(self, secret_key, mfa_type) -> tuple:
return False, 'Error msg'

View File

@@ -0,0 +1,26 @@
from users.models import User
from .base import BaseConfirm
class ConfirmMFA(BaseConfirm):
name = 'mfa'
display_name = 'MFA'
def check(self):
return self.user.active_mfa_backends and self.user.mfa_enabled
@property
def content(self):
backends = User.get_user_mfa_backends(self.user)
return [{
'name': backend.name,
'disabled': not bool(backend.is_active()),
'display_name': backend.display_name,
'placeholder': backend.placeholder,
} for backend in backends]
def authenticate(self, secret_key, mfa_type):
mfa_backend = self.user.get_mfa_backend_by_type(mfa_type)
ok, msg = mfa_backend.check_code(secret_key)
return ok, msg

View File

@@ -0,0 +1,17 @@
from django.utils.translation import ugettext_lazy as _
from authentication.mixins import authenticate
from .base import BaseConfirm
class ConfirmPassword(BaseConfirm):
name = 'password'
display_name = _('Password')
def check(self):
return self.user.is_password_authenticate()
def authenticate(self, secret_key, mfa_type):
ok = authenticate(self.request, username=self.user.username, password=secret_key)
msg = '' if ok else _('Authentication failed password incorrect')
return ok, msg

View File

@@ -0,0 +1,30 @@
from datetime import datetime
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from .base import BaseConfirm
SPECIFIED_TIME = 5
RELOGIN_ERROR = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
class ConfirmReLogin(BaseConfirm):
name = 'relogin'
display_name = 'Re-Login'
def check(self):
return not self.user.is_password_authenticate()
def authenticate(self, secret_key, mfa_type):
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
login_time = self.request.session.get('login_time')
msg = RELOGIN_ERROR
if not login_time:
return False, msg
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
return False, msg
return True, ''

View File

@@ -1,10 +1,37 @@
from django.db.models import TextChoices
from authentication.confirm import CONFIRM_BACKENDS
from .confirm import ConfirmMFA, ConfirmPassword, ConfirmReLogin
from .mfa import MFAOtp, MFASms, MFARadius
RSA_PRIVATE_KEY = 'rsa_private_key'
RSA_PUBLIC_KEY = 'rsa_public_key'
CONFIRM_BACKEND_MAP = {backend.name: backend for backend in CONFIRM_BACKENDS}
class ConfirmType(TextChoices):
RELOGIN = 'relogin', 'Re-Login'
PASSWORD = 'password', 'Password'
MFA = 'mfa', 'MFA'
ReLogin = ConfirmReLogin.name, ConfirmReLogin.display_name
PASSWORD = ConfirmPassword.name, ConfirmPassword.display_name
MFA = ConfirmMFA.name, ConfirmMFA.display_name
@classmethod
def get_can_confirm_types(cls, confirm_type):
start = cls.values.index(confirm_type)
types = cls.values[start:]
types.reverse()
return types
@classmethod
def get_can_confirm_backend_classes(cls, confirm_type):
types = cls.get_can_confirm_types(confirm_type)
backend_classes = [
CONFIRM_BACKEND_MAP[tp] for tp in types if tp in CONFIRM_BACKEND_MAP
]
return backend_classes
class MFAType(TextChoices):
OTP = MFAOtp.name, MFAOtp.display_name
SMS = MFASms.name, MFASms.display_name
Radius = MFARadius.name, MFARadius.display_name

View File

@@ -1,367 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from django.conf import settings
from rest_framework import status
from common.exceptions import JMSException
from .signals import post_auth_failed
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
reason_mfa_failed = 'mfa_failed'
reason_mfa_unset = 'mfa_unset'
reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid'
reason_user_inactive = 'user_inactive'
reason_user_expired = 'user_expired'
reason_backend_not_match = 'backend_not_match'
reason_acl_not_allow = 'acl_not_allow'
only_local_users_are_allowed = 'only_local_users_are_allowed'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
reason_password_decrypt_failed: _('Password decrypt failed'),
reason_mfa_failed: _('MFA failed'),
reason_mfa_unset: _('MFA unset'),
reason_user_not_exist: _("Username does not exist"),
reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'),
reason_user_inactive: _("This account is inactive."),
reason_user_expired: _("This account is expired"),
reason_backend_not_match: _("Auth backend not match"),
reason_acl_not_allow: _("ACL is not allowed"),
only_local_users_are_allowed: _("Only local users are allowed")
}
old_reason_choices = {
'0': '-',
'1': reason_choices[reason_password_failed],
'2': reason_choices[reason_mfa_failed],
'3': reason_choices[reason_user_not_exist],
'4': reason_choices[reason_password_expired],
}
session_empty_msg = _("No session found, check your cookie")
invalid_login_msg = _(
"The username or password you entered is incorrect, "
"please enter it again. "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
block_user_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_ip_login_msg = _(
"The ip has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_mfa_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
mfa_error_msg = _(
"{error}, "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
mfa_required_msg = _("MFA required")
mfa_unset_msg = _("MFA not set, please set it first")
login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_error_msg = _("Login confirm ticket was {}")
class AuthFailedNeedLogMixin:
username = ''
request = None
error = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
request=self.request, reason=self.error
)
class AuthFailedNeedBlockMixin:
username = ''
ip = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
LoginBlockUtil(self.username, self.ip).incr_failed_count()
class AuthFailedError(Exception):
username = ''
msg = ''
error = ''
request = None
ip = ''
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
def __str__(self):
return str(self.msg)
class BlockGlobalIpLoginError(AuthFailedError):
error = 'block_global_ip_login'
def __init__(self, username, ip, **kwargs):
self.msg = block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
LoginIpBlockUtil(ip).set_block_if_need()
super().__init__(username=username, ip=ip, **kwargs)
class CredentialError(
AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin, BlockGlobalIpLoginError, AuthFailedError
):
def __init__(self, error, username, ip, request):
super().__init__(error=error, username=username, ip=ip, request=request)
util = LoginBlockUtil(username, ip)
times_remainder = util.get_remainder_times()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder < 1:
self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
return
default_msg = invalid_login_msg.format(
times_try=times_remainder, block_time=block_time
)
if error == reason_password_failed:
self.msg = default_msg
else:
self.msg = reason_choices.get(error, default_msg)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = reason_mfa_failed
msg: str
def __init__(self, username, request, ip, mfa_type, error):
super().__init__(username=username, request=request)
util = MFABlockUtils(username, ip)
times_remainder = util.incr_failed_count()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder:
self.msg = mfa_error_msg.format(
error=error, times_try=times_remainder, block_time=block_time
)
else:
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
error = 'block_mfa'
def __init__(self, username, request, ip):
self.msg = block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, request=request, ip=ip)
class MFAUnsetError(Exception):
error = reason_mfa_unset
msg = mfa_unset_msg
def __init__(self, user, request, url):
self.url = url
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login'
def __init__(self, username, ip):
self.msg = block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, ip=ip)
class SessionEmptyError(AuthFailedError):
msg = session_empty_msg
error = 'session_empty'
class NeedMoreInfoError(Exception):
error = ''
msg = ''
def __init__(self, error='', msg=''):
if error:
self.error = error
if msg:
self.msg = msg
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
class MFARequiredError(NeedMoreInfoError):
msg = mfa_required_msg
error = 'mfa_required'
def __init__(self, error='', msg='', mfa_types=()):
super().__init__(error=error, msg=msg)
self.choices = mfa_types
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
'data': {
'choices': self.choices,
'url': reverse('api-auth:mfa-challenge')
}
}
class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
msg = reason_acl_not_allow
error = 'acl_error'
def __init__(self, msg, **kwargs):
self.msg = msg
super().__init__(**kwargs)
def as_data(self):
return {
"error": reason_acl_not_allow,
"msg": self.msg
}
class LoginIPNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("IP is not allowed"), **kwargs)
class TimePeriodNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("Time Period is not allowed"), **kwargs)
class LoginConfirmBaseError(NeedMoreInfoError):
def __init__(self, ticket_id, **kwargs):
self.ticket_id = ticket_id
super().__init__(**kwargs)
def as_data(self):
return {
"error": self.error,
"msg": self.msg,
"data": {
"ticket_id": self.ticket_id
}
}
class LoginConfirmWaitError(LoginConfirmBaseError):
msg = login_confirm_wait_msg
error = 'login_confirm_wait'
class LoginConfirmOtherError(LoginConfirmBaseError):
error = 'login_confirm_error'
def __init__(self, ticket_id, status):
msg = login_confirm_error_msg.format(status)
super().__init__(ticket_id=ticket_id, msg=msg)
class SSOAuthClosed(JMSException):
default_code = 'sso_auth_closed'
default_detail = _('SSO auth closed')
class PasswordTooSimple(JMSException):
default_code = 'passwd_too_simple'
default_detail = _('Your password is too simple, please change it for security')
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.url = url
class PasswordNeedUpdate(JMSException):
default_code = 'passwd_need_update'
default_detail = _('You should to change your password before login')
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.url = url
class PasswordRequireResetError(JMSException):
default_code = 'passwd_has_expired'
default_detail = _('Your password has expired, please reset before logging in')
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.url = url
class WeComCodeInvalid(JMSException):
default_code = 'wecom_code_invalid'
default_detail = 'Code invalid, can not get user info'
class WeComBindAlready(JMSException):
default_code = 'wecom_bind_already'
default_detail = 'WeCom already binded'
class WeComNotBound(JMSException):
default_code = 'wecom_not_bound'
default_detail = 'WeCom is not bound'
class DingTalkNotBound(JMSException):
default_code = 'dingtalk_not_bound'
default_detail = 'DingTalk is not bound'
class FeiShuNotBound(JMSException):
default_code = 'feishu_not_bound'
default_detail = 'FeiShu is not bound'
class PasswordInvalid(JMSException):
default_code = 'passwd_invalid'
default_detail = _('Your password is invalid')
class MFACodeRequiredError(AuthFailedError):
error = 'mfa_code_required'
msg = _("Please enter MFA code")
class SMSCodeRequiredError(AuthFailedError):
error = 'sms_code_required'
msg = _("Please enter SMS code")
class UserPhoneNotSet(AuthFailedError):
error = 'phone_not_set'
msg = _('Phone not set')

View File

@@ -0,0 +1,4 @@
from .const import *
from .mfa import *
from .failed import *
from .redirect import *

View File

@@ -0,0 +1,67 @@
from django.utils.translation import gettext_lazy as _
reason_password_failed = 'password_failed'
reason_password_decrypt_failed = 'password_decrypt_failed'
reason_mfa_failed = 'mfa_failed'
reason_mfa_unset = 'mfa_unset'
reason_user_not_exist = 'user_not_exist'
reason_password_expired = 'password_expired'
reason_user_invalid = 'user_invalid'
reason_user_inactive = 'user_inactive'
reason_user_expired = 'user_expired'
reason_backend_not_match = 'backend_not_match'
reason_acl_not_allow = 'acl_not_allow'
only_local_users_are_allowed = 'only_local_users_are_allowed'
reason_choices = {
reason_password_failed: _('Username/password check failed'),
reason_password_decrypt_failed: _('Password decrypt failed'),
reason_mfa_failed: _('MFA failed'),
reason_mfa_unset: _('MFA unset'),
reason_user_not_exist: _("Username does not exist"),
reason_password_expired: _("Password expired"),
reason_user_invalid: _('Disabled or expired'),
reason_user_inactive: _("This account is inactive."),
reason_user_expired: _("This account is expired"),
reason_backend_not_match: _("Auth backend not match"),
reason_acl_not_allow: _("ACL is not allowed"),
only_local_users_are_allowed: _("Only local users are allowed")
}
old_reason_choices = {
'0': '-',
'1': reason_choices[reason_password_failed],
'2': reason_choices[reason_mfa_failed],
'3': reason_choices[reason_user_not_exist],
'4': reason_choices[reason_password_expired],
}
session_empty_msg = _("No session found, check your cookie")
invalid_login_msg = _(
"The username or password you entered is incorrect, "
"please enter it again. "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
block_user_login_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_ip_login_msg = _(
"The ip has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
block_mfa_msg = _(
"The account has been locked "
"(please contact admin to unlock it or try again after {} minutes)"
)
mfa_error_msg = _(
"{error}, "
"You can also try {times_try} times "
"(The account will be temporarily locked for {block_time} minutes)"
)
mfa_required_msg = _("MFA required")
mfa_unset_msg = _("MFA not set, please set it first")
login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_error_msg = _("Login confirm ticket was {}")

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
from ..signals import post_auth_failed
from . import const
class AuthFailedNeedLogMixin:
username = ''
request = None
error = ''
msg = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
post_auth_failed.send(
sender=self.__class__, username=self.username,
request=self.request, reason=self.msg
)
class AuthFailedNeedBlockMixin:
username = ''
ip = ''
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
LoginBlockUtil(self.username, self.ip).incr_failed_count()
class AuthFailedError(Exception):
username = ''
msg = ''
error = ''
request = None
ip = ''
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
def __str__(self):
return str(self.msg)
class BlockGlobalIpLoginError(AuthFailedError):
error = 'block_global_ip_login'
def __init__(self, username, ip, **kwargs):
self.msg = const.block_ip_login_msg.format(settings.SECURITY_LOGIN_IP_LIMIT_TIME)
LoginIpBlockUtil(ip).set_block_if_need()
super().__init__(username=username, ip=ip, **kwargs)
class CredentialError(
AuthFailedNeedLogMixin, AuthFailedNeedBlockMixin,
BlockGlobalIpLoginError, AuthFailedError
):
def __init__(self, error, username, ip, request):
super().__init__(error=error, username=username, ip=ip, request=request)
util = LoginBlockUtil(username, ip)
times_remainder = util.get_remainder_times()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder < 1:
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
return
default_msg = const.invalid_login_msg.format(
times_try=times_remainder, block_time=block_time
)
if error == const.reason_password_failed:
self.msg = default_msg
else:
self.msg = const.reason_choices.get(error, default_msg)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
error = const.reason_mfa_failed
msg: str
def __init__(self, username, request, ip, mfa_type, error):
super().__init__(username=username, request=request)
util = MFABlockUtils(username, ip)
times_remainder = util.incr_failed_count()
block_time = settings.SECURITY_LOGIN_LIMIT_TIME
if times_remainder:
self.msg = const.mfa_error_msg.format(
error=error, times_try=times_remainder, block_time=block_time
)
else:
self.msg = const.block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
class BlockMFAError(AuthFailedNeedLogMixin, AuthFailedError):
error = 'block_mfa'
def __init__(self, username, request, ip):
self.msg = const.block_mfa_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, request=request, ip=ip)
class BlockLoginError(AuthFailedNeedBlockMixin, AuthFailedError):
error = 'block_login'
def __init__(self, username, ip):
self.msg = const.block_user_login_msg.format(settings.SECURITY_LOGIN_LIMIT_TIME)
super().__init__(username=username, ip=ip)
class SessionEmptyError(AuthFailedError):
msg = const.session_empty_msg
error = 'session_empty'
class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
msg = const.reason_acl_not_allow
error = 'acl_error'
def __init__(self, msg, **kwargs):
self.msg = msg
super().__init__(**kwargs)
def as_data(self):
return {
"error": const.reason_acl_not_allow,
"msg": self.msg
}
class LoginACLIPAndTimePeriodNotAllowed(ACLError):
def __init__(self, username, request, **kwargs):
self.username = username
self.request = request
super().__init__(_("Current IP and Time period is not allowed"), **kwargs)
class MFACodeRequiredError(AuthFailedError):
error = 'mfa_code_required'
msg = _("Please enter MFA code")
class SMSCodeRequiredError(AuthFailedError):
error = 'sms_code_required'
msg = _("Please enter SMS code")
class UserPhoneNotSet(AuthFailedError):
error = 'phone_not_set'
msg = _('Phone not set')

View File

@@ -0,0 +1,38 @@
from django.utils.translation import ugettext_lazy as _
from common.exceptions import JMSException
class SSOAuthClosed(JMSException):
default_code = 'sso_auth_closed'
default_detail = _('SSO auth closed')
class WeComCodeInvalid(JMSException):
default_code = 'wecom_code_invalid'
default_detail = 'Code invalid, can not get user info'
class WeComBindAlready(JMSException):
default_code = 'wecom_not_bound'
default_detail = _('WeCom is already bound')
class WeComNotBound(JMSException):
default_code = 'wecom_not_bound'
default_detail = _('WeCom is not bound')
class DingTalkNotBound(JMSException):
default_code = 'dingtalk_not_bound'
default_detail = _('DingTalk is not bound')
class FeiShuNotBound(JMSException):
default_code = 'feishu_not_bound'
default_detail = _('FeiShu is not bound')
class PasswordInvalid(JMSException):
default_code = 'passwd_invalid'
default_detail = _('Your password is invalid')

View File

@@ -0,0 +1,106 @@
from django.utils.translation import ugettext_lazy as _
from django.urls import reverse
from common.exceptions import JMSException
from . import const
class NeedMoreInfoError(Exception):
error = ''
msg = ''
def __init__(self, error='', msg=''):
if error:
self.error = error
if msg:
self.msg = msg
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
}
class NeedRedirectError(JMSException):
def __init__(self, url, *args, **kwargs):
self.url = url
class MFARequiredError(NeedMoreInfoError):
msg = const.mfa_required_msg
error = 'mfa_required'
def __init__(self, error='', msg='', mfa_types=()):
super().__init__(error=error, msg=msg)
self.choices = mfa_types
def as_data(self):
return {
'error': self.error,
'msg': self.msg,
'data': {
'choices': self.choices,
'url': reverse('api-auth:mfa-challenge')
}
}
class LoginConfirmBaseError(NeedMoreInfoError):
def __init__(self, ticket_id, **kwargs):
self.ticket_id = ticket_id
super().__init__(**kwargs)
def as_data(self):
return {
"error": self.error,
"msg": self.msg,
"data": {
"ticket_id": self.ticket_id
}
}
class LoginConfirmWaitError(LoginConfirmBaseError):
msg = const.login_confirm_wait_msg
error = 'login_confirm_wait'
class LoginConfirmOtherError(LoginConfirmBaseError):
error = 'login_confirm_error'
def __init__(self, ticket_id, status):
msg = const.login_confirm_error_msg.format(status)
super().__init__(ticket_id=ticket_id, msg=msg)
class PasswordTooSimple(NeedRedirectError):
default_code = 'passwd_too_simple'
default_detail = _('Your password is too simple, please change it for security')
def __init__(self, url, *args, **kwargs):
super().__init__(url, *args, **kwargs)
class PasswordNeedUpdate(NeedRedirectError):
default_code = 'passwd_need_update'
default_detail = _('You should to change your password before login')
def __init__(self, url, *args, **kwargs):
super().__init__(url, *args, **kwargs)
class PasswordRequireResetError(NeedRedirectError):
default_code = 'passwd_has_expired'
default_detail = _('Your password has expired, please reset before logging in')
def __init__(self, url, *args, **kwargs):
super().__init__(url, *args, **kwargs)
class MFAUnsetError(NeedRedirectError):
error = const.reason_mfa_unset
msg = const.mfa_unset_msg
def __init__(self, url, *args, **kwargs):
super().__init__(url, *args, **kwargs)

View File

@@ -46,6 +46,8 @@ class SessionCookieMiddleware(MiddlewareMixin):
@staticmethod
def set_cookie_public_key(request, response):
if request.path.startswith('/api'):
return
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)

View File

@@ -0,0 +1,89 @@
# Generated by Django 3.2.12 on 2022-07-05 11:40
import authentication.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('applications', '0021_auto_20220629_1826'),
('assets', '0091_auto_20220629_1826'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0010_temptoken'),
]
operations = [
migrations.AddField(
model_name='connectiontoken',
name='application',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='applications.application', verbose_name='Application'),
),
migrations.AddField(
model_name='connectiontoken',
name='application_display',
field=models.CharField(default='', max_length=128, verbose_name='Application display'),
),
migrations.AddField(
model_name='connectiontoken',
name='asset',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='assets.asset', verbose_name='Asset'),
),
migrations.AddField(
model_name='connectiontoken',
name='asset_display',
field=models.CharField(default='', max_length=128, verbose_name='Asset display'),
),
migrations.AddField(
model_name='connectiontoken',
name='date_expired',
field=models.DateTimeField(default=authentication.models.date_expired_default, verbose_name='Date expired'),
),
migrations.AddField(
model_name='connectiontoken',
name='org_id',
field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'),
),
migrations.AddField(
model_name='connectiontoken',
name='secret',
field=models.CharField(default='', max_length=64, verbose_name='Secret'),
),
migrations.AddField(
model_name='connectiontoken',
name='system_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to='assets.systemuser', verbose_name='System user'),
),
migrations.AddField(
model_name='connectiontoken',
name='system_user_display',
field=models.CharField(default='', max_length=128, verbose_name='System user display'),
),
migrations.AddField(
model_name='connectiontoken',
name='type',
field=models.CharField(choices=[('asset', 'Asset'), ('application', 'Application')], default='asset', max_length=16, verbose_name='Type'),
),
migrations.AddField(
model_name='connectiontoken',
name='user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_tokens', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
migrations.AddField(
model_name='connectiontoken',
name='user_display',
field=models.CharField(default='', max_length=128, verbose_name='User display'),
),
migrations.AlterField(
model_name='connectiontoken',
name='id',
field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False),
),
migrations.AlterModelOptions(
name='connectiontoken',
options={'ordering': ('-date_expired',), 'permissions': [('view_connectiontokensecret', 'Can view connection token secret')], 'verbose_name': 'Connection token'},
),
]

View File

@@ -193,8 +193,8 @@ class MFAMixin:
def _check_if_no_active_mfa(self, user):
active_mfa_mapper = user.active_mfa_backends_mapper
if not active_mfa_mapper:
url = reverse('authentication:user-otp-enable-start')
raise errors.MFAUnsetError(user, self.request, url)
set_url = reverse('authentication:user-otp-enable-start')
raise errors.MFAUnsetError(set_url, user, self.request)
def _check_login_page_mfa_if_need(self, user):
if not settings.SECURITY_MFA_IN_LOGIN_PAGE:
@@ -328,28 +328,41 @@ class AuthACLMixin:
def _check_login_acl(self, user, ip):
# ACL 限制用户登录
is_allowed, limit_type = LoginACL.allow_user_to_login(user, ip)
if is_allowed:
acl = LoginACL.match(user, ip)
if not acl:
return
if limit_type == 'ip':
raise errors.LoginIPNotAllowed(username=user.username, request=self.request)
elif limit_type == 'time':
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
def get_ticket(self):
from tickets.models import Ticket
ticket_id = self.request.session.get("auth_ticket_id")
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
if not ticket_id:
ticket = None
else:
ticket = Ticket.all().filter(id=ticket_id).first()
return ticket
acl: LoginACL
if acl.is_action(acl.ActionChoices.allow):
return
def get_ticket_or_create(self, confirm_setting):
if acl.is_action(acl.ActionChoices.reject):
raise errors.LoginACLIPAndTimePeriodNotAllowed(username=user.username, request=self.request)
if acl.is_action(acl.ActionChoices.confirm):
self.request.session['auth_confirm_required'] = '1'
self.request.session['auth_acl_id'] = str(acl.id)
return
def check_user_login_confirm_if_need(self, user):
if not self.request.session.get("auth_confirm_required"):
return
acl_id = self.request.session.get('auth_acl_id')
logger.debug('Login confirm acl id: {}'.format(acl_id))
if not acl_id:
return
acl = LoginACL.filter_acl(user).filter(id=acl_id).first()
if not acl:
return
if not acl.is_action(acl.ActionChoices.confirm):
return
self.get_ticket_or_create(acl)
self.check_user_login_confirm()
def get_ticket_or_create(self, acl):
ticket = self.get_ticket()
if not ticket or ticket.status_closed:
ticket = confirm_setting.create_confirm_ticket(self.request)
if not ticket or ticket.is_state(ticket.State.closed):
ticket = acl.create_confirm_ticket(self.request)
self.request.session['auth_ticket_id'] = str(ticket.id)
return ticket
@@ -357,31 +370,24 @@ class AuthACLMixin:
ticket = self.get_ticket()
if not ticket:
raise errors.LoginConfirmOtherError('', "Not found")
if ticket.status_open:
elif ticket.is_state(ticket.State.approved):
self.request.session["auth_confirm_required"] = ''
return
elif ticket.is_status(ticket.Status.open):
raise errors.LoginConfirmWaitError(ticket.id)
elif ticket.state_approve:
self.request.session["auth_confirm"] = "1"
return
elif ticket.state_reject:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
elif ticket.state_close:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
else:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_status_display()
)
# rejected, closed
raise errors.LoginConfirmOtherError(ticket.id, ticket.get_state_display())
def check_user_login_confirm_if_need(self, user):
ip = self.get_request_ip()
is_allowed, confirm_setting = LoginACL.allow_user_confirm_if_need(user, ip)
if self.request.session.get('auth_confirm') or not is_allowed:
return
self.get_ticket_or_create(confirm_setting)
self.check_user_login_confirm()
def get_ticket(self):
from tickets.models import ApplyLoginTicket
ticket_id = self.request.session.get("auth_ticket_id")
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
if not ticket_id:
ticket = None
else:
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
return ticket
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
@@ -442,13 +448,15 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
LoginIpBlockUtil(ip).clean_block_if_need()
return user
def mark_password_ok(self, user, auto_login=False):
def mark_password_ok(self, user, auto_login=False, auth_backend=None):
request = self.request
request.session['auth_password'] = 1
request.session['auth_password_expired_at'] = time.time() + settings.AUTH_EXPIRED_SECONDS
request.session['user_id'] = str(user.id)
request.session['auto_login'] = auto_login
request.session['auth_backend'] = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
if not auth_backend:
auth_backend = getattr(user, 'backend', settings.AUTH_BACKEND_MODEL)
request.session['auth_backend'] = auth_backend
def check_oauth2_auth(self, user: User, auth_backend):
ip = self.get_request_ip()
@@ -468,7 +476,7 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
LoginIpBlockUtil(ip).clean_block_if_need()
MFABlockUtils(user.username, ip).clean_failed_count()
self.mark_password_ok(user, False)
self.mark_password_ok(user, False, auth_backend)
return user
def get_user_or_auth(self, valid_data):
@@ -479,7 +487,9 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
return self.check_user_auth(valid_data)
def clear_auth_mark(self):
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
keys = [
'auth_password', 'user_id', 'auth_confirm_required', 'auth_ticket_id', 'auth_acl_id'
]
for k in keys:
self.request.session.pop(k, '')

View File

@@ -1,11 +1,14 @@
import uuid
from datetime import datetime, timedelta
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from rest_framework.authtoken.models import Token
from orgs.mixins.models import OrgModelMixin
from common.db import models
from common.utils import lazyproperty
from common.utils.timezone import as_current_tz
class AccessKey(models.Model):
@@ -54,16 +57,204 @@ class SSOToken(models.JMSBaseModel):
verbose_name = _('SSO token')
class ConnectionToken(models.JMSBaseModel):
# Todo: 未来可能放到这里,不记录到 redis 了,虽然方便,但是不易于审计
# Todo: add connection token 可能要授权给 普通用户, 或者放开就行
def date_expired_default():
return timezone.now() + timedelta(seconds=settings.CONNECTION_TOKEN_EXPIRATION)
class ConnectionToken(OrgModelMixin, models.JMSModel):
class Type(models.TextChoices):
asset = 'asset', _('Asset')
application = 'application', _('Application')
type = models.CharField(
max_length=16, default=Type.asset, choices=Type.choices, verbose_name=_("Type")
)
secret = models.CharField(max_length=64, default='', verbose_name=_("Secret"))
date_expired = models.DateTimeField(
default=date_expired_default, verbose_name=_("Date expired")
)
user = models.ForeignKey(
'users.User', on_delete=models.SET_NULL, verbose_name=_('User'),
related_name='connection_tokens', null=True, blank=True
)
user_display = models.CharField(max_length=128, default='', verbose_name=_("User display"))
system_user = models.ForeignKey(
'assets.SystemUser', on_delete=models.SET_NULL, verbose_name=_('System user'),
related_name='connection_tokens', null=True, blank=True
)
system_user_display = models.CharField(
max_length=128, default='', verbose_name=_("System user display")
)
asset = models.ForeignKey(
'assets.Asset', on_delete=models.SET_NULL, verbose_name=_('Asset'),
related_name='connection_tokens', null=True, blank=True
)
asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display"))
application = models.ForeignKey(
'applications.Application', on_delete=models.SET_NULL, verbose_name=_('Application'),
related_name='connection_tokens', null=True, blank=True
)
application_display = models.CharField(
max_length=128, default='', verbose_name=_("Application display")
)
class Meta:
ordering = ('-date_expired',)
verbose_name = _('Connection token')
permissions = [
('view_connectiontokensecret', _('Can view connection token secret'))
]
@classmethod
def get_default_date_expired(cls):
return date_expired_default()
@property
def is_expired(self):
return self.date_expired < timezone.now()
@property
def expire_time(self):
interval = self.date_expired - timezone.now()
seconds = interval.total_seconds()
if seconds < 0:
seconds = 0
return int(seconds)
def expire(self):
self.date_expired = timezone.now()
self.save()
@property
def is_valid(self):
return not self.is_expired
def is_type(self, tp):
return self.type == tp
def renewal(self):
""" 续期 Token将来支持用户自定义创建 token 后,续期策略要修改 """
self.date_expired = self.get_default_date_expired()
self.save()
actions = expired_at = None # actions 和 expired_at 在 check_valid() 中赋值
def check_valid(self):
from perms.utils.asset.permission import validate_permission as asset_validate_permission
from perms.utils.application.permission import validate_permission as app_validate_permission
if self.is_expired:
is_valid = False
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired))
return is_valid, error
if not self.user:
is_valid = False
error = _('User not exists')
return is_valid, error
if not self.user.is_valid:
is_valid = False
error = _('User invalid, disabled or expired')
return is_valid, error
if not self.system_user:
is_valid = False
error = _('System user not exists')
return is_valid, error
if self.is_type(self.Type.asset):
if not self.asset:
is_valid = False
error = _('Asset not exists')
return is_valid, error
if not self.asset.is_active:
is_valid = False
error = _('Asset inactive')
return is_valid, error
has_perm, actions, expired_at = asset_validate_permission(
self.user, self.asset, self.system_user
)
if not has_perm:
is_valid = False
error = _('User has no permission to access asset or permission expired')
return is_valid, error
self.actions = actions
self.expired_at = expired_at
elif self.is_type(self.Type.application):
if not self.application:
is_valid = False
error = _('Application not exists')
return is_valid, error
has_perm, actions, expired_at = app_validate_permission(
self.user, self.application, self.system_user
)
if not has_perm:
is_valid = False
error = _('User has no permission to access application or permission expired')
return is_valid, error
self.actions = actions
self.expired_at = expired_at
return True, ''
@lazyproperty
def domain(self):
if self.asset:
return self.asset.domain
if not self.application:
return
if self.application.category_remote_app:
asset = self.application.get_remote_app_asset()
domain = asset.domain if asset else None
else:
domain = self.application.domain
return domain
@lazyproperty
def gateway(self):
from assets.models import Domain
if not self.domain:
return
self.domain: Domain
return self.domain.random_gateway()
@lazyproperty
def remote_app(self):
if not self.application:
return {}
if not self.application.category_remote_app:
return {}
return self.application.get_rdp_remote_app_setting()
@lazyproperty
def asset_or_remote_app_asset(self):
if self.asset:
return self.asset
if self.application and self.application.category_remote_app:
return self.application.get_remote_app_asset()
@lazyproperty
def cmd_filter_rules(self):
from assets.models import CommandFilterRule
kwargs = {
'user_id': self.user.id,
'system_user_id': self.system_user.id,
}
if self.asset:
kwargs['asset_id'] = self.asset.id
elif self.application:
kwargs['application_id'] = self.application_id
rules = CommandFilterRule.get_queryset(**kwargs)
return rules
def load_system_user_auth(self):
if self.asset:
self.system_user.load_asset_more_auth(self.asset.id, self.user.username, self.user.id)
elif self.application:
self.system_user.load_app_more_auth(self.application.id, self.user.username, self.user.id)
class TempToken(models.JMSModel):
username = models.CharField(max_length=128, verbose_name=_("Username"))

View File

@@ -1,4 +1,4 @@
from .token import *
from .connect_token import *
from .connection_token import *
from .password_mfa import *
from .confirm import *

View File

@@ -1,11 +1,10 @@
from rest_framework import serializers
from common.drf.fields import EncryptedField
from ..const import ConfirmType
from ..const import ConfirmType, MFAType
class ConfirmSerializer(serializers.Serializer):
confirm_type = serializers.ChoiceField(
required=True, choices=ConfirmType.choices
)
secret_key = EncryptedField()
confirm_type = serializers.ChoiceField(required=True, allow_blank=True, choices=ConfirmType.choices)
mfa_type = serializers.ChoiceField(required=False, allow_blank=True, choices=MFAType.choices)
secret_key = EncryptedField(allow_blank=True)

View File

@@ -1,143 +0,0 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from users.models import User
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
from applications.models import Application
from assets.serializers import ProtocolsField
from perms.serializers.base import ActionsField
__all__ = [
'ConnectionTokenSerializer', 'ConnectionTokenApplicationSerializer',
'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer',
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer',
'SuperConnectionTokenSerializer'
]
class ConnectionTokenSerializer(serializers.Serializer):
system_user = serializers.CharField(max_length=128, required=True)
asset = serializers.CharField(max_length=128, required=False)
application = serializers.CharField(max_length=128, required=False)
@staticmethod
def validate_system_user(system_user_id):
from assets.models import SystemUser
system_user = SystemUser.objects.filter(id=system_user_id).first()
if system_user is None:
raise serializers.ValidationError('system_user id not exist')
return system_user
@staticmethod
def validate_asset(asset_id):
from assets.models import Asset
asset = Asset.objects.filter(id=asset_id).first()
if asset is None:
raise serializers.ValidationError('asset id not exist')
return asset
@staticmethod
def validate_application(app_id):
from applications.models import Application
app = Application.objects.filter(id=app_id).first()
if app is None:
raise serializers.ValidationError('app id not exist')
return app
def validate(self, attrs):
asset = attrs.get('asset')
application = attrs.get('application')
if not asset and not application:
raise serializers.ValidationError('asset or application required')
if asset and application:
raise serializers.ValidationError('asset and application should only one')
return super().validate(attrs)
class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
@staticmethod
def validate_user(user_id):
from users.models import User
user = User.objects.filter(id=user_id).first()
if user is None:
raise serializers.ValidationError('user id not exist')
return user
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'username', 'email']
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
protocols = ProtocolsField(label='Protocols', read_only=True)
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
fields = ['id', 'name', 'username', 'password', 'private_key', 'protocol', 'ad_domain', 'org_id']
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
class Meta:
model = Gateway
fields = ['id', 'ip', 'port', 'username', 'password', 'private_key']
class ConnectionTokenRemoteAppSerializer(serializers.Serializer):
program = serializers.CharField()
working_directory = serializers.CharField()
parameters = serializers.CharField()
class ConnectionTokenApplicationSerializer(serializers.ModelSerializer):
attrs = serializers.JSONField(read_only=True)
class Meta:
model = Application
fields = ['id', 'name', 'category', 'type', 'attrs', 'org_id']
class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True)
class Meta:
model = Domain
fields = ['id', 'name', 'gateways']
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterRule
fields = [
'id', 'type', 'content', 'ignore_case', 'pattern',
'priority', 'action',
'date_created',
]
class ConnectionTokenSecretSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True)
secret = serializers.CharField(read_only=True)
type = serializers.ChoiceField(choices=[('application', 'Application'), ('asset', 'Asset')])
user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True)
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
application = ConnectionTokenApplicationSerializer(read_only=True)
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
cmd_filter_rules = ConnectionTokenFilterRuleSerializer(many=True)
domain = ConnectionTokenDomainSerializer(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
actions = ActionsField()
expired_at = serializers.IntegerField()

View File

@@ -0,0 +1,194 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from authentication.models import ConnectionToken
from common.utils import pretty_string
from common.utils.random import random_string
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
from users.models import User
from applications.models import Application
from assets.serializers import ProtocolsField
from perms.serializers.base import ActionsField
__all__ = [
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
'SuperConnectionTokenSerializer', 'ConnectionTokenDisplaySerializer'
]
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
is_valid = serializers.BooleanField(read_only=True, label=_('Validity'))
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
class Meta:
model = ConnectionToken
fields_mini = ['id', 'type']
fields_small = fields_mini + [
'secret', 'date_expired', 'date_created', 'date_updated',
'created_by', 'updated_by', 'org_id', 'org_name',
]
fields_fk = [
'user', 'system_user', 'asset', 'application',
]
read_only_fields = [
# 普通 Token 不支持指定 user
'user', 'is_valid', 'expire_time',
'type_display', 'user_display', 'system_user_display',
'asset_display', 'application_display',
]
fields = fields_small + fields_fk + read_only_fields
def validate(self, attrs):
fields_attrs = self.construct_internal_fields_attrs(attrs)
attrs.update(fields_attrs)
return attrs
@property
def request_user(self):
request = self.context.get('request')
if request:
return request.user
def get_user(self, attrs):
return self.request_user
def construct_internal_fields_attrs(self, attrs):
user = self.get_user(attrs)
system_user = attrs.get('system_user') or ''
asset = attrs.get('asset') or ''
application = attrs.get('application') or ''
secret = attrs.get('secret') or random_string(16)
date_expired = attrs.get('date_expired') or ConnectionToken.get_default_date_expired()
if isinstance(asset, Asset):
tp = ConnectionToken.Type.asset
org_id = asset.org_id
elif isinstance(application, Application):
tp = ConnectionToken.Type.application
org_id = application.org_id
else:
raise serializers.ValidationError(_('Asset or application required'))
return {
'type': tp,
'user': user,
'secret': secret,
'date_expired': date_expired,
'user_display': pretty_string(str(user), max_length=128),
'system_user_display': pretty_string(str(system_user), max_length=128),
'asset_display': pretty_string(str(asset), max_length=128),
'application_display': pretty_string(str(application), max_length=128),
'org_id': org_id,
}
class ConnectionTokenDisplaySerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta):
extra_kwargs = {
'secret': {'write_only': True},
}
#
# SuperConnectionTokenSerializer
#
class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
class Meta(ConnectionTokenSerializer.Meta):
read_only_fields = [
'validity', 'user_display', 'system_user_display',
'asset_display', 'application_display',
]
def get_user(self, attrs):
return attrs.get('user') or self.request_user
#
# Connection Token Secret
#
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'username', 'email']
class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
protocols = ProtocolsField(label='Protocols', read_only=True)
class Meta:
model = Asset
fields = ['id', 'hostname', 'ip', 'protocols', 'org_id']
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
fields = [
'id', 'name', 'username', 'password', 'private_key',
'protocol', 'ad_domain', 'org_id'
]
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
class Meta:
model = Gateway
fields = ['id', 'ip', 'port', 'username', 'password', 'private_key']
class ConnectionTokenRemoteAppSerializer(serializers.Serializer):
program = serializers.CharField(allow_null=True, allow_blank=True)
working_directory = serializers.CharField(allow_null=True, allow_blank=True)
parameters = serializers.CharField(allow_null=True, allow_blank=True)
class ConnectionTokenApplicationSerializer(serializers.ModelSerializer):
attrs = serializers.JSONField(read_only=True)
class Meta:
model = Application
fields = ['id', 'name', 'category', 'type', 'attrs', 'org_id']
class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
gateways = ConnectionTokenGatewaySerializer(many=True, read_only=True)
class Meta:
model = Domain
fields = ['id', 'name', 'gateways']
class ConnectionTokenCmdFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterRule
fields = [
'id', 'type', 'content', 'ignore_case', 'pattern',
'priority', 'action', 'date_created',
]
class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
user = ConnectionTokenUserSerializer(read_only=True)
asset = ConnectionTokenAssetSerializer(read_only=True, source='asset_or_remote_app_asset')
application = ConnectionTokenApplicationSerializer(read_only=True)
remote_app = ConnectionTokenRemoteAppSerializer(read_only=True)
system_user = ConnectionTokenSystemUserSerializer(read_only=True)
gateway = ConnectionTokenGatewaySerializer(read_only=True)
domain = ConnectionTokenDomainSerializer(read_only=True)
cmd_filter_rules = ConnectionTokenCmdFilterRuleSerializer(many=True)
actions = ActionsField()
expired_at = serializers.IntegerField()
class Meta:
model = ConnectionToken
fields = [
'id', 'secret', 'type', 'user', 'asset', 'application', 'system_user',
'remote_app', 'cmd_filter_rules', 'domain', 'gateway', 'actions', 'expired_at',
]

View File

@@ -4,9 +4,8 @@ from rest_framework import serializers
from common.drf.fields import EncryptedField
__all__ = [
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
'MFAChallengeSerializer', 'MFASelectTypeSerializer',
'PasswordVerifySerializer',
]
@@ -29,7 +28,3 @@ class MFAChallengeSerializer(serializers.Serializer):
def update(self, instance, validated_data):
pass
class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6)

View File

@@ -5,16 +5,16 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<link rel="shortcut icon" href="{{ INTERFACE.favicon }}" type="image/x-icon">
<title>
{{ JMS_TITLE }}
{{ INTERFACE.login_title }}
</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% include '_head_css_js.html' %}
<!-- Stylesheets -->
<link href="{% static 'css/login-style.css' %}" rel="stylesheet">
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
<script src="{% static "js/jumpserver.js" %}"></script>
<script src="{% static "js/jumpserver.js" %}?_=9"></script>
<style>
.login-content {
@@ -32,12 +32,24 @@
font-weight: normal;
}
.hr-line-dashed {
border-top: 1px dashed #e7eaec;
color: #ffffff;
background-color: #ffffff;
height: 1px;
margin: 20px 0;
.form-group {
margin-bottom: 30px;
margin-top: 20px;
}
.extra-fields-1 .form-group {
margin-bottom: 30px;
margin-top: 15px;
}
.extra-fields-2 .form-group {
margin-bottom: 20px;
margin-top: 10px;
}
.extra-fields-3 .form-group {
margin-bottom: 10px;
margin-top: 10px;
}
.login-content {
@@ -78,34 +90,25 @@
margin-bottom: 0;
}
.captch-field .has-error .help-block {
.captcha-field .has-error .help-block {
margin-top: -8px !important;
}
.no-captcha-challenge .form-group {
margin-bottom: 20px;
}
.jms-title {
padding: 40px 10px 10px;
}
.no-captcha-challenge .jms-title {
padding: 60px 10px 10px;
}
.no-captcha-challenge .welcome-message {
padding-top: 10px;
.more-login-items {
margin-top: 10px;
}
.more-login-item {
border-right: 1px dashed #dedede;
padding-left: 5px;
padding-right: 5px;
padding: 2px 5px;
}
.more-login-item:last-child {
border: none;
border-right: none;
}
.select-con {
@@ -117,6 +120,7 @@
}
.login-page-language {
font-size: 12px!important;
margin-right: -11px !important;
padding-top: 12px !important;
padding-left: 0 !important;
@@ -125,15 +129,61 @@
font-weight: 350 !important;
min-height: auto !important;
}
.right-image {
height: 100%;
width: 100%
}
.jms-title {
font-size: 21px;
font-weight:400;
color: #151515;
letter-spacing: 0;
}
.more-methods-title {
position: relative;
margin-top: 20px;
}
.more-methods-title:before, .more-methods-title:after {
position: absolute;
top: 50%;
transform: translateY(-50%);
content: '';
border: 1px dashed #e7eaec;
width: 35%;
}
.more-methods-title:before {
left: 0;
}
.more-methods-title:after {
right: 0;
}
.more-methods-title.ja:before, .more-methods-title.ja:after{
width: 26%;
}
.captcha-field .form-group {
margin-bottom: 5px;
}
.auto-login.form-group .checkbox {
margin: 5px 0;
}
.more-login {
margin-top: 20px;
}
.has-error .more-login {
margin-top: 0;
}
</style>
</head>
<body>
<div class="login-content">
<div class="login-content extra-fields-{{ extra_fields_count }}">
<div class="right-image-box">
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver{% endif %}">
<img src="{{ LOGIN_IMAGE_URL }}" style="height: 100%; width: 100%"/>
<a href="{% if not XPACK_ENABLED %}https://github.com/jumpserver/jumpserver.git{% endif %}">
<img src="{{ INTERFACE.login_image }}" class="right-image" alt="screen-image"/>
</a>
</div>
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
@@ -142,26 +192,23 @@
<li class="dropdown">
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
{% if request.COOKIES.django_language == 'en' %}
<span>English<b class="caret"></b></span>
{% elif request.COOKIES.django_language == 'ja' %}
<span>日本語<b class="caret"></b></span>
{% else %}
<span>中文(简体)<b class="caret"></b></span>
{% endif %}
<span>{{ current_lang.title }}<b class="caret"></b></span>
</a>
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
<li> <a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}"> <span>中文(简体)</span> </a> </li>
<li> <a id="switch_en" href="{% url 'i18n-switch' lang='en' %}"> <span>English</span> </a> </li>
<li> <a id="switch_ja" href="{% url 'i18n-switch' lang='ja' %}"> <span>日本語</span> </a> </li>
{% for lang in langs %}
<li>
<a href="{% url 'i18n-switch' lang=lang.code %}">
<span>{{ lang.title }}</span>
</a>
</li>
{% endfor %}
</ul>
</li>
</ul>
<div class="jms-title">
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
<span style="">{{ INTERFACE.login_title }}</span>
</div>
<div class="contact-form col-md-10 col-md-offset-1">
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %}
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
@@ -177,7 +224,6 @@
</div>
{% bootstrap_field form.username show_label=False %}
<div class="form-group {% if form.password.errors %} has-error {% endif %}">
<input type="password" class="form-control" id="password" placeholder="{% trans 'Password' %}" required>
<input id="password-hidden" type="text" style="display:none" name="{{ form.password.html_name }}">
@@ -194,18 +240,18 @@
{% include '_mfa_login_field.html' %}
</div>
{% elif form.captcha %}
<div class="captch-field">
<div class="captcha-field">
{% bootstrap_field form.captcha show_label=False %}
</div>
{% endif %}
<div class="form-group" style="padding-top: 5px; margin-bottom: 10px">
<div class="form-group auto-login" style="margin-bottom: 10px">
<div class="row">
<div class="col-md-6" style="text-align: left">
{% if form.auto_login %}
{% bootstrap_field form.auto_login form_group_class='' %}
{% endif %}
</div>
<div class="col-md-6">
<div class="col-md-6" style="line-height: 25px">
<a id="forgot_password" href="{{ forgot_password_url }}" style="float: right">
<small>{% trans 'Forgot password' %}?</small>
</a>
@@ -213,18 +259,21 @@
</div>
</div>
<div class="form-group" style="">
<button type="submit" class="btn btn-transparent" onclick="doLogin();return false;">{% trans 'Login' %}</button>
<div class="form-group">
<button type="submit" class="btn btn-transparent" onclick="doLogin();return false;">
{% trans 'Login' %}
</button>
</div>
<div>
<div class="more-login">
{% if auth_methods %}
<div class="hr-line-dashed"></div>
<div style="display: inline-block; float: left">
<b class="text-muted text-left" >{% trans "More login options" %}</b>
<div class="more-methods-title {{ current_lang.code }}">
{% trans "More login options" %}
</div>
<div class="more-login-items">
{% for method in auth_methods %}
<a href="{{ method.url }}" class="more-login-item">
<i class="fa"><img src="{{ method.logo }}" height="13" width="13"></i> {{ method.name }}
<i class="fa"><img src="{{ method.logo }}" height="15" width="15"></i> {{ method.name }}
</a>
{% endfor %}
</div>
@@ -251,9 +300,6 @@
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
$('#login-form').submit(); //post提交
}
$(document).ready(function () {
})
</script>
</html>

View File

@@ -6,11 +6,11 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<link rel="shortcut icon" href="{{ INTERFACE.favicon }}" type="image/x-icon">
<title>{{ title }}</title>
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script src="{% static "js/jumpserver.js" %}"></script>
<script src="{% static "js/jumpserver.js" %}?_=9"></script>
</head>
@@ -20,9 +20,9 @@
<div class="col-md-12">
<div class="ibox-content">
<div>
<img src="{{ LOGO_URL }}" style="margin: auto" width="82" height="82">
<img src="{{ INTERFACE.logo_logout }}" style="margin: auto" width="82" height="82">
<h2 style="display: inline">
{{ JMS_TITLE }}
{{ INTERFACE.login_title }}
</h2>
</div>
<p></p>

View File

@@ -10,8 +10,8 @@ router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
router.register('sso', api.SSOViewSet, 'sso')
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
router.register('super-connection-token', api.UserSuperConnectionTokenViewSet, 'super-connection-token')
router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token')
router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token')
urlpatterns = [
@@ -26,13 +26,13 @@ urlpatterns = [
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('confirm/', api.ConfirmViewSet.as_view(), name='user-confirm'),
path('confirm/', api.ConfirmApi.as_view(), name='user-confirm'),
path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
path('mfa/select/', api.MFASendCodeApi.as_view(), name='mfa-select'),
path('mfa/send-code/', api.MFASendCodeApi.as_view(), name='mfa-send-codej'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
]

View File

@@ -8,17 +8,17 @@ from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView
from users.utils import is_auth_confirm_time_valid
from users.models import User
from users.permissions import IsAuthConfirmTimeValid
from users.views import UserVerifyPasswordView
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
from common.sdk.im.dingtalk import URL
from common.mixins.views import PermissionsMixin
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
from common.permissions import UserConfirmation
from authentication import errors
from authentication.mixins import AuthMixin
from authentication.const import ConfirmType
from common.sdk.im.dingtalk import DingTalk
from common.utils.common import get_request_ip
from authentication.notifications import OAuthBindMessage
@@ -30,7 +30,7 @@ logger = get_logger(__file__)
DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
class DingTalkBaseMixin(PermissionsMixin, View):
class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
@@ -119,7 +119,7 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View):
class DingTalkQRBindView(DingTalkQRMixin, View):
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
def get(self, request: HttpRequest):
user = request.user

View File

@@ -8,16 +8,17 @@ from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.permissions import IsAuthConfirmTimeValid
from users.views import UserVerifyPasswordView
from users.models import User
from users.views import UserVerifyPasswordView
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
from common.mixins.views import PermissionsMixin
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
from common.permissions import UserConfirmation
from common.sdk.im.feishu import FeiShu, URL
from common.utils.common import get_request_ip
from authentication import errors
from authentication.const import ConfirmType
from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage
@@ -27,7 +28,7 @@ logger = get_logger(__file__)
FEISHU_STATE_SESSION_KEY = '_feishu_state'
class FeiShuQRMixin(PermissionsMixin, View):
class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
@@ -89,7 +90,7 @@ class FeiShuQRMixin(PermissionsMixin, View):
class FeiShuQRBindView(FeiShuQRMixin, View):
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
def get(self, request: HttpRequest):
user = request.user

View File

@@ -4,14 +4,14 @@
from __future__ import unicode_literals
import os
import datetime
from typing import Callable
from django.templatetags.static import static
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.http import HttpResponse
from django.http import HttpResponse, HttpRequest
from django.shortcuts import reverse, redirect
from django.utils.decorators import method_decorator
from django.db import transaction
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext as _, get_language
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
@@ -35,10 +35,115 @@ __all__ = [
]
class UserLoginContextMixin:
get_user_mfa_context: Callable
request: HttpRequest
@staticmethod
def get_support_auth_methods():
auth_methods = [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'),
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': reverse('authentication:cas:cas-login'),
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'),
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'),
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': reverse('authentication:dingtalk-qr-login'),
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': reverse('authentication:feishu-qr-login'),
'logo': static('img/login_feishu_logo.png')
}
]
return [method for method in auth_methods if method['enabled']]
@staticmethod
def get_support_langs():
langs = [
{
'title': '中文(简体)',
'code': 'zh-hans'
},
{
'title': 'English',
'code': 'en'
},
{
'title': '日本語',
'code': 'ja'
}
]
return langs
def get_current_lang(self):
langs = self.get_support_langs()
matched_lang = filter(lambda x: x['code'] == get_language(), langs)
return next(matched_lang, langs[0])
@staticmethod
def get_forgot_password_url():
forgot_password_url = reverse('authentication:forgot-password')
forgot_password_url = settings.FORGOT_PASSWORD_URL or forgot_password_url
return forgot_password_url
def get_extra_fields_count(self, context):
count = 0
if self.get_support_auth_methods():
count += 1
form = context.get('form')
if not form:
return count
if set(form.fields.keys()) & {'captcha', 'challenge', 'mfa_type'}:
count += 1
if form.errors or form.non_field_errors():
count += 1
return count
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'demo_mode': os.environ.get("DEMO_MODE"),
'auth_methods': self.get_support_auth_methods(),
'langs': self.get_support_langs(),
'current_lang': self.get_current_lang(),
'forgot_password_url': self.get_forgot_password_url(),
'extra_fields_count': self.get_extra_fields_count(context),
**self.get_user_mfa_context(self.request.user)
})
return context
@method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(never_cache, name='dispatch')
class UserLoginView(mixins.AuthMixin, FormView):
class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
redirect_field_name = 'next'
template_name = 'authentication/login.html'
@@ -106,12 +211,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
context = self.get_context_data(form=new_form)
self.request.session.set_test_cookie()
return self.render_to_response(context)
except (
errors.MFAUnsetError,
errors.PasswordTooSimple,
errors.PasswordRequireResetError,
errors.PasswordNeedUpdate
) as e:
except errors.NeedRedirectError as e:
return redirect(e.url)
except (
errors.MFAFailedError,
@@ -136,67 +236,6 @@ class UserLoginView(mixins.AuthMixin, FormView):
self.request.session[RSA_PRIVATE_KEY] = None
self.request.session[RSA_PUBLIC_KEY] = None
@staticmethod
def get_support_auth_methods():
auth_methods = [
{
'name': 'OpenID',
'enabled': settings.AUTH_OPENID,
'url': reverse('authentication:openid:login'),
'logo': static('img/login_oidc_logo.png'),
'auto_redirect': True # 是否支持自动重定向
},
{
'name': 'CAS',
'enabled': settings.AUTH_CAS,
'url': reverse('authentication:cas:cas-login'),
'logo': static('img/login_cas_logo.png'),
'auto_redirect': True
},
{
'name': 'SAML2',
'enabled': settings.AUTH_SAML2,
'url': reverse('authentication:saml2:saml2-login'),
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
'url': reverse('authentication:wecom-qr-login'),
'logo': static('img/login_wecom_logo.png'),
},
{
'name': _('DingTalk'),
'enabled': settings.AUTH_DINGTALK,
'url': reverse('authentication:dingtalk-qr-login'),
'logo': static('img/login_dingtalk_logo.png')
},
{
'name': _('FeiShu'),
'enabled': settings.AUTH_FEISHU,
'url': reverse('authentication:feishu-qr-login'),
'logo': static('img/login_feishu_logo.png')
}
]
return [method for method in auth_methods if method['enabled']]
@staticmethod
def get_forgot_password_url():
forgot_password_url = reverse('authentication:forgot-password')
forgot_password_url = settings.FORGOT_PASSWORD_URL or forgot_password_url
return forgot_password_url
def get_context_data(self, **kwargs):
context = {
'demo_mode': os.environ.get("DEMO_MODE"),
'auth_methods': self.get_support_auth_methods(),
'forgot_password_url': self.get_forgot_password_url(),
**self.get_user_mfa_context(self.request.user)
}
kwargs.update(context)
return super().get_context_data(**kwargs)
class UserLoginGuardView(mixins.AuthMixin, RedirectView):
redirect_field_name = 'next'
@@ -258,8 +297,7 @@ class UserLoginWaitConfirmView(TemplateView):
if ticket:
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
ticket_detail_url = TICKET_DETAIL_URL.format(id=ticket_id, type=ticket.type)
assignees = ticket.current_node.first().ticket_assignees.all()
assignees_display = ', '.join([str(i.assignee) for i in assignees])
assignees_display = ', '.join([str(assignee) for assignee in ticket.current_assignees])
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
Don't close this page""").format(assignees_display)
else:

View File

@@ -8,18 +8,19 @@ from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView
from users.models import User
from users.permissions import IsAuthConfirmTimeValid
from users.views import UserVerifyPasswordView
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
from common.sdk.im.wecom import URL
from common.sdk.im.wecom import WeCom
from common.mixins.views import PermissionsMixin
from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin
from common.utils.common import get_request_ip
from common.permissions import UserConfirmation
from authentication import errors
from authentication.mixins import AuthMixin
from authentication.const import ConfirmType
from authentication.notifications import OAuthBindMessage
from .mixins import METAMixin
@@ -29,7 +30,7 @@ logger = get_logger(__file__)
WECOM_STATE_SESSION_KEY = '_wecom_state'
class WeComBaseMixin(PermissionsMixin, View):
class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
@@ -118,7 +119,7 @@ class WeComOAuthMixin(WeComBaseMixin, View):
class WeComQRBindView(WeComQRMixin, View):
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.ReLogin))
def get(self, request: HttpRequest):
user = request.user
@@ -299,5 +300,4 @@ class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View):
msg = e.msg
response = self.get_failed_response(login_url, title=msg, msg=msg)
return response
return self.redirect_to_guard_view()

View File

@@ -1,20 +1,32 @@
import json
from datetime import datetime
import uuid
import logging
from datetime import datetime
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone as dj_timezone
from django.db import models
from django.conf import settings
lazy_type = type(_('ugettext_lazy'))
class ModelJSONFieldEncoder(json.JSONEncoder):
""" 解决一些类型的字段不能序列化的问题 """
def default(self, obj):
if isinstance(obj, datetime):
str_cls = (models.Model, lazy_type, models.ImageField, uuid.UUID)
if isinstance(obj, str_cls):
return str(obj)
elif isinstance(obj, datetime):
obj = dj_timezone.localtime(obj)
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
if isinstance(obj, uuid.UUID):
return str(obj)
if isinstance(obj, type(_("ugettext_lazy"))):
return str(obj)
elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
and isinstance(obj[0], models.Model):
return [str(i) for i in obj]
else:
return super().default(obj)
try:
return super().default(obj)
except TypeError:
logging.error('Type error: ', type(obj))
return str(obj)

View File

@@ -13,6 +13,7 @@ import uuid
from functools import reduce, partial
import inspect
from django.db import transaction
from django.db.models import *
from django.db.models import QuerySet
from django.db.models.functions import Concat
@@ -211,3 +212,29 @@ class UnionQuerySet(QuerySet):
qs = cls(assets1, assets2)
return qs
class MultiTableChildQueryset(QuerySet):
def bulk_create(self, objs, batch_size=None):
assert batch_size is None or batch_size > 0
if not objs:
return objs
self._for_write = True
objs = list(objs)
parent_model = self.model._meta.pk.related_model
parent_objs = []
for obj in objs:
parent_values = {}
for field in [f for f in parent_model._meta.fields if hasattr(obj, f.name)]:
parent_values[field.name] = getattr(obj, field.name)
parent_objs.append(parent_model(**parent_values))
setattr(obj, self.model._meta.pk.attname, obj.id)
parent_model.objects.bulk_create(parent_objs, batch_size=batch_size)
with transaction.atomic(using=self.db, savepoint=False):
self._batched_insert(objs, self.model._meta.local_fields, batch_size)
return objs

View File

@@ -99,10 +99,16 @@ class BaseFileParser(BaseParser):
new_row_data = {}
serializer_fields = self.serializer_fields
for k, v in row_data.items():
if type(v) in [list, dict, int] or (isinstance(v, str) and k.strip() and v.strip()):
# 解决类似disk_info为字符串的'{}'的问题
if type(v) in [list, dict, int, bool] or (isinstance(v, str) and k.strip() and v.strip()):
# 处理类似disk_info为字符串的'{}'的问题
if not isinstance(v, str) and isinstance(serializer_fields[k], serializers.CharField):
v = str(v)
# 处理 BooleanField 的问题, 导出是 'True', 'False'
if isinstance(v, str) and v.strip().lower() == 'true':
v = True
elif isinstance(v, str) and v.strip().lower() == 'false':
v = False
new_row_data[k] = v
return new_row_data

View File

@@ -73,7 +73,10 @@ class BaseFileRenderer(BaseRenderer):
row = []
for field in render_fields:
value = item.get(field.field_name)
value = str(value) if value else ''
if value is None:
value = ''
else:
value = str(value)
row.append(value)
yield row

View File

@@ -41,10 +41,13 @@ class ReferencedByOthers(JMSException):
default_detail = _('Is referenced by other objects and cannot be deleted')
class MFAVerifyRequired(JMSException):
status_code = status.HTTP_400_BAD_REQUEST
default_code = 'mfa_verify_required'
default_detail = _('This action require verify your MFA')
class UserConfirmRequired(JMSException):
def __init__(self, code=None):
detail = {
'code': code,
'detail': _('This action require confirm current user')
}
super().__init__(detail=detail, code=code)
class UnexpectError(JMSException):

View File

@@ -2,16 +2,26 @@
#
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http.response import JsonResponse
from rest_framework import permissions
from rest_framework.decorators import action
from rest_framework.request import Request
from rest_framework.response import Response
from common.permissions import IsValidUser
from common.exceptions import UserConfirmRequired
from audits.utils import create_operate_log
from audits.models import OperateLog
__all__ = ["PermissionsMixin", "RecordViewLogMixin"]
__all__ = ["PermissionsMixin", "RecordViewLogMixin", "UserConfirmRequiredExceptionMixin"]
class UserConfirmRequiredExceptionMixin:
"""
异常处理
"""
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except UserConfirmRequired as e:
return JsonResponse(e.detail, status=e.status_code)
class PermissionsMixin(UserPassesTestMixin):

View File

@@ -1,9 +1,15 @@
# -*- coding: utf-8 -*-
#
import time
from rest_framework import permissions
from django.conf import settings
from common.exceptions import MFAVerifyRequired
from rest_framework import permissions
from authentication.const import ConfirmType
from common.exceptions import UserConfirmRequired
from orgs.utils import tmp_to_root_org
from authentication.models import ConnectionToken
from common.utils import get_object_or_none
class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
@@ -14,6 +20,22 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
and request.user.is_valid
class IsValidUserOrConnectionToken(IsValidUser):
def has_permission(self, request, view):
return super(IsValidUserOrConnectionToken, self).has_permission(request, view) \
or self.is_valid_connection_token(request)
@staticmethod
def is_valid_connection_token(request):
token_id = request.query_params.get('token')
if not token_id:
return False
with tmp_to_root_org():
token = get_object_or_none(ConnectionToken, id=token_id)
return token and token.is_valid
class OnlySuperUser(IsValidUser):
def has_permission(self, request, view):
return super().has_permission(request, view) \
@@ -29,18 +51,26 @@ class WithBootstrapToken(permissions.BasePermission):
return settings.BOOTSTRAP_TOKEN == request_bootstrap_token
class NeedMFAVerify(permissions.BasePermission):
class UserConfirmation(permissions.BasePermission):
ttl = 60 * 5
min_level = 1
confirm_type = ConfirmType.ReLogin
def has_permission(self, request, view):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
return True
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
return True
raise MFAVerifyRequired()
confirm_level = request.session.get('CONFIRM_LEVEL')
confirm_time = request.session.get('CONFIRM_TIME')
if not confirm_level or not confirm_time or \
confirm_level < self.min_level or \
confirm_time < time.time() - self.ttl:
raise UserConfirmRequired(code=self.confirm_type)
return True
class IsObjectOwner(IsValidUser):
def has_object_permission(self, request, view, obj):
return (super().has_object_permission(request, view, obj) and
request.user == getattr(obj, 'user', None))
@classmethod
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=300):
min_level = ConfirmType.values.index(confirm_type) + 1
name = 'UserConfirmationLevel{}TTL{}'.format(min_level, ttl)
return type(name, (cls,), {'min_level': min_level, 'ttl': ttl, 'confirm_type': confirm_type})

View File

@@ -14,7 +14,6 @@ from .local import thread_local
pattern = re.compile(r'FROM `(\w+)`')
logger = logging.getLogger("jumpserver.common")
DEBUG_DB = os.environ.get('DEBUG_DB', '0') == '1'
class Counter:
@@ -66,7 +65,7 @@ def on_request_finished_release_local(sender, **kwargs):
thread_local.__release_local__()
if settings.DEBUG and DEBUG_DB:
if settings.DEBUG_DEV:
request_finished.connect(on_request_finished_logging_db_query)
else:
request_finished.connect(on_request_finished_release_local)

View File

@@ -361,3 +361,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
end = data[-half:]
data = f'{start}{ellipsis_str}{end}'
return data
def group_by_count(it, count):
return [it[i:i+count] for i in range(0, len(it), count)]

View File

@@ -1,11 +1,9 @@
import json
import threading
import redis
from redis import Redis
from django.conf import settings
from django.core.cache import cache
from jumpserver.const import CONFIG
from common.http import is_true
from common.db.utils import safe_db_connection
from common.utils import get_logger
@@ -13,18 +11,9 @@ logger = get_logger(__name__)
def get_redis_client(db=0):
params = {
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
'password': CONFIG.REDIS_PASSWORD,
'db': db,
"ssl": is_true(CONFIG.REDIS_USE_SSL),
'ssl_cert_reqs': getattr(settings, 'REDIS_SSL_REQUIRED'),
'ssl_keyfile': getattr(settings, 'REDIS_SSL_KEYFILE'),
'ssl_certfile': getattr(settings, 'REDIS_SSL_CERTFILE'),
'ssl_ca_certs': getattr(settings, 'REDIS_SSL_CA_CERTS'),
}
return Redis(**params)
client = cache.client.get_client()
assert isinstance(client, redis.Redis)
return client
class Subscription:

View File

@@ -255,12 +255,14 @@ def decrypt_password(value):
if len(cipher) != 2:
return value
key_cipher, password_cipher = cipher
if not all([key_cipher, password_cipher]):
return value
aes_key = rsa_decrypt_by_session_pkey(key_cipher)
aes = get_aes_crypto(aes_key, 'ECB')
try:
password = aes.decrypt(password_cipher)
except UnicodeDecodeError as e:
logging.error("Decript password error: {}, {}".format(password_cipher, e))
except Exception as e:
logging.error("Decrypt password error: {}, {}".format(password_cipher, e))
return value
return password

View File

@@ -8,12 +8,14 @@ from django.utils import timezone
from django.db import models
from django.db.models.signals import post_save, pre_save
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
def reverse(view_name, urlconf=None, args=None, kwargs=None,
current_app=None, external=False, api_to_ui=False):
def reverse(
view_name, urlconf=None, args=None, kwargs=None,
current_app=None, external=False, api_to_ui=False,
is_console=False, is_audit=False, is_workbench=False
):
url = dj_reverse(view_name, urlconf=urlconf, args=args,
kwargs=kwargs, current_app=current_app)
@@ -21,7 +23,15 @@ def reverse(view_name, urlconf=None, args=None, kwargs=None,
site_url = settings.SITE_URL
url = site_url.strip('/') + url
if api_to_ui:
url = url.replace('api/v1', 'ui/#').rstrip('/')
replace_str = 'ui/#'
if is_console:
replace_str += '/console'
elif is_audit:
replace_str += '/audit'
elif is_workbench:
replace_str += '/workbench'
url = url.replace('api/v1', replace_str).rstrip('/')
return url
@@ -38,7 +48,7 @@ def date_expired_default():
years = int(settings.DEFAULT_EXPIRED_YEARS)
except TypeError:
years = 70
return timezone.now() + timezone.timedelta(days=365*years)
return timezone.now() + timezone.timedelta(days=365 * years)
def union_queryset(*args, base_queryset=None):

View File

@@ -128,6 +128,7 @@ class Config(dict):
'SECRET_KEY': '',
'BOOTSTRAP_TOKEN': '',
'DEBUG': False,
'DEBUG_DEV': False,
'LOG_LEVEL': 'DEBUG',
'LOG_DIR': os.path.join(PROJECT_DIR, 'logs'),
'DB_ENGINE': 'mysql',
@@ -139,6 +140,7 @@ class Config(dict):
'REDIS_HOST': '127.0.0.1',
'REDIS_PORT': 6379,
'REDIS_PASSWORD': '',
'REDIS_USE_SSL': False,
# Default value
'REDIS_DB_CELERY': 3,
'REDIS_DB_CACHE': 4,
@@ -325,6 +327,7 @@ class Config(dict):
'TERMINAL_MAGNUS_ENABLED': True,
'TERMINAL_KOKO_SSH_ENABLED': True,
'TERMINAL_RAZOR_ENABLED': True,
'TERMINAL_OMNIDB_ENABLED': True,
# 安全配置
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
@@ -380,6 +383,7 @@ class Config(dict):
'SESSION_COOKIE_SECURE': False,
'CSRF_COOKIE_SECURE': False,
'REFERER_CHECK_ENABLED': False,
'SESSION_ENGINE': 'cache',
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'SERVER_REPLAY_STORAGE': {},
@@ -410,7 +414,6 @@ class Config(dict):
'FORGOT_PASSWORD_URL': '',
'HEALTH_CHECK_TOKEN': '',
}
@staticmethod

View File

@@ -4,34 +4,32 @@ from django.templatetags.static import static
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
default_interface = dict((
('logo_logout', static('img/logo.png')),
('logo_index', static('img/logo_text.png')),
('login_image', static('img/login_image.jpg')),
('favicon', static('img/facio.ico')),
('login_title', _('JumpServer Open Source Bastion Host')),
('theme', 'classic_green'),
('theme_info', {}),
))
default_context = {
'DEFAULT_PK': '00000000-0000-0000-0000-000000000000',
'LOGO_URL': static('img/logo.png'),
'LOGO_TEXT_URL': static('img/logo_text.png'),
'LOGIN_IMAGE_URL': static('img/login_image.jpg'),
'FAVICON_URL': static('img/facio.ico'),
'LOGIN_CAS_LOGO_URL': static('img/login_cas_logo.png'),
'LOGIN_WECOM_LOGO_URL': static('img/login_wecom_logo.png'),
'LOGIN_DINGTALK_LOGO_URL': static('img/login_dingtalk_logo.png'),
'LOGIN_FEISHU_LOGO_URL': static('img/login_feishu_logo.png'),
'JMS_TITLE': _('JumpServer Open Source Bastion Host'),
}
default_interface = {
'login_title': default_context['JMS_TITLE'],
'logo_logout': default_context['LOGO_URL'],
'logo_index': default_context['LOGO_TEXT_URL'],
'login_image': default_context['LOGIN_IMAGE_URL'],
'favicon': default_context['FAVICON_URL'],
'LOGIN_CAS_logo_logout': static('img/login_cas_logo.png'),
'LOGIN_WECOM_logo_logout': static('img/login_wecom_logo.png'),
'LOGIN_DINGTALK_logo_logout': static('img/login_dingtalk_logo.png'),
'LOGIN_FEISHU_logo_logout': static('img/login_feishu_logo.png'),
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2022',
'INTERFACE': default_interface,
}
def jumpserver_processor(request):
# Setting default pk
context = default_context
context = {**default_context}
context.update({
'VERSION': settings.VERSION,
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2022',
'SECURITY_COMMAND_EXECUTION': settings.SECURITY_COMMAND_EXECUTION,
'SECURITY_MFA_VERIFY_TTL': settings.SECURITY_MFA_VERIFY_TTL,
'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME,

View File

@@ -0,0 +1,2 @@
from . import db

View File

@@ -0,0 +1,35 @@
import os
import sys
from django.db import models, transaction
from django.db.transaction import atomic as db_atomic
class ForeignKey(models.ForeignKey):
def __init__(self, *args, **kwargs):
kwargs['db_constraint'] = False
super().__init__(*args, **kwargs)
def atomic(using=None, savepoint=False):
return db_atomic(using=using, savepoint=savepoint)
class OneToOneField(models.OneToOneField):
def __init__(self, *args, **kwargs):
kwargs['db_constraint'] = False
super().__init__(*args, **kwargs)
def set_db_constraint():
if os.getenv('DB_CONSTRAINT', '1') != '0':
return
if sys.argv == 2 and sys.argv[1] == 'makemigrations':
return
print("Set foreignkey db constraint False")
transaction.atomic = atomic
models.ForeignKey = ForeignKey
models.OneToOneField = OneToOneField
set_db_constraint()

View File

@@ -1,50 +0,0 @@
from redis_sessions.session import (
force_unicode, SessionStore as RedisSessionStore,
RedisServer as _RedisServer, settings as redis_setting
)
from redis import exceptions, Redis
from django.conf import settings
from jumpserver.const import CONFIG
class RedisServer(_RedisServer):
__redis = {}
def get(self):
if self.connection_key in self.__redis:
return self.__redis[self.connection_key]
ssl_params = {}
if CONFIG.REDIS_USE_SSL:
ssl_params = {
'ssl_cert_reqs': getattr(settings, 'REDIS_SSL_REQUIRED'),
'ssl_keyfile': getattr(settings, 'REDIS_SSL_KEYFILE'),
'ssl_certfile': getattr(settings, 'REDIS_SSL_CERTFILE'),
'ssl_ca_certs': getattr(settings, 'REDIS_SSL_CA_CERTS'),
}
# 只根据 redis_url 方式连接
self.__redis[self.connection_key] = Redis.from_url(
redis_setting.SESSION_REDIS_URL, **ssl_params
)
return self.__redis[self.connection_key]
class SessionStore(RedisSessionStore):
def __init__(self, session_key=None):
super(SessionStore, self).__init__(session_key)
self.server = RedisServer(session_key).get()
def load(self):
try:
session_data = self.server.get(
self.get_real_stored_key(self._get_or_create_session_key())
)
return self.decode(force_unicode(session_data))
except exceptions.ConnectionError as e:
# 解决redis服务异常(如: 主从切换时)用户session立即过期的问题
raise
except:
self._session_key = None
return {}

View File

@@ -1,5 +1,6 @@
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from ops.urls.ws_urls import urlpatterns as ops_urlpatterns
from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns
@@ -12,4 +13,5 @@ application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter(urlpatterns)
),
"http": get_asgi_application(),
})

View File

@@ -3,6 +3,7 @@ import platform
if platform.system() == 'Darwin' and platform.machine() == 'arm64':
import pymysql
pymysql.version_info = (1, 4, 2, "final", 0)
pymysql.install_as_MySQLdb()
@@ -11,10 +12,19 @@ from django.urls import reverse_lazy
from .. import const
from ..const import CONFIG
def exist_or_default(path, default):
if not os.path.exists(path):
path = default
return path
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
VERSION = const.VERSION
BASE_DIR = const.BASE_DIR
PROJECT_DIR = const.PROJECT_DIR
DATA_DIR = os.path.join(PROJECT_DIR, 'data')
CERTS_DIR = os.path.join(DATA_DIR, 'certs')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
@@ -27,6 +37,8 @@ BOOTSTRAP_TOKEN = CONFIG.BOOTSTRAP_TOKEN
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.DEBUG
# SECURITY WARNING: If you run with debug turned on, more debug msg with be log
DEBUG_DEV = CONFIG.DEBUG_DEV
# Absolute url for some case, for example email link
SITE_URL = CONFIG.SITE_URL
@@ -148,19 +160,7 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# 自定义的配置SESSION_EXPIRE_AT_BROWSER_CLOSE 始终为 True, 下面这个来控制是否强制关闭后过期 cookie
SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE = CONFIG.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE
SESSION_SAVE_EVERY_REQUEST = CONFIG.SESSION_SAVE_EVERY_REQUEST
SESSION_ENGINE = 'jumpserver.rewriting.session'
SESSION_REDIS = {
'url': '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CACHE,
},
'prefix': 'auth_session',
'socket_timeout': 1,
'retry_on_timeout': False
}
SESSION_ENGINE = "django.contrib.sessions.backends.{}".format(CONFIG.SESSION_ENGINE)
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
# Database
@@ -180,12 +180,13 @@ DATABASES = {
}
}
DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'certs', 'db_ca.pem')
DB_USE_SSL = False
if CONFIG.DB_ENGINE.lower() == 'mysql':
DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'"
if os.path.isfile(DB_CA_PATH):
DB_OPTIONS['ssl'] = {'ca': DB_CA_PATH}
DB_USE_SSL = True
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
@@ -264,46 +265,53 @@ FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o755
# Cache use redis
REDIS_SSL_KEYFILE = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_client.key')
if not os.path.exists(REDIS_SSL_KEYFILE):
REDIS_SSL_KEYFILE = None
REDIS_SSL_KEY = exist_or_default(os.path.join(CERTS_DIR, 'redis_client.key'), None)
REDIS_SSL_CERT = exist_or_default(os.path.join(CERTS_DIR, 'redis_client.crt'), None)
REDIS_SSL_CA = exist_or_default(os.path.join(CERTS_DIR, 'redis_ca.pem'), None)
REDIS_SSL_CA = exist_or_default(os.path.join(CERTS_DIR, 'redis_ca.crt'), REDIS_SSL_CA)
REDIS_SSL_REQUIRED = 'none'
REDIS_USE_SSL = CONFIG.REDIS_USE_SSL
REDIS_SSL_CERTFILE = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_client.crt')
if not os.path.exists(REDIS_SSL_CERTFILE):
REDIS_SSL_CERTFILE = None
REDIS_LOCATION_NO_DB = '%(protocol)s://:%(password)s@%(host)s:%(port)s/{}' % {
'protocol': 'rediss' if REDIS_USE_SSL else 'redis',
'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
}
REDIS_SSL_CA_CERTS = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_ca.crt')
if not os.path.exists(REDIS_SSL_CA_CERTS):
REDIS_SSL_CA_CERTS = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_ca.pem')
if not os.path.exists(REDIS_SSL_CA_CERTS):
REDIS_SSL_CA_CERTS = None
REDIS_SSL_REQUIRED = CONFIG.REDIS_SSL_REQUIRED or 'none'
CACHES = {
'default': {
# 'BACKEND': 'redis_cache.RedisCache',
'BACKEND': 'redis_lock.django_cache.RedisCache',
'LOCATION': '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
'db': CONFIG.REDIS_DB_CACHE,
},
'OPTIONS': {
"REDIS_CLIENT_KWARGS": {"health_check_interval": 30},
"CONNECTION_POOL_KWARGS": {
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEYFILE,
"ssl_certfile": REDIS_SSL_CERTFILE,
"ssl_ca_certs": REDIS_SSL_CA_CERTS
} if CONFIG.REDIS_USE_SSL else {}
}
REDIS_CACHE_DEFAULT = {
'BACKEND': 'redis_lock.django_cache.RedisCache',
'LOCATION': REDIS_LOCATION_NO_DB.format(CONFIG.REDIS_DB_CACHE),
'OPTIONS': {
"REDIS_CLIENT_KWARGS": {"health_check_interval": 30},
"CONNECTION_POOL_KWARGS": {
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
"ssl_keyfile": REDIS_SSL_KEY,
"ssl_certfile": REDIS_SSL_CERT,
"ssl_ca_certs": REDIS_SSL_CA
} if REDIS_USE_SSL else {}
}
}
REDIS_CACHE_SESSION = dict(REDIS_CACHE_DEFAULT)
REDIS_CACHE_SESSION['LOCATION'] = REDIS_LOCATION_NO_DB.format(CONFIG.REDIS_DB_SESSION)
CACHES = {
'default': REDIS_CACHE_DEFAULT,
'session': REDIS_CACHE_SESSION
}
SESSION_CACHE_ALIAS = "session"
FORCE_SCRIPT_NAME = CONFIG.FORCE_SCRIPT_NAME
SESSION_COOKIE_SECURE = CONFIG.SESSION_COOKIE_SECURE
CSRF_COOKIE_SECURE = CONFIG.CSRF_COOKIE_SECURE
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# For Debug toolbar
INTERNAL_IPS = ["127.0.0.1"]
if os.environ.get('DEBUG_TOOLBAR', False):
INSTALLED_APPS = ['debug_toolbar'] + INSTALLED_APPS
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.profiling.ProfilingPanel',
]

View File

@@ -140,6 +140,7 @@ LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
TERMINAL_RAZOR_ENABLED = CONFIG.TERMINAL_RAZOR_ENABLED
TERMINAL_OMNIDB_ENABLED = CONFIG.TERMINAL_OMNIDB_ENABLED
TERMINAL_MAGNUS_ENABLED = CONFIG.TERMINAL_MAGNUS_ENABLED
TERMINAL_KOKO_SSH_ENABLED = CONFIG.TERMINAL_KOKO_SSH_ENABLED

View File

@@ -3,9 +3,13 @@
import os
import ssl
from .base import REDIS_SSL_CA_CERTS, REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE, REDIS_SSL_REQUIRED
from .base import (
REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY,
REDIS_SSL_REQUIRED, REDIS_USE_SSL
)
from ..const import CONFIG, PROJECT_DIR
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
@@ -14,10 +18,8 @@ REST_FRAMEWORK = {
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
# 'rest_framework.renderers.BrowsableAPIRenderer',
'common.drf.renders.CSVFileRenderer',
'common.drf.renders.ExcelFileRenderer',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
@@ -47,9 +49,6 @@ REST_FRAMEWORK = {
'DATETIME_INPUT_FORMATS': ['%Y/%m/%d %H:%M:%S %z', 'iso-8601', '%Y-%m-%d %H:%M:%S %z'],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler',
# 'PAGE_SIZE': 100,
# 'MAX_PAGE_SIZE': 5000
}
SWAGGER_SETTINGS = {
@@ -67,7 +66,7 @@ SWAGGER_SETTINGS = {
# Captcha settings, more see https://django-simple-captcha.readthedocs.io/en/latest/advanced.html
CAPTCHA_IMAGE_SIZE = (140, 34)
CAPTCHA_IMAGE_SIZE = (180, 38)
CAPTCHA_FOREGROUND_COLOR = '#001100'
CAPTCHA_NOISE_FUNCTIONS = ('captcha.helpers.noise_dots',)
CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge'
@@ -75,7 +74,6 @@ CAPTCHA_CHALLENGE_FUNCT = 'captcha.helpers.math_challenge'
# Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html
BOOTSTRAP3 = {
'horizontal_label_class': 'col-md-2',
# Field class to use in horizontal forms
'horizontal_field_class': 'col-md-9',
# Set placeholder attributes to label if no placeholder is provided
'set_placeholder': False,
@@ -85,15 +83,15 @@ BOOTSTRAP3 = {
# Django channels support websocket
if not CONFIG.REDIS_USE_SSL:
context = None
if not REDIS_USE_SSL:
redis_ssl = None
else:
context = ssl.SSLContext()
context.check_hostname = bool(CONFIG.REDIS_SSL_REQUIRED)
if REDIS_SSL_CA_CERTS:
context.load_verify_locations(REDIS_SSL_CA_CERTS)
if REDIS_SSL_CERTFILE and REDIS_SSL_KEYFILE:
context.load_cert_chain(REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE)
redis_ssl = ssl.SSLContext()
redis_ssl.check_hostname = bool(CONFIG.REDIS_SSL_REQUIRED)
if REDIS_SSL_CA:
redis_ssl.load_verify_locations(REDIS_SSL_CA)
if REDIS_SSL_CERT and REDIS_SSL_KEY:
redis_ssl.load_cert_chain(REDIS_SSL_CERT, REDIS_SSL_KEY)
CHANNEL_LAYERS = {
'default': {
@@ -103,7 +101,7 @@ CHANNEL_LAYERS = {
'address': (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT),
'db': CONFIG.REDIS_DB_WS,
'password': CONFIG.REDIS_PASSWORD or None,
'ssl': context
'ssl': redis_ssl
}],
},
},
@@ -116,7 +114,7 @@ CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery')
# Celery using redis as broker
CELERY_BROKER_URL = '%(protocol)s://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'protocol': 'rediss' if CONFIG.REDIS_USE_SSL else 'redis',
'protocol': 'rediss' if REDIS_USE_SSL else 'redis',
'password': CONFIG.REDIS_PASSWORD,
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
@@ -127,24 +125,19 @@ CELERY_RESULT_SERIALIZER = 'pickle'
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
CELERY_ACCEPT_CONTENT = ['json', 'pickle']
CELERY_RESULT_EXPIRES = 600
# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s'
# CELERY_WORKER_LOG_FORMAT = '%(message)s'
# CELERY_WORKER_TASK_LOG_FORMAT = '%(task_id)s %(task_name)s %(message)s'
CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s'
# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s'
CELERY_WORKER_LOG_FORMAT = '%(message)s'
CELERY_TASK_EAGER_PROPAGATES = True
CELERY_WORKER_REDIRECT_STDOUTS = True
CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
# CELERY_WORKER_HIJACK_ROOT_LOGGER = True
# CELERY_WORKER_MAX_TASKS_PER_CHILD = 40
CELERY_TASK_SOFT_TIME_LIMIT = 3600
if CONFIG.REDIS_USE_SSL:
if REDIS_USE_SSL:
CELERY_BROKER_USE_SSL = CELERY_REDIS_BACKEND_USE_SSL = {
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
'ssl_ca_certs': REDIS_SSL_CA_CERTS,
'ssl_certfile': REDIS_SSL_CERTFILE,
'ssl_keyfile': REDIS_SSL_KEYFILE
'ssl_ca_certs': REDIS_SSL_CA,
'ssl_certfile': REDIS_SSL_CERT,
'ssl_keyfile': REDIS_SSL_KEY
}
ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible')
@@ -153,3 +146,5 @@ ANSIBLE_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'ansible')
REDIS_HOST = CONFIG.REDIS_HOST
REDIS_PORT = CONFIG.REDIS_PORT
REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
DJANGO_REDIS_SCAN_ITERSIZE = 1000

View File

@@ -136,7 +136,7 @@ LOGGING = {
}
}
if os.environ.get("DEBUG_DB"):
if CONFIG.DEBUG_DEV:
LOGGING['loggers']['django.db'] = {
'handlers': ['console', 'file'],
'level': 'DEBUG'

View File

@@ -1,5 +1,6 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
import os
from django.urls import path, include, re_path
from django.conf import settings
@@ -45,8 +46,8 @@ if settings.XPACK_ENABLED:
apps = [
'users', 'assets', 'perms', 'terminal', 'ops', 'audits', 'orgs', 'auth',
'applications', 'tickets', 'settings', 'xpack',
'users', 'assets', 'perms', 'terminal', 'ops', 'audits',
'orgs', 'auth', 'applications', 'tickets', 'settings', 'xpack',
'flower', 'luna', 'koko', 'ws', 'docs', 'redocs',
]
@@ -79,6 +80,11 @@ urlpatterns += [
re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
]
if os.environ.get('DEBUG_TOOLBAR', False):
urlpatterns += [
path('__debug__/', include('debug_toolbar.urls')),
]
# 兼容之前的
old_app_pattern = '|'.join(apps)

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:132e7f59a56d1cf5b2358b21b547861e872fa456164f2e0809120fb2b13f0ec1
size 128122
oid sha256:3414a662f323c5d3b04780d67230ea4911127e2bceb040bded1b64292622ceed
size 128606

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:002f6953ebbe368642f0ea3c383f617b5f998edf2238341be63393123d4be8a9
size 105894
oid sha256:aa7ee5bbc77ab0eb8ca89fffff32ce4d31d5405312c4f75d8bec98913346850a
size 105987

File diff suppressed because it is too large Load Diff

View File

@@ -5,5 +5,5 @@ from .. import ws
app_name = 'notifications'
urlpatterns = [
path('ws/notifications/site-msg/', ws.SiteMsgWebsocket, name='site-msg-ws'),
]
path('ws/notifications/site-msg/', ws.SiteMsgWebsocket.as_asgi(), name='site-msg-ws'),
]

View File

@@ -15,8 +15,6 @@ logger = get_logger(__file__)
class JMSBaseInventory(BaseInventory):
windows_ssh_default_shell = settings.WINDOWS_SSH_DEFAULT_SHELL
def convert_to_ansible(self, asset, run_as_admin=False):
info = {
'id': asset.id,
@@ -33,7 +31,7 @@ class JMSBaseInventory(BaseInventory):
if asset.is_windows():
info["vars"].update({
"ansible_connection": "ssh",
"ansible_shell_type": self.windows_ssh_default_shell,
"ansible_shell_type": settings.WINDOWS_SSH_DEFAULT_SHELL,
})
for label in asset.labels.all():
info["vars"].update({

View File

@@ -6,7 +6,7 @@
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
<script src="{% static 'js/plugins/xterm/addons/fit/fit.js' %}"></script>
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<link rel="shortcut icon" href="{{ INTERFACE.favicon }}" type="image/x-icon">
<style>
body {
background-color: black;
@@ -88,4 +88,4 @@
}).on('resize', window, function () {
window.fit.fit(term);
});
</script>
</script>

View File

@@ -5,5 +5,5 @@ from .. import ws
app_name = 'ops'
urlpatterns = [
path('ws/ops/tasks/log/', ws.TaskLogWebsocket, name='task-log-ws'),
path('ws/ops/tasks/log/', ws.TaskLogWebsocket.as_asgi(), name='task-log-ws'),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.12 on 2022-07-18 05:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('orgs', '0012_auto_20220118_1054'),
]
operations = [
migrations.AlterModelOptions(
name='organization',
options={'permissions': (('view_rootorg', 'Can view root org'), ('view_alljoinedorg', 'Can view all joined org')), 'verbose_name': 'Organization'},
),
]

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