Compare commits

...

459 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
Jiangjie.Bai
f52a0ce960 Merge pull request #8645 from jumpserver/dev
v2.24.0
2022-07-21 15:40:57 +08:00
ibuler
9d17f27fb3 fix: 修复密码可能解密失败报错 2022-07-21 15:37:32 +08:00
feng626
36d0b8d085 fix: 组件角色绑定错误 2022-07-21 15:37:06 +08:00
fit2bot
046356728a perf: sso token 最小60秒 (#8642)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-21 13:51:15 +08:00
Jiangjie.Bai
d34c4fb7ec Merge pull request #8640 from jumpserver/dev
v2.24.0-rc5
2022-07-20 19:07:18 +08:00
Jiangjie.Bai
ca49029d8f fix: 锁定依赖 keystoneauth1==3.4.0 2022-07-20 17:22:22 +08:00
ibuler
12036f8c96 perf: 修改 django 版本 2022-07-20 17:01:06 +08:00
Jiangjie.Bai
60e455bea2 fix: 修改theme_info默认值为{} 2022-07-20 16:13:20 +08:00
fit2bot
e7dd731139 fix: 授权过期url 404 -> /console/ (#8634)
Co-authored-by: feng626 <1304903146@qq.com>
2022-07-20 15:44:00 +08:00
Jiangjie.Bai
88ae8ac67a fix: 会话列表修改翻译为终端ID 2022-07-20 13:54:43 +08:00
feng626
626b6da9c4 fix cmd为空时bug 2022-07-20 13:43:04 +08:00
fit2bot
cb8690dd63 fix: 处理组件获取connection token获取不到的问题 (#8629)
* fix: 处理组件获取connection token获取不到的问题

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

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

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

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

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

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

* fix: 修复连接令牌问题

* fix: 修复连接令牌问题

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

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

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

* feat: Endpoint 支持 oracle 版本

* feat: Endpoint 支持 oracle 版本

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

* perf: 修改主题

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* perf: 优化 flash msg page

* perf: 修改 i18n

* perf: 修改 i18n

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

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

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

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

* perf: 修改颜色

* perf: 优化 interface

* perf: 修改 avartar

* perf: css color

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

* del app

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

* perf: 修改 校验

* perf: 优化 confirm API 逻辑

* Delete django.po

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

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

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

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

feat: Endpoint 支持标签匹配

feat: Endpoint 支持标签匹配

feat: Endpoint 添加帮助信息

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

* perf: 修改 config name

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

perf: 修改支持 tdsql 5.7

revert: 欢迎之前的内容

revert: some

perf: 修改 tdsql

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

* perf: 修改 code

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

* perf: 优化 redis 时间

* perf: 优化时间

* perf: 修改 ssl

* perf: 修改 ssl

* perf: 修改 ssl name

* perf: 修改名称

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

* perf: 优化 tickets

* perf: 暂存

* perf: 建立 ticket model

* perf: 暂存一下

* perf: 修改 tickets

* perf: 修改 import

* perf: 修改model

* perf: 暂存一波

* perf: 修改...

* del process_map field

* 工单重构

* 资产 应用对接前端

* perf: 修改 ticket

* fix: bug

* 修改迁移文件

* 添加其他api

* 去掉process_map

* perf: 优化去掉 signal

* perf: 修改这里

* 修改一点

* perf: 修改工单

* perf: 修改状态

* perf: 修改工单流转

* step 状态切换

* perf: 修改 ticket open

* perf: 修改流程

* perf: stash it

* 改又改

* stash it

* perf: stash

* stash

* migrate

* perf migrate

* 调整一下

* 修复bug

* 修改一点

* 修改一点

* 优化一波

* perf: ticket migrations

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-23 13:52:28 +08:00
Jiangjie.Bai
2471787277 Merge pull request #8457 from jumpserver/pr@dev@perf_redis
perf: redis AND login page
2022-06-23 10:39:22 +08:00
ibuler
e6abdbdadc perf: 修改 req version 2022-06-22 18:17:57 +08:00
Jiangjie.Bai
5ed65ca2ff fix: 修复post方法调用AuthBook接口时500的问题 2022-06-22 17:11:35 +08:00
ibuler
ba6b1bf692 perf: 修改翻译 2022-06-22 14:30:52 +08:00
ibuler
1aa58e1486 perf: 修改 ignore 2022-06-21 19:25:56 +08:00
ibuler
fa51465485 perf: 修改去掉 导入 certs 2022-06-21 19:23:29 +08:00
ibuler
8f59bb2a48 perf: 优化登陆 2022-06-21 19:06:06 +08:00
ibuler
2366da1485 perf: redis AND login page 2022-06-21 18:43:48 +08:00
ibuler
f1a22575d3 perf: 优化登录页面 2022-06-21 16:18:13 +08:00
ibuler
7c1882bb53 perf: login 2022-06-21 10:08:14 +08:00
ibuler
97baeebb2a perf: 修改 redis scan counter 2022-06-20 19:40:07 +08:00
ibuler
8b819f3779 perf: 优化登录 2022-06-20 19:22:48 +08:00
Jiangjie.Bai
d1420de4c2 fix: 修复es类型的命令存储更新忽略证书字段不成功的问题 2022-06-20 14:47:30 +08:00
ibuler
379c7198da pref: 去掉 django-redis-cache 依赖 2022-06-20 14:11:59 +08:00
Eric
710cd0fb3b fix:修复es日期索引忽略证书的问题 2022-06-20 14:06:56 +08:00
Jiangjie.Bai
3fde31f2e0 fix: 修复工单自定义搜索时500的问题 2022-06-17 15:26:23 +08:00
Jiangjie.Bai
d3355ab0ec Merge pull request #8427 from jumpserver/dev
v2.23.0 rc6
2022-06-16 18:12:44 +08:00
Jiangjie.Bai
81598a5264 perf: 推送系统用户用户名提示信息 2022-06-16 18:04:03 +08:00
feng626
298f6ba41d fix: 修改翻译 2022-06-16 18:03:16 +08:00
feng626
8e43e9ee2b fix: 授权过期通知 2022-06-16 17:52:33 +08:00
Jiangjie.Bai
adc8a8f7d3 fix: 修改翻译 2022-06-16 17:10:21 +08:00
Jiangjie.Bai
1e3da50979 fix: 修复会话加入记录更新失败的问题 2022-06-16 16:50:51 +08:00
Jiangjie.Bai
7ac385d64c Merge pull request #8420 from jumpserver/dev
v2.23.0 rc5
2022-06-16 15:46:40 +08:00
Jiangjie.Bai
2be74c4b84 fix: 修复命令列表模糊搜索报错500的问题
fix: 修复命令列表模糊搜索报错500的问题
2022-06-16 13:45:24 +08:00
feng626
75a72fb182 fix: user confirm bug 2022-06-16 11:31:27 +08:00
Jiangjie.Bai
4c2274b14e fix: 修改翻译 2022-06-16 11:19:26 +08:00
feng626
a024f26768 fix: 授权过期消息提示 2022-06-16 11:19:26 +08:00
Jiangjie.Bai
2898c35970 Merge pull request #8411 from jumpserver/dev
v2.23.0 rc4
2022-06-15 19:38:17 +08:00
Jiangjie.Bai
62f5662bd0 fix: 修复openid用户登录时默认邮件后缀使用配置项 2022-06-15 19:33:26 +08:00
ibuler
0fe221019a pref: 优化没有获取到节点的问题 2022-06-15 19:33:26 +08:00
ibuler
d745314aa1 perf: 优化签名认证 2022-06-15 19:33:26 +08:00
feng626
153fad9ac7 feat: add client linux arm64 version 2022-06-15 19:33:26 +08:00
Jiangjie.Bai
0792c7ec49 fix: 修改推送系统用户提示文案 2022-06-15 19:33:26 +08:00
fit2bot
e617697553 fix: 修复授权过期通知bug (#8404)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-15 19:33:26 +08:00
fit2bot
9dc7da3595 perf: 优化 apt (#8398)
* pref: 修改 oracle lib path

* perf: 优化 apt

Co-authored-by: ibuler <ibuler@qq.com>
2022-06-15 19:33:26 +08:00
Jiangjie.Bai
f7f4d3a42e fix: 过滤系统用户密码过滤ansible不支持的字符 2022-06-15 19:33:26 +08:00
feng626
70fcbfe883 perf: 授权过期通知 2022-06-15 19:33:26 +08:00
Jiangjie.Bai
9e16b79abe fix: 修复openid用户登录时默认邮件后缀使用配置项 2022-06-15 19:32:36 +08:00
ibuler
8c839784fb pref: 优化没有获取到节点的问题 2022-06-15 15:31:33 +08:00
ibuler
10adb4e6b7 perf: 优化签名认证 2022-06-15 15:30:51 +08:00
feng626
75c011f1c5 feat: add client linux arm64 version 2022-06-15 15:30:13 +08:00
Jiangjie.Bai
a882ca0d51 fix: 修改推送系统用户提示文案 2022-06-15 15:20:08 +08:00
fit2bot
e0a2d03f44 fix: 修复授权过期通知bug (#8404)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-15 15:01:56 +08:00
fit2bot
2414f34a5a perf: 优化 apt (#8398)
* pref: 修改 oracle lib path

* perf: 优化 apt

Co-authored-by: ibuler <ibuler@qq.com>
2022-06-14 19:59:00 +08:00
Jiangjie.Bai
2aebfa51b2 fix: 过滤系统用户密码过滤ansible不支持的字符 2022-06-14 18:49:35 +08:00
feng626
f91bfedc50 perf: 授权过期通知 2022-06-14 18:33:49 +08:00
Jiangjie.Bai
68aad56bad Merge pull request #8379 from jumpserver/dev
v2.23.0-rc3
2022-06-13 17:42:31 +08:00
ibuler
556ce0a146 perf: 继续优化一波 2022-06-13 16:46:14 +08:00
Jiangjie.Bai
95f8b12912 fix: 修复部分 password encrypted field extra kwargs 参数不生效问题 2022-06-13 16:44:01 +08:00
fit2bot
25ae790f7d fix: 修改client 版本 (#8375)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-13 15:45:10 +08:00
ibuler
0464b1a9e6 perf: 优化迁移 rbac 速度
perf: migrate
2022-06-13 15:18:15 +08:00
Jiangjie.Bai
3755f8f33a fix: 修复推送动态用户 comment 中包含空格导致推送失败的问题 2022-06-13 14:54:29 +08:00
Jiangjie.Bai
85b2ec2e6a Merge pull request #8362 from jumpserver/dev
v2.23.0-rc2
2022-06-10 19:12:17 +08:00
Jiangjie.Bai
9d1e94d3c2 fix: 修复手动登录系统用户连接RemoteApp应用获取不到认证信息的问题 2022-06-10 18:35:39 +08:00
Jiangjie.Bai
be75edcb41 Merge pull request #8353 from jumpserver/dev
v2.23.0-rc1
2022-06-09 17:40:10 +08:00
ibuler
a5c6ba6cd6 perf: 优化 perm app node 2022-06-09 10:48:48 +08:00
fit2bot
81ef614820 fix: relogin重置MFA_VERIFY_TIME (#8348)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-08 19:32:50 +08:00
ibuler
c6949b4f68 perf: 去掉 remote app 的加密 2022-06-08 10:04:14 +08:00
fit2bot
a5acdb9f60 perf: 统一校验当前用户api (#8324)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-07 19:26:07 +08:00
Jiangjie.Bai
2366f02d10 feat: 添加组件类型 razor 并替换 XRDP_ENABLED 2022-06-07 13:43:53 +08:00
Jiangjie.Bai
dade0cadda feat: 克隆角色权限 2022-06-06 16:13:12 +08:00
ibuler
e096244e75 pref: app tree 添加 icon 2022-06-06 14:00:34 +08:00
Jiangjie.Bai
3bc307d666 perf: 设置Connection Token 默认最少5分钟 (#8331) 2022-06-01 18:00:22 +08:00
Jiangjie.Bai
810c500402 feat: 添加配置项 CONNECTION_TOKEN_EXPIRATION 2022-05-31 18:23:48 +08:00
fit2bot
6c0d0c3e92 feat: OIDC 用户添加属性映射值 (#8327)
* feat: OIDC 用户添加属性映射值

* feat: OIDC 用户添加属性映射值

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-05-31 16:09:31 +08:00
Jiangjie.Bai
af1150bb86 feat: OIDC 用户添加属性映射值 2022-05-31 16:03:39 +08:00
ibuler
f7cbcc46f4 perf: 升级 ansible version 2022-05-31 16:01:21 +08:00
ibuler
327c6beab4 fix: 修复假数据构造 2022-05-30 16:39:47 +08:00
ibuler
196663f205 perf: 修改生成假数据 2022-05-30 16:03:13 +08:00
Jiangjie.Bai
15423291cc fix: 修复ldap用户登录时用户组不设置 2022-05-30 16:02:50 +08:00
ibuler
021635b850 perf: 优化 readme 2022-05-30 15:09:04 +08:00
老广
992c1407b6 Update README.md (#8316)
* Update README.md

* Update README.md

* Update README.md

* Update README.md
2022-05-30 14:51:29 +08:00
Chayim I. Kirshen
1322106c91 bumping redis-py to 4.3.1 (latest) 2022-05-30 13:30:25 +08:00
Jiangjie.Bai
42202bd528 fix: 修改 public settings API公告字段类型为 dict 2022-05-27 17:24:09 +08:00
fit2bot
b24d2f628a perf: update download (#8304)
Co-authored-by: feng626 <1304903146@qq.com>
2022-05-27 14:14:47 +08:00
fit2bot
041302d5d2 fix: 修复获取 city 时可能的报错 (#8294)
Co-authored-by: ibuler <ibuler@qq.com>
2022-05-24 12:31:16 +08:00
feng626
a08dd5ee72 fix: 修复用户更新自己密码 url 不准确问题 2022-05-24 11:16:13 +08:00
ibuler
09ef72a4a8 fix: 修复 Migrations 错误 2022-05-24 11:01:26 +08:00
ibuler
26cf64ad2d perf: 修改 i18 2022-05-20 11:41:33 +08:00
ibuler
0a04f0f351 perf: 下载 ip 数据库 2022-05-20 10:03:13 +08:00
fit2bot
1029556902 perf: remote app 字段也加密 (#8274)
* perf: remote app 字段也加密

* perf: 修改一些加密字段

Co-authored-by: ibuler <ibuler@qq.com>
2022-05-20 10:01:41 +08:00
Jiangjie.Bai
c41fc54380 Merge pull request #8271 from jumpserver/dev
v2.22.0-rc4
2022-05-18 20:21:35 +08:00
feng626
c2fbe5c75a fix: 不支持es8 提示 2022-05-18 20:20:54 +08:00
feng626
99e1b2cf92 fix: 不支持es8 提示 2022-05-18 20:14:31 +08:00
Jiangjie.Bai
33090c4cdf Merge pull request #8268 from jumpserver/dev
v2.22.0-rc4
2022-05-18 19:49:11 +08:00
fit2bot
c8d7c7c56f fix: 修复oidc认证不区分大小写 (#8267)
Co-authored-by: feng626 <1304903146@qq.com>
2022-05-18 18:32:53 +08:00
ibuler
aa7540045b feat: 添加 session guard 2022-05-18 14:55:58 +08:00
ibuler
e5f4b8000e stash 2022-05-18 14:55:58 +08:00
ibuler
44ffd09924 fix: 修复可能的 decode error 2022-05-18 10:17:15 +08:00
ibuler
fe3059c1fd fix: 修复获取密码失败 2022-05-17 23:50:30 +08:00
Jiangjie.Bai
b76920a4bf fix: 修改组织资源统计时 org 为None的问题 2022-05-17 22:11:42 +08:00
ibuler
b5ac5c5670 perf: domain gateway 也添加 2022-05-17 21:36:40 +08:00
ibuler
c3c0f87c01 perf: domain gateway 也添加 2022-05-17 21:32:31 +08:00
Jiangjie.Bai
d672122c79 Merge pull request #8260 from jumpserver/dev
v2.22.0-rc3
2022-05-17 21:14:05 +08:00
fit2bot
0c71190337 fix: 修改 EncryptedField 字段的 write_only 属性 (#8259)
* fix: 修改 EncryptedField 字段的 write_only 属性

fix: 修改 EncryptedField 字段的 write_only 属性

* fix: 修改 EncryptedField 字段的 write_only 属性

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-05-17 21:12:59 +08:00
feng626
14710e9c9e feat: 工单审批人中去除申请人 2022-05-17 20:56:53 +08:00
ibuler
7eec50804c perf: 优化 encrypted field 2022-05-17 20:04:46 +08:00
Jiangjie.Bai
0fc5a33983 fix: 修复企业微信、钉钉、飞书登录跳转问题 2022-05-17 18:57:49 +08:00
fit2bot
07779c5a7a perf: 工单启用 (#8254)
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-05-17 18:57:04 +08:00
fit2bot
d675b1d4fc fix: k8s token 解密 (#8252)
Co-authored-by: feng626 <1304903146@qq.com>
2022-05-17 16:53:15 +08:00
Jiangjie.Bai
514fa9cf0a Merge pull request #8250 from jumpserver/dev
v2.22.0-rc2
2022-05-17 15:10:59 +08:00
ibuler
2c73611cb4 fix: 修复公告不显示的问题 2022-05-17 11:30:37 +08:00
ibuler
83571718e9 perf: 修改版本 2022-05-16 20:02:10 +08:00
ibuler
521ec0245b fix: ipdb 版本 2022-05-16 20:02:10 +08:00
jiangweidong
e80b6936a2 perf: 兼容AWS上redis[ssl]无证书无法部署的问题 2022-05-16 18:03:47 +08:00
Jiangjie.Bai
2c4f937e0b fix: 解决LDAP同步用户仪表盘总数没有刷新的问题 2022-05-16 17:52:49 +08:00
Jiangjie.Bai
2a5497de14 fix: 修改工单审批文案 2022-05-16 17:52:27 +08:00
feng626
d87dc7cbd6 fix: import ipdb 2022-05-16 17:51:37 +08:00
fit2bot
3b253e276c perf: 优化翻译 (#8244)
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-05-16 17:50:28 +08:00
feng626
525538e775 fix: 修复密码密钥翻译问题 2022-05-16 17:48:28 +08:00
ibuler
2a8f8dd709 perf: 优化使用两个 ip 库 2022-05-16 15:24:38 +08:00
ibuler
1e6e59d815 perf: 添加 ipdb 2022-05-16 15:24:38 +08:00
ibuler
475678e29b fix: 修复密码 write only 2022-05-16 12:19:52 +08:00
Jiangjie.Bai
7f52675bd3 Merge pull request #8229 from jumpserver/dev
v2.22.0 rc1
2022-05-12 17:02:01 +08:00
fit2bot
6409b7deee feat: Endpoint添加Redis Port (#8225)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2022-05-12 14:47:35 +08:00
ibuler
4f37b2b920 perf: 优化 setting 读取,避免遗漏 2022-05-12 11:52:44 +08:00
fit2bot
c692eed3c6 perf: 修改client 版本 (#8223)
Co-authored-by: feng626 <1304903146@qq.com>
2022-05-12 10:59:19 +08:00
ibuler
dab8828b03 perf: 优化 setting 获取 2022-05-11 17:57:33 +08:00
ibuler
d692188a34 perf: 修改 i18n 2022-05-11 16:03:26 +08:00
Jiangjie.Bai
bc8df72603 fix: 修改创建更新用户的密码字段 2022-05-11 16:02:56 +08:00
Jiangjie.Bai
bf466a1ba2 feat: LDAP同步用户支持组织 2022-05-11 11:09:30 +08:00
fit2bot
aff5b0035d perf: 优化加密 (#8206)
* perf: 优化加密

* perf: 优化加密

* perf: 优化加密传输

Co-authored-by: ibuler <ibuler@qq.com>
2022-05-10 17:28:10 +08:00
jiangweidong
b44fa64994 perf: 企业微信、钉钉工单审批增加拒绝功能 (#8208)
* perf: 工单直接审批增加拒绝功能

* feat: 翻译

* perf: 修改动作名词

* perf: 修改翻译
2022-05-10 16:30:25 +08:00
ibuler
094446c548 chore: 去掉一个workflow 2022-05-10 10:37:01 +08:00
jiangweidong
64eda5f28b perf: 命令存储ES可根据日期动态建立索引 (#8180)
* perf: 命令存储ES可根据日期动态建立索引

* perf: 优化合并字段

* feat: 修改逻辑
2022-05-09 16:37:31 +08:00
Jiangjie.Bai
ab737ae09b fix: 修复获取类型为null的命令显示不支持的问题
fix: 修复获取类型为null的命令显示不支持的问题
2022-05-07 17:56:50 +08:00
jiangweidong
55e04e8e9f feat: 内置AIX系统,根据系统选择算法加密密码 2022-05-07 16:25:03 +08:00
jiangweidong
5e70a8af15 feat: 支持平台关联算法,支持AIX改密 2022-05-07 16:25:03 +08:00
fit2bot
031077c298 perf: password 等使用 rsa 加密传输 (#8188)
* perf: 修改 model fields 路径

* stash it

* pref: 统一加密方式,密码字段采用 rsa 加密

* pref: 临时密码使用 rsa

* perf: 去掉 debug msg

* perf: 去掉 Debug

* perf: 去掉 debug

* perf: 抽出来

Co-authored-by: ibuler <ibuler@qq.com>
2022-05-07 16:20:12 +08:00
Jiangjie.Bai
3f856e68f0 feat: public settings 区分 public 和 open 2022-05-07 11:09:24 +08:00
fit2bot
56862a965d fix: 修复system-role获取users失败的问题 (#8196)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-05-07 10:40:12 +08:00
jiangweidong
e151548701 perf: 账号管理中查看密码记录日志 (#8157) 2022-05-05 14:42:09 +08:00
jiangweidong
c56179e9e4 feat: 支持企业微信、钉钉直接审批工单 (#8115) 2022-05-05 13:07:48 +08:00
feng626
d23953932f perf: connection token 分api权限 2022-05-05 11:45:26 +08:00
Jiangjie.Bai
2493647e5c fix: 修复windows执行ansible显示sudo失败的问题 2022-05-05 11:40:12 +08:00
Jiangjie.Bai
00ed7bb025 perf: 优化 OIDC 支持选择认证方式 2022-04-29 14:28:07 +08:00
xiaziheng
b1aadf1ee9 Fix oidc (#8165) 2022-04-29 10:59:29 +08:00
feng626
86e6982383 fix: 组织管理员 添加 view platform perm 2022-04-28 19:10:58 +08:00
halo
dc42d1caa2 perf: 修改ssh_client连接选项翻译 2022-04-28 19:09:44 +08:00
ibuler
cb5d8fa13f fix: 去掉自动生成的map文件 2022-04-26 16:46:47 +08:00
jiangweidong
3a3f7eaf71 feat: 优化SAML2生成的metadata文件内容及属性映射 2022-04-26 10:00:53 +08:00
fit2bot
9804ca5dd0 fix: workbench_orgs 去重 (#8150)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-25 11:38:15 +08:00
老广
034d0e285c Update README.md 2022-04-24 17:47:02 +08:00
feng626
104d672634 perf: client download 2022-04-24 15:09:33 +08:00
ibuler
529e3d12e0 perf: 删除 build 2022-04-24 09:12:29 +08:00
ibuler
978c1f6363 perf: 修改 Dockerfile, 优化构建 2022-04-24 09:12:29 +08:00
ibuler
d25cde1bd5 fix: 修复社区版跳转问题 2022-04-21 22:48:59 +08:00
452 changed files with 13397 additions and 7704 deletions

View File

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

2
.gitattributes vendored
View File

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

View File

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

4
.gitignore vendored
View File

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

View File

@@ -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,6 +1,6 @@
# 编译代码
FROM python:3.8-slim as stage-build
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
@@ -9,74 +9,92 @@ 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 \
procps \
redis-tools \
telnet \
vim \
unzip \
wget"
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 \
&& echo "no" | dpkg-reconfigure dash \
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
&& rm -rf /var/lib/apt/lists/*
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"
RUN mkdir -p /opt/oracle/ \
&& cd /opt/oracle/ \
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \
&& rm -f ${ORACLE_FILE}
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
WORKDIR /opt/jumpserver
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
sshpass"
ARG TOOLS=" \
curl \
default-mysql-client \
iproute2 \
iputils-ping \
locales \
procps \
redis-tools \
telnet \
vim \
wget"
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \
&& apt -y install ${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 \
&& mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
&& sed -i "s@# alias l@alias l@g" ~/.bashrc \
&& echo "set mouse-=a" > ~/.vimrc \
&& rm -rf /var/lib/apt/lists/* \
&& mv /bin/sh /bin/sh.bak \
&& ln -s /bin/bash /bin/sh
RUN mkdir -p /opt/jumpserver/oracle/ \
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
RUN --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
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
RUN echo > /opt/jumpserver/config.yml \
&& rm -rf /tmp/build
RUN echo > config.yml \
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
&& rm -rf ~/.cache/pip
WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs

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

@@ -1,10 +1,13 @@
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
<p align="center">
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
</p>
<h3 align="center">多云环境下更好用的堡垒机</h3>
<p align="center">
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
<a href="https://github.com/jumpserver/jumpserver/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jumpserver/jumpserver.svg" /></a>
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
</p>
@@ -13,24 +16,22 @@
JumpServer 是全球首款开源堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
改变世界,从一点点开始 ...
> 如需进一步了解 JumpServer 开源项目,推荐阅读 [JumpServer 的初心和使命](https://mp.weixin.qq.com/s/S6q_2rP_9MwaVwyqLQnXzA)
### 特色优势
- 开源: 零门槛,线上快速获取和安装;
- 分布式: 轻松支持大规模并发访问;
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
- 多租户: 一套系统,多个子公司或部门同时使用;
- 多云支持: 一套系统,同时管理不同云上面的资产;
- 云端存储: 审计录像云端存储,永不丢失;
- 多租户: 一套系统,多个子公司和部门同时使用;
- 多应用支持: 数据库Windows远程应用Kubernetes。
### UI 展示
@@ -55,12 +56,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [手动安装](https://github.com/jumpserver/installer)
### 组件项目
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
- [Clients](https://github.com/jumpserver/clients) JumpServer 客户端 项目
- [Installer](https://github.com/jumpserver/installer) JumpServer 安装包 项目
| 项目 | 状态 | 描述 |
| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
| [Installer](https://github.com/jumpserver/installer)| <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
### 社区
@@ -75,27 +79,13 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
感谢以下贡献者,让 JumpServer 更加完善
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
<a href="https://github.com/jumpserver/koko/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
</a>
<a href="https://github.com/jumpserver/lina/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
</a>
<a href="https://github.com/jumpserver/luna/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
</a>
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
### 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化组件 Lion 依赖
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库JumpServer Web数据库依赖
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC 协议设备JumpServer 图形化组件 Lion 依赖
- [OmniDB](https://omnidb.org/) Web 页面连接使用数据库JumpServer Web 数据库依赖
### JumpServer 企业版
@@ -103,14 +93,18 @@ 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=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
- [腾讯海外游戏:基于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)
### 安全说明
@@ -131,4 +125,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
https://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

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

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
# coding: utf-8
#
from django.shortcuts import get_object_or_404
from orgs.mixins.api import OrgBulkModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
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

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

View File

@@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-05-20 11:04
import common.fields.model
import common.db.fields
from django.db import migrations, models
import django.db.models.deletion
import uuid
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=128, verbose_name='Name')),
('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')),
('path', models.CharField(max_length=128, verbose_name='App path')),
('params', common.fields.model.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
('params', common.db.fields.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),

View File

@@ -1,7 +1,7 @@
# Generated by Django 3.1.12 on 2021-08-26 09:07
import assets.models.base
import common.fields.model
import common.db.fields
from django.conf import settings
import django.core.validators
from django.db import migrations, models
@@ -26,9 +26,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
@@ -56,9 +56,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,6 @@ from common.tree import TreeNode
from common.utils import is_uuid
from assets.models import Asset, SystemUser
from ..utils import KubernetesTree
from .. import const
@@ -174,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:
@@ -214,6 +214,8 @@ class ApplicationTreeNodeMixin:
class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
APP_TYPE = const.AppType
name = models.CharField(max_length=128, verbose_name=_('Name'))
category = models.CharField(
max_length=16, choices=const.AppCategory.choices, verbose_name=_('Category')
@@ -255,6 +257,9 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_db(self):
return self.category == const.AppCategory.db.value
def is_type(self, tp):
return self.type == tp
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app:

View File

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.serializers.base import AuthSerializerMixin
from common.drf.serializers import MethodSerializer
from common.drf.serializers import MethodSerializer, SecretReadableMixin
from .attrs import (
category_serializer_classes_mapping,
type_serializer_classes_mapping,
@@ -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
@@ -152,13 +154,8 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
return super().to_representation(instance)
class AppAccountSecretSerializer(AppAccountSerializer):
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(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

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

View File

@@ -1,6 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import EncryptedField
from ..application_category import RemoteAppSerializer
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
@@ -13,19 +14,21 @@ class ChromeSerializer(RemoteAppSerializer):
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
)
chrome_target = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True,
max_length=128, allow_blank=True, required=False,
label=_('Target URL'), allow_null=True,
)
chrome_username = serializers.CharField(
max_length=128, allow_blank=True, required=False, label=_('Chrome username'), allow_null=True,
max_length=128, allow_blank=True, required=False,
label=_('Chrome username'), allow_null=True,
)
chrome_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Chrome password'),
allow_null=True
chrome_password = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Chrome password'), allow_null=True
)
class ChromeSecretSerializer(ChromeSerializer):
chrome_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Chrome password'),
allow_null=True
chrome_password = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Chrome password'), allow_null=True, write_only=False
)

View File

@@ -1,6 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import EncryptedField
from ..application_category import RemoteAppSerializer
__all__ = ['CustomSerializer', 'CustomSecretSerializer']
@@ -19,14 +20,14 @@ class CustomSerializer(RemoteAppSerializer):
max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
allow_null=True,
)
custom_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Custom password'),
allow_null=True,
custom_password = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Custom password'), allow_null=True,
)
class CustomSecretSerializer(RemoteAppSerializer):
custom_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Custom password'),
allow_null=True,
custom_password = EncryptedField(
max_length=128, allow_blank=True, required=False, write_only=False,
label=_('Custom password'), allow_null=True,
)

View File

@@ -1,6 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import EncryptedField
from ..application_category import RemoteAppSerializer
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
@@ -29,14 +30,14 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer):
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'),
allow_null=True,
)
mysql_workbench_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Mysql workbench password'),
allow_null=True,
mysql_workbench_password = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Mysql workbench password'), allow_null=True,
)
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
mysql_workbench_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Mysql workbench password'),
allow_null=True,
mysql_workbench_password = EncryptedField(
max_length=128, allow_blank=True, required=False, write_only=False,
label=_('Mysql workbench password'), allow_null=True,
)

View File

@@ -1,6 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import EncryptedField
from ..application_category import RemoteAppSerializer
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
@@ -25,14 +26,14 @@ class VMwareClientSerializer(RemoteAppSerializer):
max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
allow_null=True
)
vmware_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Vmware password'),
allow_null=True
vmware_password = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Vmware password'), allow_null=True
)
class VMwareClientSecretSerializer(RemoteAppSerializer):
vmware_password = serializers.CharField(
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Vmware password'),
allow_null=True
vmware_password = EncryptedField(
max_length=128, allow_blank=True, required=False, write_only=False,
label=_('Vmware password'), allow_null=True
)

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
from django.db.models import F, Q
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from rest_framework.decorators import action
@@ -8,12 +8,14 @@ from rest_framework.generics import CreateAPIView
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
from common.drf.filters import BaseFilterSet
from common.permissions import NeedMFAVerify
from common.mixins import RecordViewLogMixin
from common.permissions import UserConfirmation
from authentication.const import ConfirmType
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node
from .. import serializers
__all__ = ['AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
__all__ = ['AccountFilterSet', 'AccountViewSet', 'AccountSecretsViewSet', 'AccountTaskCreateAPI']
class AccountFilterSet(BaseFilterSet):
@@ -79,7 +81,7 @@ class AccountViewSet(OrgBulkModelViewSet):
return Response(data={'task': task.id})
class AccountSecretsViewSet(AccountViewSet):
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
"""
因为可能要导出所有账号,所以单独建立了一个 viewset
"""
@@ -87,7 +89,7 @@ class AccountSecretsViewSet(AccountViewSet):
'default': serializers.AccountSecretSerializer
}
http_method_names = ['get']
permission_classes = [RBACPermission, NeedMFAVerify]
permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
rbac_perms = {
'list': 'assets.view_assetaccountsecret',
'retrieve': 'assets.view_assetaccountsecret',

View File

@@ -6,7 +6,7 @@ from django.shortcuts import get_object_or_404
from django.db.models import Q
from common.utils import get_logger, get_object_or_none
from common.mixins.api import SuggestionMixin
from common.mixins.api import SuggestionMixin, RenderToJsonMixin
from users.models import User, UserGroup
from users.serializers import UserSerializer, UserGroupSerializer
from users.filters import UserFilter
@@ -88,7 +88,7 @@ class AssetPlatformRetrieveApi(RetrieveAPIView):
return asset.platform
class AssetPlatformViewSet(ModelViewSet):
class AssetPlatformViewSet(ModelViewSet, RenderToJsonMixin):
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_fields = ['name', 'base']

View File

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

View File

@@ -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

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

@@ -4,7 +4,6 @@ from rest_framework.response import Response
from rest_framework.decorators import action
from common.utils import get_logger, get_object_or_none
from common.utils.crypto import get_aes_crypto
from common.permissions import IsValidUser
from common.mixins.api import SuggestionMixin
from orgs.mixins.api import OrgBulkModelViewSet
@@ -102,27 +101,17 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = SystemUserTempAuthSerializer
def decrypt_data_if_need(self, data):
csrf_token = self.request.META.get('CSRF_COOKIE')
aes = get_aes_crypto(csrf_token, 'ECB')
password = data.get('password', '')
try:
data['password'] = aes.decrypt(password)
except:
pass
return data
def create(self, request, *args, **kwargs):
serializer = super().get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
pk = kwargs.get('pk')
data = self.decrypt_data_if_need(serializer.validated_data)
instance_id = data.get('instance_id')
data = serializer.validated_data
asset_or_app_id = data.get('instance_id')
with tmp_to_root_org():
instance = get_object_or_404(SystemUser, pk=pk)
instance.set_temp_auth(instance_id, self.request.user.id, data)
instance.set_temp_auth(asset_or_app_id, self.request.user.id, data)
return Response(serializer.data, status=201)
@@ -219,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):
@@ -234,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

@@ -1,7 +1,7 @@
# Generated by Django 2.1.7 on 2019-06-24 13:08
import assets.models.utils
import common.fields.model
import common.db.fields
from django.db import migrations
@@ -15,61 +15,61 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adminuser',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='adminuser',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='adminuser',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
migrations.AlterField(
model_name='authbook',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='authbook',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='authbook',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
migrations.AlterField(
model_name='gateway',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='gateway',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='gateway',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
migrations.AlterField(
model_name='systemuser',
name='_password',
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
),
migrations.AlterField(
model_name='systemuser',
name='_private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='systemuser',
name='_public_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
),
]

View File

@@ -1,6 +1,6 @@
# Generated by Django 2.1.7 on 2019-07-11 12:18
import common.fields.model
import common.db.fields
from django.db import migrations
@@ -14,21 +14,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='adminuser',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='authbook',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='gateway',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
migrations.AlterField(
model_name='systemuser',
name='private_key',
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
),
]

View File

@@ -1,6 +1,6 @@
# Generated by Django 2.2.7 on 2019-12-06 07:26
import common.fields.model
import common.db.fields
from django.db import migrations, models
@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
('meta', common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
('internal', models.BooleanField(default=False, verbose_name='Internal')),
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
],

View File

@@ -1,6 +1,6 @@
# Generated by Django 3.1.6 on 2021-06-05 16:10
import common.fields.model
import common.db.fields
from django.conf import settings
import django.core.validators
from django.db import migrations, models
@@ -58,9 +58,9 @@ class Migration(migrations.Migration):
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),

View File

@@ -3,6 +3,19 @@
from django.db import migrations, models
def create_internal_platform(apps, schema_editor):
model = apps.get_model("assets", "Platform")
db_alias = schema_editor.connection.alias
type_platforms = (
('AIX', 'Unix', None),
)
for name, base, meta in type_platforms:
defaults = {'name': name, 'base': base, 'meta': meta, 'internal': True}
model.objects.using(db_alias).update_or_create(
name=name, defaults=defaults
)
class Migration(migrations.Migration):
dependencies = [
@@ -15,4 +28,5 @@ class Migration(migrations.Migration):
name='number',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
),
migrations.RunPython(create_internal_platform)
]

View File

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

View File

@@ -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

@@ -11,7 +11,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
from common.fields.model import JsonDictTextField
from common.db.fields import JsonDictTextField
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager
@@ -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
@@ -301,7 +301,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
'private_key': auth_user.private_key_file
}
if not with_become:
if not with_become or self.is_windows():
return info
if become_user:

View File

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

View File

@@ -19,7 +19,7 @@ from common.utils import (
)
from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric
from common import fields
from common.db import fields
from orgs.mixins.models import OrgModelMixin

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

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

View File

@@ -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

@@ -133,6 +133,15 @@ class AuthMixin:
self.password = password
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
# 清除认证信息
self._clean_auth_info_if_manual_login_mode()
# 先加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(app_id, user_id)
return
# Remote app
from applications.models import Application
app = get_object_or_none(Application, pk=app_id)
if app and app.category_remote_app:
@@ -141,11 +150,6 @@ class AuthMixin:
return
# Other app
self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(app_id, user_id)
return
# 更新用户名
from users.models import User
user = get_object_or_none(User, pk=user_id) if user_id else None

View File

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

View File

@@ -5,8 +5,8 @@ from assets.models import AuthBook
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin
from .utils import validate_password_contains_left_double_curly_bracket
from common.utils.encode import ssh_pubkey_gen
from common.drf.serializers import SecretReadableMixin
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
@@ -31,10 +31,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
fields = fields_small + fields_fk
extra_kwargs = {
'username': {'required': True},
'password': {
'write_only': True,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
'systemuser_display': {'label': _('System user display')}
@@ -57,7 +53,15 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return attrs
def get_protocols(self, v):
return v.protocols.replace(' ', ', ')
""" protocols 是 queryset 中返回的Post 创建成功后返回序列化时没有这个字段 """
if hasattr(v, 'protocols'):
protocols = v.protocols
elif hasattr(v, 'asset') and v.asset:
protocols = v.asset.protocols
else:
protocols = ''
protocols = protocols.replace(' ', ', ')
return protocols
@classmethod
def setup_eager_loading(cls, queryset):
@@ -70,12 +74,8 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return super().to_representation(instance)
class AccountSecretSerializer(AccountSerializer):
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},
@@ -84,6 +84,22 @@ class AccountSecretSerializer(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

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

View File

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

View File

@@ -6,12 +6,14 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
from common.drf.fields import EncryptedField
from assets.models import Type
from .utils import validate_password_for_ansible
class AuthSerializer(serializers.ModelSerializer):
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384, label=_('Private key'))
def gen_keys(self, private_key=None, password=None):
if private_key is None:
@@ -31,6 +33,13 @@ class AuthSerializer(serializers.ModelSerializer):
class AuthSerializerMixin(serializers.ModelSerializer):
password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
validators=[validate_password_for_ansible]
)
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384
)
passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512,
write_only=True, label=_('Key password')

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

@@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from common.validators import alphanumeric
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.serializers import SecretReadableMixin
from ..models import Domain, Gateway
from .base import AuthSerializerMixin
@@ -67,7 +68,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
}
class GatewayWithAuthSerializer(GatewaySerializer):
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
class Meta(GatewaySerializer.Meta):
extra_kwargs = {
'password': {'write_only': False},

View File

@@ -4,10 +4,12 @@ from django.db.models import Count
from common.mixins.serializers import BulkSerializerMixin
from common.utils import ssh_pubkey_gen
from common.drf.fields import EncryptedField
from common.drf.serializers import SecretReadableMixin
from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from ..models import SystemUser, Asset
from .utils import validate_password_contains_left_double_curly_bracket
from .utils import validate_password_for_ansible
from .base import AuthSerializerMixin
__all__ = [
@@ -23,9 +25,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
"""
系统用户
"""
password = EncryptedField(
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
trim_whitespace=False, validators=[validate_password_for_ansible],
write_only=True
)
auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True)
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint'))
token = EncryptedField(
label=_('Token'), required=False, write_only=True, style={'base_template': 'textarea.html'}
)
applications_amount = serializers.IntegerField(
source='apps_amount', read_only=True, label=_('Apps amount')
)
@@ -46,15 +56,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes']
fields = fields_small + fields_m2m
extra_kwargs = {
'password': {
"write_only": True,
'trim_whitespace': False,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'cmd_filters': {"required": False, 'label': _('Command filter')},
'public_key': {"write_only": True},
'private_key': {"write_only": True},
'token': {"write_only": True},
'nodes_amount': {'label': _('Nodes amount')},
'assets_amount': {'label': _('Assets amount')},
'login_mode_display': {'label': _('Login mode display')},
@@ -248,7 +252,7 @@ class MiniSystemUserSerializer(serializers.ModelSerializer):
fields = SystemUserSerializer.Meta.fields_mini
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
class SystemUserWithAuthInfoSerializer(SecretReadableMixin, SystemUserSerializer):
class Meta(SystemUserSerializer.Meta):
fields_mini = ['id', 'name', 'username']
fields_write_only = ['password', 'public_key', 'private_key']
@@ -264,6 +268,9 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
'assets_amount': {'label': _('Asset')},
'login_mode_display': {'label': _('Login mode display')},
'created_by': {'read_only': True},
'password': {'write_only': False},
'private_key': {'write_only': False},
'token': {'write_only': False}
}

View File

@@ -2,8 +2,16 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
def validate_password_contains_left_double_curly_bracket(password):
def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """
# validate password contains left double curly bracket
# check password not contains `{{`
# Ansible 推送的时候不支持
if '{{' in password:
raise serializers.ValidationError(_('Password can not contains `{{` '))
# Ansible Windows 推送的时候不支持
if "'" in password:
raise serializers.ValidationError(_("Password can not contains `'` "))
if '"' in password:
raise serializers.ValidationError(_('Password can not contains `"` '))

View File

@@ -1,18 +1,19 @@
import os
import time
import pandas as pd
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 = {}
@@ -48,20 +49,44 @@ class BaseAccountHandler:
_fields = cls.get_header_fields(v)
header_fields.update(_fields)
else:
header_fields[field] = v.label
header_fields[field] = str(v.label)
return header_fields
@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] = 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,24 +97,29 @@ class AssetAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_df(cls):
df_dict = 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 df_dict
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)
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
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 df_dict
return data_map
class AppAccountHandler(BaseAccountHandler):
@@ -101,19 +131,37 @@ class AppAccountHandler(BaseAccountHandler):
return filename
@classmethod
def create_df(cls):
df_dict = 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)
df_dict[sheet_name].append(row)
for k, v in df_dict.items():
df_dict[k] = pd.DataFrame(v)
logger.info('\n\033[33m- 共收集{}条应用账号\033[0m'.format(accounts.count()))
return df_dict
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
handler_map = {
@@ -137,20 +185,27 @@ 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
df_dict = handler.create_df()
if not df_dict:
data_map = handler.create_data_map(system_user_dict)
if not data_map:
continue
filename = handler.get_filename(self.plan_name)
with pd.ExcelWriter(filename) as w:
for sheet, df in df_dict.items():
sheet = sheet.replace(' ', '-')
getattr(df, 'to_excel')(w, sheet_name=sheet, index=False)
wb = Workbook(filename)
for sheet, data in data_map.items():
ws = wb.create_sheet(str(sheet))
for row in data:
ws.append(row)
wb.save(filename)
files.append(filename)
timedelta = round((time.time() - time_start), 2)
logger.info('步骤完成: 用时 {}s'.format(timedelta))

View File

@@ -32,17 +32,18 @@ def _dump_args(args: dict):
return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty])
def get_push_unixlike_system_user_tasks(system_user, username=None):
comment = system_user.name
def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
algorithm = kwargs.get('algorithm')
if username is None:
username = system_user.username
comment = system_user.name
if system_user.username_same_with_user:
from users.models import User
user = User.objects.filter(username=username).only('name', 'username').first()
if user:
comment = f'{system_user.name}[{str(user)}]'
comment = comment.replace(' ', '')
password = system_user.password
public_key = system_user.public_key
@@ -104,7 +105,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
'module': 'user',
'args': 'name={} shell={} state=present password={}'.format(
username, system_user.shell,
encrypt_password(password, salt="K3mIlKK"),
encrypt_password(password, salt="K3mIlKK", algorithm=algorithm),
),
}
})
@@ -138,7 +139,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
return tasks
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None, **kwargs):
if username is None:
username = system_user.username
password = system_user.password
@@ -176,7 +177,7 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
return tasks
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
def get_push_system_user_tasks(system_user, platform="unixlike", username=None, algorithm=None):
"""
获取推送系统用户的 ansible 命令,跟资产无关
:param system_user:
@@ -190,16 +191,16 @@ def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
}
get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
if not system_user.username_same_with_user:
return get_tasks(system_user)
return get_tasks(system_user, algorithm=algorithm)
tasks = []
# 仅推送这个username
if username is not None:
tasks.extend(get_tasks(system_user, username))
tasks.extend(get_tasks(system_user, username, algorithm=algorithm))
return tasks
users = system_user.users.all().values_list('username', flat=True)
print(_("System user is dynamic: {}").format(list(users)))
for _username in users:
tasks.extend(get_tasks(system_user, _username))
tasks.extend(get_tasks(system_user, _username, algorithm=algorithm))
return tasks
@@ -244,7 +245,11 @@ def push_system_user_util(system_user, assets, task_name, username=None):
for u in usernames:
for a in _assets:
system_user.load_asset_special_auth(a, u)
tasks = get_push_system_user_tasks(system_user, platform, username=u)
algorithm = 'des' if a.platform.name == 'AIX' else 'sha512'
tasks = get_push_system_user_tasks(
system_user, platform, username=u,
algorithm=algorithm
)
run_task(tasks, [a])
@@ -269,7 +274,7 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
# if username is None:
# username = system_user.username
task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format(
system_user.name, username, asset
system_user.name, username or system_user.username, asset
)
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)

View File

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

View File

@@ -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

@@ -3,3 +3,23 @@
from django.utils.translation import ugettext_lazy as _
DEFAULT_CITY = _("Unknown")
MODELS_NEED_RECORD = (
# users
'User', 'UserGroup',
# acls
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
# assets
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
'CommandFilter', 'Platform', 'AuthBook',
# applications
'Application',
# orgs
'Organization',
# settings
'Setting',
# perms
'AssetPermission', 'ApplicationPermission',
# xpack
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
)

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

@@ -0,0 +1,18 @@
# Generated by Django 3.1.14 on 2022-05-05 11:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0013_auto_20211130_1037'),
]
operations = [
migrations.AlterField(
model_name='operatelog',
name='action',
field=models.CharField(choices=[('create', 'Create'), ('view', 'View'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action'),
),
]

View File

@@ -49,10 +49,12 @@ class FTPLog(OrgModelMixin):
class OperateLog(OrgModelMixin):
ACTION_CREATE = 'create'
ACTION_VIEW = 'view'
ACTION_UPDATE = 'update'
ACTION_DELETE = 'delete'
ACTION_CHOICES = (
(ACTION_CREATE, _("Create")),
(ACTION_VIEW, _("View")),
(ACTION_UPDATE, _("Update")),
(ACTION_DELETE, _("Delete"))
)

View File

@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
#
import time
from django.db.models.signals import (
post_save, m2m_changed, pre_delete
)
@@ -21,7 +23,7 @@ from jumpserver.utils import current_request
from users.models import User
from users.signals import post_user_change_password
from terminal.models import Session, Command
from .utils import write_login_log
from .utils import write_login_log, create_operate_log
from . import models, serializers
from .models import OperateLog
from orgs.utils import current_org
@@ -36,26 +38,6 @@ logger = get_logger(__name__)
sys_logger = get_syslogger(__name__)
json_render = JSONRenderer()
MODELS_NEED_RECORD = (
# users
'User', 'UserGroup',
# acls
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
# assets
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
'CommandFilter', 'Platform', 'AuthBook',
# applications
'Application',
# orgs
'Organization',
# settings
'Setting',
# perms
'AssetPermission', 'ApplicationPermission',
# xpack
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
)
class AuthBackendLabelMapping(LazyObject):
@staticmethod
@@ -69,6 +51,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_SSO] = _('SSO')
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _('Auth Token')
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _('WeCom')
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _('FeiShu')
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _('DingTalk')
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _('Temporary token')
return backend_label_mapping
@@ -80,28 +63,6 @@ class AuthBackendLabelMapping(LazyObject):
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
def create_operate_log(action, sender, resource):
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
return
model_name = sender._meta.object_name
if model_name not in MODELS_NEED_RECORD:
return
with translation.override('en'):
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
data = {
"user": str(user), 'action': action, 'resource_type': resource_type,
'resource': str(resource), 'remote_addr': remote_addr,
}
with transaction.atomic():
try:
models.OperateLog.objects.create(**data)
except Exception as e:
logger.error("Create operate log error: {}".format(e))
M2M_NEED_RECORD = {
User.groups.through._meta.object_name: (
_('User and Group'),
@@ -316,6 +277,7 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
logger.debug('User login success: {}'.format(user.username))
check_different_city_login_if_need(user, request)
data = generate_data(user.username, request, login_type=login_type)
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log(**data)

View File

@@ -29,7 +29,7 @@ def clean_ftp_log_period():
now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days)
FTPLog.objects.filter(datetime__lt=expired_day).delete()
FTPLog.objects.filter(date_start__lt=expired_day).delete()
@register_as_period_task(interval=3600*24)

View File

@@ -1,9 +1,17 @@
import csv
import codecs
from django.http import HttpResponse
from .const import DEFAULT_CITY
from common.utils import validate_ip, get_ip_city
from django.http import HttpResponse
from django.db import transaction
from django.utils import translation
from audits.models import OperateLog
from common.utils import validate_ip, get_ip_city, get_request_ip, get_logger
from jumpserver.utils import current_request
from .const import DEFAULT_CITY, MODELS_NEED_RECORD
logger = get_logger(__name__)
def get_excel_response(filename):
@@ -36,3 +44,25 @@ def write_login_log(*args, **kwargs):
city = get_ip_city(ip) or DEFAULT_CITY
kwargs.update({'ip': ip, 'city': city})
UserLoginLog.objects.create(**kwargs)
def create_operate_log(action, sender, resource):
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
return
model_name = sender._meta.object_name
if model_name not in MODELS_NEED_RECORD:
return
with translation.override('en'):
resource_type = sender._meta.verbose_name
remote_addr = get_request_ip(current_request)
data = {
"user": str(user), 'action': action, 'resource_type': resource_type,
'resource': str(resource), 'remote_addr': remote_addr,
}
with transaction.atomic():
try:
OperateLog.objects.create(**data)
except Exception as e:
logger.error("Create operate log error: {}".format(e))

View File

@@ -5,6 +5,7 @@ from .connection_token import *
from .token import *
from .mfa import *
from .access_key import *
from .confirm import *
from .login_confirm import *
from .sso import *
from .wecom import *

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
#
import time
from django.utils.translation import ugettext_lazy as _
from rest_framework.generics import RetrieveAPIView, CreateAPIView
from rest_framework.response import Response
from rest_framework import status
from common.permissions import IsValidUser, UserConfirmation
from ..const import ConfirmType
from ..serializers import ConfirmSerializer
class ConfirmBindORUNBindOAuth(RetrieveAPIView):
permission_classes = (UserConfirmation.require(ConfirmType.ReLogin),)
def retrieve(self, request, *args, **kwargs):
return Response('ok')
class ConfirmApi(RetrieveAPIView, CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = ConfirmSerializer
def get_confirm_backend(self, confirm_type):
backend_classes = ConfirmType.get_can_confirm_backend_classes(confirm_type)
if not backend_classes:
return
for backend_cls in backend_classes:
backend = backend_cls(self.request.user, self.request)
if not backend.check():
continue
return backend
def retrieve(self, request, *args, **kwargs):
confirm_type = request.query_params.get('confirm_type')
backend = self.get_confirm_backend(confirm_type)
if backend is None:
msg = _('This action require verify your MFA')
return Response(data={'error': msg}, status=status.HTTP_404_NOT_FOUND)
data = {
'confirm_type': backend.name,
'content': backend.content,
}
return Response(data=data)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
validated_data = serializer.validated_data
confirm_type = validated_data.get('confirm_type')
mfa_type = validated_data.get('mfa_type')
secret_key = validated_data.get('secret_key')
backend = self.get_confirm_backend(confirm_type)
ok, msg = backend.authenticate(secret_key, mfa_type)
if ok:
request.session['CONFIRM_LEVEL'] = ConfirmType.values.index(confirm_type) + 1
request.session['CONFIRM_TIME'] = int(time.time())
return Response('ok')
return Response({'error': msg}, status=400)

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -27,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
def create(self, request, *args, **kwargs):
self.create_session_if_need()
# 如果认证没有过,检查账号密码
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
user = self.check_user_auth_if_need()
user = self.get_user_or_auth(serializer.validated_data)
self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user)

View File

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

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

@@ -198,6 +198,6 @@ class SignatureAuthentication(signature.SignatureAuthentication):
return None, None
user, secret = key.user, str(key.secret)
return user, secret
except AccessKey.DoesNotExist:
except (AccessKey.DoesNotExist, exceptions.ValidationError):
return None, None

View File

@@ -157,6 +157,8 @@ class LDAPUser(_LDAPUser):
def _populate_user_from_attributes(self):
for field, attr in self.settings.USER_ATTR_MAP.items():
if field in ['groups']:
continue
try:
value = self.attrs[attr][0]
value = value.strip()

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,13 +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__)
@@ -39,17 +43,22 @@ class UserMixin:
logger.debug(log_prompt.format('start'))
sub = claims['sub']
name = claims.get('name', sub)
username = claims.get('preferred_username', sub)
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
logger.debug(
log_prompt.format(
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
)
)
# Construct user attrs value
user_attrs = {}
for field, attr in settings.AUTH_OPENID_USER_ATTR_MAP.items():
user_attrs[field] = claims.get(attr, sub)
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))
username = user_attrs.get('username')
name = user_attrs.get('name')
user, created = get_user_model().objects.get_or_create(
username=username, defaults={"name": name, "email": email}
username=username, defaults=user_attrs
)
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
logger.debug(log_prompt.format("Send signal => openid create or update user"))
@@ -103,21 +112,44 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
# Prepares the token payload that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token payload'))
"""
The reason for need not client_id and client_secret in token_payload.
OIDC protocol indicate client's token_endpoint_auth_method only accept one type in
- client_secret_basic
- client_secret_post
- client_secret_jwt
- private_key_jwt
- none
If the client offer more than one auth method type to OIDC, OIDC will auth client failed.
OIDC default use client_secret_basic,
this type only need in headers add Authorization=Basic xxx.
More info see: https://github.com/jumpserver/jumpserver/issues/8165
More info see: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
"""
token_payload = {
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': build_absolute_uri(
'redirect_uri': build_absolute_uri_for_oidc(
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
)
}
# Prepares the token headers that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token headers'))
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET)
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())}
if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post':
token_payload.update({
'client_id': settings.AUTH_OPENID_CLIENT_ID,
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
})
headers = None
else:
# Prepares the token headers that will be used to request an authentication token to the
# token endpoint of the OIDC provider.
logger.debug(log_prompt.format('Prepares token headers'))
basic_token = "{}:{}".format(
settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET
)
headers = {
"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())
}
# Calls the token endpoint.
logger.debug(log_prompt.format('Call the token endpoint'))
@@ -182,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
@@ -240,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
@@ -258,13 +295,19 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
try:
claims_response.raise_for_status()
claims = claims_response.json()
preferred_username = claims.get('preferred_username')
if preferred_username and \
preferred_username.lower() == username.lower() and \
preferred_username != username:
return
except Exception as e:
error = "Json claims response error, claims response " \
"content is: {}, error is: {}".format(claims_response.content, str(e))
logger.debug(log_prompt.format(error))
logger.debug(log_prompt.format('Send signal => openid user login failed'))
openid_user_login_failed.send(
sender=self.__class__, request=request, username=username, reason=error
user_auth_failed.send(
sender=self.__class__, request=request, username=username, reason=error,
backend=settings.AUTH_BACKEND_OIDC_PASSWORD
)
return
@@ -276,15 +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

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