Compare commits

...

419 Commits

Author SHA1 Message Date
fit2bot
d1c11eb2d7 feat: Update v2.28.14 2023-05-09 14:04:24 +08:00
Bai
8a77a7b8b5 perf: 优化系统用户和资产,只有协议包含时才进行关联和推送 2023-05-08 19:07:59 +08:00
Bai
7eed182627 perf: 优化系统用户和资产,只有协议包含时才进行关联和推送 2023-05-08 18:14:19 +08:00
feng626
ec847d3ecb Fix v2.28.7 ssh key (#10399)
* feat: Update v2.28.7

* fix: 修复旧 ssh 私钥,解析失败的问题

* perf: 解决历史版本中因保存密码,造成 ssh 私钥解析失败问题

* fix: 动态用户可执行批量任务

---------

Co-authored-by: fit2bot <fit2bot@fit2cloud.com>
Co-authored-by: Eric <xplzv@126.com>
2023-05-08 14:28:23 +08:00
Eric
a0994e2e12 fix: 修复旧 ssh 私钥,解析失败的问题 2023-04-27 17:49:52 +08:00
ibuler
17e3ddda05 perf: 支持 rdp console 2023-04-25 14:35:28 +08:00
Bai
6e2e92be5e fix: perf: 修改OAuth2的access_token前缀格式 2023-04-20 14:59:54 +08:00
老广
e90d8c8561 Merge pull request #10121 from jumpserver/pr@v2.28@fix_ldapuserimport_v2.28
fix: 修复 LDAP 导入用户时指定其他组织,还会导入到 Default 组织的问题
2023-04-03 17:09:58 +08:00
Bai
cf972942fa fix: 修复 LDAP 导入用户时指定其他组织,还会导入到 Default 组织的问题 2023-04-03 16:54:14 +08:00
老广
72e35d5553 Merge pull request #10046 from jumpserver/pr@v2.28@fix_systemuserlist
fix: 去掉系统用户序列类中的资产数量和应用数量
2023-03-23 14:13:33 +08:00
Bai
0ba84e7e18 fix: 去掉系统用户序列类中的资产数量和应用数量 2023-03-23 11:52:18 +08:00
Bai
fbc5ae1b9b fix: 修复日志记录到syslog时中文编码问题 2023-03-15 19:45:40 +08:00
halo
2fcf045826 fix: 修复celery api 报错 2023-03-15 15:36:54 +08:00
Bai
32cba4f2a1 fix: 修复语言切换问题 2023-03-14 14:40:23 +08:00
Jiangjie.Bai
b76aa3b259 feat: 支持飞书国际版(lark) (#9916) 2023-03-10 15:49:15 +08:00
Eric
3f9a17347d fix: 修复存储故障造成的录像获取失败问题 2023-03-09 11:52:23 +08:00
老广
c01d1973d9 Merge pull request #9877 from jumpserver/pr@v2.28@fix_operatelog_hide_sth
fix: 操作日志显示用户加密后的密文,及日期格式调整
2023-03-08 14:05:20 +08:00
jiangweidong
b216a9abc0 fix: 操作日志显示用户加密后的密文,及日期格式调整 2023-03-08 11:36:48 +08:00
Bai
c628ba1c4b fix: 修复翻译 2023-03-02 12:25:29 +08:00
fit2bot
ebbae36c49 perf: k8s update api (#9833)
Co-authored-by: feng <1304903146@qq.com>
2023-03-02 11:03:29 +08:00
Bai
69ef25666e fix: 修复认证MFA失败次数清空问题 2023-02-24 14:44:32 +08:00
Bai
d0475397d0 fix: 修复第三方用户登录复核可以跳过的问题 2023-02-09 19:50:13 +08:00
老广
dad45e7ace Merge pull request #9233 from jumpserver/pr@v2.28@perf_dockerfile
build(deps): 优化龙芯构建依赖包
2022-12-22 12:43:37 +08:00
吴小白
720f9cd397 build(deps): 优化龙芯构建依赖包 2022-12-22 12:40:53 +08:00
Bai
81dee0c403 perf: 修改方法名称 check_db_port_mapper 2022-12-22 10:43:07 +08:00
老广
105ef791b8 Merge pull request #9230 from jumpserver/pr@v2.28@v2.28_perf_dbportmapper
fix: 修改db_port_mapper策略; 启动时进行check校验;
2022-12-21 18:59:47 +08:00
Bai
a19c0bde60 fix: 修改db_port_mapper策略; 启动时进行check校验; 2022-12-21 18:43:44 +08:00
halo
3996daf4a7 fix: 导入翻译引用 2022-12-16 11:47:39 +08:00
halo
ac235f788e perf: 优化oauth2的服务地址参数拼接 2022-12-16 11:47:39 +08:00
老广
67e334bf43 Merge pull request #9213 from jumpserver/pr@v2.28@perf_fingerprint
fix: 修复非 ssh 协议的系统用户存在错误私钥,引发的解析问题
2022-12-15 15:25:40 +08:00
Eric
f7f9fb1bdf fix: 修复非 ssh 协议的系统用户存在错误私钥,引发的解析问题 2022-12-15 15:17:36 +08:00
Eric
8979228e0b fix: 修复 ssh 私钥推送等问题 2022-12-13 16:21:54 +08:00
吴小白
024beca690 Merge pull request #9200 from jumpserver/pr@v2.28@perf_support_openid_pkce
perf: OpenID支持PKCE方式对接
2022-12-13 16:12:17 +08:00
jiangweidong
5c0359e394 perf: OpenID支持PKCE方式对接 2022-12-13 15:11:21 +08:00
feng
4ce4bde368 fix: ticket xss inject 2022-12-12 17:03:29 +08:00
halo
809bad271a fix: 密钥指纹参数 2022-12-09 13:41:38 +08:00
Eric
d3bfc03849 fix: 替换解析公钥的方式 2022-12-08 16:57:22 +08:00
Bai
04c0121b37 fix: 降级 Djanog==3.2.15 2022-12-08 14:53:40 +08:00
jiangweidong
b97b50ab31 perf: 支持sentinel开启ssl(Sentinel和Redis公用一套证书,无额外增加配置项) 2022-12-08 12:54:58 +08:00
Eric
d8a8c8153b fix: TraditionalOpenSSL private ssh key 2022-12-08 11:03:52 +08:00
Eric
a68ad7be68 perf: support ed25519 SSH Key
fix: codacy ci
fix: password use bytes
2022-12-08 11:03:52 +08:00
Bai
4041f1aeec fix: 修改 random_string 方法,支持只生成随机数字 2022-12-01 20:13:47 +08:00
feng
59388655ea fix: es 默认存储500 2022-11-18 17:04:43 +08:00
Bai
ef7463c588 fix: flower db file 持久化存储flower信息 2022-11-18 15:36:21 +08:00
Bryan
7e7d6d94e6 fix: 修复 channels-redis 库升级导致 ws 查看任务日志失败的问题; 修改 REDIS_LAYERS_HOST 变量; 修改 Channel SSL 配置项; 2022-11-18 15:26:44 +08:00
Jiangjie.Bai
6febc104de Merge pull request #9096 from jumpserver/dev
v2.28.0
2022-11-17 17:43:44 +08:00
Jiangjie.Bai
e629e6fb3f fix: i18n 2022-11-17 17:40:35 +08:00
halo
1b99a7e06f fix: 账户备份邮件发送找不到文件bug 2022-11-17 17:39:53 +08:00
Jiangjie.Bai
733b95ee99 Merge pull request #9089 from jumpserver/dev
v2.28.0-rc5
2022-11-17 14:14:18 +08:00
ibuler
a42641ca9a fix: 修复 redis 连接导致的 pub sub 错误 2022-11-17 14:07:43 +08:00
halo
90c48d303e fix: celery任务时间字段差8小时问题 2022-11-17 14:06:31 +08:00
jiangweidong
949166eaed fix: Redis哨兵切换时导致core和celery组件监听线程异常退出 2022-11-17 12:19:35 +08:00
Jiangjie.Bai
b179264127 Merge pull request #9080 from jumpserver/dev
v2.28.0-rc4
2022-11-16 21:05:05 +08:00
Jiangjie.Bai
a2b9a5cee3 fix: 修复删除数据库由于端口数量限制导致不能删除的问题 2022-11-16 21:04:12 +08:00
Jiangjie.Bai
9e80b70c12 fix: 修改用户前一文件 2022-11-16 20:48:56 +08:00
jiangweidong
2454a07d38 feat: 支持具有超级工单权限的用户申请工单可以指定申请人 2022-11-16 20:42:46 +08:00
Jiangjie.Bai
ad7647c0cc fix: 修复第三方用户忘记密码页面的提示,不允许修改 2022-11-16 20:40:46 +08:00
jiangweidong
18986bc805 perf: 增加clickhouse端口帮助文案及注释 2022-11-16 10:53:20 +08:00
fit2bot
157d81f117 fix: 翻译 (#9067)
Co-authored-by: feng <1304903146@qq.com>
2022-11-15 18:09:06 +08:00
fit2bot
8558204450 perf: 修改工单授权过期时间 (#9065)
Co-authored-by: feng <1304903146@qq.com>
2022-11-15 17:30:29 +08:00
Jiangjie.Bai
c18388e27a Merge pull request #9060 from jumpserver/dev
v2.28.0-rc3
2022-11-14 18:02:44 +08:00
jiangweidong
b6f86e8fb6 perf: 优化忘记密码,用户无手机号时无法使用手机号重置密码 2022-11-14 18:01:07 +08:00
fit2bot
7280c6726e fix: 删除用户 record 500 (#9058)
Co-authored-by: feng <1304903146@qq.com>
2022-11-14 17:00:10 +08:00
Jiangjie.Bai
6f135c34c1 fix: 修复测试邮箱服务器字段为null的问题 2022-11-14 16:24:05 +08:00
Jiangjie.Bai
52830db500 Merge pull request #9052 from jumpserver/dev
v2.28.0-rc2
2022-11-14 09:54:50 +08:00
jiangweidong
6411642ced perf: 优化操作日志一些内容 2022-11-14 09:50:42 +08:00
jiangweidong
791b175465 perf: 优化找回密码页面 (#9045)
* perf: 优化找回密码页面

* perf: 优化找回密码操作页面
2022-11-14 09:48:55 +08:00
jiangweidong
d29e8317e5 feat: 添加操作日志ES默认的配置信息 2022-11-14 09:40:12 +08:00
jiangweidong
a868751edd perf: 补充reids ssl配置项 2022-11-14 09:40:12 +08:00
jiangweidong
e03f49e52b perf: 添加Redis哨兵配置的默认值 2022-11-14 09:40:12 +08:00
Jiangjie.Bai
2324cdc14e Merge pull request #9040 from jumpserver/dev
v2.28.0-rc1
2022-11-10 17:48:40 +08:00
feng
f3c90c6bbd perf: login mfa style 2022-11-10 17:38:10 +08:00
jiangweidong
ac6ffb24ee feat: 云资产同步支持选择IP类型-补充翻译 2022-11-10 17:35:16 +08:00
jiangweidong
6f3ae4f73f perf: 修改Redis哨兵配置参数名称 2022-11-10 17:13:46 +08:00
fit2bot
1345998acd perf: download add open ssh (#8886)
Co-authored-by: feng626 <1304903146@qq.com>
2022-11-10 16:00:06 +08:00
Jiangjie.Bai
6812cbe314 perf: 优化优先使用系统设置中的 rdp_resolution 配置 2022-11-07 18:47:06 +08:00
Jiangjie.Bai
3bba8e4043 fix: 修改翻译文件 2022-11-04 15:50:38 +08:00
Jiangjie.Bai
8abf954015 fix: 处理一波迁移文件 2022-11-04 15:36:45 +08:00
Jiangjie.Bai
1a77dc44cd fix: django.mo 2022-11-04 15:00:58 +08:00
fit2bot
13ae50f5d0 feat: 自定义MFA认证逻辑 (#8891)
* feat: 自定义MFA认证

* feat: 修改翻译信息

Co-authored-by: halo <wuyihuangw@gmail.com>
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-11-04 14:29:15 +08:00
jiangweidong
2029e9f8df feat: 重构操作日志 (#8941)
* feat:重构操作日志模块

* feat: 改密计划增加操作日志记录

* feat: 支持操作日志接入ES,且接口limit支持自定义限制大小

* feat:翻译

* feat: 生成迁移文件

* feat: 优化迁移文件

* feat: 优化多对多日志记录

* feat: 命令存储ES部分和日志存储ES部分代码优化

* feat: 优化敏感字段脱敏

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-11-04 14:22:38 +08:00
jiangweidong
1e97a23bc5 feat: 忘记密码支持手机短信找回,并修改邮箱方式和手机方式统一 (#8960)
* feat: 忘记密码支持通过手机找回,邮箱方式修改为和手机方式一致

* feat: 翻译

* feat: 修改翻译

* fix: 还原

Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-11-04 13:56:55 +08:00
jiangweidong
262d070f3c feat: 支持纳管 clickhouse[Web Terminal] 2022-11-04 11:41:54 +08:00
dependabot[bot]
4ceaba60ed build(deps): bump django from 3.2.15 to 3.2.16 in /requirements
Bumps [django](https://github.com/django/django) from 3.2.15 to 3.2.16.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.2.15...3.2.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-04 11:13:51 +08:00
jiangweidong
40b7331ec0 feat: 支持网域网关应用改密-添加依赖 2022-11-04 11:13:07 +08:00
jiangweidong
04cda3da0b feat: 云同步支持金山云 2022-11-04 11:09:02 +08:00
jiangweidong
1951c595ed fix: 修改数据类型 2022-11-01 17:07:52 +08:00
jiangweidong
c74584f63c fix: Redis Sentinel参数类型问题,导致报错 2022-11-01 17:07:52 +08:00
jiangweidong
c295f44d9c perf: 支持部署在开启认证的Redis Sentinel 2022-11-01 14:55:33 +08:00
jiangweidong
ead824a03c feat: 支持部署在Redis哨兵集群上 2022-10-31 14:34:53 +08:00
halo
50be7c6fc8 fix: rdp客户端拉起后窗口标题中文乱码 2022-10-28 16:29:28 +08:00
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
445 changed files with 14523 additions and 8593 deletions

View File

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

1
.gitattributes vendored
View File

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

View File

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

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

95
Dockerfile.loong64 Normal file
View File

@@ -0,0 +1,95 @@
FROM python:3.8-slim as stage-build
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
ARG TARGETARCH
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
libpq-dev \
libffi-dev \
libjpeg-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
libaio-dev \
openssh-client \
sshpass"
ARG TOOLS=" \
ca-certificates \
curl \
default-mysql-client \
iputils-ping \
locales \
procps \
redis-tools \
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"]

1
GITSHA Normal file
View File

@@ -0,0 +1 @@
8a77a7b8b57c5b0fb8455f3b34fc9b6f3c3a24c1

View File

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

View File

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

View File

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

View File

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

56
Vagrantfile vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.serializers import BulkModelSerializer
from common.drf.serializers import MethodSerializer

View File

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

View File

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

@@ -27,6 +27,7 @@ class AppType(models.TextChoices):
sqlserver = 'sqlserver', 'SQLServer'
redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB'
clickhouse = 'clickhouse', 'ClickHouse'
# remote-app category
chrome = 'chrome', 'Chrome'
@@ -42,7 +43,7 @@ class AppType(models.TextChoices):
return {
AppCategory.db: [
cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
cls.sqlserver, cls.redis, cls.mongodb
cls.sqlserver, cls.redis, cls.mongodb, cls.clickhouse
],
AppCategory.remote_app: [
cls.chrome, cls.mysql_workbench,
@@ -82,4 +83,4 @@ class AppType(models.TextChoices):
if AppCategory.is_xpack(category):
return True
return tp in ['oracle', 'postgresql', 'sqlserver']
return tp in ['oracle', 'postgresql', 'sqlserver', 'clickhouse']

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

@@ -6,6 +6,7 @@ from .pgsql import *
from .sqlserver import *
from .redis import *
from .mongodb import *
from .clickhouse import *
from .chrome import *
from .mysql_workbench import *

View File

@@ -23,7 +23,7 @@ class ChromeSerializer(RemoteAppSerializer):
)
chrome_password = EncryptedField(
max_length=128, allow_blank=True, required=False,
label=_('Chrome password'), allow_null=True
label=_('Chrome password'), allow_null=True, encrypted_key='chrome_password'
)

View File

@@ -0,0 +1,16 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..application_category import DBSerializer
__all__ = ['ClickHouseSerializer']
class ClickHouseSerializer(DBSerializer):
port = serializers.IntegerField(
default=9000, label=_('Port'), allow_null=True,
help_text=_(
'Typically, the port is 9000'
'the HTTP interface and the native interface use different ports'
),
)

View File

@@ -31,6 +31,7 @@ type_serializer_classes_mapping = {
const.AppType.sqlserver.value: application_type.SQLServerSerializer,
const.AppType.redis.value: application_type.RedisSerializer,
const.AppType.mongodb.value: application_type.MongoDBSerializer,
const.AppType.clickhouse.value: application_type.ClickHouseSerializer,
# cloud
const.AppType.k8s.value: application_type.K8SSerializer
}

View File

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

View File

@@ -1,18 +1,14 @@
# -*- coding: utf-8 -*-
from urllib3.exceptions import MaxRetryError
from urllib.parse import urlencode
from kubernetes import client
from kubernetes.client import api_client
from kubernetes.client.api import core_v1_api
from kubernetes import client
from kubernetes.client.exceptions import ApiException
from rest_framework.generics import get_object_or_404
from common.utils import get_logger
from common.tree import TreeNode
from assets.models import SystemUser
from common.tree import TreeNode
from common.utils import get_logger
from .. import const
logger = get_logger(__file__)
@@ -23,7 +19,8 @@ class KubernetesClient:
self.url = url
self.token = token
def get_api(self):
@property
def api(self):
configuration = client.Configuration()
configuration.host = self.url
configuration.verify_ssl = False
@@ -32,63 +29,46 @@ class KubernetesClient:
api = core_v1_api.CoreV1Api(c)
return api
def get_namespace_list(self):
api = self.get_api()
namespace_list = []
for ns in api.list_namespace().items:
namespace_list.append(ns.metadata.name)
return namespace_list
def get_namespaces(self):
namespaces = []
resp = self.api.list_namespace()
for ns in resp.items:
namespaces.append(ns.metadata.name)
return namespaces
def get_services(self):
api = self.get_api()
ret = api.list_service_for_all_namespaces(watch=False)
for i in ret.items:
print("%s \t%s \t%s \t%s \t%s \n" % (
i.kind, i.metadata.namespace, i.metadata.name, i.spec.cluster_ip, i.spec.ports))
def get_pods(self, namespace):
pods = []
resp = self.api.list_namespaced_pod(namespace)
for pd in resp.items:
pods.append(pd.metadata.name)
return pods
def get_pod_info(self, namespace, pod):
api = self.get_api()
resp = api.read_namespaced_pod(namespace=namespace, name=pod)
return resp
def get_containers(self, namespace, pod_name):
containers = []
resp = self.api.read_namespaced_pod(pod_name, namespace)
for container in resp.spec.containers:
containers.append(container.name)
return containers
def get_pod_logs(self, namespace, pod):
api = self.get_api()
log_content = api.read_namespaced_pod_log(pod, namespace, pretty=True, tail_lines=200)
return log_content
@classmethod
def run(cls, asset, secret, tp='namespace'):
k8s_url = f'{asset.address}'
k8s = cls(k8s_url, secret)
func_name = f'get_{tp}s'
if hasattr(k8s, func_name):
return getattr(k8s, func_name)()
return []
def get_pods(self):
api = self.get_api()
try:
ret = api.list_pod_for_all_namespaces(watch=False, _request_timeout=(3, 3))
except MaxRetryError:
logger.warning('Kubernetes connection timed out')
return
except ApiException as e:
if e.status == 401:
logger.warning('Kubernetes User not authenticated')
else:
logger.warning(e)
return
data = {}
for i in ret.items:
namespace = i.metadata.namespace
pod_info = {
'pod_name': i.metadata.name,
'containers': [j.name for j in i.spec.containers]
}
if namespace in data:
data[namespace].append(pod_info)
else:
data[namespace] = [pod_info, ]
return data
@staticmethod
def get_kubernetes_data(app_id, system_user_id):
@classmethod
def get_kubernetes_data(cls, app_id, system_user_id, tp, *args):
from ..models import Application
app = get_object_or_404(Application, id=app_id)
system_user = get_object_or_404(SystemUser, id=system_user_id)
k8s = KubernetesClient(app.attrs['cluster'], system_user.token)
return k8s.get_pods()
k8s = cls(app.attrs['cluster'], system_user.token)
func_name = f'get_{tp}s'
if hasattr(k8s, func_name):
return getattr(k8s, func_name)(*args)
return []
class KubernetesTree:
@@ -118,11 +98,10 @@ class KubernetesTree:
)
return node
def as_namespace_pod_tree_node(self, name, meta, type, counts=0, is_container=False):
def as_namespace_pod_tree_node(self, name, meta, type, is_container=False):
from ..models import ApplicationTreeNodeMixin
i = ApplicationTreeNodeMixin.create_tree_id(self.tree_id, type, name)
meta.update({type: name})
name = name if is_container else f'{name}({counts})'
node = self.create_tree_node(
i, self.tree_id, name, type, meta, icon='cloud', is_container=is_container
)
@@ -157,30 +136,30 @@ class KubernetesTree:
system_user_id = parent_info.get('system_user_id')
tree_nodes = []
data = KubernetesClient.get_kubernetes_data(app_id, system_user_id)
if not data:
return tree_nodes
if pod_name:
for container in next(
filter(
lambda x: x['pod_name'] == pod_name, data[namespace]
)
)['containers']:
tp = 'container'
containers = KubernetesClient.get_kubernetes_data(
app_id, system_user_id, tp, namespace, pod_name
)
for container in containers:
container_node = self.as_namespace_pod_tree_node(
container, parent_info, 'container', is_container=True
container, parent_info, tp, is_container=True
)
tree_nodes.append(container_node)
elif namespace:
for pod in data[namespace]:
pod_nodes = self.as_namespace_pod_tree_node(
pod['pod_name'], parent_info, 'pod', len(pod['containers'])
tp = 'pod'
pods = KubernetesClient.get_kubernetes_data(app_id, system_user_id, tp, namespace)
for pod in pods:
pod_node = self.as_namespace_pod_tree_node(
pod, parent_info, tp
)
tree_nodes.append(pod_nodes)
tree_nodes.append(pod_node)
elif system_user_id:
for namespace, pods in data.items():
tp = 'namespace'
namespaces = KubernetesClient.get_kubernetes_data(app_id, system_user_id, tp)
for namespace in namespaces:
namespace_node = self.as_namespace_pod_tree_node(
namespace, parent_info, 'namespace', len(pods)
namespace, parent_info, tp
)
tree_nodes.append(namespace_node)
return tree_nodes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -21,8 +21,8 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=64, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('date_created', models.DateTimeField(auto_now_add=True)),
('date_updated', models.DateTimeField(auto_now=True)),
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('created_by', models.CharField(blank=True, default='', max_length=128, verbose_name='Created by')),
],
options={

View File

@@ -20,7 +20,7 @@ class Migration(migrations.Migration):
fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('is_periodic', models.BooleanField(default=False)),
('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')),
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')),
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),

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

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

View File

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

View File

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

@@ -1,28 +1,27 @@
# -*- coding: utf-8 -*-
#
import io
import os
import uuid
from hashlib import md5
import sshpubkeys
from django.conf import settings
from django.core.cache import cache
from django.db import models
from django.db.models import QuerySet
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from django.db.models import QuerySet
from common.utils import random_string, signer
from common.db import fields
from common.utils import random_string
from common.utils import (
ssh_key_string_to_obj, ssh_key_gen, get_logger, lazyproperty
)
from common.utils.encode import ssh_pubkey_gen
from common.validators import alphanumeric
from common.db import fields
from common.utils.encode import (
parse_ssh_public_key_str, parse_ssh_private_key_str
)
from orgs.mixins.models import OrgModelMixin
logger = get_logger(__file__)
@@ -64,16 +63,16 @@ class AuthMixin:
@property
def ssh_key_fingerprint(self):
public_key = None
if self.public_key:
public_key = self.public_key
elif self.private_key:
try:
public_key = ssh_pubkey_gen(private_key=self.private_key, password=self.password)
public_key = parse_ssh_public_key_str(self.private_key, password=self.password)
except IOError as e:
return str(e)
else:
if not public_key:
return ''
public_key_obj = sshpubkeys.SSHKey(public_key)
fingerprint = public_key_obj.hash_md5()
return fingerprint
@@ -88,24 +87,29 @@ class AuthMixin:
@property
def private_key_file(self):
if not self.private_key_obj:
if not self.private_key:
return None
private_key_str = self.get_private_key()
if not private_key_str:
return None
project_dir = settings.PROJECT_DIR
tmp_dir = os.path.join(project_dir, 'tmp')
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
key_path = os.path.join(tmp_dir, key_name)
if not os.path.exists(key_path):
self.private_key_obj.write_private_key_file(key_path)
with open(key_path, 'w') as f:
f.write(private_key_str)
os.chmod(key_path, 0o400)
return key_path
def get_private_key(self):
if not self.private_key_obj:
if not self.private_key:
return None
string_io = io.StringIO()
self.private_key_obj.write_private_key(string_io)
private_key = string_io.getvalue()
return private_key
private_key_str = parse_ssh_private_key_str(self.private_key, password=self.password)
if not private_key_str and self.password:
# 由于历史原因,密码可能是真实的密码,而非私钥的 passphrase所以这里再尝试一次
private_key_str = parse_ssh_private_key_str(self.private_key)
return private_key_str
@property
def public_key_obj(self):
@@ -234,4 +238,3 @@ class BaseUser(OrgModelMixin, AuthMixin):
class Meta:
abstract = True

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")
@@ -46,8 +50,8 @@ class CommandFilter(OrgModelMixin):
)
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
comment = models.TextField(blank=True, default='', verbose_name=_("Comment"))
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created'))
date_updated = models.DateTimeField(auto_now=True, verbose_name=_('Date updated'))
created_by = models.CharField(
max_length=128, blank=True, default='', verbose_name=_('Created by')
)
@@ -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,31 +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)
applicant = session.user_obj
ticket = ApplyCommandTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant)
ticket.open(applicant)
ticket.open_by_system(assignees)
return ticket
@classmethod
def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None,
asset_id=None, application_id=None, org_id=None):
asset_id=None, node_id=None, application_id=None, org_id=None):
# user & user_group
user_groups = []
user = get_object_or_none(User, pk=user_id)
if user:
@@ -198,8 +203,18 @@ class CommandFilterRule(OrgModelMixin):
if user_group:
org_id = user_group.org_id
user_groups.append(user_group)
system_user = get_object_or_none(SystemUser, pk=system_user_id)
# asset & node
nodes = []
asset = get_object_or_none(Asset, pk=asset_id)
if asset:
nodes.extend(asset.get_all_nodes())
node = get_object_or_none(Node, pk=node_id)
if node:
org_id = node.org_id
nodes.extend(list(node.get_ancestors(with_self=True)))
system_user = get_object_or_none(SystemUser, pk=system_user_id)
application = get_object_or_none(Application, pk=application_id)
q = Q()
if user:
@@ -212,6 +227,8 @@ class CommandFilterRule(OrgModelMixin):
if asset:
org_id = asset.org_id
q |= Q(assets=asset)
if nodes:
q |= Q(nodes__in=set(nodes))
if application:
org_id = application.org_id
q |= Q(applications=application)

View File

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

@@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.cache import cache
from common.utils import signer, get_object_or_none
from common.utils import signer, get_object_or_none, is_uuid
from .base import BaseUser
from .asset import Asset
from .authbook import AuthBook
@@ -34,6 +34,7 @@ class ProtocolMixin:
sqlserver = 'sqlserver', 'SQLServer'
redis = 'redis', 'Redis'
mongodb = 'mongodb', 'MongoDB'
clickhouse = 'clickhouse', 'ClickHouse'
k8s = 'k8s', 'K8S'
SUPPORT_PUSH_PROTOCOLS = [Protocol.ssh, Protocol.rdp]
@@ -46,7 +47,7 @@ class ProtocolMixin:
]
APPLICATION_CATEGORY_DB_PROTOCOLS = [
Protocol.mysql, Protocol.mariadb, Protocol.oracle,
Protocol.postgresql, Protocol.sqlserver,
Protocol.postgresql, Protocol.sqlserver, Protocol.clickhouse,
Protocol.redis, Protocol.mongodb
]
APPLICATION_CATEGORY_CLOUD_PROTOCOLS = [
@@ -321,9 +322,20 @@ class SystemUser(ProtocolMixin, AuthMixin, BaseUser):
assets = Asset.objects.filter(id__in=asset_ids)
return assets
def filter_contain_protocol_assets(self, assets_or_ids):
if not assets_or_ids:
return assets_or_ids
if is_uuid(assets_or_ids[0]):
assets = Asset.objects.filter(id__in=assets_or_ids)
else:
assets = assets_or_ids
assets = [asset for asset in assets if self.protocol in asset.protocols_as_dict]
return assets
def add_related_assets(self, assets_or_ids):
self.assets.add(*tuple(assets_or_ids))
self.add_related_assets_to_su_from_if_need(assets_or_ids)
assets = self.filter_contain_protocol_assets(assets_or_ids)
self.assets.add(*tuple(assets))
self.add_related_assets_to_su_from_if_need(assets)
def add_related_assets_to_su_from_if_need(self, assets_or_ids):
if self.protocol not in [self.Protocol.ssh.value]:

View File

@@ -20,6 +20,6 @@ class AccountBackupExecutionTaskMsg(object):
"please go to personal information -> file encryption password to set the encryption password").format(name)
def publish(self, attachment_list=None):
send_mail_attachment_async.delay(
send_mail_attachment_async(
self.subject, self.message, [self.user.email], attachment_list
)

View File

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

View File

@@ -53,7 +53,15 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return attrs
def get_protocols(self, v):
return v.protocols.replace(' ', ', ')
""" protocols 是 queryset 中返回的Post 创建成功后返回序列化时没有这个字段 """
if hasattr(v, 'protocols'):
protocols = v.protocols
elif hasattr(v, 'asset') and v.asset:
protocols = v.asset.protocols
else:
protocols = ''
protocols = protocols.replace(' ', ', ')
return protocols
@classmethod
def setup_eager_loading(cls, queryset):
@@ -68,10 +76,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class Meta(AccountSerializer.Meta):
fields_backup = [
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
'private_key', 'public_key', 'date_created', 'date_updated', 'version'
]
extra_kwargs = {
'password': {'write_only': False},
'private_key': {'write_only': False},
@@ -80,6 +84,22 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
}
class AccountBackUpSerializer(AccountSecretSerializer):
class Meta(AccountSecretSerializer.Meta):
fields = [
'id', 'hostname', 'ip', 'username', 'password',
'private_key', 'public_key', 'date_created',
'date_updated', 'version'
]
@classmethod
def setup_eager_loading(cls, queryset):
return queryset
def to_representation(self, instance):
return super(AccountSerializer, self).to_representation(instance)
class AccountTaskSerializer(serializers.Serializer):
ACTION_CHOICES = (
('test', 'test'),

View File

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

@@ -1,24 +1,24 @@
# -*- coding: utf-8 -*-
#
from io import StringIO
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 common.drf.fields import EncryptedField
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str, parse_ssh_public_key_str
from .utils import validate_password_for_ansible
class AuthSerializer(serializers.ModelSerializer):
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096, label=_('Private key'))
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=16384,
label=_('Private key'))
def gen_keys(self, private_key=None, password=None):
if private_key is None:
return None, None
public_key = ssh_pubkey_gen(private_key=private_key, password=password)
public_key = parse_ssh_public_key_str(text=private_key, password=password)
return private_key, public_key
def save(self, **kwargs):
@@ -38,7 +38,7 @@ class AuthSerializerMixin(serializers.ModelSerializer):
validators=[validate_password_for_ansible]
)
private_key = EncryptedField(
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=4096
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=16384
)
passphrase = serializers.CharField(
allow_blank=True, allow_null=True, required=False, max_length=512,
@@ -57,10 +57,7 @@ class AuthSerializerMixin(serializers.ModelSerializer):
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
private_key = ssh_private_key_gen(private_key, password=passphrase)
string_io = StringIO()
private_key.write_private_key(string_io)
private_key = string_io.getvalue()
private_key = parse_ssh_private_key_str(private_key, password=passphrase)
return private_key
def validate_public_key(self, public_key):

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

@@ -1,16 +1,16 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from django.db.models import Count
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
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.mixins.serializers import BulkSerializerMixin
from common.utils import parse_ssh_public_key_str
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_for_ansible
from .base import AuthSerializerMixin
from .utils import validate_password_for_ansible
from ..models import SystemUser, Asset
__all__ = [
'SystemUserSerializer', 'MiniSystemUserSerializer',
@@ -36,9 +36,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
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')
)
class Meta:
model = SystemUser
@@ -53,7 +50,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
'su_enabled', 'su_from',
'date_created', 'date_updated', 'comment', 'created_by',
]
fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes']
fields_m2m = ['cmd_filters', 'nodes']
fields = fields_small + fields_m2m
extra_kwargs = {
'cmd_filters': {"required": False, 'label': _('Command filter')},
@@ -214,7 +211,7 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
elif attrs.get('private_key'):
private_key = attrs['private_key']
password = attrs.get('password')
public_key = ssh_pubkey_gen(private_key, password=password, username=username)
public_key = parse_ssh_public_key_str(private_key, password=password)
attrs['public_key'] = public_key
return attrs
@@ -241,7 +238,6 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset \
.annotate(assets_amount=Count("assets")) \
.prefetch_related('nodes', 'cmd_filters')
return queryset

View File

@@ -1,6 +1,8 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
def validate_password_for_ansible(password):
""" 校验 Ansible 不支持的特殊字符 """
@@ -15,3 +17,9 @@ def validate_password_for_ansible(password):
if '"' in password:
raise serializers.ValidationError(_('Password can not contains `"` '))
def validate_ssh_key(ssh_key, passphrase=None):
valid = validate_ssh_private_key(ssh_key, password=passphrase)
if not valid:
raise serializers.ValidationError(_("private key invalid or passphrase error"))
return parse_ssh_private_key_str(ssh_key, passphrase)

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
#
import os
import threading
from django.db.models.signals import (
m2m_changed, post_save, post_delete
@@ -9,15 +7,15 @@ from django.db.models.signals import (
from django.dispatch import receiver
from django.utils.functional import LazyObject
from common.signals import django_ready
from common.utils.connection import RedisPubSub
from common.utils import get_logger
from assets.models import Asset, Node
from common.signals import django_ready
from common.utils import get_logger
from common.utils.connection import RedisPubSub
from orgs.models import Organization
logger = get_logger(__file__)
# clear node assets mapping for memory
# ------------------------------------
@@ -78,9 +76,4 @@ def subscribe_node_assets_mapping_expire(sender, **kwargs):
Node.expire_node_all_asset_ids_mapping_from_memory(org_id)
Node.expire_node_all_asset_ids_mapping_from_memory(root_org_id)
def keep_subscribe_node_assets_relation():
node_assets_mapping_for_memory_pub_sub.subscribe(handle_node_relation_change)
t = threading.Thread(target=keep_subscribe_node_assets_relation)
t.daemon = True
t.start()
node_assets_mapping_for_memory_pub_sub.subscribe(handle_node_relation_change)

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

@@ -50,6 +50,10 @@ def clean_ansible_task_hosts(assets, system_user=None):
for asset in assets:
if not check_asset_can_run_ansible(asset):
continue
# 资产平台不包含系统用户的协议, 不推送
if system_user and system_user.protocol not in asset.protocols_as_dict:
logger.info(_('Asset protocol not support system user protocol, skipped: {}').format(system_user.protocol))
continue
cleaned_assets.append(asset)
if not cleaned_assets:
logger.info(_("No assets matched, stop task"))

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

@@ -1,20 +1,29 @@
# -*- coding: utf-8 -*-
#
from rest_framework.mixins import ListModelMixin, CreateModelMixin
from importlib import import_module
from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin
from django.db.models import F, Value
from django.db.models.functions import Concat
from django.conf import settings
from rest_framework.permissions import IsAuthenticated
from rest_framework import generics
from common.drf.api import JMSReadOnlyModelViewSet
from common.plugins.es import QuerySet as ESQuerySet
from common.drf.filters import DatetimeRangeFilter
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 .backends import TYPE_ENGINE_MAPPING
from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog
from .serializers import FTPLogSerializer, UserLoginLogSerializer, CommandExecutionSerializer
from .serializers import OperateLogSerializer, PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
from .serializers import (
OperateLogSerializer, OperateLogActionDetailSerializer,
PasswordChangeLogSerializer, CommandExecutionHostsRelationSerializer
)
class FTPLogViewSet(CreateModelMixin,
@@ -67,7 +76,7 @@ class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView):
return qs
class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet):
model = OperateLog
serializer_class = OperateLogSerializer
extra_filter_backends = [DatetimeRangeFilter]
@@ -78,6 +87,22 @@ class OperateLogViewSet(ListModelMixin, OrgGenericViewSet):
search_fields = ['resource']
ordering = ['-datetime']
def get_serializer_class(self):
if self.request.query_params.get('type') == 'action_detail':
return OperateLogActionDetailSerializer
return super().get_serializer_class()
def get_queryset(self):
qs = OperateLog.objects.all()
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
if es_config:
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
store = engine_mod.OperateLogStore(es_config)
if store.ping(timeout=2):
qs = ESQuerySet(store)
qs.model = OperateLog
return qs
class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
queryset = PasswordChangeLog.objects.all()
@@ -126,9 +151,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

@@ -0,0 +1,18 @@
from importlib import import_module
from django.conf import settings
TYPE_ENGINE_MAPPING = {
'db': 'audits.backends.db',
'es': 'audits.backends.es',
}
def get_operate_log_storage(default=False):
engine_mod = import_module(TYPE_ENGINE_MAPPING['db'])
es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG
if not default and es_config:
engine_mod = import_module(TYPE_ENGINE_MAPPING['es'])
storage = engine_mod.OperateLogStore(es_config)
return storage

View File

@@ -0,0 +1,38 @@
# ~*~ coding: utf-8 ~*~
from django.utils.translation import ugettext_lazy as _
from audits.models import OperateLog
class OperateLogStore(object):
def __init__(self, config):
self.model = OperateLog
self.max_length = 1024
self.max_length_tip_msg = _(
'The text content is too long. Use Elasticsearch to store operation logs'
)
@staticmethod
def ping(timeout=None):
return True
def save(self, **kwargs):
log_id = kwargs.get('id', '')
before = kwargs.get('before') or {}
after = kwargs.get('after') or {}
if len(str(before)) > self.max_length:
before = {_('Tips'): self.max_length_tip_msg}
if len(str(after)) > self.max_length:
after = {_('Tips'): self.max_length_tip_msg}
op_log = self.model.objects.filter(pk=log_id).first()
if op_log is not None:
raw_after = op_log.after or {}
raw_before = op_log.before or {}
raw_before.update(before)
raw_after.update(after)
op_log.before = raw_before
op_log.after = raw_after
op_log.save()
else:
self.model.objects.create(**kwargs)

View File

@@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
#
import uuid
from common.utils.timezone import local_now_display
from common.utils import get_logger
from common.utils.encode import Singleton
from common.plugins.es import ES
logger = get_logger(__file__)
class OperateLogStore(ES, metaclass=Singleton):
def __init__(self, config):
properties = {
"id": {
"type": "keyword"
},
"user": {
"type": "keyword"
},
"action": {
"type": "keyword"
},
"resource_type": {
"type": "keyword"
},
"org_id": {
"type": "keyword"
},
"datetime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
exact_fields = {}
match_fields = {
'id', 'user', 'action', 'resource_type',
'resource', 'remote_addr', 'org_id'
}
keyword_fields = {
'id', 'user', 'action', 'resource_type', 'org_id'
}
if not config.get('INDEX'):
config['INDEX'] = 'jumpserver_operate_log'
super().__init__(config, properties, keyword_fields, exact_fields, match_fields)
self.pre_use_check()
@staticmethod
def make_data(data):
op_id = data.get('id', str(uuid.uuid4()))
datetime_param = data.get('datetime', local_now_display())
data = {
'id': op_id, 'user': data['user'], 'action': data['action'],
'resource_type': data['resource_type'], 'resource': data['resource'],
'remote_addr': data['remote_addr'], 'datetime': datetime_param,
'before': data['before'], 'after': data['after'], 'org_id': data['org_id']
}
return data
def save(self, **kwargs):
log_id = kwargs.get('id', '')
before = kwargs.get('before') or {}
after = kwargs.get('after') or {}
op_log = self.get({'id': log_id})
if op_log is not None:
data = {'doc': {}}
raw_after = op_log.get('after') or {}
raw_before = op_log.get('before') or {}
raw_before.update(before)
raw_after.update(after)
data['doc']['before'] = raw_before
data['doc']['after'] = raw_after
self.es.update(
index=self.index, doc_type=self.doc_type,
id=op_log.get('es_id'), body=data, refresh=True
)
else:
data = self.make_data(kwargs)
self.es.index(
index=self.index, doc_type=self.doc_type, body=data,
refresh=True
)

View File

@@ -7,19 +7,30 @@ DEFAULT_CITY = _("Unknown")
MODELS_NEED_RECORD = (
# users
'User', 'UserGroup',
# authentication
'AccessKey', 'TempToken',
# acls
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
# assets
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
'CommandFilter', 'Platform', 'AuthBook',
'CommandFilter', 'Platform', 'Label',
# applications
'Application',
# account
'AuthBook',
# orgs
'Organization',
# settings
'Setting',
# perms
'AssetPermission', 'ApplicationPermission',
# notifications
'SystemMsgSubscription', 'UserMsgSubscription',
# Terminal
'Terminal', 'Endpoint', 'EndpointRule', 'CommandStorage', 'ReplayStorage',
# rbac
'Role', 'SystemRole', 'OrgRole', 'RoleBinding', 'OrgRoleBinding', 'SystemRoleBinding',
# xpack
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'ApplicationChangeAuthPlan',
'GatherUserTask', 'Interface',
)

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

191
apps/audits/handler.py Normal file
View File

@@ -0,0 +1,191 @@
from datetime import datetime
from django.db import transaction
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _
from common.utils import get_request_ip, get_logger
from common.utils.timezone import as_current_tz
from common.utils.encode import Singleton
from common.local import encrypted_field_set
from settings.serializers import SettingsSerializer
from jumpserver.utils import current_request
from audits.models import OperateLog
from orgs.utils import get_current_org_id
from .backends import get_operate_log_storage
logger = get_logger(__name__)
class ModelClient:
@staticmethod
def save(**kwargs):
log_id = kwargs.get('id', '')
op_log = OperateLog.objects.filter(pk=log_id).first()
if op_log is not None:
raw_after = op_log.after or {}
raw_before = op_log.before or {}
cur_before = kwargs.get('before') or {}
cur_after = kwargs.get('after') or {}
raw_before.update(cur_before)
raw_after.update(cur_after)
op_log.before = raw_before
op_log.after = raw_after
op_log.save()
else:
OperateLog.objects.create(**kwargs)
class OperatorLogHandler(metaclass=Singleton):
CACHE_KEY = 'OPERATOR_LOG_CACHE_KEY'
def __init__(self):
self.log_client = self.get_storage_client()
@staticmethod
def get_storage_client():
client = get_operate_log_storage()
return client
@staticmethod
def _consistent_type_to_str(value1, value2):
if isinstance(value1, datetime):
value1 = as_current_tz(value1).strftime('%Y-%m-%d %H:%M:%S')
if isinstance(value2, datetime):
value2 = as_current_tz(value2).strftime('%Y-%m-%d %H:%M:%S')
return value1, value2
def _look_for_two_dict_change(self, left_dict, right_dict):
# 以右边的字典为基础
before, after = {}, {}
for key, value in right_dict.items():
pre_value = left_dict.get(key, '')
pre_value, value = self._consistent_type_to_str(pre_value, value)
if sorted(str(value)) == sorted(str(pre_value)):
continue
if pre_value:
before[key] = pre_value
if value:
after[key] = value
return before, after
def cache_instance_before_data(self, instance_dict):
instance_id = instance_dict.get('id')
if instance_id is None:
return
key = '%s_%s' % (self.CACHE_KEY, instance_id)
cache.set(key, instance_dict, 3 * 60)
def get_instance_dict_from_cache(self, instance_id):
if instance_id is None:
return None
key = '%s_%s' % (self.CACHE_KEY, instance_id)
cache_instance = cache.get(key, {})
log_id = cache_instance.get('operate_log_id')
return log_id, cache_instance
def get_instance_current_with_cache_diff(self, current_instance):
log_id, before, after = None, None, None
instance_id = current_instance.get('id')
if instance_id is None:
return log_id, before, after
log_id, cache_instance = self.get_instance_dict_from_cache(instance_id)
if not cache_instance:
return log_id, before, after
before, after = self._look_for_two_dict_change(
cache_instance, current_instance
)
return log_id, before, after
@staticmethod
def get_resource_display_from_setting(resource):
resource_display = None
setting_serializer = SettingsSerializer()
label = setting_serializer.get_field_label(resource)
if label is not None:
resource_display = label
return resource_display
def get_resource_display(self, resource):
resource_display = str(resource)
return_value = self.get_resource_display_from_setting(resource_display)
if return_value is not None:
resource_display = return_value
return resource_display
@staticmethod
def serialized_value(value: (list, tuple)):
if len(value) == 0:
return ''
if isinstance(value[0], str):
return ','.join(value)
return ','.join([i['value'] for i in value if i.get('value')])
def __data_processing(self, dict_item, loop=True):
encrypt_value = '******'
for key, value in dict_item.items():
if isinstance(value, bool):
value = _('Yes') if value else _('No')
elif isinstance(value, (list, tuple)):
value = self.serialized_value(value)
elif isinstance(value, dict) and loop:
self.__data_processing(value, loop=False)
if key in encrypted_field_set:
value = encrypt_value
dict_item[key] = value
return dict_item
def data_processing(self, before, after):
if before:
before = self.__data_processing(before)
if after:
after = self.__data_processing(after)
return before, after
def create_or_update_operate_log(
self, action, resource_type, resource=None,
force=False, log_id=None, before=None, after=None
):
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
return
remote_addr = get_request_ip(current_request)
resource_display = self.get_resource_display(resource)
before, after = self.data_processing(before, after)
if not force and not any([before, after]):
# 前后都没变化,没必要生成日志,除非手动强制保存
return
data = {
'id': log_id, "user": str(user), 'action': action,
'resource_type': str(resource_type), 'resource': resource_display,
'remote_addr': remote_addr, 'before': before, 'after': after,
'org_id': get_current_org_id(),
}
with transaction.atomic():
if self.log_client.ping(timeout=1):
client = self.log_client
else:
logger.info('Switch default operate log storage save.')
client = get_operate_log_storage(default=True)
try:
client.save(**data)
except Exception as e:
error_msg = 'An error occurred saving OperateLog.' \
'Error: %s, Data: %s' % (e, data)
logger.error(error_msg)
op_handler = OperatorLogHandler()
create_or_update_operate_log = op_handler.create_or_update_operate_log
cache_instance_before_data = op_handler.cache_instance_before_data
get_instance_current_with_cache_diff = op_handler.get_instance_current_with_cache_diff
get_instance_dict_from_cache = op_handler.get_instance_dict_from_cache

View File

@@ -0,0 +1,24 @@
# Generated by Django 3.2.14 on 2022-10-11 09:45
import common.db.encoder
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('audits', '0014_auto_20220505_1902'),
]
operations = [
migrations.AddField(
model_name='operatelog',
name='after',
field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True),
),
migrations.AddField(
model_name='operatelog',
name='before',
field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True),
),
]

View File

@@ -4,8 +4,9 @@ from django.db import models
from django.db.models import Q
from django.utils.translation import gettext, ugettext_lazy as _
from django.utils import timezone
from common.utils import lazyproperty
from common.utils import lazyproperty
from common.db.encoder import ModelJSONFieldEncoder
from orgs.mixins.models import OrgModelMixin, Organization
from orgs.utils import current_org
@@ -65,6 +66,8 @@ class OperateLog(OrgModelMixin):
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True)
before = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True)
after = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True)
def __str__(self):
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
@@ -78,6 +81,21 @@ class OperateLog(OrgModelMixin):
self.org_id = Organization.ROOT_ID
return super(OperateLog, self).save(*args, **kwargs)
@classmethod
def from_dict(cls, d):
self = cls()
for k, v in d.items():
setattr(self, k, v)
return self
@classmethod
def from_multi_dict(cls, l):
operate_logs = []
for d in l:
operate_log = cls.from_dict(d)
operate_logs.append(operate_log)
return operate_logs
class Meta:
verbose_name = _("Operate log")

