Compare commits

...

197 Commits

Author SHA1 Message Date
Jiangjie.Bai
bab4562820 Merge pull request #8980 from jumpserver/dev
v2.27.0
2022-10-20 20:39:39 +08:00
Jiangjie.Bai
104dd9721b perf: 优化smart匹配数据库端口数量失败时的错误提示信息 2022-10-20 17:38:15 +08:00
Jiangjie.Bai
cdcfdeefc5 perf: 优化smart匹配数据库端口数量失败时的错误提示信息 2022-10-20 17:38:15 +08:00
Jiangjie.Bai
613a7d63b5 Merge pull request #8973 from jumpserver/dev
v2.27.0-rc5
2022-10-19 20:30:13 +08:00
Jiangjie.Bai
c6a3a141bb perf: 优化Magnus Ports端口映射配置项 2022-10-19 20:14:20 +08:00
Jiangjie.Bai
93e5a0ba5c fix: 修改初始化 DB Port Mapper 时的日志输出 2022-10-19 17:54:10 +08:00
Jiangjie.Bai
129c0e1bf4 Merge pull request #8968 from jumpserver/dev
v2.27.0-rc4
2022-10-18 20:48:37 +08:00
Jiangjie.Bai
62c57d2fdf fix: 修复创建目录时指定权限为 755 2022-10-18 18:09:57 +08:00
Jiangjie.Bai
4711813af8 fix: 修复创建目录时指定权限为 755 2022-10-18 18:09:57 +08:00
Jiangjie.Bai
384873b4cb Merge pull request #8964 from jumpserver/dev
v2.27.0-rc3
2022-10-18 11:19:59 +08:00
fit2bot
33860bb955 fix: 修复资产详情 查看授权用户500问题 (#8963)
Co-authored-by: 小冯 <xiaofeng@xiaofengdeMacBook-Pro.local>
2022-10-18 10:51:51 +08:00
Jiangjie.Bai
9e410bb389 Merge pull request #8962 from jumpserver/dev
v2.27.0-rc2
2022-10-14 11:00:50 +08:00
吴小白
db2ab1513e fix: 修正龙芯架构缺失依赖包 2022-10-14 10:59:06 +08:00
Jiangjie.Bai
18e525c943 fix: 修改命令过滤器权限 2022-10-14 10:58:06 +08:00
Jiangjie.Bai
9337463471 Merge pull request #8957 from jumpserver/dev
v2.27.0-rc1
2022-10-13 19:03:33 +08:00
Jiangjie.Bai
8fdd89e67c fix: 修复初始化DB port mapper的逻辑 2022-10-13 19:01:09 +08:00
fit2bot
c7882a615f perf: 升级依赖 (#8955)
Co-authored-by: feng626 <1304903146@qq.com>
2022-10-13 18:24:57 +08:00
Jiangjie.Bai
e6d50cc8b4 Merge pull request #8951 from jumpserver/dev
v2.27.0-rc1
2022-10-13 15:05:53 +08:00
“huailei000”
3bd7410ab8 perf: update jquery 2022-10-13 14:44:36 +08:00
老广
c610ec797f docs: Change README description
Well
2022-10-13 13:55:34 +08:00
Jiangjie.Bai
188a2846ed fix: 修复 OAuth2 用户本地被禁用后,页面一直跳转的问题. 2022-10-11 18:46:05 +08:00
Jiangjie.Bai
df99067ee3 perf: 删除消息订阅时 websocket 重连的 redis 断开日志 2022-10-11 16:40:12 +08:00
feng626
ca17faaf01 fix: 修复创建工单无备注信息bug 2022-10-10 16:55:56 +08:00
feng626
a487d30001 perf: 密码首位不包含特殊字符 2022-10-09 20:21:26 +08:00
Jiangjie.Bai
fae5d07df6 feat: 优化命令过滤器支持关联节点; 2022-10-09 19:53:34 +08:00
Jiangjie.Bai
df31f47c68 feat: 命令过滤器支持关联节点; 添加端点规则迁移文件 2022-10-09 19:01:11 +08:00
evlic
d1acab3aa9 docs: fix README ambiguity 2022-10-08 15:47:11 +08:00
吴小白
15363a7f72 perf: 更新缓存规则 2022-09-29 20:21:15 +08:00
吴小白
d573ade525 fix: 修复使用缓存构建 2022-09-29 20:21:15 +08:00
吴小白
7ac00d5fdf perf: 多步骤构建 2022-09-29 17:05:20 +08:00
吴小白
2f6c9f8260 perf: 清理不需要的缓存 2022-09-29 17:05:20 +08:00
吴小白
41732d7a7b perf: 不需要清理缓存 2022-09-29 17:05:20 +08:00
吴小白
28d19fd91f perf: 构建时使用缓存 2022-09-29 17:05:20 +08:00
Jiangjie.Bai
65269db849 fix: 修复es存储失效时,会话命令列表页面报错的问题 2022-09-28 17:03:22 +08:00
Jiangjie.Bai
df2858470a fix: 修复命令存储es失效时, 会话、命令记录列表创建和查看失败的问题 2022-09-28 17:03:22 +08:00
吴小白
1c8ad40565 perf: 优化语言包生成方式 2022-09-28 14:49:07 +08:00
吴小白
78de2a2403 feat: 添加 Dockerfile.loong64 2022-09-28 14:49:07 +08:00
Jiangjie.Bai
218f917f69 fix: 锁定依赖包版本 pyOpenSSL==22.0.0 2022-09-27 15:47:23 +08:00
Aaron3S
bb25bf7621 fix: 修改解密异常抛出范围 2022-09-27 15:46:36 +08:00
Aaron3S
f6cc7046a2 fix: 修复空字符串加密报错的问题 2022-09-27 11:28:11 +08:00
Aaron3S
1bc6e50b06 perf: 优化去除结尾空字节的写法 2022-09-26 15:29:53 +08:00
吴小白
1d3135d2d7 perf: flower 开启持久化 2022-09-26 14:42:08 +08:00
Aaron3S
308d87d021 feat: 增加PIICO设备配置项 2022-09-26 14:40:48 +08:00
Aaron3S
db04f6ca18 feat: 增加国密配置项 2022-09-26 14:40:48 +08:00
Aaron3S
a7cd0bc0fe fix: 修复密码后空格的问题 2022-09-26 14:39:15 +08:00
Jiangjie.Bai
24708a6c5e feat: 优化 端口范围显示为 30000-30999 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
55a10a8d1d feat: 优化 DBPortManger 处理 port 的数据类型 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
32b6a1f1a4 feat: 修改翻译信息 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
c1c70849e9 feat: 修改 DBPortMapper 异常处理问题; DBListenPort API 迁移至 terminal app 中 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
7a6ed91f62 feat: 添加翻译信息 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
497a52a509 feat: 修改 DBPortManager 处理逻辑 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
57e12256e7 feat: 修改 Endpoint 获取 Manugs DB listen port 的逻辑 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
b8ec60dea1 feat: 优化 DB Listen Port 映射规则逻辑 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
c9afd94714 feat: 优化 DB Listen Port 映射规则逻辑 2022-09-22 19:23:39 +08:00
Jiangjie.Bai
a0c61ab8cb feat: 增加 DB Listen Port 映射规则 2022-09-22 19:23:39 +08:00
feng626
567b62516a fix: reset ssh url problem 2022-09-21 18:35:06 +08:00
吴小白
404fadd899 fix: 修复 redis 异常后 celery 旧任务不执行的问题 2022-09-21 18:33:35 +08:00
ibuler
ee1ec6aeee fix: 修复 celery 丢失心跳不会重连的问题 2022-09-21 18:33:35 +08:00
老广
783bddf2c7 perf: remove lgtm action
chore: remove lgtm action
2022-09-21 14:34:50 +08:00
ibuler
5ae49295e9 chore: remove lgtm action 2022-09-21 14:32:24 +08:00
老广
8d6d188ac7 perf: update some commit msg
perf: download ipdb if not found (maybe without lfs)
2022-09-21 14:29:56 +08:00
ibuler
912ff3df24 perf: download ipdb if not found (maybe without lfs) 2022-09-21 14:28:01 +08:00
ibuler
995d8cadb9 fix: warning after reboot 2022-09-21 14:27:09 +08:00
ibuler
6e5cea49ae perf: remove unused config 2022-09-21 14:26:05 +08:00
ibuler
a33a452434 chore: add english version secrity info 2022-09-21 14:25:07 +08:00
ibuler
fe2f54fcf6 chore: upgrade GPL to v3 2022-09-21 14:24:25 +08:00
ibuler
1e3154d9b6 pref: add openssh client to dockerfile 2022-09-21 14:23:24 +08:00
ibuler
a1c09591d3 chore: change contributing content 2022-09-21 14:22:31 +08:00
ibuler
d4e0a51a08 perf: set data dir to ignore 2022-09-21 14:21:42 +08:00
ibuler
bba4c15d6d perf: add ipdb to git lfs 2022-09-21 14:20:48 +08:00
ibuler
3e33c74b64 perf: add .git for ignore 2022-09-21 14:20:03 +08:00
ibuler
556d29360e pref: add debug tool bar 2022-09-21 14:18:59 +08:00
ibuler
9329a1563c chore: keep dir git 2022-09-21 14:17:38 +08:00
老广
8bf11c9ade perf: some commit tips
perf: some commit tips
2022-09-21 14:13:12 +08:00
ibuler
bbb802d894 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2022-09-21 14:09:57 +08:00
ibuler
8e7226d9dc pref: change run_server script 2022-09-21 14:09:28 +08:00
ibuler
2bd889e505 chore: add english readme 2022-09-21 14:07:23 +08:00
ibuler
3dcfd0035a chore: add code of conduct 2022-09-21 14:06:46 +08:00
ibuler
edfda5825c chore: keep dir on git 2022-09-21 14:05:47 +08:00
ibuler
3a196f0814 chore: keep log dir on git 2022-09-21 14:05:04 +08:00
ibuler
a4a671afd4 docs: redirect to doc site 2022-09-21 14:04:16 +08:00
ibuler
c337bbff8f perf: remove old warning msg 2022-09-21 14:02:47 +08:00
老广
863140e185 Merge pull request #8733 from jumpserver/dependabot/pip/requirements/django-3.2.15
build(deps): bump django from 3.2.14 to 3.2.15 in /requirements
2022-09-19 10:14:54 +08:00
老广
ad0d264c2a Merge pull request #8859 from jumpserver/dependabot/pip/requirements/flower-1.2.0
build(deps): bump flower from 1.0.0 to 1.2.0 in /requirements
2022-09-19 10:14:26 +08:00
老广
7f85e503d5 Merge pull request #8870 from QuentinM-Hilbtec/saml_fix
Fix issue #8287 with blank SAML's RelayState
2022-09-19 10:13:22 +08:00
Quentin Machu
61ff3db0f1 fix: address issue #8287 with blank SAML's RelayState 2022-09-16 13:51:40 -04:00
Jiangjie.Bai
fa08517bea Merge pull request #8868 from jumpserver/dev
v2.26.0-rc4
2022-09-15 16:16:51 +08:00
Jiangjie.Bai
f86d045c01 fix: 更新翻译 2022-09-15 16:12:12 +08:00
吴小白
1a7fd58abf perf: 修复容器重启页面报错 2022-09-15 15:05:01 +08:00
Jiangjie.Bai
d808256e6a Merge pull request #8864 from jumpserver/dev
v2.26.0-rc3
2022-09-14 20:44:13 +08:00
jiangweidong
305a1b10ed feat: 补充翻译 2022-09-14 20:43:21 +08:00
fit2bot
8c277e8875 fix: 修复mfa失效日期 失效问题 (#8862)
Co-authored-by: feng626 <1304903146@qq.com>
2022-09-14 16:17:51 +08:00
dependabot[bot]
ca965aca9e build(deps): bump flower from 1.0.0 to 1.2.0 in /requirements
Bumps [flower](https://github.com/mher/flower) from 1.0.0 to 1.2.0.
- [Release notes](https://github.com/mher/flower/releases)
- [Commits](https://github.com/mher/flower/compare/v1.0.0...v1.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-13 23:07:48 +00:00
Jiangjie.Bai
061b60ef59 Merge pull request #8858 from jumpserver/dev
v2.26.0-rc2
2022-09-13 17:40:13 +08:00
fit2bot
c008115888 fix: 修复配置mfa失效日期 失效问题 (#8856)
Co-authored-by: feng626 <1304903146@qq.com>
2022-09-13 17:39:09 +08:00
feng626
8d1fb84aaf perf: 工单新增相关过滤 2022-09-13 17:39:09 +08:00
jiangweidong
43d61b5348 feat: 支持对开启SSL/TLS的MongoDb数据库改密 2022-09-13 17:39:09 +08:00
ibuler
c26a786287 perf: 优化加密,没有rsa则不加密 2022-09-13 17:39:09 +08:00
fit2bot
cb2bd0cf2c fix: 修复账号备份失败问题 (#8852)
Co-authored-by: feng626 <1304903146@qq.com>
2022-09-13 17:39:09 +08:00
jiangweidong
3048e6311b fix: 修复华为短信配置错误,前端提示不对的问题 2022-09-13 17:39:09 +08:00
fit2bot
5e16b6387a fix: 修复配置mfa失效日期 失效问题 (#8856)
Co-authored-by: feng626 <1304903146@qq.com>
2022-09-13 17:20:09 +08:00
feng626
93e1adf376 perf: 工单新增相关过滤 2022-09-13 15:28:46 +08:00
jiangweidong
556bd3682e feat: 支持对开启SSL/TLS的MongoDb数据库改密 2022-09-13 15:27:54 +08:00
ibuler
6bbbe312a2 perf: 优化加密,没有rsa则不加密 2022-09-13 15:27:20 +08:00
fit2bot
1ac64db0ba fix: 修复账号备份失败问题 (#8852)
Co-authored-by: feng626 <1304903146@qq.com>
2022-09-09 16:01:08 +08:00
jiangweidong
fa54a98d6c fix: 修复华为短信配置错误,前端提示不对的问题 2022-09-08 18:55:01 +08:00
Jiangjie.Bai
31de9375e7 Merge pull request #8846 from jumpserver/dev
v2.26.0-rc1
2022-09-08 15:43:18 +08:00
halo
697270e3e6 perf: 优化清理任务偶发错误 2022-09-08 15:40:23 +08:00
halo
56c324b04e perf: utf-8编码忽略报错 2022-09-07 17:50:34 +08:00
jiangweidong
984b94c874 perf: 修改数据库应用ssl相关字段名 (#8840)
* 修改变量名

* 修改变量名
2022-09-07 16:08:37 +08:00
jiangweidong
50df7f1304 perf: 支持连接开启ssl且自签证书的db时 2022-09-07 11:23:18 +08:00
dependabot[bot]
7bd7be78a4 build(deps): bump django from 3.2.14 to 3.2.15 in /requirements
Bumps [django](https://github.com/django/django) from 3.2.14 to 3.2.15.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.14...3.2.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-06 07:50:20 +00:00
jiangweidong
8e5833aef0 修改一下顺序 2022-09-06 15:49:36 +08:00
jiangweidong
f20b465ddf feat: 改密计划支持MongoDB改密 2022-09-06 15:49:36 +08:00
jiangweidong
409d254a2e feat: 支持MFA可配置华为云平台短信对接 2022-09-06 15:48:33 +08:00
halo
e6d30fa77d perf: telnet系统工具输出使用utf-8编码 2022-09-06 14:59:16 +08:00
jiangweidong
b25404cac1 feat: 支持OAuth2协议自定义注销功能 2022-09-06 14:58:48 +08:00
feng626
ef4cc5f646 perf: 优化账号备份 2022-09-06 14:40:59 +08:00
Jiangjie.Bai
f0dc519423 perf: 优化 windows ad帮助链接地址 2022-08-25 15:24:33 +08:00
老广
2cb6da3129 Merge pull request #8811 from jumpserver/pr@dev@perf_customauth
perf: 优化 custom 认证模块加载逻辑,判断MD5值,启动时只加载一次
2022-08-25 15:23:08 +08:00
Jiangjie.Bai
1819083a25 perf: 优化 custom 认证模块加载逻辑,判断MD5值,启动时只加载一次 2022-08-25 15:04:45 +08:00
老广
bdeec0d3cb Merge pull request #8803 from jumpserver/pr@dev@feat_customauthbackend
feat: 支持自定义认证 backend;统一其他认证方式的信号触发逻辑;
2022-08-24 18:44:05 +08:00
Jiangjie.Bai
8fc5c4cf9e feat: 支持自定义认证 backend;统一其他认证方式的信号触发逻辑;通过配置文件控制 2022-08-24 18:41:47 +08:00
Jiangjie.Bai
89051b2c67 feat: 支持自定义认证 backend;统一其他认证方式的信号触发逻辑; 2022-08-24 18:04:22 +08:00
Jiangjie.Bai
9123839b48 feat: 支持自定义认证 backend;统一其他认证方式的信号触发逻辑; 2022-08-24 17:38:17 +08:00
老广
258c8a30d1 Merge pull request #8800 from jumpserver/pr@dev@feat_support_piico_gm
feat: 支持 piico 设备国密加密
2022-08-24 14:58:24 +08:00
jiangweidong
af75b5269c ca_cert不做大小限制 2022-08-24 14:51:38 +08:00
jiangweidong
0a66693a41 feat: MongoDB支持连接SSL类型 2022-08-24 14:51:38 +08:00
Jiangjie.Bai
7151201d58 feat: 支持自定义认证 backend;统一其他认证方式的信号触发逻辑; 2022-08-24 11:41:48 +08:00
Aaron3S
51820f23bf perf: 优化代码表达 2022-08-23 20:19:53 +08:00
Aaron3S
8772cd8c71 feat: 支持 piico 设备国密加密 2022-08-23 17:40:01 +08:00
ibuler
60cb1f8136 fix: 修复默认 gcm key padding 2022-08-22 14:26:11 +08:00
吴小白
5f1b7ff8f9 fix: 修正任务报错 2022-08-22 14:12:11 +08:00
feng626
37b150bc04 fix: 表单提交csrftoken问题 2022-08-19 17:22:59 +08:00
吴小白
1432fe1609 fix: 添加 openssh-client 依赖包 2022-08-19 17:13:56 +08:00
Jiangjie.Bai
8ae98887ee Revert "fix: 修复服务端渲染请求缺少csrf token 问题" (#8780)
This reverts commit 24a1738e73.
2022-08-19 14:19:47 +08:00
feng626
24a1738e73 fix: 修复服务端渲染请求缺少csrf token 问题 2022-08-19 10:52:59 +08:00
Jiangjie.Bai
188c04c9a6 Merge pull request #8776 from jumpserver/dev
v2.25.0
2022-08-18 16:12:16 +08:00
吴小白
bb4da12366 perf: 更新 pypi 镜像 2022-08-18 12:10:41 +08:00
fit2bot
382112ee33 perf: 批量命令搜索优化 (#8772)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-18 11:48:58 +08:00
fit2bot
3e69e6840b fix: oauth2不属于密码认证 (#8771)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-18 10:30:20 +08:00
Jiangjie.Bai
a82ed3e924 Merge pull request #8768 from jumpserver/dev
v2.25.0-rc5
2022-08-17 18:57:22 +08:00
fit2bot
b347acd5ec perf: 替换 mirrors (#8765)
* perf: 替换 mirrors

* perf: 使用中科大 mirrors

Co-authored-by: 吴小白 <296015668@qq.com>
2022-08-17 18:50:47 +08:00
Jiangjie.Bai
ccd6b01020 fix: 修复开启仅允许已存在用户登录并且是第三方用户认证时报错instance没有id的问题 2022-08-17 18:47:36 +08:00
Jiangjie.Bai
831b67eae4 Merge pull request #8763 from jumpserver/dev
v2.25.0-rc4
2022-08-17 16:52:28 +08:00
Jiangjie.Bai
3ab634d88e fix: 翻译 2022-08-17 16:43:47 +08:00
Jiangjie.Bai
867ad94a30 fix: 修改认证重定向地址 scheme 取值逻辑 2022-08-17 15:23:35 +08:00
fit2bot
7d0a19635a fix: 修复登录符合拒绝时 登录日志类型异常问题 (#8758)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-17 14:45:02 +08:00
Jiangjie.Bai
4642804077 Merge pull request #8756 from jumpserver/dev
v2.25.0-rc3
2022-08-16 19:07:42 +08:00
fit2bot
d405bae205 fix: 修复认证失败后错误信息总是 IP block 的问题 (#8755)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-08-16 17:46:17 +08:00
fit2bot
68841d1f15 fix: 配置仅已存在用户登录后 cas用户首次登录报403 (#8752)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-16 17:24:58 +08:00
fit2bot
4cad5affec fix: 修复工单火狐浏览器上页面展示 (#8753)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-16 17:15:21 +08:00
fit2bot
2f8a07e665 perf: 批量命令新增过滤选项 (#8749)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-16 13:56:19 +08:00
fit2bot
78133b0c60 fix: 修复后台手机号校验 (#8747)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-15 17:39:54 +08:00
Jiangjie.Bai
88d9078c43 fix: 修改 OAuth2.0 认证的字段的必填项 2022-08-15 16:56:34 +08:00
fit2bot
5559f112db fix: 用户登录复合500 (#8743)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-15 16:21:27 +08:00
Jiangjie.Bai
9a4b32cb3c perf: 优化 metadata 类型添加 float 2022-08-15 16:08:56 +08:00
feng626
ddf4b61c9f fix: 修复全局组织批量删除资产500 2022-08-15 16:08:23 +08:00
feng626
0eaaa7b4f6 fix: 用户异地登陆bug 2022-08-15 10:45:22 +08:00
Jiangjie.Bai
09160fed5d Merge pull request #8740 from jumpserver/dev
v2.25.0-rc2
2022-08-12 18:05:13 +08:00
fit2bot
18af5e8c4a fix: 【登录日志】登录复核用户被拒绝,登录日志无登录日志记录】 (#8739)
* fix: 【登录】第三方用户登录复核,拒绝状态,未真正拦截

* fix: 【登录日志】登录复核用户被拒绝,登录日志无登录日志记录】

* fix: 【登录日志】用户设置登录复核,登录。此时不处理工单,管理员全局组织下查看登录日志,日志无限新增,且无记录用户名】

Co-authored-by: huangzhiwen <zhiwen.huang@fit2cloud.com>
2022-08-12 18:01:04 +08:00
fit2bot
1ed388459b fix: 工单流 全局组织不能更新 (#8735)
Co-authored-by: feng626 <1304903146@qq.com>
2022-08-12 14:29:13 +08:00
feng626
2e944c6898 perf: 修改下载版本号 2022-08-11 16:25:32 +08:00
Jiangjie.Bai
8409523fee Merge pull request #8728 from jumpserver/dev
v2.25.0-rc1
2022-08-11 14:12:23 +08:00
吴小白
16634907b4 perf: ldap 支持客户端证书认证 2022-08-11 14:09:57 +08:00
feng626
cfa5de13ab feat: 节点树搜索 2022-08-11 14:08:45 +08:00
feng626
28c8ec1fab feat: 添加app 获取对应actions接口 2022-08-10 19:34:29 +08:00
huangzhiwen
a14ebc5f0f fix: 解决第三方登录无限重定向问题 2022-08-10 19:32:39 +08:00
Jiangjie.Bai
6af20d298d perf: 修改翻译 2022-08-10 19:07:22 +08:00
Jiangjie.Bai
795d6e01dc fix: 修改测试IP地址工具的默认超市时间为 0.5s 2022-08-10 19:07:22 +08:00
Eric
acf8b5798b perf: 优化rdp文件名的显示 2022-08-10 18:41:38 +08:00
jiangweidong
abcd12f645 perf: 补充cmpp2翻译及部分报错提示 (#8717)
* 修改CMPPv2.0翻译内容

* perf: 捕捉连接网关出错问题

* perf: 测试短信验证失败提示错误信息

* perf: 修改翻译
2022-08-10 17:32:28 +08:00
fit2bot
30fe5214c7 fix: 增加上了第三方用户登录失败的原因 (#8714)
* feat: OAuth2.0登录方式加上用户登录规则校验

* fix: 修复第三方用户登录规则(复核)问题

* fix: 增加上了第三方用户登录失败的原因

* fix: 修改变量名称

Co-authored-by: huangzhiwen <zhiwen.huang@fit2cloud.com>
2022-08-10 11:03:51 +08:00
jiangweidong
708a87c903 feat: 支持CMPPv2.0协议短信网关 (#8591)
* feat: 支持CMPPv2.0协议短信网关

* 修改翻译

Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-08-09 16:09:20 +08:00
huangzhiwen
6a30e0739d feat: OAuth2.0登录方式加上用户登录规则校验 2022-08-09 11:38:59 +08:00
fit2bot
3951b8b080 fix(auth): 第三方用户(saml2)登录规则设置无效 (#8648)
* fix: 修复 OpenID、CAS、SAML2登录规则设置无效

* refactor: auth_third_party_required写到一个地方和优化代码结构

* refactor: 优化代码结构

* refactor: 修改变量名称

Co-authored-by: huangzhiwen <zhiwen.huang@fit2cloud.com>
2022-08-09 11:24:28 +08:00
Jiangjie.Bai
c295f1451a fix: 修复登录失败日志的原因信息 2022-08-08 15:49:03 +08:00
Jiangjie.Bai
c4a94876cc fix: 增加配置项 SECURE_PROXY_SSL_HEADER request build url 时获取对应的 scheme 2022-08-08 15:00:26 +08:00
feng626
dcab934d9f fix: 修复用户自动登录bug 2022-08-08 11:42:45 +08:00
fit2bot
4ecb0b760f perf: 支持配置文件加密 (#8699)
* crypto

* perf: 暂存一下

* perf: 支持配置文件加密

* perf: 修改位置

* perf: 优化拆分出去

* stash

* perf: js 强制 key 最大 16

* pref: 修改语法

* fix: 修复启用 gm 后,又关闭导致的用户无法登录

Co-authored-by: ibuler <ibuler@qq.com>
2022-08-05 14:53:23 +08:00
fit2bot
b27b02eb9d feat: Cloud 支持局域网 IP 扫描 (#8589)
* feat: Cloud 支持局域网 IP 扫描

* feat: Cloud 支持局域网 IP 扫描

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-08-05 14:45:25 +08:00
Jiangjie.Bai
70cf847cd9 perf: update readme 2022-08-04 18:33:14 +08:00
jiangweidong
2099baaaff feat: 认证方式支持OAuth2.0协议 (#8686)
* feat: 认证方式支持OAuth2.0协议

* perf: 优化 OAuth2 认证逻辑和Logo (对接 Github)

* perf: 优化 OAuth2 认证逻辑和Logo,支持上传图标

* perf: 优化 OAuth2 认证逻辑和Logo,支持上传图标

* perf: 优化 OAuth2 认证逻辑和Logo,支持上传图标

* perf: 优化 OAuth2 认证逻辑和Logo,支持上传图标

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-08-04 14:40:33 +08:00
ibuler
b22aed0cc3 feat: 用户密码 hash 采用 gmsm3 2022-08-03 15:05:22 +08:00
“huailei000”
3e7f83d44e fix:修复忘记密码页布局错位问题 2022-08-02 16:42:55 +08:00
Jiangjie.Bai
40f8b99242 fix: 修复更新资产账号不成功的问题(末尾:) 2022-08-02 16:42:25 +08:00
Jiangjie.Bai
9ff345747b fix: 修复系统平台不能导入的问题 2022-08-02 14:55:32 +08:00
Jiangjie.Bai
9319c4748c perf: 修改用户登录 ACL 翻译信息 2022-08-02 14:54:09 +08:00
老广
e8b4ee5c40 Update README.md 2022-07-29 14:24:08 +08:00
fit2bot
429e838973 perf: 优化用户登录ACL根据规则优先级进行匹配 (#8672)
* perf: 优化用户登录ACL根据规则优先级进行匹配

* perf: 修改冲突

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-07-29 11:37:16 +08:00
fit2bot
ee1aff243c feat: 新增ping、telnet系统工具 (#8666)
* feat: 新增ping、telnet系统工具

* perf: 消息返回

Co-authored-by: halo <wuyihuangw@gmail.com>
2022-07-29 10:02:23 +08:00
fit2bot
ea7133dea0 fix: translate (#8664)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-28 13:47:32 +08:00
jiangweidong
e7229963bf perf: 更换oracle依赖包 2022-07-27 13:42:14 +08:00
feng626
0f7b41d177 fix: super ticket close bug 2022-07-26 18:34:35 +08:00
fit2bot
c4146744e5 perf: 优化授权过期提醒 (#8654)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-25 14:02:07 +08:00
fit2bot
dc32224294 feat: 应用工单支持选择动作 (#8651)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-22 16:24:57 +08:00
fit2bot
d07a230ba6 feat: 添加默认工单授权时间 (#8649)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-22 15:23:16 +08:00
178 changed files with 4594 additions and 1554 deletions

View File

@@ -7,4 +7,5 @@ django.db
celerybeat.pid
### Vagrant ###
.vagrant/
apps/xpack/.git
apps/xpack/.git

1
.gitattributes vendored
View File

@@ -1,3 +1,4 @@
*.mmdb filter=lfs diff=lfs merge=lfs -text
*.mo filter=lfs diff=lfs merge=lfs -text
*.ipdb filter=lfs diff=lfs merge=lfs -text

View File

@@ -41,4 +41,5 @@ version-resolver:
default: patch
template: |
## 版本变化 Whats Changed
$CHANGES
$CHANGES

1
.gitignore vendored
View File

@@ -41,3 +41,4 @@ release/*
releashe
/apps/script.py
data/*

View File

@@ -126,3 +126,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -23,3 +23,4 @@ When reporting issues, always include:
Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can
replace those parts with "REDACTED" or other strings like "****".

View File

@@ -1,54 +1,66 @@
FROM python:3.8-slim as stage-build
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
sshpass"
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
libjpeg-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
openssh-client \
sshpass"
ARG TOOLS=" \
curl \
default-mysql-client \
iproute2 \
iputils-ping \
locales \
procps \
redis-tools \
telnet \
vim \
unzip \
wget"
ca-certificates \
curl \
default-mysql-client \
iputils-ping \
locales \
procps \
redis-tools \
telnet \
vim \
unzip \
wget"
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update && sleep 1 && apt update \
&& apt -y install ${BUILD_DEPENDENCIES} \
&& apt -y install ${DEPENDENCIES} \
&& apt -y install ${TOOLS} \
&& localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \
&& rm -rf /var/lib/apt/lists/* \
&& mv /bin/sh /bin/sh.bak \
&& ln -s /bin/bash /bin/sh
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& rm -rf /var/lib/apt/lists/*
ARG TARGETARCH
ARG ORACLE_LIB_MAJOR=19
ARG ORACLE_LIB_MINOR=10
ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
@@ -65,25 +77,22 @@ RUN mkdir -p /opt/oracle/ \
WORKDIR /tmp/build
COPY ./requirements ./requirements
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip
ARG VERSION
ENV VERSION=$VERSION
RUN --mount=type=cache,target=/root/.cache/pip \
set -ex \
&& pip config set global.index-url ${PIP_MIRROR} \
&& pip install --upgrade pip \
&& pip install --upgrade setuptools wheel \
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install -r requirements/requirements.txt
ADD . .
RUN cd utils \
&& bash -ixeu build.sh \
&& mv ../release/jumpserver /opt/jumpserver \
&& rm -rf /tmp/build \
&& echo > /opt/jumpserver/config.yml
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN echo > /opt/jumpserver/config.yml \
&& rm -rf /tmp/build
WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data

95
Dockerfile.loong64 Normal file
View File

@@ -0,0 +1,95 @@
FROM python:3.8-slim as stage-build
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
libjpeg-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
openssh-client \
sshpass"
ARG TOOLS=" \
ca-certificates \
curl \
default-mysql-client \
iputils-ping \
locales \
netcat \
redis-server \
telnet \
vim \
unzip \
wget"
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
set -ex \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /tmp/build
COPY ./requirements ./requirements
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
RUN --mount=type=cache,target=/root/.cache/pip \
set -ex \
&& pip config set global.index-url ${PIP_MIRROR} \
&& pip install --upgrade pip \
&& pip install --upgrade setuptools wheel \
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-36.0.1-cp38-cp38-linux_loongarch64.whl \
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp38-cp38-linux_loongarch64.whl \
&& pip install $(grep 'PyNaCl' requirements/requirements.txt) \
&& GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install -r requirements/requirements.txt
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN echo > /opt/jumpserver/config.yml \
&& rm -rf /tmp/build
WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs
ENV LANG=zh_CN.UTF-8
EXPOSE 8070
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -671,4 +671,5 @@ into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -16,15 +16,13 @@
JumpServer 是全球首款开源堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
改变世界,从一点点开始 ...
> 如需进一步了解 JumpServer 开源项目,推荐阅读 [JumpServer 的初心和使命](https://mp.weixin.qq.com/s/S6q_2rP_9MwaVwyqLQnXzA)
### 特色优势
@@ -95,11 +93,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
### 案例研究
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
- [万华化学:通过JumpServer管理全球化分布式IT资产并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
- [顺丰科技:JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
- [携程JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [大智慧JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [小红书JumpServer 堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [中手游JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)

View File

@@ -92,4 +92,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
https://www.gnu.org/licenses/gpl-3.0.htmll
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -18,3 +18,4 @@ All security bugs should be reported to the contact as below:
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755

56
Vagrantfile vendored
View File

@@ -1,56 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
# Every Vagrant development environment requires a box. You can search for
# boxes at https://vagrantcloud.com/search.
config.vm.box_check_update = false
config.vm.box = "centos/7"
config.vm.hostname = "jumpserver"
config.vm.network "private_network", ip: "172.17.8.101"
config.vm.provider "virtualbox" do |vb|
vb.memory = "4096"
vb.cpus = 2
vb.name = "jumpserver"
end
config.vm.synced_folder ".", "/vagrant", type: "rsync",
rsync__verbose: true,
rsync__exclude: ['.git*', 'node_modules*','*.log','*.box','Vagrantfile']
config.vm.provision "shell", inline: <<-SHELL
## 设置yum的阿里云源
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
sudo sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo
sudo curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
sudo yum makecache
## 安装依赖包
sudo yum install -y python36 python36-devel python36-pip \
libtiff-devel libjpeg-devel libzip-devel freetype-devel \
lcms2-devel libwebp-devel tcl-devel tk-devel sshpass \
openldap-devel mariadb-devel mysql-devel libffi-devel \
openssh-clients telnet openldap-clients gcc
## 配置pip阿里云源
mkdir /home/vagrant/.pip
cat << EOF | sudo tee -a /home/vagrant/.pip/pip.conf
[global]
timeout = 6000
index-url = https://mirrors.aliyun.com/pypi/simple/
[install]
use-mirrors = true
mirrors = https://mirrors.aliyun.com/pypi/simple/
trusted-host=mirrors.aliyun.com
EOF
python3.6 -m venv /home/vagrant/venv
source /home/vagrant/venv/bin/activate
echo 'source /home/vagrant/venv/bin/activate' >> /home/vagrant/.bash_profile
SHELL
end

View File

@@ -44,58 +44,29 @@ 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)
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
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
def create_confirm_ticket(self, request):
from tickets import const

View File

@@ -4,6 +4,7 @@ from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from common.tree import TreeNodeSerializer
from common.mixins.api import SuggestionMixin
from .. import serializers

View File

@@ -7,3 +7,7 @@ from django.apps import AppConfig
class ApplicationsConfig(AppConfig):
name = 'applications'
verbose_name = _('Applications')
def ready(self):
from . import signal_handlers
super().ready()

View File

@@ -83,9 +83,3 @@ 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

@@ -10,9 +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
@@ -175,6 +173,7 @@ class ApplicationTreeNodeMixin:
return pid
def as_tree_node(self, pid, k8s_as_tree=False):
from ..utils import KubernetesTree
if self.type == const.AppType.k8s and k8s_as_tree:
node = KubernetesTree(pid).as_tree_node(self)
else:
@@ -304,15 +303,6 @@ 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

@@ -16,7 +16,7 @@ from .. import const
__all__ = [
'AppSerializer', 'MiniAppSerializer', 'AppSerializerMixin',
'AppAccountSerializer', 'AppAccountSecretSerializer'
'AppAccountSerializer', 'AppAccountSecretSerializer', 'AppAccountBackUpSerializer'
]
@@ -32,21 +32,23 @@ class AppSerializerMixin(serializers.Serializer):
return instance
def get_attrs_serializer(self):
default_serializer = serializers.Serializer(read_only=True)
instance = self.app
if instance:
_type = instance.type
_category = instance.category
else:
_type = self.context['request'].query_params.get('type')
_category = self.context['request'].query_params.get('category')
if _type:
if isinstance(self, AppAccountSecretSerializer):
serializer_class = type_secret_serializer_classes_mapping.get(_type)
tp = getattr(self, 'tp', None)
default_serializer = serializers.Serializer(read_only=True)
if not tp:
if instance:
tp = instance.type
category = instance.category
else:
serializer_class = type_serializer_classes_mapping.get(_type)
elif _category:
serializer_class = category_serializer_classes_mapping.get(_category)
tp = self.context['request'].query_params.get('type')
category = self.context['request'].query_params.get('category')
if tp:
if isinstance(self, AppAccountBackUpSerializer):
serializer_class = type_secret_serializer_classes_mapping.get(tp)
else:
serializer_class = type_serializer_classes_mapping.get(tp)
elif category:
serializer_class = category_serializer_classes_mapping.get(category)
else:
serializer_class = default_serializer
@@ -154,11 +156,6 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
class Meta(AppAccountSerializer.Meta):
fields_backup = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
'public_key', 'date_created', 'date_updated', 'version'
]
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
@@ -166,3 +163,22 @@ class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
'app_display': {'label': _('Application display')},
'systemuser_display': {'label': _('System User')}
}
class AppAccountBackUpSerializer(AppAccountSecretSerializer):
class Meta(AppAccountSecretSerializer.Meta):
fields = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
'public_key', 'date_created', 'date_updated', 'version'
]
def __init__(self, *args, **kwargs):
self.tp = kwargs.pop('tp', None)
super().__init__(*args, **kwargs)
@classmethod
def setup_eager_loading(cls, queryset):
return queryset
def to_representation(self, instance):
return super(AppAccountSerializer, self).to_representation(instance)

View File

@@ -13,3 +13,14 @@ class DBSerializer(serializers.Serializer):
database = serializers.CharField(
max_length=128, required=True, allow_null=True, label=_('Database')
)
use_ssl = serializers.BooleanField(default=False, label=_('Use SSL'))
ca_cert = serializers.CharField(
required=False, allow_null=True, label=_('CA certificate')
)
client_cert = serializers.CharField(
required=False, allow_null=True, label=_('Client certificate file')
)
cert_key = serializers.CharField(
required=False, allow_null=True, label=_('Certificate key file')
)
allow_invalid_cert = serializers.BooleanField(default=False, label=_('Allow invalid cert'))

View File

@@ -2,15 +2,9 @@ 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

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

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

@@ -24,7 +24,7 @@ class SerializeToTreeNodeMixin:
'title': _name(node),
'pId': node.parent_key,
'isParent': True,
'open': node.is_org_root(),
'open': True,
'meta': {
'data': {
"id": node.id,

View File

@@ -101,6 +101,8 @@ class NodeListAsTreeApi(generics.ListAPIView):
class NodeChildrenApi(generics.ListCreateAPIView):
serializer_class = serializers.NodeSerializer
search_fields = ('value',)
instance = None
is_initial = False
@@ -179,8 +181,15 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi):
"""
model = Node
def filter_queryset(self, queryset):
if not self.request.GET.get('search'):
return queryset
queryset = super().filter_queryset(queryset)
queryset = self.model.get_ancestor_queryset(queryset)
return queryset
def list(self, request, *args, **kwargs):
nodes = self.get_queryset().order_by('value')
nodes = self.filter_queryset(self.get_queryset()).order_by('value')
nodes = self.serialize_nodes(nodes, with_asset_amount=True)
assets = self.get_assets()
data = [*nodes, *assets]

View File

@@ -208,7 +208,7 @@ class SystemUserTaskApi(generics.CreateAPIView):
class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
rbac_perms = {
'list': 'assets.view_commandfilterule'
'list': 'assets.view_commandfilterule',
}
def get_serializer_class(self):
@@ -223,12 +223,14 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView):
if not system_user:
system_user_id = self.request.query_params.get('system_user_id')
asset_id = self.request.query_params.get('asset_id')
node_id = self.request.query_params.get('node_id')
application_id = self.request.query_params.get('application_id')
rules = CommandFilterRule.get_queryset(
user_id=user_id,
user_group_id=user_group_id,
system_user_id=system_user_id,
asset_id=asset_id,
node_id=node_id,
application_id=application_id
)
return rules

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.15 on 2022-10-09 09:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('assets', '0091_auto_20220629_1826'),
]
operations = [
migrations.AddField(
model_name='commandfilter',
name='nodes',
field=models.ManyToManyField(blank=True, related_name='cmd_filters', to='assets.Node', verbose_name='Nodes'),
),
]

View File

@@ -116,9 +116,9 @@ class NodesRelationMixin:
nodes = []
for node in self.get_nodes():
_nodes = node.get_ancestors(with_self=True)
nodes.append(_nodes)
nodes.extend(list(_nodes))
if flat:
nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
nodes = list(set([node.id for node in nodes]))
return nodes

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from users.models import User, UserGroup
from applications.models import Application
from ..models import SystemUser, Asset
from ..models import SystemUser, Asset, Node
from common.utils import lazyproperty, get_logger, get_object_or_none
from orgs.mixins.models import OrgModelMixin
@@ -33,6 +33,10 @@ class CommandFilter(OrgModelMixin):
'users.UserGroup', related_name='cmd_filters', blank=True,
verbose_name=_("User group"),
)
nodes = models.ManyToManyField(
'assets.Node', related_name='cmd_filters', blank=True,
verbose_name=_("Nodes")
)
assets = models.ManyToManyField(
'assets.Asset', related_name='cmd_filters', blank=True,
verbose_name=_("Asset")
@@ -189,7 +193,8 @@ class CommandFilterRule(OrgModelMixin):
@classmethod
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None,
asset_id=None, application_id=None, org_id=None):
asset_id=None, node_id=None, application_id=None, org_id=None):
# user & user_group
user_groups = []
user = get_object_or_none(User, pk=user_id)
if user:
@@ -198,8 +203,18 @@ class CommandFilterRule(OrgModelMixin):
if user_group:
org_id = user_group.org_id
user_groups.append(user_group)
system_user = get_object_or_none(SystemUser, pk=system_user_id)
# asset & node
nodes = []
asset = get_object_or_none(Asset, pk=asset_id)
if asset:
nodes.extend(asset.get_all_nodes())
node = get_object_or_none(Node, pk=node_id)
if node:
org_id = node.org_id
nodes.extend(list(node.get_ancestors(with_self=True)))
system_user = get_object_or_none(SystemUser, pk=system_user_id)
application = get_object_or_none(Application, pk=application_id)
q = Q()
if user:
@@ -212,6 +227,8 @@ class CommandFilterRule(OrgModelMixin):
if asset:
org_id = asset.org_id
q |= Q(assets=asset)
if nodes:
q |= Q(nodes__in=set(nodes))
if application:
org_id = application.org_id
q |= Q(applications=application)

View File

@@ -25,7 +25,6 @@ from orgs.mixins.models import OrgModelMixin, OrgManager
from orgs.utils import get_current_org, tmp_to_org, tmp_to_root_org
from orgs.models import Organization
__all__ = ['Node', 'FamilyMixin', 'compute_parent_key', 'NodeQuerySet']
logger = get_logger(__name__)
@@ -98,6 +97,14 @@ class FamilyMixin:
q |= Q(key=self.key)
return Node.objects.filter(q)
@classmethod
def get_ancestor_queryset(cls, queryset, with_self=True):
parent_keys = set()
for i in queryset:
parent_keys.update(set(i.get_ancestor_keys(with_self=with_self)))
queryset = queryset.model.objects.filter(key__in=list(parent_keys)).distinct()
return queryset
@property
def children(self):
return self.get_children(with_self=False)
@@ -396,7 +403,7 @@ class NodeAllAssetsMappingMixin:
mapping[ancestor_key].update(asset_ids)
t3 = time.time()
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2-t1, t3-t2))
logger.info('t1-t2(DB Query): {} s, t3-t2(Generate mapping): {} s'.format(t2 - t1, t3 - t2))
return mapping

View File

@@ -76,10 +76,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class Meta(AccountSerializer.Meta):
fields_backup = [
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
'private_key', 'public_key', 'date_created', 'date_updated', 'version'
]
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
@@ -88,6 +84,22 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
}
class AccountBackUpSerializer(AccountSecretSerializer):
class Meta(AccountSecretSerializer.Meta):
fields = [
'id', 'hostname', 'ip', 'username', 'password',
'private_key', 'public_key', 'date_created',
'date_updated', 'version'
]
@classmethod
def setup_eager_loading(cls, queryset):
return queryset
def to_representation(self, instance):
return super(AccountSerializer, self).to_representation(instance)
class AccountTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),

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

@@ -21,7 +21,7 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
'comment', 'created_by',
]
fields_fk = ['rules']
fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications']
fields_m2m = ['users', 'user_groups', 'system_users', 'nodes', 'assets', 'applications']
fields = fields_small + fields_fk + fields_m2m
extra_kwargs = {
'rules': {'read_only': True},

View File

@@ -4,15 +4,16 @@ from openpyxl import Workbook
from collections import defaultdict, OrderedDict
from django.conf import settings
from django.db.models import F
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from assets.models import AuthBook
from assets.serializers import AccountSecretSerializer
from assets.models import AuthBook, SystemUser, Asset
from assets.serializers import AccountBackUpSerializer
from assets.notifications import AccountBackupExecutionTaskMsg
from applications.models import Account
from applications.models import Account, Application
from applications.const import AppType
from applications.serializers import AppAccountSecretSerializer
from applications.serializers import AppAccountBackUpSerializer
from users.models import User
from common.utils import get_logger
from common.utils.timezone import local_now_display
@@ -38,7 +39,7 @@ class BaseAccountHandler:
@classmethod
def get_header_fields(cls, serializer: serializers.Serializer):
try:
backup_fields = getattr(serializer, 'Meta').fields_backup
backup_fields = getattr(serializer, 'Meta').fields
except AttributeError:
backup_fields = serializer.fields.keys()
header_fields = {}
@@ -51,17 +52,41 @@ class BaseAccountHandler:
header_fields[field] = str(v.label)
return header_fields
@staticmethod
def load_auth(tp, value, system_user):
if value:
return value
if system_user:
return getattr(system_user, tp, '')
return ''
@classmethod
def create_row(cls, account, serializer_cls, header_fields=None):
serializer = serializer_cls(account)
if not header_fields:
header_fields = cls.get_header_fields(serializer)
data = cls.unpack_data(serializer.data)
def replace_auth(cls, account, system_user_dict):
system_user = system_user_dict.get(account.systemuser_id)
account.username = cls.load_auth('username', account.username, system_user)
account.password = cls.load_auth('password', account.password, system_user)
account.private_key = cls.load_auth('private_key', account.private_key, system_user)
account.public_key = cls.load_auth('public_key', account.public_key, system_user)
return account
@classmethod
def create_row(cls, data, header_fields):
data = cls.unpack_data(data)
row_dict = {}
for field, header_name in header_fields.items():
row_dict[header_name] = str(data[field])
row_dict[header_name] = str(data.get(field, field))
return row_dict
@classmethod
def add_rows(cls, data, header_fields, sheet):
data_map = defaultdict(list)
for i in data:
row = cls.create_row(i, header_fields)
if sheet not in data_map:
data_map[sheet].append(list(row.keys()))
data_map[sheet].append(list(row.values()))
return data_map
class AssetAccountHandler(BaseAccountHandler):
@staticmethod
@@ -72,22 +97,27 @@ class AssetAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_data_map(cls):
data_map = defaultdict(list)
def replace_account_info(cls, account, asset_dict, system_user_dict):
asset = asset_dict.get(account.asset_id)
account.ip = asset.ip if asset else ''
account.hostname = asset.hostname if asset else ''
account = cls.replace_auth(account, system_user_dict)
return account
@classmethod
def create_data_map(cls, system_user_dict):
sheet_name = AuthBook._meta.verbose_name
assets = Asset.objects.only('id', 'hostname', 'ip')
asset_dict = {asset.id: asset for asset in assets}
accounts = AuthBook.objects.all()
if not accounts.exists():
return
accounts = AuthBook.get_queryset().select_related('systemuser')
if not accounts.first():
return data_map
header_fields = cls.get_header_fields(AccountSecretSerializer(accounts.first()))
header_fields = cls.get_header_fields(AccountBackUpSerializer(accounts.first()))
for account in accounts:
account.load_auth()
row = cls.create_row(account, AccountSecretSerializer, header_fields)
if sheet_name not in data_map:
data_map[sheet_name].append(list(row.keys()))
data_map[sheet_name].append(list(row.values()))
cls.replace_account_info(account, asset_dict, system_user_dict)
data = AccountBackUpSerializer(accounts, many=True).data
data_map = cls.add_rows(data, header_fields, sheet_name)
logger.info('\n\033[33m- 共收集 {} 条资产账号\033[0m'.format(accounts.count()))
return data_map
@@ -101,18 +131,36 @@ class AppAccountHandler(BaseAccountHandler):
return filename
@classmethod
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
def replace_account_info(cls, account, app_dict, system_user_dict):
app = app_dict.get(account.app_id)
account.type = app.type if app else ''
account.app_display = app.name if app else ''
account.category = app.category if app else ''
account = cls.replace_auth(account, system_user_dict)
return account
@classmethod
def create_data_map(cls, system_user_dict):
apps = Application.objects.only('id', 'type', 'name', 'category')
app_dict = {app.id: app for app in apps}
qs = Account.objects.all().annotate(app_type=F('app__type'))
if not qs.exists():
return
account_type_map = defaultdict(list)
for i in qs:
account_type_map[i.app_type].append(i)
data_map = {}
for app_type, accounts in account_type_map.items():
sheet_name = AppType.get_label(app_type)
row = cls.create_row(account, AppAccountSecretSerializer)
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()))
header_fields = cls.get_header_fields(AppAccountBackUpSerializer(tp=app_type))
if not accounts:
continue
for account in accounts:
cls.replace_account_info(account, app_dict, system_user_dict)
data = AppAccountBackUpSerializer(accounts, many=True, tp=app_type).data
data_map.update(cls.add_rows(data, header_fields, sheet_name))
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(qs.count()))
return data_map
@@ -137,12 +185,16 @@ class AccountBackupHandler:
# Print task start date
time_start = time.time()
files = []
system_user_qs = SystemUser.objects.only(
'id', 'username', 'password', 'private_key', 'public_key'
)
system_user_dict = {i.id: i for i in system_user_qs}
for account_type in self.execution.types:
handler = handler_map.get(account_type)
if not handler:
continue
data_map = handler.create_data_map()
data_map = handler.create_data_map(system_user_dict)
if not data_map:
continue

View File

@@ -12,6 +12,7 @@ from common.api import CommonGenericViewSet
from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet, OrgRelationMixin
from orgs.utils import current_org
from ops.models import CommandExecution
from . import filters
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
@@ -126,9 +127,7 @@ class CommandExecutionViewSet(ListModelMixin, OrgGenericViewSet):
class CommandExecutionHostRelationViewSet(OrgRelationMixin, OrgBulkModelViewSet):
serializer_class = CommandExecutionHostsRelationSerializer
m2m_field = CommandExecution.hosts.field
filterset_fields = [
'id', 'asset', 'commandexecution'
]
filterset_class = filters.CommandExecutionFilter
search_fields = ('asset__hostname', )
http_method_names = ['options', 'get']
rbac_perms = {

View File

@@ -1,10 +1,14 @@
from django.db.models import F, Value
from django.db.models.functions import Concat
from django_filters.rest_framework import CharFilter
from rest_framework import filters
from rest_framework.compat import coreapi, coreschema
from orgs.utils import current_org
from ops.models import CommandExecution
from common.drf.filters import BaseFilterSet
__all__ = ['CurrentOrgMembersFilter']
__all__ = ['CurrentOrgMembersFilter', 'CommandExecutionFilter']
class CurrentOrgMembersFilter(filters.BaseFilterBackend):
@@ -30,3 +34,22 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
else:
queryset = queryset.filter(user__in=self._get_user_list())
return queryset
class CommandExecutionFilter(BaseFilterSet):
hostname_ip = CharFilter(method='filter_hostname_ip')
class Meta:
model = CommandExecution.hosts.through
fields = (
'id', 'asset', 'commandexecution', 'hostname_ip'
)
def filter_hostname_ip(self, queryset, name, value):
queryset = queryset.annotate(
hostname_ip=Concat(
F('asset__hostname'), Value('('),
F('asset__ip'), Value(')')
)
).filter(hostname_ip__icontains=value)
return queryset

View File

@@ -22,7 +22,6 @@ from ..serializers import (
)
from ..models import ConnectionToken
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
@@ -63,12 +62,15 @@ class ConnectionTokenMixin:
def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset:
target_instance = asset
target_ip = asset.get_target_ip()
elif application:
target_instance = application
target_ip = application.get_target_ip()
else:
target_instance = None
target_ip = ''
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
endpoint = EndpointRule.match_endpoint(target_instance, target_ip, protocol, self.request)
return endpoint
@staticmethod
@@ -174,9 +176,8 @@ class ConnectionTokenMixin:
rdp_options['remoteapplicationname:s'] = name
else:
name = '*'
filename = "{}-{}-jumpserver".format(token.user.username, name)
filename = urllib.parse.quote(filename)
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
content = ''
for k, v in rdp_options.items():
@@ -184,6 +185,15 @@ class ConnectionTokenMixin:
return filename, content
@staticmethod
def get_connect_filename(prefix_name):
prefix_name = prefix_name.replace('/', '_')
prefix_name = prefix_name.replace('\\', '_')
prefix_name = prefix_name.replace('.', '_')
filename = f'{prefix_name}-jumpserver'
filename = urllib.parse.quote(filename)
return filename
def get_ssh_token(self, token: ConnectionToken):
if token.asset:
name = token.asset.hostname
@@ -191,7 +201,8 @@ class ConnectionTokenMixin:
name = token.application.name
else:
name = '*'
filename = f'{token.user.username}-{name}-jumpserver'
prefix_name = f'{token.user.username}-{name}'
filename = self.get_connect_filename(prefix_name)
endpoint = self.get_smart_endpoint(
protocol='ssh', asset=token.asset, application=token.application
@@ -326,4 +337,3 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
'msg': f'Token is renewed, date expired: {date_expired}'
}
return Response(data=data, status=status.HTTP_200_OK)

View File

@@ -6,6 +6,8 @@ from rest_framework.permissions import AllowAny
from common.utils import get_logger
from .. import errors, mixins
from django.contrib.auth import logout as auth_logout
__all__ = ['TicketStatusApi']
logger = get_logger(__name__)
@@ -17,7 +19,15 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
def get(self, request, *args, **kwargs):
try:
self.check_user_login_confirm()
self.request.session['auth_third_party_done'] = 1
return Response({"msg": "ok"})
except errors.LoginConfirmOtherError as e:
reason = e.msg
username = e.username
self.send_auth_signal(success=False, username=username, reason=reason)
# 若为三方登录,此时应退出登录
auth_logout(request)
return Response(e.as_data(), status=200)
except errors.NeedMoreInfoError as e:
return Response(e.as_data(), status=200)

View File

@@ -49,7 +49,7 @@ class JMSBaseAuthBackend:
if not allow:
info = 'User {} skip authentication backend {}, because it not in {}'
info = info.format(username, backend_name, ','.join(allowed_backend_names))
logger.debug(info)
logger.info(info)
return allow

View File

@@ -3,9 +3,10 @@
from django.urls import path
import django_cas_ng.views
from .views import CASLoginView
urlpatterns = [
path('login/', django_cas_ng.views.LoginView.as_view(), name='cas-login'),
path('login/', CASLoginView.as_view(), name='cas-login'),
path('logout/', django_cas_ng.views.LogoutView.as_view(), name='cas-logout'),
path('callback/', django_cas_ng.views.CallbackView.as_view(), name='cas-proxy-callback'),
]

View File

@@ -0,0 +1,15 @@
from django_cas_ng.views import LoginView
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
__all__ = ['LoginView']
class CASLoginView(LoginView):
def get(self, request):
try:
return super().get(request)
except PermissionDenied:
return HttpResponseRedirect('/')

View File

@@ -0,0 +1,61 @@
from django.conf import settings
from django.utils.module_loading import import_string
from common.utils import get_logger
from django.contrib.auth import get_user_model
from authentication.signals import user_auth_failed, user_auth_success
from .base import JMSModelBackend
logger = get_logger(__file__)
custom_authenticate_method = None
if settings.AUTH_CUSTOM:
""" 保证自定义认证方法在服务运行时不能被更改,只在第一次调用时加载一次 """
try:
custom_auth_method_path = 'data.auth.main.authenticate'
custom_authenticate_method = import_string(custom_auth_method_path)
except Exception as e:
logger.warning('Import custom auth method failed: {}, Maybe not enabled'.format(e))
class CustomAuthBackend(JMSModelBackend):
def is_enabled(self):
return settings.AUTH_CUSTOM and callable(custom_authenticate_method)
@staticmethod
def get_or_create_user_from_userinfo(userinfo: dict):
username = userinfo['username']
attrs = ['name', 'username', 'email', 'is_active']
defaults = {attr: userinfo[attr] for attr in attrs}
user, created = get_user_model().objects.get_or_create(
username=username, defaults=defaults
)
return user, created
def authenticate(self, request, username=None, password=None, **kwargs):
try:
userinfo: dict = custom_authenticate_method(
username=username, password=password, **kwargs
)
user, created = self.get_or_create_user_from_userinfo(userinfo)
except Exception as e:
logger.error('Custom authenticate error: {}'.format(e))
return None
if self.user_can_authenticate(user):
logger.info(f'Custom authenticate success: {user.username}')
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_CUSTOM
)
return user
else:
logger.info(f'Custom authenticate failed: {user.username}')
user_auth_failed.send(
sender=self.__class__, request=request, username=user.username,
reason=_('User invalid, disabled or expired'),
backend=settings.AUTH_BACKEND_CUSTOM
)
return None

View File

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

View File

@@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
#
import requests
from django.contrib.auth import get_user_model
from django.utils.http import urlencode
from django.conf import settings
from django.urls import reverse
from common.utils import get_logger
from users.utils import construct_user_email
from authentication.utils import build_absolute_uri
from authentication.signals import user_auth_failed, user_auth_success
from common.exceptions import JMSException
from .signals import (
oauth2_create_or_update_user
)
from ..base import JMSModelBackend
__all__ = ['OAuth2Backend']
logger = get_logger(__name__)
class OAuth2Backend(JMSModelBackend):
@staticmethod
def is_enabled():
return settings.AUTH_OAUTH2
def get_or_create_user_from_userinfo(self, request, userinfo):
log_prompt = "Get or Create user [OAuth2Backend]: {}"
logger.debug(log_prompt.format('start'))
# Construct user attrs value
user_attrs = {}
for field, attr in settings.AUTH_OAUTH2_USER_ATTR_MAP.items():
user_attrs[field] = userinfo.get(attr, '')
username = user_attrs.get('username')
if not username:
error_msg = 'username is missing'
logger.error(log_prompt.format(error_msg))
raise JMSException(error_msg)
email = user_attrs.get('email', '')
email = construct_user_email(user_attrs.get('username'), email)
user_attrs.update({'email': email})
logger.debug(log_prompt.format(user_attrs))
user, created = get_user_model().objects.get_or_create(
username=username, defaults=user_attrs
)
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => oauth2 create or update user"))
oauth2_create_or_update_user.send(
sender=self.__class__, request=request, user=user, created=created,
attrs=user_attrs
)
return user, created
@staticmethod
def get_response_data(response_data):
if response_data.get('data') is not None:
response_data = response_data['data']
return response_data
@staticmethod
def get_query_dict(response_data, query_dict):
query_dict.update({
'uid': response_data.get('uid', ''),
'access_token': response_data.get('access_token', '')
})
return query_dict
def authenticate(self, request, code=None, **kwargs):
log_prompt = "Process authenticate [OAuth2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if code is None:
logger.error(log_prompt.format('code is missing'))
return None
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID,
'client_secret': settings.AUTH_OAUTH2_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
access_token_url = '{url}?{query}'.format(
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, query=urlencode(query_dict)
)
token_method = settings.AUTH_OAUTH2_ACCESS_TOKEN_METHOD.lower()
requests_func = getattr(requests, token_method, requests.get)
logger.debug(log_prompt.format('Call the access token endpoint[method: %s]' % token_method))
headers = {
'Accept': 'application/json'
}
access_token_response = requests_func(access_token_url, headers=headers)
try:
access_token_response.raise_for_status()
access_token_response_data = access_token_response.json()
response_data = self.get_response_data(access_token_response_data)
except Exception as e:
error = "Json access token response error, access token response " \
"content is: {}, error is: {}".format(access_token_response.content, str(e))
logger.error(log_prompt.format(error))
return None
query_dict = self.get_query_dict(response_data, query_dict)
headers = {
'Accept': 'application/json',
'Authorization': 'token {}'.format(response_data.get('access_token', ''))
}
logger.debug(log_prompt.format('Get userinfo endpoint'))
userinfo_url = '{url}?{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT,
query=urlencode(query_dict)
)
userinfo_response = requests.get(userinfo_url, headers=headers)
try:
userinfo_response.raise_for_status()
userinfo_response_data = userinfo_response.json()
if 'data' in userinfo_response_data:
userinfo = userinfo_response_data['data']
else:
userinfo = userinfo_response_data
except Exception as e:
error = "Json userinfo response error, userinfo response " \
"content is: {}, error is: {}".format(userinfo_response.content, str(e))
logger.error(log_prompt.format(error))
return None
try:
logger.debug(log_prompt.format('Update or create oauth2 user'))
user, created = self.get_or_create_user_from_userinfo(request, userinfo)
except JMSException:
return None
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OAuth2 user login success'))
logger.debug(log_prompt.format('Send signal => oauth2 user login success'))
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_OAUTH2
)
return user
else:
logger.debug(log_prompt.format('OAuth2 user login failed'))
logger.debug(log_prompt.format('Send signal => oauth2 user login failed'))
user_auth_failed.send(
sender=self.__class__, request=request, username=user.username,
reason=_('User invalid, disabled or expired'),
backend=settings.AUTH_BACKEND_OAUTH2
)
return None

View File

@@ -0,0 +1,7 @@
from django.dispatch import Signal
oauth2_create_or_update_user = Signal(
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
)

View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
#
from django.urls import path
from . import views
urlpatterns = [
path('login/', views.OAuth2AuthRequestView.as_view(), name='login'),
path('callback/', views.OAuth2AuthCallbackView.as_view(), name='login-callback'),
path('logout/', views.OAuth2EndSessionView.as_view(), name='logout')
]

View File

@@ -0,0 +1,90 @@
from django.views import View
from django.conf import settings
from django.contrib import auth
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
from authentication.utils import build_absolute_uri
from common.utils import get_logger
from authentication.mixins import authenticate
logger = get_logger(__file__)
class OAuth2AuthRequestView(View):
def get(self, request):
log_prompt = "Process OAuth2 GET requests: {}"
logger.debug(log_prompt.format('Start'))
query_dict = {
'client_id': settings.AUTH_OAUTH2_CLIENT_ID, 'response_type': 'code',
'scope': settings.AUTH_OAUTH2_SCOPE,
'redirect_uri': build_absolute_uri(
request, path=reverse(settings.AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
redirect_url = '{url}?{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT,
query=urlencode(query_dict)
)
logger.debug(log_prompt.format('Redirect login url'))
return HttpResponseRedirect(redirect_url)
class OAuth2AuthCallbackView(View):
http_method_names = ['get', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OAuth2AuthCallbackView]: {}"
logger.debug(log_prompt.format('Start'))
callback_params = request.GET
if 'code' in callback_params:
logger.debug(log_prompt.format('Process authenticate'))
user = authenticate(code=callback_params['code'], request=request)
if user and user.is_valid:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(
settings.AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI
)
logger.debug(log_prompt.format('Redirect'))
# OAuth2 服务端认证成功, 但是用户被禁用了, 这时候需要调用服务端的logout
redirect_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
return HttpResponseRedirect(redirect_url)
class OAuth2EndSessionView(View):
http_method_names = ['get', 'post', ]
def get(self, request):
""" Processes GET requests. """
log_prompt = "Process GET requests [OAuth2EndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
return self.post(request)
def post(self, request):
""" Processes POST requests. """
log_prompt = "Process POST requests [OAuth2EndSessionView]: {}"
logger.debug(log_prompt.format('Start'))
logout_url = settings.LOGOUT_REDIRECT_URL or '/'
# Log out the current user.
if request.user.is_authenticated:
logger.debug(log_prompt.format('Log out the current user: {}'.format(request.user)))
auth.logout(request)
if settings.AUTH_OAUTH2_LOGOUT_COMPLETELY:
logger.debug(log_prompt.format('Log out OAUTH2 platform user session synchronously'))
next_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
return HttpResponseRedirect(next_url)
logger.debug(log_prompt.format('Redirect'))
return HttpResponseRedirect(logout_url)

View File

@@ -9,6 +9,7 @@
import base64
import requests
from rest_framework.exceptions import ParseError
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
@@ -18,14 +19,16 @@ from django.urls import reverse
from django.conf import settings
from common.utils import get_logger
from authentication.utils import build_absolute_uri_for_oidc
from users.utils import construct_user_email
from ..base import JMSBaseAuthBackend
from .utils import validate_and_return_id_token, build_absolute_uri
from .utils import validate_and_return_id_token
from .decorator import ssl_verification
from .signals import (
openid_create_or_update_user, openid_user_login_failed, openid_user_login_success
openid_create_or_update_user
)
from authentication.signals import user_auth_success, user_auth_failed
logger = get_logger(__file__)
@@ -127,7 +130,7 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
token_payload = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': build_absolute_uri(
'redirect_uri': build_absolute_uri_for_oidc(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
@@ -211,14 +214,18 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(sender=self.__class__, request=request, user=user)
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_OIDC_CODE
)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
user_auth_failed.send(
sender=self.__class__, request=request, username=user.username,
reason="User is invalid"
reason="User is invalid", backend=settings.AUTH_BACKEND_OIDC_CODE
)
return None
@@ -269,8 +276,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
"content is: {}, error is: {}".format(token_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
user_auth_failed.send(
sender=self.__class__, request=request, username=username, reason=error,
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return
@@ -297,8 +305,9 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
user_auth_failed.send(
sender=self.__class__, request=request, username=username, reason=error,
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return
@@ -310,13 +319,16 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('OpenID user login success'))
logger.debug(log_prompt.format('Send signal => openid user login success'))
openid_user_login_success.send(
sender=self.__class__, request=request, user=user
user_auth_success.send(
sender=self.__class__, request=request, user=user,
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return user
else:
logger.debug(log_prompt.format('OpenID user login failed'))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid"
user_auth_failed.send(
sender=self.__class__, request=request, username=username, reason="User is invalid",
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return None

View File

@@ -13,6 +13,4 @@ from django.dispatch import Signal
openid_create_or_update_user = Signal(
providing_args=['request', 'user', 'created', 'name', 'username', 'email']
)
openid_user_login_success = Signal(providing_args=['request', 'user'])
openid_user_login_failed = Signal(providing_args=['request', 'username', 'reason'])

View File

@@ -8,7 +8,7 @@
import datetime as dt
from calendar import timegm
from urllib.parse import urlparse, urljoin
from urllib.parse import urlparse
from django.core.exceptions import SuspiciousOperation
from django.utils.encoding import force_bytes, smart_bytes
@@ -110,17 +110,3 @@ def _validate_claims(id_token, nonce=None, validate_nonce=True):
raise SuspiciousOperation('Incorrect id_token: nonce')
logger.debug(log_prompt.format('End'))
def build_absolute_uri(request, path=None):
"""
Build absolute redirect uri
"""
if path is None:
path = '/'
if settings.BASE_SITE_URL:
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
else:
redirect_uri = request.build_absolute_uri(path)
return redirect_uri

View File

@@ -20,7 +20,8 @@ from django.utils.crypto import get_random_string
from django.utils.http import is_safe_url, urlencode
from django.views.generic import View
from .utils import get_logger, build_absolute_uri
from authentication.utils import build_absolute_uri_for_oidc
from .utils import get_logger
logger = get_logger(__file__)
@@ -50,7 +51,7 @@ class OIDCAuthRequestView(View):
'scope': settings.AUTH_OPENID_SCOPES,
'response_type': 'code',
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'redirect_uri': build_absolute_uri(
'redirect_uri': build_absolute_uri_for_oidc(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
})
@@ -216,7 +217,7 @@ class OIDCEndSessionView(View):
""" Returns the end-session URL. """
q = QueryDict(mutable=True)
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_REDIRECT_URI_PARAMETER] = \
build_absolute_uri(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
build_absolute_uri_for_oidc(self.request, path=settings.LOGOUT_REDIRECT_URL or '/')
q[settings.AUTH_OPENID_PROVIDER_END_SESSION_ID_TOKEN_PARAMETER] = \
self.request.session['oidc_auth_id_token']
return '{}?{}'.format(settings.AUTH_OPENID_PROVIDER_END_SESSION_ENDPOINT, q.urlencode())

View File

@@ -7,9 +7,9 @@ from django.db import transaction
from common.utils import get_logger
from authentication.errors import reason_choices, reason_user_invalid
from .signals import (
saml2_user_authenticated, saml2_user_authentication_failed,
saml2_create_or_update_user
)
from authentication.signals import user_auth_failed, user_auth_success
from ..base import JMSModelBackend
__all__ = ['SAML2Backend']
@@ -39,7 +39,7 @@ class SAML2Backend(JMSModelBackend):
return user, created
def authenticate(self, request, saml_user_data=None, **kwargs):
log_prompt = "Process authenticate [SAML2AuthCodeBackend]: {}"
log_prompt = "Process authenticate [SAML2Backend]: {}"
logger.debug(log_prompt.format('Start'))
if saml_user_data is None:
logger.error(log_prompt.format('saml_user_data is missing'))
@@ -48,21 +48,23 @@ class SAML2Backend(JMSModelBackend):
logger.debug(log_prompt.format('saml data, {}'.format(saml_user_data)))
username = saml_user_data.get('username')
if not username:
logger.debug(log_prompt.format('username is missing'))
logger.warning(log_prompt.format('username is missing'))
return None
user, created = self.get_or_create_from_saml_data(request, **saml_user_data)
if self.user_can_authenticate(user):
logger.debug(log_prompt.format('SAML2 user login success'))
saml2_user_authenticated.send(
sender=self, request=request, user=user, created=created
user_auth_success.send(
sender=self.__class__, request=request, user=user, created=created,
backend=settings.AUTH_BACKEND_SAML2
)
return user
else:
logger.debug(log_prompt.format('SAML2 user login failed'))
saml2_user_authentication_failed.send(
sender=self, request=request, username=username,
reason=reason_choices.get(reason_user_invalid)
user_auth_failed.send(
sender=self.__class__, request=request, username=username,
reason=reason_choices.get(reason_user_invalid),
backend=settings.AUTH_BACKEND_SAML2
)
return None

View File

@@ -2,5 +2,3 @@ from django.dispatch import Signal
saml2_create_or_update_user = Signal(providing_args=('user', 'created', 'request', 'attrs'))
saml2_user_authenticated = Signal(providing_args=('user', 'created', 'request'))
saml2_user_authentication_failed = Signal(providing_args=('request', 'username', 'reason'))

View File

@@ -3,7 +3,7 @@ import copy
from urllib import parse
from django.views import View
from django.contrib import auth as auth
from django.contrib import auth
from django.urls import reverse
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
@@ -271,7 +271,10 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
auth.login(self.request, user)
logger.debug(log_prompt.format('Redirect'))
next_url = saml_instance.redirect_to(post_data.get('RelayState', '/'))
redir = post_data.get('RelayState')
if not redir or len(redir) == 0:
redir = "/"
next_url = saml_instance.redirect_to(redir)
return HttpResponseRedirect(next_url)
@csrf_exempt

View File

@@ -12,12 +12,13 @@ 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.error
request=self.request, reason=self.msg
)
@@ -55,7 +56,8 @@ 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)
if not self.msg:
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)
@@ -65,22 +67,21 @@ class CredentialError(
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)
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)
# 先处理 msg 在 super记录日志时原因才准确
super().__init__(error=error, username=username, ip=ip, request=request)
class MFAFailedError(AuthFailedNeedLogMixin, AuthFailedError):
@@ -138,18 +139,11 @@ class ACLError(AuthFailedNeedLogMixin, AuthFailedError):
}
class LoginIPNotAllowed(ACLError):
class LoginACLIPAndTimePeriodNotAllowed(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)
super().__init__(_("Current IP and Time period is not allowed"), **kwargs)
class MFACodeRequiredError(AuthFailedError):

View File

@@ -69,10 +69,16 @@ class LoginConfirmWaitError(LoginConfirmBaseError):
class LoginConfirmOtherError(LoginConfirmBaseError):
error = 'login_confirm_error'
def __init__(self, ticket_id, status):
def __init__(self, ticket_id, status, username):
self.username = username
msg = const.login_confirm_error_msg.format(status)
super().__init__(ticket_id=ticket_id, msg=msg)
def as_data(self):
ret = super().as_data()
ret['data']['username'] = self.username
return ret
class PasswordTooSimple(NeedRedirectError):
default_code = 'passwd_too_simple'

View File

@@ -1,11 +1,16 @@
import base64
from django.shortcuts import redirect, reverse
from django.shortcuts import redirect, reverse, render
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse
from django.conf import settings
from django.utils.translation import ugettext as _
from django.contrib.auth import logout as auth_logout
from apps.authentication import mixins
from common.utils import gen_key_pair
from common.utils import get_request_ip
from .signals import post_auth_failed
class MFAMiddleware:
@@ -13,6 +18,7 @@ class MFAMiddleware:
这个 中间件 是用来全局拦截开启了 MFA 却没有认证的,如 OIDC, CAS使用第三方库做的登录直接 login 了,
所以只能在 Middleware 中控制
"""
def __init__(self, get_response):
self.get_response = get_response
@@ -42,6 +48,50 @@ class MFAMiddleware:
return redirect(url)
class ThirdPartyLoginMiddleware(mixins.AuthMixin):
"""OpenID、CAS、SAML2登录规则设置验证"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# 没有认证过,证明不是从 第三方 来的
if request.user.is_anonymous:
return response
if not request.session.get('auth_third_party_required'):
return response
ip = get_request_ip(request)
try:
self.request = request
self._check_login_acl(request.user, ip)
except Exception as e:
post_auth_failed.send(
sender=self.__class__, username=request.user.username,
request=self.request, reason=e.msg
)
auth_logout(request)
context = {
'title': _('Authentication failed'),
'message': _('Authentication failed (before login check failed): {}').format(e),
'interval': 10,
'redirect_url': reverse('authentication:login'),
'auto_redirect': True,
}
response = render(request, 'authentication/auth_fail_flash_message_standalone.html', context)
else:
if not self.request.session['auth_confirm_required']:
return response
guard_url = reverse('authentication:login-guard')
args = request.META.get('QUERY_STRING', '')
if args:
guard_url = "%s?%s" % (guard_url, args)
response = redirect(guard_url)
finally:
request.session.pop('auth_third_party_required', '')
return response
class SessionCookieMiddleware(MiddlewareMixin):
@staticmethod

View File

@@ -328,13 +328,59 @@ 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)
acl: LoginACL
if acl.is_action(acl.ActionChoices.allow):
return
if acl.is_action(acl.ActionChoices.reject):
raise errors.LoginACLIPAndTimePeriodNotAllowed(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.is_state(ticket.State.closed):
ticket = acl.create_confirm_ticket(self.request)
self.request.session['auth_ticket_id'] = str(ticket.id)
return ticket
def check_user_login_confirm(self):
ticket = self.get_ticket()
if not ticket:
raise errors.LoginConfirmOtherError('', "Not found")
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)
else:
# rejected, closed
ticket_id = ticket.id
status = ticket.get_state_display()
username = ticket.applicant.username
raise errors.LoginConfirmOtherError(ticket_id, status, username)
def get_ticket(self):
from tickets.models import ApplyLoginTicket
@@ -346,44 +392,6 @@ class AuthACLMixin:
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
return ticket
def get_ticket_or_create(self, confirm_setting):
ticket = self.get_ticket()
if not ticket or ticket.is_status(ticket.Status.closed):
ticket = confirm_setting.create_confirm_ticket(self.request)
self.request.session['auth_ticket_id'] = str(ticket.id)
return ticket
def check_user_login_confirm(self):
ticket = self.get_ticket()
if not ticket:
raise errors.LoginConfirmOtherError('', "Not found")
if ticket.is_status(ticket.Status.open):
raise errors.LoginConfirmWaitError(ticket.id)
elif ticket.is_state(ticket.State.approved):
self.request.session["auth_confirm"] = "1"
return
elif ticket.is_state(ticket.State.rejected):
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
elif ticket.is_state(ticket.State.closed):
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
else:
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_status_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()
class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPostCheckMixin):
request = None
@@ -482,7 +490,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

@@ -6,13 +6,8 @@ from django.core.cache import cache
from django.dispatch import receiver
from django_cas_ng.signals import cas_user_authenticated
from authentication.backends.oidc.signals import (
openid_user_login_failed, openid_user_login_success
)
from authentication.backends.saml2.signals import (
saml2_user_authenticated, saml2_user_authentication_failed
)
from .signals import post_auth_success, post_auth_failed
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
@receiver(user_logged_in)
@@ -25,7 +20,9 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
and user.mfa_enabled \
and not request.session.get('auth_mfa'):
request.session['auth_mfa_required'] = 1
if not request.session.get("auth_third_party_done") and \
request.session.get('auth_backend') in AUTHENTICATION_BACKENDS_THIRD_PARTY:
request.session['auth_third_party_required'] = 1
# 单点登录,超过了自动退出
if settings.USER_LOGIN_SINGLE_MACHINE_ENABLED:
lock_key = 'single_machine_login_' + str(user.id)
@@ -39,31 +36,19 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
request.session['auth_session_expiration_required'] = 1
@receiver(openid_user_login_success)
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
post_auth_success.send(sender, user=user, request=request)
@receiver(openid_user_login_failed)
def on_oidc_user_login_failed(sender, username, request, reason, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_OIDC_CODE
post_auth_failed.send(sender, username=username, request=request, reason=reason)
@receiver(cas_user_authenticated)
def on_cas_user_login_success(sender, request, user, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_CAS
post_auth_success.send(sender, user=user, request=request)
@receiver(saml2_user_authenticated)
def on_saml2_user_login_success(sender, request, user, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
@receiver(user_auth_success)
def on_user_login_success(sender, request, user, backend, create=False, **kwargs):
request.session['auth_backend'] = backend
post_auth_success.send(sender, user=user, request=request)
@receiver(saml2_user_authentication_failed)
def on_saml2_user_login_failed(sender, request, username, reason, **kwargs):
request.session['auth_backend'] = settings.AUTH_BACKEND_SAML2
@receiver(user_auth_failed)
def on_user_login_failed(sender, username, request, reason, backend, **kwargs):
request.session['auth_backend'] = backend
post_auth_failed.send(sender, username=username, request=request, reason=reason)

View File

@@ -3,3 +3,7 @@ from django.dispatch import Signal
post_auth_success = Signal(providing_args=('user', 'request'))
post_auth_failed = Signal(providing_args=('username', 'request', 'reason'))
user_auth_success = Signal(providing_args=('user', 'request', 'backend', 'create'))
user_auth_failed = Signal(providing_args=('username', 'request', 'reason', 'backend'))

View File

@@ -0,0 +1,70 @@
{% extends '_base_only_content.html' %}
{% load static %}
{% load i18n %}
{% block html_title %} {{ title }} {% endblock %}
{% block title %} {{ title }}{% endblock %}
{% block content %}
<style>
.alert.alert-msg {
background: #F5F5F7;
}
</style>
<div>
<p>
<div class="alert alert-msg" id="messages">
{% if error %}
{{ error }}
{% else %}
{{ message|safe }}
{% endif %}
</div>
</p>
<div class="row">
{% if has_cancel %}
<div class="col-sm-3">
<a href="{{ cancel_url }}" class="btn btn-default block full-width m-b">
{% trans 'Cancel' %}
</a>
</div>
{% endif %}
<div class="col-sm-3">
<a href="{{ redirect_url }}" class="btn btn-primary block full-width m-b">
{% if confirm_button %}
{{ confirm_button }}
{% else %}
{% trans 'Confirm' %}
{% endif %}
</a>
</div>
</div>
</div>
{% endblock %}
{% block custom_foot_js %}
<script>
var message = ''
var time = '{{ interval }}'
{% if error %}
message = '{{ error }}'
{% else %}
message = '{{ message|safe }}'
{% endif %}
function redirect_page() {
if (time >= 0) {
var msg = message + ' <b>' + time + '</b> ...';
$('#messages').html(msg);
time--;
setTimeout(redirect_page, 1000);
} else {
window.location.href = "{{ redirect_url }}";
}
}
{% if auto_redirect %}
window.onload = redirect_page;
{% endif %}
</script>
{% endblock %}

View File

@@ -79,6 +79,9 @@ function doRequestAuth() {
requestApi({
url: url,
method: "GET",
headers: {
"X-JMS-LOGIN-TYPE": "W"
},
success: function (data) {
if (!data.error && data.msg === 'ok') {
window.onbeforeunload = function(){};
@@ -98,7 +101,7 @@ function doRequestAuth() {
},
error: function (text, data) {
},
flash_message: false
flash_message: false, // 是否显示flash消息
})
}
function initClipboard() {

View File

@@ -56,9 +56,11 @@ urlpatterns = [
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
name='user-otp-disable'),
# openid
# other authentication protocol
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),
path('openid/', include(('authentication.backends.oidc.urls', 'authentication'), namespace='openid')),
path('saml2/', include(('authentication.backends.saml2.urls', 'authentication'), namespace='saml2')),
path('oauth2/', include(('authentication.backends.oauth2.urls', 'authentication'), namespace='oauth2')),
path('captcha/', include('captcha.urls')),
]

View File

@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
#
import ipaddress
from urllib.parse import urljoin, urlparse
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from common.utils import validate_ip, get_ip_city, get_request_ip
from common.utils import get_logger
@@ -22,10 +25,34 @@ def check_different_city_login_if_need(user, request):
else:
city = get_ip_city(ip) or DEFAULT_CITY
city_white = ['LAN', ]
if city not in city_white:
city_white = [_('LAN'), 'LAN']
is_private = ipaddress.ip_address(ip).is_private
if not is_private:
last_user_login = UserLoginLog.objects.exclude(city__in=city_white) \
.filter(username=user.username, status=True).first()
if last_user_login and last_user_login.city != city:
DifferentCityLoginMessage(user, ip, city).publish_async()
def build_absolute_uri(request, path=None):
""" Build absolute redirect """
if path is None:
path = '/'
site_url = urlparse(settings.SITE_URL)
scheme = site_url.scheme or request.scheme
host = request.get_host()
url = f'{scheme}://{host}'
redirect_uri = urljoin(url, path)
return redirect_uri
def build_absolute_uri_for_oidc(request, path=None):
""" Build absolute redirect uri for OIDC """
if path is None:
path = '/'
if settings.BASE_SITE_URL:
# OIDC 专用配置项
redirect_uri = urljoin(settings.BASE_SITE_URL, path)
return redirect_uri
return build_absolute_uri(request, path=path)

View File

@@ -21,7 +21,7 @@ from django.conf import settings
from django.urls import reverse_lazy
from django.contrib.auth import BACKEND_SESSION_KEY
from common.utils import FlashMessageUtil
from common.utils import FlashMessageUtil, static_or_direct
from users.utils import (
redirect_user_first_login_or_index
)
@@ -39,8 +39,7 @@ class UserLoginContextMixin:
get_user_mfa_context: Callable
request: HttpRequest
@staticmethod
def get_support_auth_methods():
def get_support_auth_methods(self):
auth_methods = [
{
'name': 'OpenID',
@@ -63,6 +62,13 @@ class UserLoginContextMixin:
'logo': static('img/login_saml2_logo.png'),
'auto_redirect': True
},
{
'name': settings.AUTH_OAUTH2_PROVIDER,
'enabled': settings.AUTH_OAUTH2,
'url': reverse('authentication:oauth2:login'),
'logo': static_or_direct(settings.AUTH_OAUTH2_LOGO_PATH),
'auto_redirect': True
},
{
'name': _('WeCom'),
'enabled': settings.AUTH_WECOM,
@@ -324,6 +330,8 @@ class UserLogoutView(TemplateView):
return settings.CAS_LOGOUT_URL_NAME
elif 'saml2' in backend:
return settings.SAML2_LOGOUT_URL_NAME
elif 'oauth2' in backend:
return settings.AUTH_OAUTH2_LOGOUT_URL_NAME
return None
def get(self, request, *args, **kwargs):

View File

@@ -10,5 +10,5 @@ celery_task_pre_key = "CELERY_"
KEY_CACHE_RESOURCE_IDS = "RESOURCE_IDS_{}"
# AD User AccountDisable
# https://blog.csdn.net/bytxl/article/details/17763975
# https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties
LDAP_AD_ACCOUNT_DISABLE = 2

View File

@@ -67,7 +67,7 @@ class SimpleMetadataWithFilters(SimpleMetadata):
default = getattr(field, 'default', None)
if default is not None and default != empty:
if isinstance(default, (str, int, bool, datetime.datetime, list)):
if isinstance(default, (str, int, bool, float, datetime.datetime, list)):
field_info['default'] = default
for attr in self.attrs:

View File

@@ -0,0 +1 @@
from .sm3 import PBKDF2SM3PasswordHasher

View File

@@ -0,0 +1,23 @@
from gmssl import sm3, func
from django.contrib.auth.hashers import PBKDF2PasswordHasher
class Hasher:
name = 'sm3'
def __init__(self, key):
self.key = key
def hexdigest(self):
return sm3.sm3_hash(func.bytes_to_list(self.key))
@staticmethod
def hash(msg):
return Hasher(msg)
class PBKDF2SM3PasswordHasher(PBKDF2PasswordHasher):
algorithm = "pbkdf2_sm3"
digest = Hasher.hash

View File

@@ -30,7 +30,9 @@ class CeleryBaseService(BaseService):
'-l', 'INFO',
'-c', str(self.num),
'-Q', self.queue,
'-n', f'{self.queue}@{server_hostname}'
'--heartbeat-interval', '10',
'-n', f'{self.queue}@{server_hostname}',
'--without-mingle',
]
return cmd

View File

@@ -19,11 +19,13 @@ class FlowerService(BaseService):
'celery',
'-A', 'ops',
'flower',
'-l', 'INFO',
'-logging=info',
'--url_prefix=/core/flower',
'--auto_refresh=False',
'--max_tasks=1000',
'--tasks_columns=uuid,name,args,state,received,started,runtime,worker'
'--persistent=True',
'-db=/opt/jumpserver/data/flower.db',
'--state_save_interval=600000'
]
return cmd

View File

@@ -62,15 +62,22 @@ class UserConfirmation(permissions.BasePermission):
confirm_level = request.session.get('CONFIRM_LEVEL')
confirm_time = request.session.get('CONFIRM_TIME')
ttl = self.get_ttl()
if not confirm_level or not confirm_time or \
confirm_level < self.min_level or \
confirm_time < time.time() - self.ttl:
confirm_time < time.time() - ttl:
raise UserConfirmRequired(code=self.confirm_type)
return True
def get_ttl(self):
if self.confirm_type == ConfirmType.MFA:
ttl = settings.SECURITY_MFA_VERIFY_TTL
else:
ttl = self.ttl
return ttl
@classmethod
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=300):
def require(cls, confirm_type=ConfirmType.ReLogin, ttl=60 * 5):
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

View File

@@ -0,0 +1,7 @@
from .device import Device
def open_piico_device(driver_path) -> Device:
d = Device()
d.open(driver_path)
return d

View File

@@ -0,0 +1,59 @@
cipher_alg_id = {
"sm4_ebc": 0x00000401,
"sm4_cbc": 0x00000402,
}
class ECCCipher:
def __init__(self, session, public_key, private_key):
self._session = session
self.public_key = public_key
self.private_key = private_key
def encrypt(self, plain_text):
return self._session.ecc_encrypt(self.public_key, plain_text, 0x00020800)
def decrypt(self, cipher_text):
return self._session.ecc_decrypt(self.private_key, cipher_text, 0x00020800)
class EBCCipher:
def __init__(self, session, key_val):
self._session = session
self._key = self.__get_key(key_val)
self._alg = "sm4_ebc"
self._iv = None
def __get_key(self, key_val):
key_val = self.__padding(key_val)
return self._session.import_key(key_val)
@staticmethod
def __padding(val):
# padding
val = bytes(val)
while len(val) == 0 or len(val) % 16 != 0:
val += b'\0'
return val
def encrypt(self, plain_text):
plain_text = self.__padding(plain_text)
cipher_text = self._session.encrypt(plain_text, self._key, cipher_alg_id[self._alg], self._iv)
return bytes(cipher_text)
def decrypt(self, cipher_text):
plain_text = self._session.decrypt(cipher_text, self._key, cipher_alg_id[self._alg], self._iv)
return bytes(plain_text)
def destroy(self):
self._session.destroy_cipher_key(self._key)
self._session.close()
class CBCCipher(EBCCipher):
def __init__(self, session, key, iv):
super().__init__(session, key)
self._iv = iv
self._alg = "sm4_cbc"

View File

@@ -0,0 +1,70 @@
from ctypes import *
from .exception import PiicoError
from .session import Session
from .cipher import *
from .digest import *
class Device:
_driver = None
__device = None
def open(self, driver_path="./libpiico_ccmu.so"):
# load driver
self.__load_driver(driver_path)
# open device
self.__open_device()
def close(self):
if self.__device is None:
raise Exception("device not turned on")
ret = self._driver.SDF_CloseDevice(self.__device)
if ret != 0:
raise Exception("turn off device failed")
self.__device = None
def new_session(self):
session = c_void_p()
ret = self._driver.SDF_OpenSession(self.__device, pointer(session))
if ret != 0:
raise Exception("create session failed")
return Session(self._driver, session)
def generate_ecc_key_pair(self):
session = self.new_session()
return session.generate_ecc_key_pair(alg_id=0x00020200)
def generate_random(self, length=64):
session = self.new_session()
return session.generate_random(length)
def new_sm2_ecc_cipher(self, public_key, private_key):
session = self.new_session()
return ECCCipher(session, public_key, private_key)
def new_sm4_ebc_cipher(self, key_val):
session = self.new_session()
return EBCCipher(session, key_val)
def new_sm4_cbc_cipher(self, key_val, iv):
session = self.new_session()
return CBCCipher(session, key_val, iv)
def new_digest(self, mode="sm3"):
session = self.new_session()
return Digest(session, mode)
def __load_driver(self, path):
# check driver status
if self._driver is not None:
raise Exception("already load driver")
# load driver
self._driver = cdll.LoadLibrary(path)
def __open_device(self):
device = c_void_p()
ret = self._driver.SDF_OpenDevice(pointer(device))
if ret != 0:
raise PiicoError("open piico device failed", ret)
self.__device = device

View File

@@ -0,0 +1,32 @@
hash_alg_id = {
"sm3": 0x00000001,
"sha1": 0x00000002,
"sha256": 0x00000004,
"sha512": 0x00000008,
}
class Digest:
def __init__(self, session, alg_name="sm3"):
if hash_alg_id[alg_name] is None:
raise Exception("unsupported hash alg {}".format(alg_name))
self._alg_name = alg_name
self._session = session
self.__init_hash()
def __init_hash(self):
self._session.hash_init(hash_alg_id[self._alg_name])
def update(self, data):
self._session.hash_update(data)
def final(self):
return self._session.hash_final()
def reset(self):
self.__init_hash()
def destroy(self):
self._session.close()

View File

@@ -0,0 +1,71 @@
from ctypes import *
ECCref_MAX_BITS = 512
ECCref_MAX_LEN = int((ECCref_MAX_BITS + 7) / 8)
class EncodeMixin:
def encode(self):
raise NotImplementedError
class ECCrefPublicKey(Structure, EncodeMixin):
_fields_ = [
('bits', c_uint),
('x', c_ubyte * ECCref_MAX_LEN),
('y', c_ubyte * ECCref_MAX_LEN),
]
def encode(self):
return bytes([0x04]) + bytes(self.x[32:]) + bytes(self.y[32:])
class ECCrefPrivateKey(Structure, EncodeMixin):
_fields_ = [
('bits', c_uint,),
('K', c_ubyte * ECCref_MAX_LEN),
]
def encode(self):
return bytes(self.K[32:])
class ECCCipherEncode(EncodeMixin):
def __init__(self):
self.x = None
self.y = None
self.M = None
self.C = None
self.L = None
def encode(self):
c1 = bytes(self.x[32:]) + bytes(self.y[32:])
c2 = bytes(self.C[:self.L])
c3 = bytes(self.M)
return bytes([0x04]) + c1 + c2 + c3
def new_ecc_cipher_cla(length):
_cache = {}
cla_name = "ECCCipher{}".format(length)
if _cache.__contains__(cla_name):
return _cache[cla_name]
else:
cla = type(cla_name, (Structure, ECCCipherEncode), {
"_fields_": [
('x', c_ubyte * ECCref_MAX_LEN),
('y', c_ubyte * ECCref_MAX_LEN),
('M', c_ubyte * 32),
('L', c_uint),
('C', c_ubyte * length)
]
})
_cache[cla_name] = cla
return cla
class ECCKeyPair:
def __init__(self, public_key, private_key):
self.public_key = public_key
self.private_key = private_key

View File

@@ -0,0 +1,12 @@
class PiicoError(Exception):
def __init__(self, msg, ret):
super().__init__(self)
self.__ret = ret
self.__msg = msg
def __str__(self):
return "piico error: {} return code: {}".format(self.__msg, self.hex_ret(self.__ret))
@staticmethod
def hex_ret(ret):
return hex(ret & ((1 << 32) - 1))

View File

@@ -0,0 +1,36 @@
from ctypes import *
from .ecc import ECCrefPublicKey, ECCrefPrivateKey, ECCKeyPair
from .exception import PiicoError
from .session_mixin import SM3Mixin, SM4Mixin, SM2Mixin
class Session(SM2Mixin, SM3Mixin, SM4Mixin):
def __init__(self, driver, session):
super().__init__()
self._session = session
self._driver = driver
def get_device_info(self):
pass
def generate_random(self, length=64):
random_data = (c_ubyte * length)()
ret = self._driver.SDF_GenerateRandom(self._session, c_int(length), random_data)
if ret != 0:
raise PiicoError("generate random error", ret)
return bytes(random_data)
def generate_ecc_key_pair(self, alg_id):
public_key = ECCrefPublicKey()
private_key = ECCrefPrivateKey()
ret = self._driver.SDF_GenerateKeyPair_ECC(self._session, c_int(alg_id), c_int(256), pointer(public_key),
pointer(private_key))
if ret != 0:
raise PiicoError("generate ecc key pair failed", ret)
return ECCKeyPair(public_key.encode(), private_key.encode())
def close(self):
ret = self._driver.SDF_CloseSession(self._session)
if ret != 0:
raise PiicoError("close session failed", ret)

View File

@@ -0,0 +1,129 @@
from .ecc import *
from .exception import PiicoError
class BaseMixin:
def __init__(self):
self._driver = None
self._session = None
class SM2Mixin(BaseMixin):
def ecc_encrypt(self, public_key, plain_text, alg_id):
pos = 1
k1 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
k1 = (c_ubyte * len(k1))(*k1)
pos += 32
k2 = bytes([0] * 32) + bytes(public_key[pos:pos + 32])
pk = ECCrefPublicKey(c_uint(0x40), (c_ubyte * len(k1))(*k1), (c_ubyte * len(k2))(*k2))
plain_text = (c_ubyte * len(plain_text))(*plain_text)
ecc_data = new_ecc_cipher_cla(len(plain_text))()
ret = self._driver.SDF_ExternalEncrypt_ECC(self._session, c_int(alg_id), pointer(pk), plain_text,
c_int(len(plain_text)), pointer(ecc_data))
if ret != 0:
raise Exception("ecc encrypt failed", ret)
return ecc_data.encode()
def ecc_decrypt(self, private_key, cipher_text, alg_id):
k = bytes([0] * 32) + bytes(private_key[:32])
vk = ECCrefPrivateKey(c_uint(0x40), (c_ubyte * len(k))(*k))
pos = 1
# c1
x = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
pos += 32
y = bytes([0] * 32) + bytes(cipher_text[pos:pos + 32])
pos += 32
# c2
c = bytes(cipher_text[pos:-32])
l = len(c)
# c3
m = bytes(cipher_text[-32:])
ecc_data = new_ecc_cipher_cla(l)(
(c_ubyte * 64)(*x),
(c_ubyte * 64)(*y),
(c_ubyte * 32)(*m),
c_uint(l),
(c_ubyte * l)(*c),
)
temp_data = (c_ubyte * l)()
temp_data_length = c_int()
ret = self._driver.SDF_ExternalDecrypt_ECC(self._session, c_int(alg_id), pointer(vk),
pointer(ecc_data),
temp_data, pointer(temp_data_length))
if ret != 0:
raise Exception("ecc decrypt failed", ret)
return bytes(temp_data[:temp_data_length.value])
class SM3Mixin(BaseMixin):
def hash_init(self, alg_id):
ret = self._driver.SDF_HashInit(self._session, c_int(alg_id), None, None, c_int(0))
if ret != 0:
raise PiicoError("hash init failed,alg id is {}".format(alg_id), ret)
def hash_update(self, data):
data = (c_ubyte * len(data))(*data)
ret = self._driver.SDF_HashUpdate(self._session, data, c_int(len(data)))
if ret != 0:
raise PiicoError("hash update failed", ret)
def hash_final(self):
result_data = (c_ubyte * 32)()
result_length = c_int()
ret = self._driver.SDF_HashFinal(self._session, result_data, pointer(result_length))
if ret != 0:
raise PiicoError("hash final failed", ret)
return bytes(result_data[:result_length.value])
class SM4Mixin(BaseMixin):
def import_key(self, key_val):
# to c lang
key_val = (c_ubyte * len(key_val))(*key_val)
key = c_void_p()
ret = self._driver.SDF_ImportKey(self._session, key_val, c_int(len(key_val)), pointer(key))
if ret != 0:
raise PiicoError("import key failed", ret)
return key
def destroy_cipher_key(self, key):
ret = self._driver.SDF_DestroyKey(self._session, key)
if ret != 0:
raise Exception("destroy key failed")
def encrypt(self, plain_text, key, alg, iv=None):
return self.__do_cipher_action(plain_text, key, alg, iv, True)
def decrypt(self, cipher_text, key, alg, iv=None):
return self.__do_cipher_action(cipher_text, key, alg, iv, False)
def __do_cipher_action(self, text, key, alg, iv=None, encrypt=True):
text = (c_ubyte * len(text))(*text)
if iv is not None:
iv = (c_ubyte * len(iv))(*iv)
temp_data = (c_ubyte * len(text))()
temp_data_length = c_int()
if encrypt:
ret = self._driver.SDF_Encrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
pointer(temp_data_length))
if ret != 0:
raise PiicoError("encrypt failed", ret)
else:
ret = self._driver.SDF_Decrypt(self._session, key, c_int(alg), iv, text, c_int(len(text)), temp_data,
pointer(temp_data_length))
if ret != 0:
raise PiicoError("decrypt failed", ret)
return temp_data[:temp_data_length.value]

View File

View File

@@ -17,4 +17,8 @@ class BaseSMSClient:
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
raise NotImplementedError
@staticmethod
def need_pre_check():
return True

View File

@@ -0,0 +1,329 @@
import hashlib
import socket
import struct
import time
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from common.utils import get_logger
from common.exceptions import JMSException
from .base import BaseSMSClient
logger = get_logger(__file__)
CMPP_CONNECT = 0x00000001 # 请求连接
CMPP_CONNECT_RESP = 0x80000001 # 请求连接应答
CMPP_TERMINATE = 0x00000002 # 终止连接
CMPP_TERMINATE_RESP = 0x80000002 # 终止连接应答
CMPP_SUBMIT = 0x00000004 # 提交短信
CMPP_SUBMIT_RESP = 0x80000004 # 提交短信应答
CMPP_DELIVER = 0x00000005 # 短信下发
CMPP_DELIVER_RESP = 0x80000005 # 下发短信应答
class CMPPBaseRequestInstance(object):
def __init__(self):
self.command_id = ''
self.body = b''
self.length = 0
def get_header(self, sequence_id):
length = struct.pack('!L', 12 + self.length)
command_id = struct.pack('!L', self.command_id)
sequence_id = struct.pack('!L', sequence_id)
return length + command_id + sequence_id
def get_message(self, sequence_id):
return self.get_header(sequence_id) + self.body
class CMPPConnectRequestInstance(CMPPBaseRequestInstance):
def __init__(self, sp_id, sp_secret):
if len(sp_id) != 6:
raise ValueError(_("sp_id is 6 bits"))
super().__init__()
source_addr = sp_id.encode('utf-8')
sp_secret = sp_secret.encode('utf-8')
version = struct.pack('!B', 0x02)
timestamp = struct.pack('!L', int(self.get_now()))
authenticator_source = source_addr + 9 * b'\x00' + sp_secret + self.get_now().encode('utf-8')
auth_source_md5 = hashlib.md5(authenticator_source).digest()
self.body = source_addr + auth_source_md5 + version + timestamp
self.length = len(self.body)
self.command_id = CMPP_CONNECT
@staticmethod
def get_now():
return time.strftime('%m%d%H%M%S', time.localtime(time.time()))
class CMPPSubmitRequestInstance(CMPPBaseRequestInstance):
def __init__(self, msg_src, dest_terminal_id, msg_content, src_id,
service_id='', dest_usr_tl=1):
if len(msg_content) >= 70:
raise JMSException('The message length should be within 70 characters')
if len(dest_terminal_id) > 100:
raise JMSException('The number of users receiving information should be less than 100')
super().__init__()
msg_id = 8 * b'\x00'
pk_total = struct.pack('!B', 1)
pk_number = struct.pack('!B', 1)
registered_delivery = struct.pack('!B', 0)
msg_level = struct.pack('!B', 0)
service_id = ((10 - len(service_id)) * '\x00' + service_id).encode('utf-8')
fee_user_type = struct.pack('!B', 2)
fee_terminal_id = ('0' * 21).encode('utf-8')
tp_pid = struct.pack('!B', 0)
tp_udhi = struct.pack('!B', 0)
msg_fmt = struct.pack('!B', 8)
fee_type = '01'.encode('utf-8')
fee_code = '000000'.encode('utf-8')
valid_time = ('\x00' * 17).encode('utf-8')
at_time = ('\x00' * 17).encode('utf-8')
src_id = ((21 - len(src_id)) * '\x00' + src_id).encode('utf-8')
reserve = b'\x00' * 8
_msg_length = struct.pack('!B', len(msg_content) * 2)
_msg_src = msg_src.encode('utf-8')
_dest_usr_tl = struct.pack('!B', dest_usr_tl)
_msg_content = msg_content.encode('utf-16-be')
_dest_terminal_id = b''.join([
(i + (21 - len(i)) * '\x00').encode('utf-8') for i in dest_terminal_id
])
self.length = 126 + 21 * dest_usr_tl + len(_msg_content)
self.command_id = CMPP_SUBMIT
self.body = msg_id + pk_total + pk_number + registered_delivery \
+ msg_level + service_id + fee_user_type + fee_terminal_id \
+ tp_pid + tp_udhi + msg_fmt + _msg_src + fee_type + fee_code \
+ valid_time + at_time + src_id + _dest_usr_tl + _dest_terminal_id \
+ _msg_length + _msg_content + reserve
class CMPPTerminateRequestInstance(CMPPBaseRequestInstance):
def __init__(self):
super().__init__()
self.body = b''
self.command_id = CMPP_TERMINATE
class CMPPDeliverRespRequestInstance(CMPPBaseRequestInstance):
def __init__(self, msg_id, result=0):
super().__init__()
msg_id = struct.pack('!Q', msg_id)
result = struct.pack('!B', result)
self.length = len(self.body)
self.body = msg_id + result
class CMPPResponseInstance(object):
def __init__(self):
self.command_id = None
self.length = None
self.response_handler_map = {
CMPP_CONNECT_RESP: self.connect_response_parse,
CMPP_SUBMIT_RESP: self.submit_response_parse,
CMPP_DELIVER: self.deliver_request_parse,
}
@staticmethod
def connect_response_parse(body):
status, = struct.unpack('!B', body[0:1])
authenticator_ISMG = body[1:17]
version, = struct.unpack('!B', body[17:18])
return {
'Status': status,
'AuthenticatorISMG': authenticator_ISMG,
'Version': version
}
@staticmethod
def submit_response_parse(body):
msg_id = body[:8]
result = struct.unpack('!B', body[8:9])
return {
'Msg_Id': msg_id, 'Result': result[0]
}
@staticmethod
def deliver_request_parse(body):
msg_id, = struct.unpack('!Q', body[0:8])
dest_id = body[8:29]
service_id = body[29:39]
tp_pid = struct.unpack('!B', body[39:40])
tp_udhi = struct.unpack('!B', body[40:41])
msg_fmt = struct.unpack('!B', body[41:42])
src_terminal_id = body[42:63]
registered_delivery = struct.unpack('!B', body[63:64])
msg_length = struct.unpack('!B', body[64:65])
msg_content = body[65:msg_length[0]+65]
return {
'Msg_Id': msg_id, 'Dest_Id': dest_id, 'Service_Id': service_id,
'TP_pid': tp_pid, 'TP_udhi': tp_udhi, 'Msg_Fmt': msg_fmt,
'Src_terminal_Id': src_terminal_id, 'Registered_Delivery': registered_delivery,
'Msg_Length': msg_length, 'Msg_content': msg_content
}
def parse_header(self, data):
self.command_id, = struct.unpack('!L', data[4:8])
sequence_id, = struct.unpack('!L', data[8:12])
return {
'length': self.length,
'command_id': hex(self.command_id),
'sequence_id': sequence_id
}
def parse_body(self, body):
response_body_func = self.response_handler_map.get(self.command_id)
if response_body_func is None:
raise JMSException('Unable to parse the returned result: %s' % body)
return response_body_func(body)
def parse(self, data):
self.length, = struct.unpack('!L', data[0:4])
header = self.parse_header(data)
body = self.parse_body(data[12:self.length])
return header, body
class CMPPClient(object):
def __init__(self, host, port, sp_id, sp_secret, src_id, service_id):
self.ip = host
self.port = port
self.sp_id = sp_id
self.sp_secret = sp_secret
self.src_id = src_id
self.service_id = service_id
self._sequence_id = 0
self._is_connect = False
self._times = 3
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._connect()
@property
def sequence_id(self):
s = self._sequence_id
self._sequence_id += 1
return s
def _connect(self):
self.__socket.settimeout(5)
error_msg = _('Failed to connect to the CMPP gateway server, err: {}')
for i in range(self._times):
try:
self.__socket.connect((self.ip, self.port))
except Exception as err:
error_msg = error_msg.format(str(err))
logger.warning(error_msg)
time.sleep(1)
else:
self._is_connect = True
break
else:
raise JMSException(error_msg)
def send(self, instance):
if isinstance(instance, CMPPBaseRequestInstance):
message = instance.get_message(sequence_id=self.sequence_id)
else:
message = instance
self.__socket.send(message)
def recv(self):
raw_length = self.__socket.recv(4)
length, = struct.unpack('!L', raw_length)
header, body = CMPPResponseInstance().parse(
raw_length + self.__socket.recv(length - 4)
)
return header, body
def close(self):
if self._is_connect:
terminate_request = CMPPTerminateRequestInstance()
self.send(terminate_request)
self.__socket.close()
def _cmpp_connect(self):
connect_request = CMPPConnectRequestInstance(self.sp_id, self.sp_secret)
self.send(connect_request)
header, body = self.recv()
if body['Status'] != 0:
raise JMSException('CMPPv2.0 authentication failed: %s' % body)
def _cmpp_send_sms(self, dest, sign_name, template_code, template_param):
"""
优先发送template_param中message的信息
若该内容不存在则根据template_code构建验证码发送
"""
message = template_param.get('message')
if message is None:
code = template_param.get('code')
message = template_code.replace('{code}', code)
msg = '%s%s' % (sign_name, message)
submit_request = CMPPSubmitRequestInstance(
msg_src=self.sp_id, src_id=self.src_id, msg_content=msg,
dest_usr_tl=len(dest), dest_terminal_id=dest,
service_id=self.service_id
)
self.send(submit_request)
header, body = self.recv()
command_id = header.get('command_id')
if command_id == CMPP_DELIVER:
deliver_request = CMPPDeliverRespRequestInstance(
msg_id=body['Msg_Id'], result=body['Result']
)
self.send(deliver_request)
def send_sms(self, dest, sign_name, template_code, template_param):
try:
self._cmpp_connect()
self._cmpp_send_sms(dest, sign_name, template_code, template_param)
except Exception as e:
logger.error('CMPPv2.0 Error: %s', e)
self.close()
raise JMSException(e)
class CMPP2SMS(BaseSMSClient):
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'CMPP2'
@classmethod
def new_from_settings(cls):
return cls(
host=settings.CMPP2_HOST, port=settings.CMPP2_PORT,
sp_id=settings.CMPP2_SP_ID, sp_secret=settings.CMPP2_SP_SECRET,
service_id=settings.CMPP2_SERVICE_ID, src_id=getattr(settings, 'CMPP2_SRC_ID', ''),
)
def __init__(self, host: str, port: int, sp_id: str, sp_secret: str, service_id: str, src_id=''):
try:
self.client = CMPPClient(
host=host, port=port, sp_id=sp_id, sp_secret=sp_secret, src_id=src_id, service_id=service_id
)
except Exception as err:
self.client = None
logger.warning(err)
raise JMSException(err)
@staticmethod
def need_pre_check():
return False
def send_sms(self, phone_numbers: list, sign_name: str, template_code: str, template_param: dict, **kwargs):
try:
logger.info(f'CMPPv2.0 sms send: '
f'phone_numbers={phone_numbers} '
f'sign_name={sign_name} '
f'template_code={template_code} '
f'template_param={template_param}')
self.client.send_sms(phone_numbers, sign_name, template_code, template_param)
except Exception as e:
raise JMSException(e)
client = CMPP2SMS

View File

@@ -15,6 +15,8 @@ logger = get_logger(__name__)
class BACKENDS(TextChoices):
ALIBABA = 'alibaba', _('Alibaba cloud')
TENCENT = 'tencent', _('Tencent cloud')
HUAWEI = 'huawei', _('Huawei Cloud')
CMPP2 = 'cmpp2', _('CMPP v2.0')
class SMS:
@@ -43,7 +45,7 @@ class SMS:
sign_name = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_SIGN_NAME')
template_code = getattr(settings, f'{self.client.SIGN_AND_TMPL_SETTING_FIELD_PREFIX}_VERIFY_TEMPLATE_CODE')
if not (sign_name and template_code):
if self.client.need_pre_check() and not (sign_name and template_code):
raise JMSException(
code='verify_code_sign_tmpl_invalid',
detail=_('SMS verification code signature or template invalid')

View File

@@ -0,0 +1,94 @@
import base64
import hashlib
import time
import uuid
import requests
from collections import OrderedDict
from django.conf import settings
from common.exceptions import JMSException
from common.utils import get_logger
from .base import BaseSMSClient
logger = get_logger(__file__)
class HuaweiClient:
def __init__(self, app_key, app_secret, url, sign_channel_num):
self.url = url[:-1] if url.endswith('/') else url
self.app_key = app_key
self.app_secret = app_secret
self.sign_channel_num = sign_channel_num
def build_wsse_header(self):
now = time.strftime('%Y-%m-%dT%H:%M:%SZ')
nonce = str(uuid.uuid4()).replace('-', '')
digest = hashlib.sha256((nonce + now + self.app_secret).encode()).hexdigest()
digestBase64 = base64.b64encode(digest.encode()).decode()
formatter = 'UsernameToken Username="{}",PasswordDigest="{}",Nonce="{}",Created="{}"'
return formatter.format(self.app_key, digestBase64, nonce, now)
def send_sms(self, receiver, signature, template_id, template_param):
sms_url = '%s/%s' % (self.url, 'sms/batchSendSms/v1')
headers = {
'Authorization': 'WSSE realm="SDP",profile="UsernameToken",type="Appkey"',
'X-WSSE': self.build_wsse_header()
}
body = {
'from': self.sign_channel_num, 'to': receiver, 'templateId': template_id,
'templateParas': template_param, 'signature': signature
}
try:
response = requests.post(sms_url, headers=headers, data=body)
msg = response.json()
except Exception as error:
raise JMSException(code='response_bad', detail=error)
return msg
class HuaweiSMS(BaseSMSClient):
SIGN_AND_TMPL_SETTING_FIELD_PREFIX = 'HUAWEI'
@classmethod
def new_from_settings(cls):
return cls(
app_key=settings.HUAWEI_APP_KEY,
app_secret=settings.HUAWEI_APP_SECRET,
url=settings.HUAWEI_SMS_ENDPOINT,
sign_channel_num=settings.HUAWEI_SIGN_CHANNEL_NUM
)
def __init__(self, app_key: str, app_secret: str, url: str, sign_channel_num: str):
self.client = HuaweiClient(app_key, app_secret, url, sign_channel_num)
def send_sms(
self, phone_numbers: list, sign_name: str, template_code: str,
template_param: OrderedDict, **kwargs
):
phone_numbers_str = ','.join(phone_numbers)
template_param = '["%s"]' % template_param.get('code')
req_params = {
'receiver': phone_numbers_str, 'signature': sign_name,
'template_id': template_code, 'template_param': template_param
}
try:
logger.info(f'Huawei sms send: '
f'phone_numbers={phone_numbers} '
f'sign_name={sign_name} '
f'template_code={template_code} '
f'template_param={template_param}')
resp_msg = self.client.send_sms(**req_params)
except Exception as error:
raise JMSException(code='response_bad', detail=error)
if resp_msg.get('code') != '000000':
raise JMSException(code='response_bad', detail=resp_msg)
return resp_msg
client = HuaweiSMS

View File

@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
#
import re
import socket
from django.templatetags.static import static
from collections import OrderedDict
from itertools import chain
import logging
@@ -365,3 +367,30 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
def group_by_count(it, count):
return [it[i:i+count] for i in range(0, len(it), count)]
def test_ip_connectivity(host, port, timeout=0.5):
"""
timeout: seconds
"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((host, int(port)))
sock.close()
if result == 0:
connectivity = True
else:
connectivity = False
return connectivity
def static_or_direct(logo_path):
if logo_path.startswith('img/'):
return static(logo_path)
else:
return logo_path
def make_dirs(name, mode=0o755, exist_ok=False):
""" 默认权限设置为 0o755 """
return os.makedirs(name, mode=mode, exist_ok=exist_ok)

View File

@@ -53,7 +53,9 @@ class Subscription:
error(msg, item)
logger.error('Subscribe handler handle msg error: {}'.format(e))
except Exception as e:
logger.error('Consume msg error: {}'.format(e))
# 正常的 websocket 断开时, redis 会断开连接,避免日志太多
# logger.error('Consume msg error: {}'.format(e))
pass
try:
complete()

View File

@@ -1,31 +1,38 @@
import base64
import logging
import re
from Cryptodome.Cipher import AES, PKCS1_v1_5
from Cryptodome.Util.Padding import pad
from Cryptodome.Random import get_random_bytes
from Cryptodome.PublicKey import RSA
from Cryptodome.Util.Padding import pad
from Cryptodome import Random
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from common.sdk.gm import piico
def process_key(key):
secret_pattern = re.compile(r'password|secret|key|token', re.IGNORECASE)
def padding_key(key, max_length=32):
"""
返回32 bytes 的key
"""
if not isinstance(key, bytes):
key = bytes(key, encoding='utf-8')
if len(key) >= 32:
return key[:32]
if len(key) >= max_length:
return key[:max_length]
return pad(key, 32)
while len(key) % 16 != 0:
key += b'\0'
return key
class BaseCrypto:
def encrypt(self, text):
return base64.urlsafe_b64encode(
self._encrypt(bytes(text, encoding='utf8'))
@@ -45,7 +52,7 @@ class BaseCrypto:
class GMSM4EcbCrypto(BaseCrypto):
def __init__(self, key):
self.key = process_key(key)
self.key = padding_key(key, 16)
self.sm4_encryptor = CryptSM4()
self.sm4_encryptor.set_key(self.key, SM4_ENCRYPT)
@@ -59,6 +66,26 @@ class GMSM4EcbCrypto(BaseCrypto):
return self.sm4_decryptor.crypt_ecb(data)
class PiicoSM4EcbCrypto(BaseCrypto):
@staticmethod
def to_16(key):
while len(key) % 16 != 0:
key += b'\0'
return key # 返回bytes
def __init__(self, key, device: piico.Device):
key = padding_key(key, 16)
self.cipher = device.new_sm4_ebc_cipher(key)
def _encrypt(self, data: bytes) -> bytes:
return self.cipher.encrypt(self.to_16(data))
def _decrypt(self, data: bytes) -> bytes:
bs = self.cipher.decrypt(data)
return bs.rstrip(b'\0')
class AESCrypto:
"""
AES
@@ -70,9 +97,8 @@ class AESCrypto:
"""
def __init__(self, key):
if len(key) > 32:
key = key[:32]
self.key = self.to_16(key)
self.key = padding_key(key, 32)
self.aes = AES.new(self.key, AES.MODE_ECB)
@staticmethod
def to_16(key):
@@ -87,17 +113,15 @@ class AESCrypto:
return key # 返回bytes
def aes(self):
return AES.new(self.key, AES.MODE_ECB) # 初始化加密器
return AES.new(self.key, AES.MODE_ECB)
def encrypt(self, text):
aes = self.aes()
cipher = base64.encodebytes(aes.encrypt(self.to_16(text)))
cipher = base64.encodebytes(self.aes.encrypt(self.to_16(text)))
return str(cipher, encoding='utf8').replace('\n', '') # 加密
def decrypt(self, text):
aes = self.aes()
text_decoded = base64.decodebytes(bytes(text, encoding='utf8'))
return str(aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
return str(self.aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
class AESCryptoGCM:
@@ -106,7 +130,15 @@ class AESCryptoGCM:
"""
def __init__(self, key):
self.key = process_key(key)
self.key = self.process_key(key)
@staticmethod
def process_key(key):
if not isinstance(key, bytes):
key = bytes(key, encoding='utf-8')
if len(key) >= 32:
return key[:32]
return pad(key, 32)
def encrypt(self, text):
"""
@@ -133,7 +165,6 @@ class AESCryptoGCM:
nonce = base64.b64decode(metadata[24:48])
tag = base64.b64decode(metadata[48:])
ciphertext = base64.b64decode(text[72:])
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
cipher.update(header)
@@ -144,11 +175,10 @@ class AESCryptoGCM:
def get_aes_crypto(key=None, mode='GCM'):
if key is None:
key = settings.SECRET_KEY
if mode == 'ECB':
a = AESCrypto(key)
elif mode == 'GCM':
a = AESCryptoGCM(key)
return a
if mode == 'GCM':
return AESCryptoGCM(key)
else:
return AESCrypto(key)
def get_gm_sm4_ecb_crypto(key=None):
@@ -156,44 +186,65 @@ def get_gm_sm4_ecb_crypto(key=None):
return GMSM4EcbCrypto(key)
def get_piico_gm_sm4_ecb_crypto(device, key=None):
key = key or settings.SECRET_KEY
return PiicoSM4EcbCrypto(key, device)
aes_ecb_crypto = get_aes_crypto(mode='ECB')
aes_crypto = get_aes_crypto(mode='GCM')
gm_sm4_ecb_crypto = get_gm_sm4_ecb_crypto()
class Crypto:
cryptoes = {
cryptor_map = {
'aes_ecb': aes_ecb_crypto,
'aes_gcm': aes_crypto,
'aes': aes_crypto,
'gm_sm4_ecb': gm_sm4_ecb_crypto,
'gm': gm_sm4_ecb_crypto,
}
cryptos = []
def __init__(self):
cryptoes = self.__class__.cryptoes.copy()
crypto = cryptoes.pop(settings.SECURITY_DATA_CRYPTO_ALGO, None)
if crypto is None:
crypt_algo = settings.SECURITY_DATA_CRYPTO_ALGO
if not crypt_algo:
if settings.GMSSL_ENABLED:
if settings.PIICO_DEVICE_ENABLE:
piico_driver_path = settings.PIICO_DRIVER_PATH if settings.PIICO_DRIVER_PATH \
else "./lib/libpiico_ccmu.so"
device = piico.open_piico_device(piico_driver_path)
self.cryptor_map["piico_gm"] = get_piico_gm_sm4_ecb_crypto(device)
crypt_algo = 'piico_gm'
else:
crypt_algo = 'gm'
else:
crypt_algo = 'aes'
cryptor = self.cryptor_map.get(crypt_algo, None)
if cryptor is None:
raise ImproperlyConfigured(
f'Crypto method not supported {settings.SECURITY_DATA_CRYPTO_ALGO}'
)
self.cryptoes = [crypto, *cryptoes.values()]
others = set(self.cryptor_map.values()) - {cryptor}
self.cryptos = [cryptor, *others]
@property
def encryptor(self):
return self.cryptoes[0]
return self.cryptos[0]
def encrypt(self, text):
if text is None:
return text
return self.encryptor.encrypt(text)
def decrypt(self, text):
for decryptor in self.cryptoes:
for cryptor in self.cryptos:
try:
origin_text = decryptor.decrypt(text)
origin_text = cryptor.decrypt(text)
if origin_text:
# 有时不同算法解密不报错,但是返回空字符串
return origin_text
except (TypeError, ValueError, UnicodeDecodeError, IndexError):
except Exception:
continue
@@ -255,6 +306,8 @@ 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:

View File

@@ -196,7 +196,8 @@ def encrypt_password(password, salt=None, algorithm='sha512'):
return des_crypt.hash(password, salt=salt[:2])
support_algorithm = {
'sha512': sha512, 'des': des
'sha512': sha512,
'des': des
}
if isinstance(algorithm, str):
@@ -222,9 +223,6 @@ def ensure_last_char_is_ascii(data):
remain = ''
secret_pattern = re.compile(r'password|secret|key', re.IGNORECASE)
def data_to_json(data, sort_keys=True, indent=2, cls=None):
if cls is None:
cls = DjangoJSONEncoder

View File

@@ -4,6 +4,10 @@ import csv
import pyzipper
import requests
from hashlib import md5
from django.conf import settings
def create_csv_file(filename, headers, rows, ):
with open(filename, 'w', encoding='utf-8-sig')as f:
@@ -28,3 +32,18 @@ def download_file(src, path):
with open(path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
def save_content_to_temp_path(content, file_mode=0o400):
if not content:
return
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
filename = '.' + md5(content.encode('utf-8')).hexdigest()
filepath = os.path.join(tmp_dir, filename)
if not os.path.exists(filepath):
with open(filepath, 'w') as f:
f.write(content)
os.chmod(filepath, file_mode)
return filepath

View File

@@ -35,7 +35,7 @@ def random_string(length, lower=True, upper=True, digit=True, special_char=False
if special_char:
spc = random.choice(string_punctuation)
i = random.choice(range(len(password)))
i = random.choice(range(1, len(password)))
password[i] = spc
password = ''.join(password)

View File

@@ -15,18 +15,23 @@ import errno
import json
import yaml
import copy
import base64
import logging
from importlib import import_module
from urllib.parse import urljoin, urlparse
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.urls import reverse_lazy
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR)
XPACK_DIR = os.path.join(BASE_DIR, 'xpack')
HAS_XPACK = os.path.isdir(XPACK_DIR)
logger = logging.getLogger('jumpserver.conf')
def import_string(dotted_path):
try:
@@ -39,9 +44,9 @@ def import_string(dotted_path):
try:
return getattr(module, class_name)
except AttributeError as err:
raise ImportError('Module "%s" does not define a "%s" attribute/class' % (
module_path, class_name)
) from err
raise ImportError(
'Module "%s" does not define a "%s" attribute/class' %
(module_path, class_name)) from err
def is_absolute_uri(uri):
@@ -80,6 +85,59 @@ class DoesNotExist(Exception):
pass
class ConfigCrypto:
secret_keys = [
'SECRET_KEY', 'DB_PASSWORD', 'REDIS_PASSWORD',
]
def __init__(self, key):
self.safe_key = self.process_key(key)
self.sm4_encryptor = CryptSM4()
self.sm4_encryptor.set_key(self.safe_key, SM4_ENCRYPT)
self.sm4_decryptor = CryptSM4()
self.sm4_decryptor.set_key(self.safe_key, SM4_DECRYPT)
@staticmethod
def process_key(secret_encrypt_key):
key = secret_encrypt_key.encode()
if len(key) >= 16:
key = key[:16]
else:
key += b'\0' * (16 - len(key))
return key
def encrypt(self, data):
data = bytes(data, encoding='utf8')
return base64.b64encode(self.sm4_encryptor.crypt_ecb(data)).decode('utf8')
def decrypt(self, data):
data = base64.urlsafe_b64decode(bytes(data, encoding='utf8'))
return self.sm4_decryptor.crypt_ecb(data).decode('utf8')
def decrypt_if_need(self, value, item):
if item not in self.secret_keys:
return value
try:
plaintext = self.decrypt(value)
if plaintext:
value = plaintext
except Exception as e:
logger.error('decrypt %s error: %s', item, e)
return value
@classmethod
def get_secret_encryptor(cls):
# 使用 SM4 加密配置文件敏感信息
# https://the-x.cn/cryptography/Sm4.aspx
secret_encrypt_key = os.environ.get('SECRET_ENCRYPT_KEY', '')
if not secret_encrypt_key:
return None
print('Info: Using SM4 to encrypt config secret value')
return cls(secret_encrypt_key)
class Config(dict):
"""Works exactly like a dict but provides ways to fill it from files
or special dictionaries. There are two common patterns to populate the
@@ -160,12 +218,15 @@ class Config(dict):
'SESSION_COOKIE_DOMAIN': None,
'CSRF_COOKIE_DOMAIN': None,
'SESSION_COOKIE_NAME_PREFIX': None,
'SESSION_COOKIE_AGE': 3600,
'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'LOGIN_URL': reverse_lazy('authentication:login'),
'CONNECTION_TOKEN_EXPIRATION': 5 * 60,
# Custom Config
'AUTH_CUSTOM': False,
'AUTH_CUSTOM_FILE_MD5': '',
# Auth LDAP settings
'AUTH_LDAP': False,
'AUTH_LDAP_SERVER_URI': 'ldap://localhost:389',
@@ -265,6 +326,24 @@ class Config(dict):
'AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT': '/',
'AUTH_SAML2_AUTHENTICATION_FAILURE_REDIRECT_URI': '/',
# OAuth2 认证
'AUTH_OAUTH2': False,
'AUTH_OAUTH2_LOGO_PATH': 'img/login_oauth2_logo.png',
'AUTH_OAUTH2_PROVIDER': 'OAuth2',
'AUTH_OAUTH2_ALWAYS_UPDATE_USER': True,
'AUTH_OAUTH2_CLIENT_ID': 'client-id',
'AUTH_OAUTH2_SCOPE': '',
'AUTH_OAUTH2_CLIENT_SECRET': '',
'AUTH_OAUTH2_LOGOUT_COMPLETELY': True,
'AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT': 'https://oauth2.example.com/authorize',
'AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT': 'https://oauth2.example.com/userinfo',
'AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT': 'https://oauth2.example.com/logout',
'AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT': 'https://oauth2.example.com/access_token',
'AUTH_OAUTH2_ACCESS_TOKEN_METHOD': 'GET',
'AUTH_OAUTH2_USER_ATTR_MAP': {
'name': 'name', 'username': 'username', 'email': 'email'
},
'AUTH_TEMP_TOKEN': False,
# 企业微信
@@ -302,6 +381,22 @@ class Config(dict):
'TENCENT_VERIFY_SIGN_NAME': '',
'TENCENT_VERIFY_TEMPLATE_CODE': '',
'HUAWEI_APP_KEY': '',
'HUAWEI_APP_SECRET': '',
'HUAWEI_SMS_ENDPOINT': '',
'HUAWEI_SIGN_CHANNEL_NUM': '',
'HUAWEI_VERIFY_SIGN_NAME': '',
'HUAWEI_VERIFY_TEMPLATE_CODE': '',
'CMPP2_HOST': '',
'CMPP2_PORT': 7890,
'CMPP2_SP_ID': '',
'CMPP2_SP_SECRET': '',
'CMPP2_SRC_ID': '',
'CMPP2_SERVICE_ID': '',
'CMPP2_VERIFY_SIGN_NAME': '',
'CMPP2_VERIFY_TEMPLATE_CODE': '{code}',
# Email
'EMAIL_CUSTOM_USER_CREATED_SUBJECT': _('Create account successfully'),
'EMAIL_CUSTOM_USER_CREATED_HONORIFIC': _('Hello'),
@@ -387,7 +482,10 @@ class Config(dict):
'SESSION_SAVE_EVERY_REQUEST': True,
'SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE': False,
'SERVER_REPLAY_STORAGE': {},
'SECURITY_DATA_CRYPTO_ALGO': 'aes',
'SECURITY_DATA_CRYPTO_ALGO': None,
'GMSSL_ENABLED': False,
# Magnus 组件需要监听的端口范围
'MAGNUS_PORTS': '30000-30100',
# 记录清理清理
'LOGIN_LOG_KEEP_DAYS': 200,
@@ -405,6 +503,7 @@ class Config(dict):
'CONNECTION_TOKEN_ENABLED': False,
'PERM_SINGLE_ASSET_TO_UNGROUP_NODE': False,
'TICKET_AUTHORIZE_DEFAULT_TIME': 7,
'WINDOWS_SSH_DEFAULT_SHELL': 'cmd',
'PERIOD_TASK_ENABLED': True,
@@ -416,6 +515,10 @@ class Config(dict):
'HEALTH_CHECK_TOKEN': '',
}
def __init__(self, *args):
super().__init__(*args)
self.secret_encryptor = ConfigCrypto.get_secret_encryptor()
@staticmethod
def convert_keycloak_to_openid(keycloak_config):
"""
@@ -427,7 +530,6 @@ class Config(dict):
"""
openid_config = copy.deepcopy(keycloak_config)
auth_openid = openid_config.get('AUTH_OPENID')
auth_openid_realm_name = openid_config.get('AUTH_OPENID_REALM_NAME')
auth_openid_server_url = openid_config.get('AUTH_OPENID_SERVER_URL')
@@ -556,13 +658,12 @@ class Config(dict):
def get(self, item):
# 再从配置文件中获取
value = self.get_from_config(item)
if value is not None:
return value
# 其次从环境变量来
value = self.get_from_env(item)
if value is not None:
return value
value = self.defaults.get(item)
if value is None:
value = self.get_from_env(item)
if value is None:
value = self.defaults.get(item)
if self.secret_encryptor:
value = self.secret_encryptor.decrypt_if_need(value, item)
return value
def __getitem__(self, item):

View File

@@ -4,10 +4,10 @@ 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
from settings.urls.ws_urls import urlpatterns as setting_urlpatterns
urlpatterns = []
urlpatterns += ops_urlpatterns \
+ notifications_urlpatterns
urlpatterns += ops_urlpatterns + notifications_urlpatterns + setting_urlpatterns
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(

View File

@@ -2,7 +2,6 @@
#
import os
import ldap
from django.utils.translation import ugettext_lazy as _
from ..const import CONFIG, PROJECT_DIR, BASE_DIR
@@ -24,9 +23,15 @@ AUTH_LDAP_GLOBAL_OPTIONS = {
ldap.OPT_X_TLS_REQUIRE_CERT: ldap.OPT_X_TLS_NEVER,
ldap.OPT_REFERRALS: CONFIG.AUTH_LDAP_OPTIONS_OPT_REFERRALS
}
LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
LDAP_CACERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_ca.pem")
if os.path.isfile(LDAP_CACERT_FILE):
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CACERT_FILE
LDAP_CERT_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.pem")
if os.path.isfile(LDAP_CERT_FILE):
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CACERTFILE] = LDAP_CERT_FILE
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_CERTFILE] = LDAP_CERT_FILE
LDAP_KEY_FILE = os.path.join(PROJECT_DIR, "data", "certs", "ldap_cert.key")
if os.path.isfile(LDAP_KEY_FILE):
AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_X_TLS_KEYFILE] = LDAP_KEY_FILE
# AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU
# AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER
# AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
@@ -143,6 +148,26 @@ SAML2_SP_ADVANCED_SETTINGS = CONFIG.SAML2_SP_ADVANCED_SETTINGS
SAML2_LOGIN_URL_NAME = "authentication:saml2:saml2-login"
SAML2_LOGOUT_URL_NAME = "authentication:saml2:saml2-logout"
# OAuth2 auth
AUTH_OAUTH2 = CONFIG.AUTH_OAUTH2
AUTH_OAUTH2_LOGO_PATH = CONFIG.AUTH_OAUTH2_LOGO_PATH
AUTH_OAUTH2_PROVIDER = CONFIG.AUTH_OAUTH2_PROVIDER
AUTH_OAUTH2_ALWAYS_UPDATE_USER = CONFIG.AUTH_OAUTH2_ALWAYS_UPDATE_USER
AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT
AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT
AUTH_OAUTH2_ACCESS_TOKEN_METHOD = CONFIG.AUTH_OAUTH2_ACCESS_TOKEN_METHOD
AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT
AUTH_OAUTH2_CLIENT_SECRET = CONFIG.AUTH_OAUTH2_CLIENT_SECRET
AUTH_OAUTH2_CLIENT_ID = CONFIG.AUTH_OAUTH2_CLIENT_ID
AUTH_OAUTH2_SCOPE = CONFIG.AUTH_OAUTH2_SCOPE
AUTH_OAUTH2_USER_ATTR_MAP = CONFIG.AUTH_OAUTH2_USER_ATTR_MAP
AUTH_OAUTH2_LOGOUT_COMPLETELY = CONFIG.AUTH_OAUTH2_LOGOUT_COMPLETELY
AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT
AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oauth2:login-callback'
AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/'
AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/'
AUTH_OAUTH2_LOGOUT_URL_NAME = "authentication:oauth2:logout"
# 临时 token
AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
@@ -170,8 +195,9 @@ AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend'
AUTH_BACKEND_TEMP_TOKEN = 'authentication.backends.token.TempTokenAuthBackend'
AUTH_BACKEND_CUSTOM = 'authentication.backends.custom.CustomAuthBackend'
AUTHENTICATION_BACKENDS = [
# 只做权限校验
@@ -180,12 +206,37 @@ AUTHENTICATION_BACKENDS = [
AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY, AUTH_BACKEND_LDAP, AUTH_BACKEND_RADIUS,
# 跳转形式
AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
AUTH_BACKEND_OAUTH2,
# 扫码模式
AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU,
# Token模式
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN,
]
def get_file_md5(filepath):
import hashlib
# 创建md5对象
m = hashlib.md5()
with open(filepath, 'rb') as f:
while True:
data = f.read(4096)
if not data:
break
# 更新md5对象
m.update(data)
# 返回md5对象
return m.hexdigest()
AUTH_CUSTOM = CONFIG.AUTH_CUSTOM
AUTH_CUSTOM_FILE_MD5 = CONFIG.AUTH_CUSTOM_FILE_MD5
AUTH_CUSTOM_FILE_PATH = os.path.join(PROJECT_DIR, 'data', 'auth', 'main.py')
if AUTH_CUSTOM and AUTH_CUSTOM_FILE_MD5 == get_file_md5(AUTH_CUSTOM_FILE_PATH):
# 自定义认证模块
AUTHENTICATION_BACKENDS.append(AUTH_BACKEND_CUSTOM)
AUTHENTICATION_BACKENDS_THIRD_PARTY = [AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_CAS, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2]
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE

View File

@@ -43,6 +43,9 @@ DEBUG_DEV = CONFIG.DEBUG_DEV
# Absolute url for some case, for example email link
SITE_URL = CONFIG.SITE_URL
# https://docs.djangoproject.com/en/4.1/ref/settings/
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# LOG LEVEL
LOG_LEVEL = CONFIG.LOG_LEVEL
@@ -106,6 +109,7 @@ MIDDLEWARE = [
'authentication.backends.oidc.middleware.OIDCRefreshIDTokenMiddleware',
'authentication.backends.cas.middleware.CASMiddleware',
'authentication.middleware.MFAMiddleware',
'authentication.middleware.ThirdPartyLoginMiddleware',
'authentication.middleware.SessionCookieMiddleware',
'simple_history.middleware.HistoryRequestMiddleware',
]
@@ -138,6 +142,7 @@ WSGI_APPLICATION = 'jumpserver.wsgi.application'
LOGIN_REDIRECT_URL = reverse_lazy('index')
LOGIN_URL = reverse_lazy('authentication:login')
LOGOUT_REDIRECT_URL = CONFIG.LOGOUT_REDIRECT_URL
SESSION_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN = CONFIG.SESSION_COOKIE_DOMAIN
@@ -307,6 +312,21 @@ CSRF_COOKIE_SECURE = CONFIG.CSRF_COOKIE_SECURE
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
GMSSL_ENABLED = CONFIG.GMSSL_ENABLED
GM_HASHER = 'common.hashers.PBKDF2SM3PasswordHasher'
if GMSSL_ENABLED:
PASSWORD_HASHERS.insert(0, GM_HASHER)
else:
PASSWORD_HASHERS.append(GM_HASHER)
# For Debug toolbar
INTERNAL_IPS = ["127.0.0.1"]
if os.environ.get('DEBUG_TOOLBAR', False):
@@ -315,3 +335,4 @@ if os.environ.get('DEBUG_TOOLBAR', False):
DEBUG_TOOLBAR_PANELS = [
'debug_toolbar.panels.profiling.ProfilingPanel',
]

View File

@@ -85,6 +85,7 @@ TERMINAL_TELNET_REGEX = CONFIG.TERMINAL_TELNET_REGEX
BACKEND_ASSET_USER_AUTH_VAULT = False
PERM_SINGLE_ASSET_TO_UNGROUP_NODE = CONFIG.PERM_SINGLE_ASSET_TO_UNGROUP_NODE
TICKET_AUTHORIZE_DEFAULT_TIME = CONFIG.TICKET_AUTHORIZE_DEFAULT_TIME
PERM_EXPIRED_CHECK_PERIODIC = CONFIG.PERM_EXPIRED_CHECK_PERIODIC
WINDOWS_SSH_DEFAULT_SHELL = CONFIG.WINDOWS_SSH_DEFAULT_SHELL
FLOWER_URL = CONFIG.FLOWER_URL
@@ -110,6 +111,8 @@ HTTP_LISTEN_PORT = CONFIG.HTTP_LISTEN_PORT
WS_LISTEN_PORT = CONFIG.WS_LISTEN_PORT
LOGIN_LOG_KEEP_DAYS = CONFIG.LOGIN_LOG_KEEP_DAYS
TASK_LOG_KEEP_DAYS = CONFIG.TASK_LOG_KEEP_DAYS
OPERATE_LOG_KEEP_DAYS = CONFIG.OPERATE_LOG_KEEP_DAYS
FTP_LOG_KEEP_DAYS = CONFIG.FTP_LOG_KEEP_DAYS
ORG_CHANGE_TO_URL = CONFIG.ORG_CHANGE_TO_URL
WINDOWS_SKIP_ALL_MANUAL_PASSWORD = CONFIG.WINDOWS_SKIP_ALL_MANUAL_PASSWORD
@@ -174,3 +177,6 @@ HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key'
SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
# Magnus DB Port
MAGNUS_PORTS = CONFIG.MAGNUS_PORTS

View File

@@ -9,7 +9,6 @@ from .base import (
)
from ..const import CONFIG, PROJECT_DIR
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
@@ -64,7 +63,6 @@ SWAGGER_SETTINGS = {
'DEFAULT_INFO': 'jumpserver.views.swagger.api_info',
}
# Captcha settings, more see https://django-simple-captcha.readthedocs.io/en/latest/advanced.html
CAPTCHA_IMAGE_SIZE = (180, 38)
CAPTCHA_FOREGROUND_COLOR = '#001100'
@@ -81,7 +79,6 @@ BOOTSTRAP3 = {
'required_css_class': 'required',
}
# Django channels support websocket
if not REDIS_USE_SSL:
redis_ssl = None
@@ -101,14 +98,13 @@ CHANNEL_LAYERS = {
'address': (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT),
'db': CONFIG.REDIS_DB_WS,
'password': CONFIG.REDIS_PASSWORD or None,
'ssl': redis_ssl
'ssl': redis_ssl
}],
},
},
}
ASGI_APPLICATION = 'jumpserver.routing.application'
# Dump all celery log to here
CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery')
@@ -131,6 +127,7 @@ CELERY_TASK_EAGER_PROPAGATES = True
CELERY_WORKER_REDIRECT_STDOUTS = True
CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
CELERY_TASK_SOFT_TIME_LIMIT = 3600
CELERY_WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS = True
if REDIS_USE_SSL:
CELERY_BROKER_USE_SSL = CELERY_REDIS_BACKEND_USE_SSL = {
@@ -148,3 +145,7 @@ REDIS_PORT = CONFIG.REDIS_PORT
REDIS_PASSWORD = CONFIG.REDIS_PASSWORD
DJANGO_REDIS_SCAN_ITERSIZE = 1000
# GM DEVICE
PIICO_DEVICE_ENABLE = CONFIG.PIICO_DEVICE_ENABLE
PIICO_DRIVER_PATH = CONFIG.PIICO_DRIVER_PATH

View File

@@ -154,4 +154,4 @@ if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2:
})
if not os.path.isdir(LOG_DIR):
os.makedirs(LOG_DIR)
os.makedirs(LOG_DIR, mode=0o755)

View File

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f2fdd3a7bd34a26d068fc6ce521d0ea9983c477b13536ba3f51700a554d4ae3
size 128706
oid sha256:7522cd9a7e7853d078c81006cea7f6dbe4fb9d51ae7c6dddd50e8471536d4c0d
size 133026

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