View File

@@ -47,6 +47,12 @@ class UserLoginLogSerializer(serializers.ModelSerializer):
}
class OperateLogActionDetailSerializer(serializers.ModelSerializer):
class Meta:
model = models.OperateLog
fields = ('before', 'after')
class OperateLogSerializer(serializers.ModelSerializer):
action_display = serializers.CharField(source='get_action_display', label=_('Action'))

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
#
import time
import uuid
from django.db.models.signals import (
post_save, m2m_changed, pre_delete
post_save, m2m_changed, pre_delete, pre_save
)
from django.dispatch import receiver
from django.conf import settings
@@ -16,24 +16,32 @@ from django.utils import translation
from rest_framework.renderers import JSONRenderer
from rest_framework.request import Request
from assets.models import Asset, SystemUser
from users.models import User
from assets.models import Asset, SystemUser, CommandFilter
from terminal.models import Session, Command
from perms.models import AssetPermission, ApplicationPermission
from rbac.models import Role
from audits.utils import model_to_dict_for_operate_log as model_to_dict
from audits.handler import (
get_instance_current_with_cache_diff, cache_instance_before_data,
create_or_update_operate_log, get_instance_dict_from_cache
)
from authentication.signals import post_auth_failed, post_auth_success
from authentication.utils import check_different_city_login_if_need
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, create_operate_log
from .utils import write_login_log
from . import models, serializers
from .models import OperateLog
from orgs.utils import current_org
from perms.models import AssetPermission, ApplicationPermission
from .const import MODELS_NEED_RECORD
from terminal.backends.command.serializers import SessionCommandSerializer
from terminal.serializers import SessionSerializer
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR
from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL
from common.utils import get_request_ip, get_logger, get_syslogger
from common.utils.encode import data_to_json
logger = get_logger(__name__)
sys_logger = get_syslogger(__name__)
json_render = JSONRenderer()
@@ -51,6 +59,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
@@ -61,70 +70,6 @@ class AuthBackendLabelMapping(LazyObject):
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
M2M_NEED_RECORD = {
User.groups.through._meta.object_name: (
_('User and Group'),
_('{User} JOINED {UserGroup}'),
_('{User} LEFT {UserGroup}')
),
SystemUser.assets.through._meta.object_name: (
_('Asset and SystemUser'),
_('{Asset} ADD {SystemUser}'),
_('{Asset} REMOVE {SystemUser}')
),
Asset.nodes.through._meta.object_name: (
_('Node and Asset'),
_('{Node} ADD {Asset}'),
_('{Node} REMOVE {Asset}')
),
AssetPermission.users.through._meta.object_name: (
_('User asset permissions'),
_('{AssetPermission} ADD {User}'),
_('{AssetPermission} REMOVE {User}'),
),
AssetPermission.user_groups.through._meta.object_name: (
_('User group asset permissions'),
_('{AssetPermission} ADD {UserGroup}'),
_('{AssetPermission} REMOVE {UserGroup}'),
),
AssetPermission.assets.through._meta.object_name: (
_('Asset permission'),
_('{AssetPermission} ADD {Asset}'),
_('{AssetPermission} REMOVE {Asset}'),
),
AssetPermission.nodes.through._meta.object_name: (
_('Node permission'),
_('{AssetPermission} ADD {Node}'),
_('{AssetPermission} REMOVE {Node}'),
),
AssetPermission.system_users.through._meta.object_name: (
_('Asset permission and SystemUser'),
_('{AssetPermission} ADD {SystemUser}'),
_('{AssetPermission} REMOVE {SystemUser}'),
),
ApplicationPermission.users.through._meta.object_name: (
_('User application permissions'),
_('{ApplicationPermission} ADD {User}'),
_('{ApplicationPermission} REMOVE {User}'),
),
ApplicationPermission.user_groups.through._meta.object_name: (
_('User group application permissions'),
_('{ApplicationPermission} ADD {UserGroup}'),
_('{ApplicationPermission} REMOVE {UserGroup}'),
),
ApplicationPermission.applications.through._meta.object_name: (
_('Application permission'),
_('{ApplicationPermission} ADD {Application}'),
_('{ApplicationPermission} REMOVE {Application}'),
),
ApplicationPermission.system_users.through._meta.object_name: (
_('Application permission and SystemUser'),
_('{ApplicationPermission} ADD {SystemUser}'),
_('{ApplicationPermission} REMOVE {SystemUser}'),
),
}
M2M_ACTION = {
POST_ADD: OperateLog.ACTION_CREATE,
POST_REMOVE: OperateLog.ACTION_DELETE,
@@ -136,60 +81,115 @@ M2M_ACTION = {
def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
if action not in M2M_ACTION:
return
user = current_request.user if current_request else None
if not user or not user.is_authenticated:
if not instance:
return
sender_name = sender._meta.object_name
if sender_name in M2M_NEED_RECORD:
org_id = current_org.id
remote_addr = get_request_ip(current_request)
user = str(user)
resource_type, resource_tmpl_add, resource_tmpl_remove = M2M_NEED_RECORD[sender_name]
action = M2M_ACTION[action]
if action == OperateLog.ACTION_CREATE:
resource_tmpl = resource_tmpl_add
elif action == OperateLog.ACTION_DELETE:
resource_tmpl = resource_tmpl_remove
resource_type = instance._meta.verbose_name
current_instance = model_to_dict(instance, include_model_fields=False)
to_create = []
objs = model.objects.filter(pk__in=pk_set)
instance_id = current_instance.get('id')
log_id, before_instance = get_instance_dict_from_cache(instance_id)
instance_name = instance._meta.object_name
instance_value = str(instance)
field_name = str(model._meta.verbose_name)
objs = model.objects.filter(pk__in=pk_set)
objs_display = [str(o) for o in objs]
action = M2M_ACTION[action]
changed_field = current_instance.get(field_name, [])
model_name = model._meta.object_name
after, before, before_value = None, None, None
if action == OperateLog.ACTION_CREATE:
before_value = list(set(changed_field) - set(objs_display))
elif action == OperateLog.ACTION_DELETE:
before_value = list(
set(changed_field).symmetric_difference(set(objs_display))
)
for obj in objs:
resource = resource_tmpl.format(**{
instance_name: instance_value,
model_name: str(obj)
})[:128] # `resource` 字段只有 128 个字符长 😔
if changed_field:
after = {field_name: changed_field}
if before_value:
before = {field_name: before_value}
to_create.append(OperateLog(
user=user, action=action, resource_type=resource_type,
resource=resource, remote_addr=remote_addr, org_id=org_id
))
OperateLog.objects.bulk_create(to_create)
if sorted(str(before)) == sorted(str(after)):
return
create_or_update_operate_log(
OperateLog.ACTION_UPDATE, resource_type,
resource=instance, log_id=log_id, before=before, after=after
)
def signal_of_operate_log_whether_continue(sender, instance, created, update_fields=None):
condition = True
if not instance:
condition = False
if instance and getattr(instance, SKIP_SIGNAL, False):
condition = False
# 终端模型的 create 事件由系统产生,不记录
if instance._meta.object_name == 'Terminal' and created:
condition = False
# last_login 改变是最后登录日期, 每次登录都会改变
if instance._meta.object_name == 'User' and \
update_fields and 'last_login' in update_fields:
condition = False
# 不在记录白名单中,跳过
if sender._meta.object_name not in MODELS_NEED_RECORD:
condition = False
return condition
@receiver(pre_save)
def on_object_pre_create_or_update(sender, instance=None, raw=False, using=None, update_fields=None, **kwargs):
ok = signal_of_operate_log_whether_continue(
sender, instance, False, update_fields
)
if not ok:
return
instance_before_data = {'id': instance.id}
raw_instance = type(instance).objects.filter(pk=instance.id).first()
if raw_instance:
instance_before_data = model_to_dict(raw_instance)
operate_log_id = str(uuid.uuid4())
instance_before_data['operate_log_id'] = operate_log_id
setattr(instance, 'operate_log_id', operate_log_id)
cache_instance_before_data(instance_before_data)
@receiver(post_save)
def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs):
# last_login 改变是最后登录日期, 每次登录都会改变
if instance._meta.object_name == 'User' and \
update_fields and 'last_login' in update_fields:
ok = signal_of_operate_log_whether_continue(
sender, instance, created, update_fields
)
if not ok:
return
log_id, before, after = None, None, None
if created:
action = models.OperateLog.ACTION_CREATE
after = model_to_dict(instance)
log_id = getattr(instance, 'operate_log_id', None)
else:
action = models.OperateLog.ACTION_UPDATE
create_operate_log(action, sender, instance)
current_instance = model_to_dict(instance)
log_id, before, after = get_instance_current_with_cache_diff(current_instance)
resource_type = sender._meta.verbose_name
create_or_update_operate_log(
action, resource_type, resource=instance,
log_id=log_id, before=before, after=after
)
@receiver(pre_delete)
def on_object_delete(sender, instance=None, **kwargs):
create_operate_log(models.OperateLog.ACTION_DELETE, sender, instance)
ok = signal_of_operate_log_whether_continue(sender, instance, False)
if not ok:
return
resource_type = sender._meta.verbose_name
create_or_update_operate_log(
models.OperateLog.ACTION_DELETE, resource_type,
resource=instance, before=model_to_dict(instance)
)
@receiver(post_user_change_password, sender=User)
@@ -277,7 +277,6 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
check_different_city_login_if_need(user, request)
data = generate_data(user.username, request, login_type=login_type)
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
request.session["MFA_VERIFY_TIME"] = int(time.time())
data.update({'mfa': int(user.mfa_enabled), 'status': True})
write_login_log(**data)

View File

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

View File

@@ -1,14 +1,16 @@
import csv
import codecs
from django.http import HttpResponse
from django.db import transaction
from django.utils import translation
from itertools import chain
from datetime import datetime
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
from django.http import HttpResponse
from django.db import models
from settings.serializers import SettingsSerializer
from common.utils.timezone import as_current_tz
from common.utils import validate_ip, get_ip_city, get_logger
from .const import DEFAULT_CITY
logger = get_logger(__name__)
@@ -46,23 +48,59 @@ def write_login_log(*args, **kwargs):
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)
def get_resource_display(resource):
resource_display = str(resource)
setting_serializer = SettingsSerializer()
label = setting_serializer.get_field_label(resource_display)
if label is not None:
resource_display = label
return resource_display
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))
def model_to_dict_for_operate_log(
instance, include_model_fields=True, include_related_fields=True
):
model_need_continue_fields = ['date_updated']
m2m_need_continue_fields = ['history_passwords']
opts = instance._meta
data = {}
for f in chain(opts.concrete_fields, opts.private_fields):
if isinstance(f, (models.FileField, models.ImageField)):
continue
if getattr(f, 'attname', None) in model_need_continue_fields:
continue
value = getattr(instance, f.name) or getattr(instance, f.attname)
if not isinstance(value, bool) and not value:
continue
if getattr(f, 'primary_key', False):
f.verbose_name = 'id'
elif isinstance(value, list):
value = [str(v) for v in value]
elif isinstance(value, datetime):
value = as_current_tz(value).strftime('%Y-%m-%d %H:%M:%S')
if include_model_fields or getattr(f, 'primary_key', False):
data[str(f.verbose_name)] = value
if include_related_fields:
for f in chain(opts.many_to_many, opts.related_objects):
value = []
if instance.pk is not None:
related_name = getattr(f, 'attname', '') or getattr(f, 'related_name', '')
if not related_name or related_name in m2m_need_continue_fields:
continue
try:
value = [str(i) for i in getattr(instance, related_name).all()]
except:
pass
if not value:
continue
try:
field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name
data[str(field_key)] = value
except:
pass
return data

View File

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

View File

@@ -1,84 +1,105 @@
# -*- coding: utf-8 -*-
#
import urllib.parse
import json
from typing import Callable
import os
import abc
import base64
import ctypes
import json
import os
import urllib.parse
from django.core.cache import cache
from django.shortcuts import get_object_or_404
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 django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers
from django.conf import settings
from rest_framework.request import Request
from rest_framework.response import Response
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 ..models import ConnectionToken
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
SuperConnectionTokenSerializer, ConnectionTokenDisplaySerializer,
)
logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', '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')
user = user if user else self.request.user
return asset, application, system_user, user
@staticmethod
def parse_env_bool(env_key, env_default, true_value, false_value):
return true_value if is_true(os.getenv(env_key, env_default)) else false_value
def get_rdp_file_content(self, serializer):
options = {
def get_client_protocol_data(self, token: ConnectionToken):
from assets.models import SystemUser
protocol = token.system_user.protocol
username = token.user.username
rdp_config = ssh_token = ''
if protocol == SystemUser.Protocol.rdp:
filename, rdp_config = self.get_rdp_file_info(token)
elif protocol == SystemUser.Protocol.ssh:
filename, ssh_token = self.get_ssh_token(token)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
filename = urllib.parse.unquote(filename)
return {
"filename": filename,
"protocol": protocol,
"username": username,
"token": ssh_token,
"config": rdp_config
}
def get_rdp_file_info(self, token: ConnectionToken):
rdp_options = {
'full address:s': '',
'username:s': '',
# 'screen mode id:i': '1',
@@ -111,412 +132,189 @@ class ClientProtocolMixin:
# 'remoteapplicationcmdline:s': '',
}
asset, application, system_user, user = self.get_request_resource(serializer)
# 设置磁盘挂载
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
if drives_redirect:
actions = Action.choices_to_value(token.actions)
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
rdp_options['drivestoredirect:s'] = '*'
# 设置全屏
full_screen = is_true(self.request.query_params.get('full_screen'))
rdp_options['screen mode id:i'] = '2' if full_screen else '1'
# 设置 RDP Server 地址
endpoint = self.get_smart_endpoint(
protocol='rdp', asset=token.asset, application=token.application
)
rdp_options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 设置用户名
rdp_options['username:s'] = '{}|{}'.format(token.user.username, str(token.id))
if token.system_user.ad_domain:
rdp_options['domain:s'] = token.system_user.ad_domain
# 设置宽高
height = self.request.query_params.get('height')
width = self.request.query_params.get('width')
full_screen = is_true(self.request.query_params.get('full_screen'))
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
token, secret = self.create_token(user, asset, application, system_user)
# 设置磁盘挂载
if drives_redirect:
actions = 0
if asset:
actions = get_asset_actions(user, asset, system_user)
elif application:
actions = get_application_actions(user, application, system_user)
if actions & Action.UPDOWNLOAD == Action.UPDOWNLOAD:
options['drivestoredirect:s'] = '*'
# 全屏
options['screen mode id:i'] = '2' if full_screen else '1'
# RDP Server 地址
endpoint = self.get_smart_endpoint(
protocol='rdp', asset=asset, application=application
)
options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 用户名
options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain:
options['domain:s'] = system_user.ad_domain
# 宽高
if width and height:
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
rdp_options['desktopwidth:i'] = width
rdp_options['desktopheight:i'] = height
rdp_options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
# 设置其他选项
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
if asset:
name = asset.hostname
elif application:
name = application.name
application.get_rdp_remote_app_setting()
if token.asset and token.asset.platform.meta.get('console', None) == 'true':
rdp_options['administrative session:i:'] = '1'
app = f'||jmservisor'
options['remoteapplicationmode:i'] = '1'
options['alternate shell:s'] = app
options['remoteapplicationprogram:s'] = app
options['remoteapplicationname:s'] = name
if token.asset:
name = token.asset.hostname
elif token.application and token.application.category_remote_app:
app = '||jmservisor'
name = token.application.name
rdp_options['remoteapplicationmode:i'] = '1'
rdp_options['alternate shell:s'] = app
rdp_options['remoteapplicationprogram:s'] = app
rdp_options['remoteapplicationname:s'] = name
else:
name = '*'
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
def get_valid_serializer(self):
if self.request.method == 'GET':
data = self.request.query_params
else:
data = self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
return serializer
def get_client_protocol_data(self, serializer):
asset, application, system_user, user = self.get_request_resource(serializer)
protocol = system_user.protocol
username = user.username
config, token = '', ''
if protocol == 'rdp':
name, config = self.get_rdp_file_content(serializer)
elif protocol == 'ssh':
name, token = self.get_ssh_token(serializer)
else:
raise ValueError('Protocol not support: {}'.format(protocol))
filename = "{}-{}-jumpserver".format(username, name)
data = {
"filename": filename,
"protocol": system_user.protocol,
"username": username,
"token": token,
"config": config
}
return data
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs):
if self.request.method == 'GET':
data = self.request.query_params
else:
data = self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
name, data = self.get_rdp_file_content(serializer)
response = HttpResponse(data, content_type='application/octet-stream')
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
filename = urllib.parse.quote(filename)
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
serializer = self.get_valid_serializer()
try:
protocol_data = self.get_client_protocol_data(serializer)
except ValueError as e:
return Response({'error': str(e)}, status=401)
protocol_data = json.dumps(protocol_data).encode()
protocol_data = base64.b64encode(protocol_data).decode()
data = {
'url': 'jms://{}'.format(protocol_data),
}
return Response(data=data)
token = json.dumps(data)
return filename, token
class SecretDetailMixin:
valid_token: Callable
request: Request
get_serializer: Callable
@staticmethod
def _get_application_secret_detail(application):
gateway = None
remote_app = None
asset = None
if application.category_remote_app:
remote_app = application.get_rdp_remote_app_setting()
asset = application.get_remote_app_asset()
domain = asset.domain
else:
domain = application.domain
if domain and domain.has_gateway():
gateway = domain.random_gateway()
return {
'asset': asset,
'application': application,
'gateway': gateway,
'domain': domain,
'remote_app': remote_app,
}
@staticmethod
def _get_asset_secret_detail(asset):
gateway = None
if asset and asset.domain and asset.domain.has_gateway():
gateway = asset.domain.random_gateway()
return {
'asset': asset,
'application': None,
'domain': asset.domain,
'gateway': gateway,
'remote_app': None,
}
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
perm_required = 'authentication.view_connectiontokensecret'
# 非常重要的 api再逻辑层再判断一下双重保险
if not request.user.has_perm(perm_required):
raise PermissionDenied('Not allow to view secret')
token = request.data.get('token', '')
try:
value, user, system_user, asset, app, expired_at, actions = self.valid_token(token)
except serializers.ValidationError as e:
post_auth_failed.send(
sender=self.__class__, username='', request=self.request,
reason=_('Invalid token')
)
raise e
data = dict(
id=token, secret=value.get('secret', ''),
user=user, system_user=system_user,
expired_at=expired_at, actions=actions
)
cmd_filter_kwargs = {
'system_user_id': system_user.id,
'user_id': user.id,
}
if asset:
asset_detail = self._get_asset_secret_detail(asset)
system_user.load_asset_more_auth(asset.id, user.username, user.id)
data['type'] = 'asset'
data.update(asset_detail)
cmd_filter_kwargs['asset_id'] = asset.id
else:
app_detail = self._get_application_secret_detail(app)
system_user.load_app_more_auth(app.id, user.username, user.id)
data['type'] = 'application'
data.update(app_detail)
cmd_filter_kwargs['application_id'] = app.id
from assets.models import CommandFilterRule
cmd_filter_rules = CommandFilterRule.get_queryset(**cmd_filter_kwargs)
data['cmd_filter_rules'] = cmd_filter_rules
serializer = self.get_serializer(data)
return Response(data=serializer.data, status=200)
class TokenCacheMixin:
""" endpoint smart view 用到此类来解析token中的资产、应用 """
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
def renewal_token(self, token, ttl=None):
value = self.get_token_from_cache(token)
if value:
pre_ttl = self.get_token_ttl(token)
self.set_token_to_cache(token, value, ttl)
post_ttl = self.get_token_ttl(token)
ok = True
msg = f'{pre_ttl}s is renewed to {post_ttl}s.'
else:
ok = False
msg = 'Token is not found.'
data = {
'ok': ok,
'msg': msg
}
return data
def get_token_ttl(self, token):
key = self.get_token_cache_key(token)
return cache.ttl(key)
def set_token_to_cache(self, token, value, ttl=None):
key = self.get_token_cache_key(token)
ttl = ttl or settings.CONNECTION_TOKEN_EXPIRATION
cache.set(key, value, timeout=ttl)
def get_token_from_cache(self, token):
key = self.get_token_cache_key(token)
value = cache.get(key, None)
return value
def get_token_cache_key(self, token):
return self.CACHE_KEY_PREFIX.format(token)
class BaseUserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
TokenCacheMixin, GenericViewSet
):
@staticmethod
def check_resource_permission(user, asset, application, system_user):
from perms.utils.asset import has_asset_system_permission
from perms.utils.application import has_application_system_permission
if asset and not has_asset_system_permission(user, asset, system_user):
error = f'User not has this asset and system user permission: ' \
f'user={user.id} system_user={system_user.id} asset={asset.id}'
raise PermissionDenied(error)
if application and not has_application_system_permission(user, application, system_user):
error = f'User not has this application and system user permission: ' \
f'user={user.id} system_user={system_user.id} application={application.id}'
raise PermissionDenied(error)
return True
def create_token(self, user, asset, application, system_user, ttl=None):
self.check_resource_permission(user, asset, application, system_user)
token = random_string(36)
secret = random_string(16)
value = {
'id': token,
'secret': secret,
'user': str(user.id),
'username': user.username,
'system_user': str(system_user.id),
'system_user_name': system_user.name,
'created_by': str(self.request.user),
'date_created': str(timezone.now())
}
if asset:
value.update({
'type': 'asset',
'asset': str(asset.id),
'hostname': asset.hostname,
})
elif application:
value.update({
'type': 'application',
'application': application.id,
'application_name': str(application)
})
self.set_token_to_cache(token, value, ttl)
return token, secret
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
asset, application, system_user, user = self.get_request_resource(serializer)
token, secret = self.create_token(user, asset, application, system_user)
tp = 'app' if application else 'asset'
data = {
"id": token, 'secret': secret,
'type': tp, 'protocol': system_user.protocol,
'expire_time': self.get_token_ttl(token),
}
return Response(data, status=201)
class UserConnectionTokenViewSet(BaseUserConnectionTokenViewSet, SecretDetailMixin):
class ConnectionTokenViewSet(ConnectionTokenMixin, RootOrgViewMixin, JMSModelViewSet):
filterset_fields = (
'type', 'user_display', 'system_user_display',
'application_display', 'asset_display'
)
search_fields = filterset_fields
serializer_classes = {
'default': ConnectionTokenSerializer,
'list': ConnectionTokenDisplaySerializer,
'retrieve': ConnectionTokenDisplaySerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'GET': 'authentication.view_connectiontoken',
'retrieve': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'expire': 'authentication.add_connectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
def valid_token(self, token):
from users.models import User
from assets.models import SystemUser, Asset
from applications.models import Application
from perms.utils.asset.permission import validate_permission as asset_validate_permission
from perms.utils.application.permission import validate_permission as app_validate_permission
def get_queryset(self):
return ConnectionToken.objects.filter(user=self.request.user)
value = self.get_token_from_cache(token)
if not value:
raise serializers.ValidationError('Token not found')
def get_request_resource_user(self, serializer):
return self.request.user
user = get_object_or_404(User, id=value.get('user'))
if not user.is_valid:
raise serializers.ValidationError("User not valid, disabled or expired")
system_user = get_object_or_404(SystemUser, id=value.get('system_user'))
asset = None
app = None
if value.get('type') == 'asset':
asset = get_object_or_404(Asset, id=value.get('asset'))
if not asset.is_active:
raise serializers.ValidationError("Asset disabled")
has_perm, actions, expired_at = asset_validate_permission(user, asset, system_user)
def get_object(self):
if self.request.user.is_service_account:
# TODO: 组件获取 token 详情,将来放在 Super-connection-token API 中
obj = get_object_or_404(ConnectionToken, pk=self.kwargs.get('pk'))
else:
app = get_object_or_404(Application, id=value.get('application'))
has_perm, actions, expired_at = app_validate_permission(user, app, system_user)
obj = super(ConnectionTokenViewSet, self).get_object()
return obj
if not has_perm:
raise serializers.ValidationError('Permission expired or invalid')
return value, user, system_user, asset, app, expired_at, actions
def create_connection_token(self):
data = self.request.query_params if self.request.method == 'GET' else self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
token: ConnectionToken = serializer.instance
return token
def get(self, request):
token = request.query_params.get('token')
value = self.get_token_from_cache(token)
if not value:
return Response('', status=404)
return Response(value)
def perform_create(self, serializer):
user, asset, application, system_user = self.get_request_resources(serializer)
self.check_user_has_resource_permission(user, asset, application, system_user)
return super(ConnectionTokenViewSet, self).perform_create(serializer)
@action(methods=['POST'], detail=False, url_path='secret-info/detail')
def get_secret_detail(self, request, *args, **kwargs):
# 非常重要的 api在逻辑层再判断一下双重保险
perm_required = 'authentication.view_connectiontokensecret'
if not request.user.has_perm(perm_required):
raise PermissionDenied('Not allow to view secret')
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
self.check_token_valid(token)
token.load_system_user_auth()
serializer = self.get_serializer(instance=token)
return Response(serializer.data, status=status.HTTP_200_OK)
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
def get_rdp_file(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_valid(token)
filename, content = self.get_rdp_file_info(token)
filename = '{}.rdp'.format(filename)
response = HttpResponse(content, content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
token = self.create_connection_token()
self.check_token_valid(token)
try:
protocol_data = self.get_client_protocol_data(token)
except ValueError as e:
return Response(data={'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
protocol_data = json.dumps(protocol_data).encode()
protocol_data = base64.b64encode(protocol_data).decode()
data = {
'url': 'jms://{}'.format(protocol_data)
}
return Response(data=data)
@action(methods=['PATCH'], detail=True)
def expire(self, request, *args, **kwargs):
instance = self.get_object()
instance.expire()
return Response(status=status.HTTP_204_NO_CONTENT)
class UserSuperConnectionTokenViewSet(
BaseUserConnectionTokenViewSet, TokenCacheMixin, GenericViewSet
):
class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
serializer_classes = {
'default': SuperConnectionTokenSerializer,
}
@@ -525,10 +323,21 @@ class UserSuperConnectionTokenViewSet(
'renewal': 'authentication.add_superconnectiontoken'
}
@action(methods=[PATCH], detail=False)
def get_request_resource_user(self, serializer):
return serializer.validated_data.get('user')
@action(methods=['PATCH'], detail=False)
def renewal(self, request, *args, **kwargs):
""" 续期 Token """
token = request.data.get('token', '')
data = self.renewal_token(token)
status_code = 200 if data.get('ok') else 404
return Response(data=data, status=status_code)
from common.utils.timezone import as_current_tz
token_id = request.data.get('token') or ''
token = get_object_or_404(ConnectionToken, pk=token_id)
date_expired = as_current_tz(token.date_expired)
if token.is_expired:
raise PermissionDenied('Token is expired at: {}'.format(date_expired))
token.renewal()
data = {
'ok': True,
'msg': f'Token is renewed, date expired: {date_expired}'
}
return Response(data=data, status=status.HTTP_200_OK)

View File

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

View File

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

View File

@@ -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,16 @@ 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
self.request.session.pop('auth_third_party_required', '')
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 +36,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

@@ -1,13 +1,71 @@
from rest_framework.generics import CreateAPIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
from django.core.cache import cache
from django.shortcuts import reverse
from authentication.serializers import PasswordVerifySerializer
from common.utils.verify_code import SendAndVerifyCodeUtil
from common.permissions import IsValidUser
from common.utils.random import random_string
from common.utils import get_object_or_none
from authentication.serializers import (
PasswordVerifySerializer, ResetPasswordCodeSerializer
)
from settings.utils import get_login_title
from users.models import User
from authentication.mixins import authenticate
from authentication.errors import PasswordInvalid
from authentication.mixins import AuthMixin
class UserResetPasswordSendCodeApi(CreateAPIView):
permission_classes = (AllowAny,)
serializer_class = ResetPasswordCodeSerializer
@staticmethod
def is_valid_user(**kwargs):
user = get_object_or_none(User, **kwargs)
if not user:
err_msg = _('User does not exist: {}').format(_("No user matched"))
return None, err_msg
if not user.is_local:
err_msg = _(
'The user is from {}, please go to the corresponding system to change the password'
).format(user.get_source_display())
return None, err_msg
return user, None
def create(self, request, *args, **kwargs):
token = request.GET.get('token')
userinfo = cache.get(token)
if not userinfo:
return reverse('authentication:forgot-previewing')
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
username = userinfo.get('username')
form_type = serializer.validated_data['form_type']
code = random_string(6, lower=False, upper=False)
other_args = {}
target = serializer.validated_data[form_type]
query_key = 'phone' if form_type == 'sms' else form_type
user, err = self.is_valid_user(username=username, **{query_key: target})
if not user:
return Response({'error': err}, status=400)
subject = '%s: %s' % (get_login_title(), _('Forgot password'))
context = {
'user': user, 'title': subject, 'code': code,
}
message = render_to_string('authentication/_msg_reset_password_code.html', context)
other_args['subject'], other_args['message'] = subject, message
SendAndVerifyCodeUtil(target, code, backend=form_type, **other_args).gen_and_send_async()
return Response({'data': 'ok'}, status=200)
class UserPasswordVerifyApi(AuthMixin, CreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = PasswordVerifySerializer

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
#
import requests
from django.utils.translation import gettext_lazy as _
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)
)
}
if '?' in settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT:
separator = '&'
else:
separator = '?'
access_token_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_ACCESS_TOKEN_ENDPOINT, separator=separator, 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': 'Bearer {}'.format(response_data.get('access_token', ''))
}
logger.debug(log_prompt.format('Get userinfo endpoint'))
if '?' in settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT:
separator = '&'
else:
separator = '?'
userinfo_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_USERINFO_ENDPOINT, separator=separator,
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,95 @@
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)
)
}
if '?' in settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT:
separator = '&'
else:
separator = '?'
redirect_url = '{url}{separator}{query}'.format(
url=settings.AUTH_OAUTH2_PROVIDER_AUTHORIZATION_ENDPOINT,
separator=separator,
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)

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