Compare commits

...

242 Commits

Author SHA1 Message Date
Jiangjie.Bai
d3355ab0ec Merge pull request #8427 from jumpserver/dev
v2.23.0 rc6
2022-06-16 18:12:44 +08:00
Jiangjie.Bai
81598a5264 perf: 推送系统用户用户名提示信息 2022-06-16 18:04:03 +08:00
feng626
298f6ba41d fix: 修改翻译 2022-06-16 18:03:16 +08:00
feng626
8e43e9ee2b fix: 授权过期通知 2022-06-16 17:52:33 +08:00
Jiangjie.Bai
adc8a8f7d3 fix: 修改翻译 2022-06-16 17:10:21 +08:00
Jiangjie.Bai
1e3da50979 fix: 修复会话加入记录更新失败的问题 2022-06-16 16:50:51 +08:00
Jiangjie.Bai
7ac385d64c Merge pull request #8420 from jumpserver/dev
v2.23.0 rc5
2022-06-16 15:46:40 +08:00
Jiangjie.Bai
2be74c4b84 fix: 修复命令列表模糊搜索报错500的问题
fix: 修复命令列表模糊搜索报错500的问题
2022-06-16 13:45:24 +08:00
feng626
75a72fb182 fix: user confirm bug 2022-06-16 11:31:27 +08:00
Jiangjie.Bai
4c2274b14e fix: 修改翻译 2022-06-16 11:19:26 +08:00
feng626
a024f26768 fix: 授权过期消息提示 2022-06-16 11:19:26 +08:00
Jiangjie.Bai
2898c35970 Merge pull request #8411 from jumpserver/dev
v2.23.0 rc4
2022-06-15 19:38:17 +08:00
Jiangjie.Bai
62f5662bd0 fix: 修复openid用户登录时默认邮件后缀使用配置项 2022-06-15 19:33:26 +08:00
ibuler
0fe221019a pref: 优化没有获取到节点的问题 2022-06-15 19:33:26 +08:00
ibuler
d745314aa1 perf: 优化签名认证 2022-06-15 19:33:26 +08:00
feng626
153fad9ac7 feat: add client linux arm64 version 2022-06-15 19:33:26 +08:00
Jiangjie.Bai
0792c7ec49 fix: 修改推送系统用户提示文案 2022-06-15 19:33:26 +08:00
fit2bot
e617697553 fix: 修复授权过期通知bug (#8404)
Co-authored-by: feng626 <1304903146@qq.com>
2022-06-15 19:33:26 +08:00
fit2bot
9dc7da3595 perf: 优化 apt (#8398)
* pref: 修改 oracle lib path

* perf: 优化 apt

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

* perf: 优化 apt

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

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

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

* Update README.md

* Update README.md

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

* perf: 修改一些加密字段

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

fix: 修改 EncryptedField 字段的 write_only 属性

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

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

* perf: 优化加密

* perf: 优化加密传输

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

* feat: 翻译

* perf: 修改动作名词

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

* perf: 优化合并字段

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

* stash it

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

* pref: 临时密码使用 rsa

* perf: 去掉 debug msg

* perf: 去掉 Debug

* perf: 去掉 debug

* perf: 抽出来

Co-authored-by: ibuler <ibuler@qq.com>
2022-05-07 16:20:12 +08:00
Jiangjie.Bai
3f856e68f0 feat: public settings 区分 public 和 open 2022-05-07 11:09:24 +08:00
fit2bot
56862a965d fix: 修复system-role获取users失败的问题 (#8196)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-05-07 10:40:12 +08:00
jiangweidong
e151548701 perf: 账号管理中查看密码记录日志 (#8157) 2022-05-05 14:42:09 +08:00
jiangweidong
c56179e9e4 feat: 支持企业微信、钉钉直接审批工单 (#8115) 2022-05-05 13:07:48 +08:00
feng626
d23953932f perf: connection token 分api权限 2022-05-05 11:45:26 +08:00
Jiangjie.Bai
2493647e5c fix: 修复windows执行ansible显示sudo失败的问题 2022-05-05 11:40:12 +08:00
Jiangjie.Bai
00ed7bb025 perf: 优化 OIDC 支持选择认证方式 2022-04-29 14:28:07 +08:00
xiaziheng
b1aadf1ee9 Fix oidc (#8165) 2022-04-29 10:59:29 +08:00
feng626
86e6982383 fix: 组织管理员 添加 view platform perm 2022-04-28 19:10:58 +08:00
halo
dc42d1caa2 perf: 修改ssh_client连接选项翻译 2022-04-28 19:09:44 +08:00
ibuler
cb5d8fa13f fix: 去掉自动生成的map文件 2022-04-26 16:46:47 +08:00
jiangweidong
3a3f7eaf71 feat: 优化SAML2生成的metadata文件内容及属性映射 2022-04-26 10:00:53 +08:00
fit2bot
9804ca5dd0 fix: workbench_orgs 去重 (#8150)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-25 11:38:15 +08:00
老广
034d0e285c Update README.md 2022-04-24 17:47:02 +08:00
feng626
104d672634 perf: client download 2022-04-24 15:09:33 +08:00
ibuler
529e3d12e0 perf: 删除 build 2022-04-24 09:12:29 +08:00
ibuler
978c1f6363 perf: 修改 Dockerfile, 优化构建 2022-04-24 09:12:29 +08:00
ibuler
d25cde1bd5 fix: 修复社区版跳转问题 2022-04-21 22:48:59 +08:00
Jiangjie.Bai
a4be0ff2f3 Merge pull request #8131 from jumpserver/dev
v2.21.0
2022-04-21 18:11:21 +08:00
Jiangjie.Bai
a6d61721dd fix: 修改csrftoken获取问题 2022-04-21 16:17:33 +08:00
fit2bot
c3de7b78c2 fix: 远程应用授权时 有些资产已经不存在了 导致授权失败 (#8127)
Co-authored-by: feng626 <1304903146@qq.com>
2022-04-21 15:36:40 +08:00
Jiangjie.Bai
e83d676712 Merge pull request #8119 from jumpserver/dev
v2.21.0-rc6
2022-04-20 20:25:43 +08:00
Jiangjie.Bai
63ee2dd8fb fix: 修复获取权限树权限控制 2022-04-20 20:14:44 +08:00
feng626
74f88d842d fix: 修改replay download perm 2022-04-20 19:14:42 +08:00
fit2bot
e61bae5ee4 perf: 优化权限位 (#8110)
* perf: 优化权限位

* perf: 优化返回的组织

* perf: 保证结果是 ok

* perf: 去掉 distinct

* perf: tree count

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

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

* perf: 优化组织

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

* feat: 设置SessionCookieNamePrefix

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

* perf: 修改id

* perf: 修改 翻译

* perf: 修改 tmp token

* perf: 修改 token

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

* perf: add lgtm

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

* feat: add Endpoint EndpointRule EndpointProtocol API

* feat: modify protocols field

* feat: 修改序列类

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

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

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

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

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

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

* feat: 修改翻译

* feat: 修改smart endpoint

* feat: 修改翻译

* feat: smart API 添加token解析

* feat: 删除 smart serializer

* feat: 修改迁移逻辑

* feat: 解决冲突

* feat: 修改匹配 endpoint

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

* perf: workspace => workbench

* perf: 修改 workspace codename

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

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

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

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

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

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

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

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

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

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

* perf: remove mixin

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

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

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

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

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

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

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

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

* fix: 修复参数错误

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

* perf: 添加 setting 到 api

* perf: 放出 mongodb

Co-authored-by: ibuler <ibuler@qq.com>
2022-03-25 14:45:08 +08:00
ibuler
8718dc6751 pref: 优雅一发 2022-03-25 14:44:33 +08:00
ibuler
9e284f96e5 perf: 修改依赖写法,避免 github 认错 2022-03-25 14:44:33 +08:00
Eric
fc06295d04 perf: 优化 windows rdp 窗口显示 2022-03-25 10:50:34 +08:00
fit2bot
9b73727bbc fix: 修复系统组件绑定角色bug (#7962)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-23 10:56:23 +08:00
Jiangjie.Bai
6bde31cdd0 fix: 修复获取远程应用认证信息问题 2022-03-22 18:54:25 +08:00
ibuler
2721793b8f fix: 修复权限 view 没有 Model 的问题 2022-03-22 16:59:01 +08:00
feng626
2ec0cb8a2c fix: 修复用户绑定角色重大bug 2022-03-22 16:55:53 +08:00
feng626
d01d44b48d fix: api docs 2022-03-22 13:05:44 +08:00
老广
0ef7a9571c Merge pull request #7800 from jumpserver/pr@dev@feat_core_redis_support_ssl
feat: JumpServer支持部署在使用了ssl的redis上
2022-03-22 10:34:32 +08:00
ibuler
54fd1fb0c8 perf: 移动到信号中 2022-03-21 19:27:23 +08:00
ibuler
87c6eec619 perf: 优化 role bingding,优化 is_superuser 2022-03-21 19:27:23 +08:00
jiangweidong
e35fbfc7e9 Update session.py 2022-03-21 19:02:30 +08:00
fit2bot
3345456dc2 fix: 修复wateway api (#7947)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-21 17:53:16 +08:00
Jiangjie.Bai
9ae74120ed fix: 修复用户API权限 2022-03-21 12:00:15 +08:00
Jiangjie.Bai
9e5c132485 fix: 修复用户API权限 2022-03-21 11:58:29 +08:00
fit2bot
5cc2fdae4f fix: 修复api docs打不开的问题 (#7938)
Co-authored-by: Jiangjie.Bai <bugatti_it@163.com>
2022-03-21 11:14:49 +08:00
jiangweidong
e993f31b6d feat: 支持纳管百度云资产 (#7921)
Co-authored-by: ibuler <ibuler@qq.com>
2022-03-21 10:39:47 +08:00
Jiangjie.Bai
60edbb36a1 fix: 应用树隐藏mongodb节点 2022-03-18 18:02:15 +08:00
fit2bot
5da1ec55a7 perf: org del ticket perm (#7932)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-18 17:44:20 +08:00
Jiangjie.Bai
b8c083af7e fix: 工单权限位放到sys角色中 2022-03-18 17:36:22 +08:00
Jiangjie.Bai
996621f303 fix: 移除权限dashboard 2022-03-18 17:10:34 +08:00
fit2bot
ec9e5da653 fix: 修复apikey perm (#7918)
Co-authored-by: feng626 <1304903146@qq.com>
2022-03-18 15:53:34 +08:00
Jiangjie.Bai
d4e4015d91 fix: 修复去除rolebiding change 权限 2022-03-18 15:38:33 +08:00
jiangweidong
794139782f feat: JumpServer支持部署在使用了ssl的redis上,可使用证书连接 2022-03-14 11:35:14 +08:00
jiangweidong
f04378eaf8 feat: JumpServer支持部署在使用了ssl的redis上 2022-03-11 12:56:22 +08:00
285 changed files with 12274 additions and 4428 deletions

1
.gitattributes vendored
View File

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

@@ -1,20 +1,5 @@
# 编译代码
FROM python:3.8-slim as stage-build
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN cd utils && bash -ixeu build.sh
FROM python:3.8-slim
ARG PIP_MIRROR=https://pypi.douban.com/simple
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
WORKDIR /opt/jumpserver
MAINTAINER JumpServer Team <ibuler@qq.com>
ARG BUILD_DEPENDENCIES=" \
g++ \
@@ -44,11 +29,12 @@ ARG TOOLS=" \
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 \
&& apt update && sleep 1 && apt update \
&& apt -y install ${BUILD_DEPENDENCIES} \
&& apt -y install ${DEPENDENCIES} \
&& apt -y install ${TOOLS} \
@@ -62,21 +48,44 @@ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& mv /bin/sh /bin/sh.bak \
&& ln -s /bin/bash /bin/sh
RUN mkdir -p /opt/jumpserver/oracle/ \
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
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"
RUN mkdir -p /opt/oracle/ \
&& cd /opt/oracle/ \
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
&& rm -f ${ORACLE_FILE}
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
WORKDIR /tmp/build
COPY ./requirements ./requirements
RUN echo > config.yml \
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
ENV PIP_MIRROR=$PIP_MIRROR
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/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
ADD . .
RUN cd utils \
&& bash -ixeu build.sh \
&& mv ../release/jumpserver /opt/jumpserver \
&& rm -rf /tmp/build \
&& echo > /opt/jumpserver/config.yml
WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs

View File

@@ -1,10 +1,13 @@
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
<p align="center">
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
</p>
<h3 align="center">多云环境下更好用的堡垒机</h3>
<p align="center">
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
<a href="https://github.com/jumpserver/jumpserver/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jumpserver/jumpserver.svg" /></a>
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
</p>
@@ -15,7 +18,7 @@
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
@@ -28,9 +31,9 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- 开源: 零门槛,线上快速获取和安装;
- 分布式: 轻松支持大规模并发访问;
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
- 多租户: 一套系统,多个子公司或部门同时使用;
- 多云支持: 一套系统,同时管理不同云上面的资产;
- 云端存储: 审计录像云端存储,永不丢失;
- 多租户: 一套系统,多个子公司和部门同时使用;
- 多应用支持: 数据库Windows远程应用Kubernetes。
### UI 展示
@@ -55,12 +58,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
- [手动安装](https://github.com/jumpserver/installer)
### 组件项目
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
- [Clients](https://github.com/jumpserver/clients) JumpServer 客户端 项目
- [Installer](https://github.com/jumpserver/installer) JumpServer 安装包 项目
| 项目 | 状态 | 描述 |
| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
| [Installer](https://github.com/jumpserver/installer)| <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
### 社区
@@ -75,27 +81,13 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
感谢以下贡献者,让 JumpServer 更加完善
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
<a href="https://github.com/jumpserver/koko/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
</a>
<a href="https://github.com/jumpserver/lina/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
</a>
<a href="https://github.com/jumpserver/luna/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
</a>
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
### 致谢
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备JumpServer 图形化组件 Lion 依赖
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库JumpServer Web数据库依赖
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC 协议设备JumpServer 图形化组件 Lion 依赖
- [OmniDB](https://omnidb.org/) Web 页面连接使用数据库JumpServer Web 数据库依赖
### JumpServer 企业版
@@ -103,14 +95,14 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
### 案例研究
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
### 安全说明
@@ -131,4 +123,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
https://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -123,6 +123,8 @@ class LoginACL(BaseACL):
'org_id': Organization.ROOT_ID,
}
ticket = Ticket.objects.create(**data)
ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(self.user)
applicant = self.user
assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant)
ticket.open(applicant)
return ticket

View File

@@ -97,7 +97,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
ticket.create_process_map_and_node(assignees)
ticket.create_process_map_and_node(assignees, user)
ticket.open(applicant=user)
return ticket

View File

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

View File

@@ -6,6 +6,7 @@ from django.db.models import F, Q
from common.drf.filters import BaseFilterSet
from common.drf.api import JMSBulkModelViewSet
from common.mixins import RecordViewLogMixin
from rbac.permissions import RBACPermission
from assets.models import SystemUser
from ..models import Account
@@ -54,7 +55,7 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
perm_model = SystemUser
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
serializer_class = serializers.AppAccountSecretSerializer
permission_classes = [RBACPermission, NeedMFAVerify]
http_method_names = ['get', 'options']

View File

@@ -41,7 +41,7 @@ class AppType(models.TextChoices):
def category_types_mapper(cls):
return {
AppCategory.db: [
cls.mysql, cls.oracle, cls.pgsql, cls.mariadb,
cls.mysql, cls.mariadb, cls.oracle, cls.pgsql,
cls.sqlserver, cls.redis, cls.mongodb
],
AppCategory.remote_app: [

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ from django.conf import settings
from orgs.mixins.models import OrgModelMixin
from common.mixins import CommonModelMixin
from common.tree import TreeNode
from common.utils import is_uuid
from assets.models import Asset, SystemUser
from ..utils import KubernetesTree
@@ -19,6 +20,7 @@ class ApplicationTreeNodeMixin:
name: str
type: str
category: str
attrs: dict
@staticmethod
def create_tree_id(pid, type, v):
@@ -99,6 +101,7 @@ class ApplicationTreeNodeMixin:
temp_pid = pid
type_category_mapper = const.AppType.type_category_mapper()
types = const.AppType.type_category_mapper().keys()
for tp in types:
if not settings.XPACK_ENABLED and const.AppType.is_xpack(tp):
continue
@@ -142,7 +145,6 @@ class ApplicationTreeNodeMixin:
pid, counts, show_empty=show_empty,
show_count=show_count
)
return tree_nodes
@classmethod
@@ -171,13 +173,18 @@ class ApplicationTreeNodeMixin:
pid = self.create_tree_id(pid, 'type', self.type)
return pid
def as_tree_node(self, pid, is_luna=False):
if is_luna and self.type == const.AppType.k8s:
def as_tree_node(self, pid, k8s_as_tree=False):
if self.type == const.AppType.k8s and k8s_as_tree:
node = KubernetesTree(pid).as_tree_node(self)
else:
node = self._as_tree_node(pid)
return node
def _attrs_to_tree(self):
if self.category == const.AppCategory.db:
return self.attrs
return {}
def _as_tree_node(self, pid):
icon_skin_category_mapper = {
'remote_app': 'chrome',
@@ -199,6 +206,7 @@ class ApplicationTreeNodeMixin:
'data': {
'category': self.category,
'type': self.type,
'attrs': self._attrs_to_tree()
}
}
})
@@ -239,6 +247,14 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
def category_remote_app(self):
return self.category == const.AppCategory.remote_app.value
@property
def category_cloud(self):
return self.category == const.AppCategory.cloud.value
@property
def category_db(self):
return self.category == const.AppCategory.db.value
def get_rdp_remote_app_setting(self):
from applications.serializers.attrs import get_serializer_class_by_application_type
if not self.category_remote_app:
@@ -264,12 +280,23 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin):
'parameters': parameters
}
def get_remote_app_asset(self):
def get_remote_app_asset(self, raise_exception=True):
asset_id = self.attrs.get('asset')
if not asset_id:
if is_uuid(asset_id):
return Asset.objects.filter(id=asset_id).first()
if raise_exception:
raise ValueError("Remote App not has asset attr")
asset = Asset.objects.filter(id=asset_id).first()
return asset
def get_target_ip(self):
target_ip = ''
if self.category_remote_app:
asset = self.get_remote_app_asset()
target_ip = asset.ip if asset else target_ip
elif self.category_cloud:
target_ip = self.attrs.get('cluster')
elif self.category_db:
target_ip = self.attrs.get('host')
return target_ip
class ApplicationUser(SystemUser):

View File

@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from assets.serializers.base import AuthSerializerMixin
from common.drf.serializers import MethodSerializer
from common.drf.serializers import MethodSerializer, SecretReadableMixin
from .attrs import (
category_serializer_classes_mapping,
type_serializer_classes_mapping,
@@ -152,7 +152,7 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
return super().to_representation(instance)
class AppAccountSecretSerializer(AppAccountSerializer):
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
class Meta(AppAccountSerializer.Meta):
fields_backup = [
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ from rest_framework.generics import CreateAPIView
from orgs.mixins.api import OrgBulkModelViewSet
from rbac.permissions import RBACPermission
from common.drf.filters import BaseFilterSet
from common.mixins import RecordViewLogMixin
from common.permissions import NeedMFAVerify
from ..tasks.account_connectivity import test_accounts_connectivity_manual
from ..models import AuthBook, Node
@@ -79,7 +80,7 @@ class AccountViewSet(OrgBulkModelViewSet):
return Response(data={'task': task.id})
class AccountSecretsViewSet(AccountViewSet):
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
"""
因为可能要导出所有账号,所以单独建立了一个 viewset
"""

View File

@@ -16,7 +16,7 @@ from perms.filters import AssetPermissionFilter
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.mixins import generics
from assets.api import FilterAssetByNodeMixin
from ..models import Asset, Node, Platform
from ..models import Asset, Node, Platform, Gateway
from .. import serializers
from ..tasks import (
update_assets_hardware_info_manual, test_assets_connectivity_manual,
@@ -181,7 +181,7 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView):
def check_permissions(self, request):
action = request.data.get('action')
action_perm_require = {
'refresh': 'assets.refresh_assethardwareinfo1',
'refresh': 'assets.refresh_assethardwareinfo',
}
perm_required = action_perm_require.get(action)
has = self.request.user.has_perm(perm_required)
@@ -199,7 +199,7 @@ class AssetGatewayListApi(generics.ListAPIView):
asset_id = self.kwargs.get('pk')
asset = get_object_or_404(Asset, pk=asset_id)
if not asset.domain:
return []
return Gateway.objects.none()
queryset = asset.domain.gateways.filter(protocol='ssh')
return queryset

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,32 @@
# Generated by Django 3.1.14 on 2022-04-12 03:45
from django.db import migrations, models
def create_internal_platform(apps, schema_editor):
model = apps.get_model("assets", "Platform")
db_alias = schema_editor.connection.alias
type_platforms = (
('AIX', 'Unix', None),
)
for name, base, meta in type_platforms:
defaults = {'name': name, 'base': base, 'meta': meta, 'internal': True}
model.objects.using(db_alias).update_or_create(
name=name, defaults=defaults
)
class Migration(migrations.Migration):
dependencies = [
('assets', '0089_auto_20220310_0616'),
]
operations = [
migrations.AlterField(
model_name='asset',
name='number',
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
),
migrations.RunPython(create_internal_platform)
]

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#
import uuid
import logging
@@ -11,7 +11,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import ValidationError
from common.fields.model import JsonDictTextField
from common.db.fields import JsonDictTextField
from common.utils import lazyproperty
from orgs.mixins.models import OrgModelMixin, OrgManager
@@ -223,7 +223,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
# Some information
public_ip = models.CharField(max_length=128, blank=True, null=True, verbose_name=_('Public IP'))
number = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Asset number'))
number = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Asset number'))
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
created_by = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Created by'))
@@ -235,6 +235,9 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
def __str__(self):
return '{0.hostname}({0.ip})'.format(self)
def get_target_ip(self):
return self.ip
def set_admin_user_relation(self):
from .authbook import AuthBook
if not self.admin_user:
@@ -280,16 +283,44 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
def is_support_ansible(self):
return self.has_protocol('ssh') and self.platform_base not in ("Other",)
def get_auth_info(self):
def get_auth_info(self, with_become=False):
if not self.admin_user:
return {}
self.admin_user.load_asset_special_auth(self)
if self.is_unixlike() and self.admin_user.su_enabled and self.admin_user.su_from:
auth_user = self.admin_user.su_from
become_user = self.admin_user
else:
auth_user = self.admin_user
become_user = None
auth_user.load_asset_special_auth(self)
info = {
'username': self.admin_user.username,
'password': self.admin_user.password,
'private_key': self.admin_user.private_key_file,
'username': auth_user.username,
'password': auth_user.password,
'private_key': auth_user.private_key_file
}
if not with_become or self.is_windows():
return info
if become_user:
become_user.load_asset_special_auth(self)
become_method = 'su'
become_username = become_user.username
become_pass = become_user.password
else:
become_method = 'sudo'
become_username = 'root'
become_pass = auth_user.password
become_info = {
'become': {
'method': become_method,
'username': become_username,
'pass': become_pass
}
}
info.update(become_info)
return info
def nodes_display(self):

View File

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

View File

@@ -181,8 +181,10 @@ class CommandFilterRule(OrgModelMixin):
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(applicant=session.user_obj)
applicant = session.user_obj
assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant)
ticket.open(applicant)
return ticket
@classmethod

View File

@@ -133,11 +133,23 @@ class AuthMixin:
self.password = password
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
# 清除认证信息
self._clean_auth_info_if_manual_login_mode()
# 加载临时认证信息
# 先加载临时认证信息
if self.login_mode == self.LOGIN_MANUAL:
self._load_tmp_auth_if_has(app_id, user_id)
return
# Remote app
from applications.models import Application
app = get_object_or_none(Application, pk=app_id)
if app and app.category_remote_app:
# Remote app
self._load_remoteapp_more_auth(app, username, user_id)
return
# Other app
# 更新用户名
from users.models import User
user = get_object_or_none(User, pk=user_id) if user_id else None
@@ -148,6 +160,11 @@ class AuthMixin:
_username = username
self.username = _username
def _load_remoteapp_more_auth(self, app, username, user_id):
asset = app.get_remote_app_asset(raise_exception=False)
if asset:
self.load_asset_more_auth(asset_id=asset.id, username=username, user_id=user_id)
def load_asset_special_auth(self, asset, username=''):
"""
AuthBook 的数据状态

View File

@@ -5,8 +5,8 @@ from assets.models import AuthBook
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import AuthSerializerMixin
from .utils import validate_password_contains_left_double_curly_bracket
from common.utils.encode import ssh_pubkey_gen
from common.drf.serializers import SecretReadableMixin
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
@@ -31,10 +31,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
fields = fields_small + fields_fk
extra_kwargs = {
'username': {'required': True},
'password': {
'write_only': True,
"validators": [validate_password_contains_left_double_curly_bracket]
},
'private_key': {'write_only': True},
'public_key': {'write_only': True},
'systemuser_display': {'label': _('System user display')}
@@ -70,7 +66,7 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
return super().to_representation(instance)
class AccountSecretSerializer(AccountSerializer):
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class Meta(AccountSerializer.Meta):
fields_backup = [
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',

View File

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

View File

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

View File

@@ -31,24 +31,24 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer):
class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer):
type_display = serializers.ReadOnlyField(source='get_type_display')
action_display = serializers.ReadOnlyField(source='get_action_display')
type_display = serializers.ReadOnlyField(source='get_type_display', label=_("Type display"))
action_display = serializers.ReadOnlyField(source='get_action_display', label=_("Action display"))
class Meta:
model = CommandFilterRule
fields_mini = ['id']
fields_small = fields_mini + [
'type', 'type_display', 'content', 'ignore_case', 'pattern', 'priority',
'action', 'action_display', 'reviewers',
'date_created', 'date_updated',
'comment', 'created_by',
'type', 'type_display', 'content', 'ignore_case', 'pattern',
'priority', 'action', 'action_display', 'reviewers',
'date_created', 'date_updated', 'comment', 'created_by',
]
fields_fk = ['filter']
fields = fields_small + fields_fk
extra_kwargs = {
'date_created': {'label': _("Date created")},
'date_updated': {'label': _("Date updated")},
'action_display': {'label': _("Action display")}
'action_display': {'label': _("Action display")},
'pattern': {'label': _("Pattern")}
}
def __init__(self, *args, **kwargs):

View File

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

View File

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

View File

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

View File

@@ -156,10 +156,7 @@ class AccountBackupHandler:
logger.info('步骤完成: 用时 {}s'.format(timedelta))
return files
def send_backup_mail(self, files):
recipients = self.execution.plan_snapshot.get('recipients')
if not recipients:
return
def send_backup_mail(self, files, recipients):
if not files:
return
recipients = User.objects.filter(id__in=list(recipients))
@@ -198,8 +195,16 @@ class AccountBackupHandler:
is_success = False
error = '-'
try:
files = self.create_excel()
self.send_backup_mail(files)
recipients = self.execution.plan_snapshot.get('recipients')
if not recipients:
logger.info(
'\n'
'\033[32m>>> 该备份任务未分配收件人\033[0m'
''
)
else:
files = self.create_excel()
self.send_backup_mail(files, recipients)
except Exception as e:
self.is_frozen = True
logger.error('任务执行被异常中断')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,11 @@ from .connection_token import *
from .token import *
from .mfa import *
from .access_key import *
from .confirm import *
from .login_confirm import *
from .sso import *
from .wecom import *
from .dingtalk import *
from .feishu import *
from .password import *
from .temp_token import *

View File

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

View File

@@ -0,0 +1,85 @@
# -*- 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.response import Response
from common.permissions import IsValidUser
from ..mfa import MFAOtp
from ..const import ConfirmType
from ..mixins import authenticate
from ..serializers import ConfirmSerializer
class ConfirmViewSet(ListCreateAPIView):
permission_classes = (IsValidUser,)
serializer_class = ConfirmSerializer
def check(self, confirm_type: str):
if confirm_type == ConfirmType.MFA:
return self.user.mfa_enabled
if confirm_type == ConfirmType.PASSWORD:
return self.user.is_password_authenticate()
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)
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')
secret_key = validated_data.get('secret_key')
ok, msg = self.authenticate(confirm_type, secret_key)
if ok:
request.session["MFA_VERIFY_TIME"] = int(time.time())
return Response('ok')
return Response({'error': msg}, status=400)

View File

@@ -7,7 +7,6 @@ import os
import base64
import ctypes
from django.conf import settings
from django.core.cache import cache
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
@@ -19,6 +18,7 @@ from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework import serializers
from django.conf import settings
from applications.models import Application
from authentication.signals import post_auth_failed
@@ -30,13 +30,14 @@ from common.http import is_true
from perms.models.base import Action
from perms.utils.application.permission import get_application_actions
from perms.utils.asset.permission import get_asset_actions
from common.const.http import PATCH
from terminal.models import EndpointRule
from ..serializers import (
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer
)
logger = get_logger(__name__)
__all__ = ['UserConnectionTokenViewSet']
__all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', 'TokenCacheMixin']
class ClientProtocolMixin:
@@ -51,6 +52,17 @@ class ClientProtocolMixin:
request: Request
get_serializer: Callable
create_token: Callable
get_serializer_context: Callable
def get_smart_endpoint(self, protocol, asset=None, application=None):
if asset:
target_ip = asset.get_target_ip()
elif application:
target_ip = application.get_target_ip()
else:
target_ip = ''
endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request)
return endpoint
def get_request_resource(self, serializer):
asset = serializer.validated_data.get('asset')
@@ -58,8 +70,7 @@ class ClientProtocolMixin:
system_user = serializer.validated_data['system_user']
user = serializer.validated_data.get('user')
if not user or not self.request.user.is_superuser:
user = self.request.user
user = user if user else self.request.user
return asset, application, system_user, user
@staticmethod
@@ -92,8 +103,8 @@ class ClientProtocolMixin:
'autoreconnection enabled:i': '1',
'bookmarktype:i': '3',
'use redirection server name:i': '0',
'smart sizing:i': '0',
#'drivestoredirect:s': '*',
'smart sizing:i': '1',
# 'drivestoredirect:s': '*',
# 'domain:s': ''
# 'alternate shell:s:': '||MySQLWorkbench',
# 'remoteapplicationname:s': 'Firefox',
@@ -122,10 +133,10 @@ class ClientProtocolMixin:
options['screen mode id:i'] = '2' if full_screen else '1'
# RDP Server 地址
address = settings.TERMINAL_RDP_ADDR
if not address or address == 'localhost:3389':
address = self.request.get_host().split(':')[0] + ':3389'
options['full address:s'] = address
endpoint = self.get_smart_endpoint(
protocol='rdp', asset=asset, application=application
)
options['full address:s'] = f'{endpoint.host}:{endpoint.rdp_port}'
# 用户名
options['username:s'] = '{}|{}'.format(user.username, token)
if system_user.ad_domain:
@@ -134,8 +145,7 @@ class ClientProtocolMixin:
if width and height:
options['desktopwidth:i'] = width
options['desktopheight:i'] = height
else:
options['smart sizing:i'] = '1'
options['winposstr:s:'] = f'0,1,0,0,{width},{height}'
options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
@@ -151,7 +161,6 @@ class ClientProtocolMixin:
options['alternate shell:s'] = app
options['remoteapplicationprogram:s'] = app
options['remoteapplicationname:s'] = name
options['remoteapplicationcmdline:s'] = '- ' + self.get_encrypt_cmdline(application)
else:
name = '*'
@@ -160,6 +169,28 @@ class ClientProtocolMixin:
content += f'{k}:{v}\n'
return name, content
def get_ssh_token(self, serializer):
asset, application, system_user, user = self.get_request_resource(serializer)
token, secret = self.create_token(user, asset, application, system_user)
if asset:
name = asset.hostname
elif application:
name = application.name
else:
name = '*'
endpoint = self.get_smart_endpoint(
protocol='ssh', asset=asset, application=application
)
content = {
'ip': endpoint.host,
'port': str(endpoint.ssh_port),
'username': f'JMS-{token}',
'password': secret
}
token = json.dumps(content)
return name, token
def get_encrypt_cmdline(self, app: Application):
parameters = app.get_rdp_remote_app_setting()['parameters']
parameters = parameters.encode('ascii')
@@ -173,6 +204,37 @@ class ClientProtocolMixin:
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':
@@ -188,38 +250,6 @@ class ClientProtocolMixin:
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
def get_valid_serializer(self):
if self.request.method == 'GET':
data = self.request.query_params
else:
data = self.request.data
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
return serializer
def get_client_protocol_data(self, serializer):
asset, application, system_user, user = self.get_request_resource(serializer)
protocol = system_user.protocol
username = user.username
if protocol == 'rdp':
name, config = self.get_rdp_file_content(serializer)
elif protocol == 'ssh':
# Todo:
name = ''
config = 'ssh://system_user@asset@user@jumpserver-ssh'
else:
raise ValueError('Protocol not support: {}'.format(protocol))
filename = "{}-{}-jumpserver".format(username, name)
data = {
"filename": filename,
"protocol": system_user.protocol,
"username": username,
"config": config
}
return data
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
def get_client_protocol_url(self, request, *args, **kwargs):
serializer = self.get_valid_serializer()
@@ -327,22 +357,49 @@ class SecretDetailMixin:
return Response(data=serializer.data, status=200)
class UserConnectionTokenViewSet(
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
SecretDetailMixin, GenericViewSet
):
serializer_classes = {
'default': ConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
class TokenCacheMixin:
""" endpoint smart view 用到此类来解析token中的资产、应用 """
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
rbac_perms = {
'GET': 'authentication.view_connectiontoken',
'create': 'authentication.add_connectiontoken',
'get_secret_detail': 'authentication.view_connectiontokensecret',
'get_rdp_file': 'authentication.add_connectiontoken',
'get_client_protocol_url': 'authentication.add_connectiontoken',
}
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):
@@ -359,11 +416,7 @@ class UserConnectionTokenViewSet(
raise PermissionDenied(error)
return True
def create_token(self, user, asset, application, system_user, ttl=5 * 60):
# 再次强调一下权限
perm_required = 'authentication.add_superconnectiontoken'
if user != self.request.user and not self.request.user.has_perm(perm_required):
raise PermissionDenied('Only can create user token')
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)
@@ -391,8 +444,7 @@ class UserConnectionTokenViewSet(
'application_name': str(application)
})
key = self.CACHE_KEY_PREFIX.format(token)
cache.set(key, value, timeout=ttl)
self.set_token_to_cache(token, value, ttl)
return token, secret
def create(self, request, *args, **kwargs):
@@ -404,10 +456,25 @@ class UserConnectionTokenViewSet(
tp = 'app' if application else 'asset'
data = {
"id": token, 'secret': secret,
'type': tp, 'protocol': system_user.protocol
'type': tp, 'protocol': system_user.protocol,
'expire_time': self.get_token_ttl(token),
}
return Response(data, status=201)
class UserConnectionTokenViewSet(BaseUserConnectionTokenViewSet, SecretDetailMixin):
serializer_classes = {
'default': ConnectionTokenSerializer,
'get_secret_detail': ConnectionTokenSecretSerializer,
}
rbac_perms = {
'GET': 'authentication.view_connectiontoken',
'create': '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
@@ -415,8 +482,7 @@ class UserConnectionTokenViewSet(
from perms.utils.asset.permission import validate_permission as asset_validate_permission
from perms.utils.application.permission import validate_permission as app_validate_permission
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
value = self.get_token_from_cache(token)
if not value:
raise serializers.ValidationError('Token not found')
@@ -442,9 +508,27 @@ class UserConnectionTokenViewSet(
def get(self, request):
token = request.query_params.get('token')
key = self.CACHE_KEY_PREFIX.format(token)
value = cache.get(key, None)
value = self.get_token_from_cache(token)
if not value:
return Response('', status=404)
return Response(value)
class UserSuperConnectionTokenViewSet(
BaseUserConnectionTokenViewSet, TokenCacheMixin, GenericViewSet
):
serializer_classes = {
'default': SuperConnectionTokenSerializer,
}
rbac_perms = {
'create': 'authentication.add_superconnectiontoken',
'renewal': 'authentication.add_superconnectiontoken'
}
@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)

View File

@@ -2,7 +2,7 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.mixins.api import RoleUserMixin, RoleAdminMixin
@@ -26,9 +26,8 @@ class DingTalkQRUnBindBase(APIView):
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,)
permission_classes = (IsAuthConfirmTimeValid,)
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
user_id_url_kwarg = 'user_id'

View File

@@ -2,7 +2,7 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.mixins.api import RoleUserMixin, RoleAdminMixin
@@ -26,7 +26,7 @@ class FeiShuQRUnBindBase(APIView):
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,)
permission_classes = (IsAuthConfirmTimeValid,)
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):

View File

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

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
from django.shortcuts import redirect
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView
@@ -28,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
def create(self, request, *args, **kwargs):
self.create_session_if_need()
# 如果认证没有过,检查账号密码
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
try:
user = self.check_user_auth_if_need()
user = self.get_user_or_auth(serializer.validated_data)
self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user)
self.send_auth_signal(success=True, user=user)

View File

@@ -2,7 +2,7 @@ from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from users.permissions import IsAuthPasswdTimeValid
from users.permissions import IsAuthConfirmTimeValid
from users.models import User
from common.utils import get_logger
from common.mixins.api import RoleUserMixin, RoleAdminMixin
@@ -26,9 +26,8 @@ class WeComQRUnBindBase(APIView):
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
permission_classes = (IsAuthPasswdTimeValid,)
permission_classes = (IsAuthConfirmTimeValid,)
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
user_id_url_kwarg = 'user_id'

View File

@@ -1,10 +1,11 @@
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth import get_user_model
from users.models import User
from common.utils import get_logger
UserModel = get_user_model()
logger = get_logger(__file__)

View File

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

View File

@@ -53,7 +53,7 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
else:
built = False
return (user, built)
return user, built
def pre_check(self, username, password):
if not settings.AUTH_LDAP:
@@ -75,6 +75,9 @@ class LDAPAuthorizationBackend(JMSBaseAuthBackend, LDAPBackend):
def authenticate(self, request=None, username=None, password=None, **kwargs):
logger.info('Authentication LDAP backend')
if username is None or password is None:
logger.info('No username or password')
return None
match, msg = self.pre_check(username, password)
if not match:
logger.info('Authenticate failed: {}'.format(msg))
@@ -154,6 +157,8 @@ class LDAPUser(_LDAPUser):
def _populate_user_from_attributes(self):
for field, attr in self.settings.USER_ATTR_MAP.items():
if field in ['groups']:
continue
try:
value = self.attrs[attr][0]
value = value.strip()

View File

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

View File

@@ -13,20 +13,23 @@ User = get_user_model()
class CreateUserMixin:
def get_django_user(self, username, password=None, *args, **kwargs):
@staticmethod
def get_django_user(username, password=None, *args, **kwargs):
if isinstance(username, bytes):
username = username.decode()
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
if '@' in username:
email = username
else:
email_suffix = settings.EMAIL_SUFFIX
email = '{}@{}'.format(username, email_suffix)
user = User(username=username, name=username, email=email)
user.source = user.Source.radius.value
user.save()
user = User.objects.filter(username=username).first()
if user:
return user
if '@' in username:
email = username
else:
email_suffix = settings.EMAIL_SUFFIX
email = '{}@{}'.format(username, email_suffix)
user = User(username=username, name=username, email=email)
user.source = user.Source.radius.value
user.save()
return user
def _perform_radius_auth(self, client, packet):

View File

@@ -14,7 +14,7 @@ from ..base import JMSModelBackend
__all__ = ['SAML2Backend']
logger = get_logger(__file__)
logger = get_logger(__name__)
class SAML2Backend(JMSModelBackend):

View File

@@ -74,27 +74,37 @@ class PrepareRequestMixin:
return idp_settings
@staticmethod
def get_attribute_consuming_service():
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
if attr_mapping and isinstance(attr_mapping, dict):
attr_list = [
{
"name": sp_key,
"friendlyName": idp_key, "isRequired": True
}
for idp_key, sp_key in attr_mapping.items()
]
request_attribute_template = {
"attributeConsumingService": {
"isDefault": False,
"serviceName": "JumpServer",
"serviceDescription": "JumpServer",
"requestedAttributes": attr_list
}
def get_request_attributes():
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES or {}
attr_map_reverse = {v: k for k, v in attr_mapping.items()}
need_attrs = (
('username', 'username', True),
('email', 'email', True),
('name', 'name', False),
('phone', 'phone', False),
('comment', 'comment', False),
)
attr_list = []
for name, friend_name, is_required in need_attrs:
rename_name = attr_map_reverse.get(friend_name)
name = rename_name if rename_name else name
attr_list.append({
"name": name, "isRequired": is_required,
"friendlyName": friend_name,
})
return attr_list
def get_attribute_consuming_service(self):
attr_list = self.get_request_attributes()
request_attribute_template = {
"attributeConsumingService": {
"isDefault": False,
"serviceName": "JumpServer",
"serviceDescription": "JumpServer",
"requestedAttributes": attr_list
}
return request_attribute_template
else:
return {}
}
return request_attribute_template
@staticmethod
def get_advanced_settings():
@@ -167,11 +177,14 @@ class PrepareRequestMixin:
def get_attributes(self, saml_instance):
user_attrs = {}
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
attrs = saml_instance.get_attributes()
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
for attr, value in attrs.items():
attr = attr.rsplit('/', 1)[-1]
if attr_mapping and attr_mapping.get(attr):
attr = attr_mapping.get(attr)
if attr not in valid_attrs:
continue
user_attrs[attr] = self.value_to_str(value)

View File

@@ -0,0 +1,26 @@
from django.utils import timezone
from django.conf import settings
from django.core.exceptions import PermissionDenied
from authentication.models import TempToken
from .base import JMSModelBackend
class TempTokenAuthBackend(JMSModelBackend):
model = TempToken
def authenticate(self, request, username='', password='', *args, **kwargs):
token = self.model.objects.filter(username=username, secret=password).first()
if not token:
return None
if not token.is_valid:
raise PermissionDenied('Token is invalid, expired at {}'.format(token.date_expired))
token.verified = True
token.date_verified = timezone.now()
token.save()
return token.user
@staticmethod
def is_enabled():
return settings.AUTH_TEMP_TOKEN

View File

@@ -1,2 +1,10 @@
from django.db.models import TextChoices
RSA_PRIVATE_KEY = 'rsa_private_key'
RSA_PUBLIC_KEY = 'rsa_public_key'
class ConfirmType(TextChoices):
RELOGIN = 'relogin', 'Re-Login'
PASSWORD = 'password', 'Password'
MFA = 'mfa', 'MFA'

View File

@@ -1,15 +1,25 @@
# -*- coding: utf-8 -*-
#
from django import forms
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from captcha.fields import CaptchaField, CaptchaTextInput
from common.utils import get_logger, decrypt_password
logger = get_logger(__name__)
class EncryptedField(forms.CharField):
def to_python(self, value):
value = super().to_python(value)
return decrypt_password(value)
class UserLoginForm(forms.Form):
days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24)
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or days_auto_login < 1
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE \
or days_auto_login < 1
username = forms.CharField(
label=_('Username'), max_length=100,
@@ -18,7 +28,7 @@ class UserLoginForm(forms.Form):
'autofocus': 'autofocus'
})
)
password = forms.CharField(
password = EncryptedField(
label=_('Password'), widget=forms.PasswordInput,
max_length=1024, strip=False
)

View File

@@ -1,5 +1,11 @@
import base64
from django.shortcuts import redirect, reverse
from django.utils.deprecation import MiddlewareMixin
from django.http import HttpResponse
from django.conf import settings
from common.utils import gen_key_pair
class MFAMiddleware:
@@ -34,3 +40,48 @@ class MFAMiddleware:
url = reverse('authentication:login-mfa') + '?_=middleware'
return redirect(url)
class SessionCookieMiddleware(MiddlewareMixin):
@staticmethod
def set_cookie_public_key(request, response):
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
public_key = request.session.get(pub_key_name)
cookie_key = request.COOKIES.get(pub_key_name)
if public_key and public_key == cookie_key:
return
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key, public_key = gen_key_pair()
public_key_decode = base64.b64encode(public_key.encode()).decode()
request.session[pub_key_name] = public_key_decode
request.session[pri_key_name] = private_key
response.set_cookie(pub_key_name, public_key_decode)
@staticmethod
def set_cookie_session_prefix(request, response):
key = settings.SESSION_COOKIE_NAME_PREFIX_KEY
value = settings.SESSION_COOKIE_NAME_PREFIX
if request.COOKIES.get(key) == value:
return response
response.set_cookie(key, value)
@staticmethod
def set_cookie_session_expire(request, response):
if not request.session.get('auth_session_expiration_required'):
return
value = 'age'
if settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or \
not request.session.get('auto_login', False):
value = 'close'
age = request.session.get_expiry_age()
response.set_cookie('jms_session_expire', value, max_age=age)
request.session.pop('auth_session_expiration_required', None)
def process_response(self, request, response: HttpResponse):
self.set_cookie_session_prefix(request, response)
self.set_cookie_public_key(request, response)
self.set_cookie_session_expire(request, response)
return response

View File

@@ -0,0 +1,32 @@
# Generated by Django 3.1.14 on 2022-04-08 07:04
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
('authentication', '0009_auto_20220310_0616'),
]
operations = [
migrations.CreateModel(
name='TempToken',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('secret', models.CharField(max_length=64, verbose_name='Secret')),
('verified', models.BooleanField(default=False, verbose_name='Verified')),
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
('date_expired', models.DateTimeField(verbose_name='Date expired')),
],
options={
'verbose_name': 'Temporary token',
},
),
]

View File

@@ -23,9 +23,7 @@ from acls.models import LoginACL
from users.models import User
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
from . import errors
from .utils import rsa_decrypt, gen_key_pair
from .signals import post_auth_success, post_auth_failed
from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
logger = get_logger(__name__)
@@ -58,6 +56,7 @@ def authenticate(request=None, **credentials):
for backend, backend_path in _get_backends(return_tuples=True):
# 检查用户名是否允许认证 (预先检查,不浪费认证时间)
logger.info('Try using auth backend: {}'.format(str(backend)))
if not backend.username_allow_authenticate(username):
continue
@@ -91,46 +90,8 @@ def authenticate(request=None, **credentials):
auth.authenticate = authenticate
class PasswordEncryptionViewMixin:
request = None
def get_decrypted_password(self, password=None, username=None):
request = self.request
if hasattr(request, 'data'):
data = request.data
else:
data = request.POST
username = username or data.get('username')
password = password or data.get('password')
password = self.decrypt_passwd(password)
if not password:
self.raise_password_decrypt_failed(username=username)
return password
def raise_password_decrypt_failed(self, username):
ip = self.get_request_ip()
raise errors.CredentialError(
error=errors.reason_password_decrypt_failed,
username=username, ip=ip, request=self.request
)
def decrypt_passwd(self, raw_passwd):
# 获取解密密钥,对密码进行解密
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
if rsa_private_key is None:
return raw_passwd
try:
return rsa_decrypt(raw_passwd, rsa_private_key)
except Exception as e:
logger.error(e, exc_info=True)
logger.error(
f'Decrypt password failed: password[{raw_passwd}] '
f'rsa_private_key[{rsa_private_key}]'
)
return None
class CommonMixin:
request: Request
def get_request_ip(self):
ip = ''
@@ -139,26 +100,6 @@ class PasswordEncryptionViewMixin:
ip = ip or get_request_ip(self.request)
return ip
def get_context_data(self, **kwargs):
# 生成加解密密钥对public_key传递给前端private_key存入session中供解密使用
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
if not all([rsa_private_key, rsa_public_key]):
rsa_private_key, rsa_public_key = gen_key_pair()
rsa_public_key = rsa_public_key.replace('\n', '\\n')
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
kwargs.update({
'rsa_public_key': rsa_public_key,
})
return super().get_context_data(**kwargs)
class CommonMixin(PasswordEncryptionViewMixin):
request: Request
get_request_ip: Callable
def raise_credential_error(self, error):
raise self.partial_credential_error(error=error)
@@ -193,20 +134,13 @@ class CommonMixin(PasswordEncryptionViewMixin):
user.backend = self.request.session.get("auth_backend")
return user
def get_auth_data(self, decrypt_passwd=False):
def get_auth_data(self, data):
request = self.request
if hasattr(request, 'data'):
data = request.data
else:
data = request.POST
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
username, password, challenge, public_key, auto_login = bulk_get(data, items, default='')
ip = self.get_request_ip()
self._set_partial_credential_error(username=username, ip=ip, request=request)
if decrypt_passwd:
password = self.get_decrypted_password()
password = password + challenge.strip()
return username, password, public_key, ip, auto_login
@@ -482,10 +416,10 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
need = cache.get(self.key_prefix_captcha.format(ip))
return need
def check_user_auth(self, decrypt_passwd=False):
def check_user_auth(self, valid_data=None):
# pre check
self.check_is_block()
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
username, password, public_key, ip, auto_login = self.get_auth_data(valid_data)
self._check_only_allow_exists_user_auth(username)
# check auth
@@ -537,11 +471,12 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
self.mark_password_ok(user, False)
return user
def check_user_auth_if_need(self, decrypt_passwd=False):
def get_user_or_auth(self, valid_data):
request = self.request
if not request.session.get('auth_password'):
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
return self.get_user_from_session()
if request.session.get('auth_password'):
return self.get_user_from_session()
else:
return self.check_user_auth(valid_data)
def clear_auth_mark(self):
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']

View File

@@ -1,8 +1,9 @@
import uuid
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from django.conf import settings
from rest_framework.authtoken.models import Token
from common.db import models
@@ -64,6 +65,27 @@ class ConnectionToken(models.JMSBaseModel):
]
class TempToken(models.JMSModel):
username = models.CharField(max_length=128, verbose_name=_("Username"))
secret = models.CharField(max_length=64, verbose_name=_("Secret"))
verified = models.BooleanField(default=False, verbose_name=_("Verified"))
date_verified = models.DateTimeField(null=True, verbose_name=_("Date verified"))
date_expired = models.DateTimeField(verbose_name=_("Date expired"))
class Meta:
verbose_name = _("Temporary token")
@property
def user(self):
from users.models import User
return User.objects.filter(username=self.username).first()
@property
def is_valid(self):
not_expired = self.date_expired and self.date_expired > timezone.now()
return not self.verified and not_expired
class SuperConnectionToken(ConnectionToken):
class Meta:
proxy = True

View File

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

View File

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

View File

@@ -1,123 +1,28 @@
# -*- coding: utf-8 -*-
#
from django.utils import timezone
from rest_framework import serializers
from common.utils import get_object_or_none
from users.models import User
from assets.models import Asset, SystemUser, Gateway, Domain, CommandFilterRule
from applications.models import Application
from users.serializers import UserProfileSerializer
from assets.serializers import ProtocolsField
from perms.serializers.base import ActionsField
from .models import AccessKey
__all__ = [
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
'MFAChallengeSerializer', 'SSOTokenSerializer',
'ConnectionTokenSerializer', 'ConnectionTokenSecretSerializer',
'PasswordVerifySerializer', 'MFASelectTypeSerializer',
'ConnectionTokenSerializer', 'ConnectionTokenApplicationSerializer',
'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer',
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer',
'SuperConnectionTokenSerializer'
]
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret', 'is_active', 'date_created']
read_only_fields = ['id', 'secret', 'date_created']
class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6)
class PasswordVerifySerializer(serializers.Serializer):
password = serializers.CharField()
class BearerTokenSerializer(serializers.Serializer):
username = serializers.CharField(allow_null=True, required=False, write_only=True)
password = serializers.CharField(write_only=True, allow_null=True,
required=False, allow_blank=True)
public_key = serializers.CharField(write_only=True, allow_null=True,
allow_blank=True, required=False)
token = serializers.CharField(read_only=True)
keyword = serializers.SerializerMethodField()
date_expired = serializers.DateTimeField(read_only=True)
user = UserProfileSerializer(read_only=True)
@staticmethod
def get_keyword(obj):
return 'Bearer'
def update_last_login(self, user):
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
def get_request_user(self):
request = self.context.get('request')
if request.user and request.user.is_authenticated:
user = request.user
else:
user_id = request.session.get('user_id')
user = get_object_or_none(User, pk=user_id)
if not user:
raise serializers.ValidationError(
"user id {} not exist".format(user_id)
)
return user
def create(self, validated_data):
request = self.context.get('request')
user = self.get_request_user()
token, date_expired = user.create_bearer_token(request)
self.update_last_login(user)
instance = {
"token": token,
"date_expired": date_expired,
"user": user
}
return instance
class MFASelectTypeSerializer(serializers.Serializer):
type = serializers.CharField()
username = serializers.CharField(required=False, allow_blank=True, allow_null=True)
class MFAChallengeSerializer(serializers.Serializer):
type = serializers.CharField(write_only=True, required=False, allow_blank=True)
code = serializers.CharField(write_only=True)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
class SSOTokenSerializer(serializers.Serializer):
username = serializers.CharField(write_only=True)
login_url = serializers.CharField(read_only=True)
next = serializers.CharField(write_only=True, allow_blank=True, required=False, allow_null=True)
class ConnectionTokenSerializer(serializers.Serializer):
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
system_user = serializers.CharField(max_length=128, required=True)
asset = serializers.CharField(max_length=128, required=False)
application = serializers.CharField(max_length=128, required=False)
@staticmethod
def validate_user(user_id):
from users.models import User
user = User.objects.filter(id=user_id).first()
if user is None:
raise serializers.ValidationError('user id not exist')
return user
@staticmethod
def validate_system_user(system_user_id):
from assets.models import SystemUser
@@ -152,6 +57,18 @@ class ConnectionTokenSerializer(serializers.Serializer):
return super().validate(attrs)
class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
@staticmethod
def validate_user(user_id):
from users.models import User
user = User.objects.filter(id=user_id).first()
if user is None:
raise serializers.ValidationError('user id not exist')
return user
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
@@ -201,7 +118,6 @@ class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterRule
fields = [

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.drf.fields import EncryptedField
__all__ = [
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
'PasswordVerifySerializer',
]
class PasswordVerifySerializer(serializers.Serializer):
password = EncryptedField()
class MFASelectTypeSerializer(serializers.Serializer):
type = serializers.CharField()
username = serializers.CharField(required=False, allow_blank=True, allow_null=True)
class MFAChallengeSerializer(serializers.Serializer):
type = serializers.CharField(write_only=True, required=False, allow_blank=True)
code = serializers.CharField(write_only=True)
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass
class OtpVerifySerializer(serializers.Serializer):
code = serializers.CharField(max_length=6, min_length=6)

View File

@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
#
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.utils import get_object_or_none, random_string
from users.models import User
from users.serializers import UserProfileSerializer
from ..models import AccessKey, TempToken
__all__ = [
'AccessKeySerializer', 'BearerTokenSerializer',
'SSOTokenSerializer', 'TempTokenSerializer',
]
class AccessKeySerializer(serializers.ModelSerializer):
class Meta:
model = AccessKey
fields = ['id', 'secret', 'is_active', 'date_created']
read_only_fields = ['id', 'secret', 'date_created']
class BearerTokenSerializer(serializers.Serializer):
username = serializers.CharField(allow_null=True, required=False, write_only=True)
password = serializers.CharField(write_only=True, allow_null=True,
required=False, allow_blank=True)
public_key = serializers.CharField(write_only=True, allow_null=True,
allow_blank=True, required=False)
token = serializers.CharField(read_only=True)
keyword = serializers.SerializerMethodField()
date_expired = serializers.DateTimeField(read_only=True)
user = UserProfileSerializer(read_only=True)
@staticmethod
def get_keyword(obj):
return 'Bearer'
def update_last_login(self, user):
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
def get_request_user(self):
request = self.context.get('request')
if request.user and request.user.is_authenticated:
user = request.user
else:
user_id = request.session.get('user_id')
user = get_object_or_none(User, pk=user_id)
if not user:
raise serializers.ValidationError(
"user id {} not exist".format(user_id)
)
return user
def create(self, validated_data):
request = self.context.get('request')
user = self.get_request_user()
token, date_expired = user.create_bearer_token(request)
self.update_last_login(user)
instance = {
"token": token,
"date_expired": date_expired,
"user": user
}
return instance
class SSOTokenSerializer(serializers.Serializer):
username = serializers.CharField(write_only=True)
login_url = serializers.CharField(read_only=True)
next = serializers.CharField(write_only=True, allow_blank=True, required=False, allow_null=True)
class TempTokenSerializer(serializers.ModelSerializer):
is_valid = serializers.BooleanField(label=_("Is valid"), read_only=True)
class Meta:
model = TempToken
fields = [
'id', 'username', 'secret', 'verified', 'is_valid',
'date_created', 'date_updated', 'date_verified',
'date_expired',
]
read_only_fields = fields
def create(self, validated_data):
request = self.context.get('request')
if not request or not request.user:
raise PermissionError()
secret = random_string(36)
username = request.user.username
kwargs = {
'username': username, 'secret': secret,
'date_expired': timezone.now() + timezone.timedelta(seconds=5*60),
}
token = TempToken(**kwargs)
token.save()
return token

View File

@@ -35,6 +35,9 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
session.delete()
cache.set(lock_key, request.session.session_key, None)
# 标记登录,设置 cookie前端可以控制刷新, Middleware 会拦截这个生成 cookie
request.session['auth_session_expiration_required'] = 1
@receiver(openid_user_login_success)
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):

View File

@@ -142,15 +142,18 @@
<li class="dropdown">
<a class="dropdown-toggle login-page-language" data-toggle="dropdown" href="#" target="_blank">
<i class="fa fa-globe fa-lg" style="margin-right: 2px"></i>
{% ifequal request.COOKIES.django_language 'en' %}
{% if request.COOKIES.django_language == 'en' %}
<span>English<b class="caret"></b></span>
{% elif request.COOKIES.django_language == 'ja' %}
<span>日本語<b class="caret"></b></span>
{% else %}
<span>中文(简体)<b class="caret"></b></span>
{% endifequal %}
{% endif %}
</a>
<ul class="dropdown-menu profile-dropdown dropdown-menu-right">
<li> <a id="switch_cn" href="{% url 'i18n-switch' lang='zh-hans' %}"> <span>中文(简体)</span> </a> </li>
<li> <a id="switch_en" href="{% url 'i18n-switch' lang='en' %}"> <span>English</span> </a> </li>
<li> <a id="switch_ja" href="{% url 'i18n-switch' lang='ja' %}"> <span>日本語</span> </a> </li>
</ul>
</li>
</ul>
@@ -158,6 +161,7 @@
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
</div>
<div class="contact-form col-md-10 col-md-offset-1">
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %}
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
@@ -237,21 +241,13 @@
</body>
{% include '_foot_js.html' %}
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/cryptojs/crypto-js.min.js"></script>
<script type="text/javascript" src="/static/js/plugins/buffer/buffer.min.js"></script>
<script>
function encryptLoginPassword(password, rsaPublicKey) {
if (!password) {
return ''
}
var jsencrypt = new JSEncrypt(); //加密对象
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
return jsencrypt.encrypt(password); //加密
}
function doLogin() {
//公钥加密
var rsaPublicKey = "{{ rsa_public_key }}"
var password = $('#password').val(); //明文密码
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
var passwordEncrypted = encryptPassword(password)
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
$('#login-form').submit(); //post提交
}

View File

@@ -9,7 +9,9 @@ app_name = 'authentication'
router = DefaultRouter()
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
router.register('sso', api.SSOViewSet, 'sso')
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
router.register('super-connection-token', api.UserSuperConnectionTokenViewSet, 'super-connection-token')
urlpatterns = [
@@ -24,6 +26,7 @@ urlpatterns = [
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('confirm/', api.ConfirmViewSet.as_view(), name='user-confirm'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),

View File

@@ -27,12 +27,16 @@ urlpatterns = [
path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'),
path('wecom/qr/bind/<uuid:user_id>/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'),
path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'),
path('wecom/oauth/login/', views.WeComOAuthLoginView.as_view(), name='wecom-oauth-login'),
path('wecom/oauth/login/callback/', views.WeComOAuthLoginCallbackView.as_view(), name='wecom-oauth-login-callback'),
path('dingtalk/bind/start/', views.DingTalkEnableStartView.as_view(), name='dingtalk-bind-start'),
path('dingtalk/qr/bind/', views.DingTalkQRBindView.as_view(), name='dingtalk-qr-bind'),
path('dingtalk/qr/login/', views.DingTalkQRLoginView.as_view(), name='dingtalk-qr-login'),
path('dingtalk/qr/bind/<uuid:user_id>/callback/', views.DingTalkQRBindCallbackView.as_view(), name='dingtalk-qr-bind-callback'),
path('dingtalk/qr/login/callback/', views.DingTalkQRLoginCallbackView.as_view(), name='dingtalk-qr-login-callback'),
path('dingtalk/oauth/login/', views.DingTalkOAuthLoginView.as_view(), name='dingtalk-oauth-login'),
path('dingtalk/oauth/login/callback/', views.DingTalkOAuthLoginCallbackView.as_view(), name='dingtalk-oauth-login-callback'),
path('feishu/bind/start/', views.FeiShuEnableStartView.as_view(), name='feishu-bind-start'),
path('feishu/qr/bind/', views.FeiShuQRBindView.as_view(), name='feishu-qr-bind'),
@@ -51,7 +55,6 @@ urlpatterns = [
path('profile/otp/enable/bind/', users_view.UserOtpEnableBindView.as_view(), name='user-otp-enable-bind'),
path('profile/otp/disable/', users_view.UserOtpDisableView.as_view(),
name='user-otp-disable'),
path('first-login/', users_view.UserFirstLoginView.as_view(), name='user-first-login'),
# openid
path('cas/', include(('authentication.backends.cas.urls', 'authentication'), namespace='cas')),

View File

@@ -1,63 +1,22 @@
# -*- coding: utf-8 -*-
#
import base64
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5
from Cryptodome import Random
from django.conf import settings
from .notifications import DifferentCityLoginMessage
from common.utils import validate_ip, get_ip_city, get_request_ip
from common.utils import get_logger
from audits.models import UserLoginLog
from audits.const import DEFAULT_CITY
from common.utils import get_request_ip
from common.utils import validate_ip, get_ip_city
from common.utils import get_logger
from .notifications import DifferentCityLoginMessage
logger = get_logger(__file__)
def gen_key_pair():
""" 生成加密key
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
"""
random_generator = Random.new().read
rsa = RSA.generate(1024, random_generator)
rsa_private_key = rsa.exportKey().decode()
rsa_public_key = rsa.publickey().exportKey().decode()
return rsa_private_key, rsa_public_key
def rsa_encrypt(message, rsa_public_key):
""" 加密登录密码 """
key = RSA.importKey(rsa_public_key)
cipher = PKCS1_v1_5.new(key)
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
return cipher_text
def rsa_decrypt(cipher_text, rsa_private_key=None):
""" 解密登录密码 """
if rsa_private_key is None:
# rsa_private_key 为 None可以能是API请求认证不需要解密
return cipher_text
key = RSA.importKey(rsa_private_key)
cipher = PKCS1_v1_5.new(key)
cipher_decoded = base64.b64decode(cipher_text.encode())
# Todo: 弄明白为何要以下这么写https://xbuba.com/questions/57035263
if len(cipher_decoded) == 127:
hex_fixed = '00' + cipher_decoded.hex()
cipher_decoded = base64.b16decode(hex_fixed.upper())
message = cipher.decrypt(cipher_decoded, b'error').decode()
return message
def check_different_city_login_if_need(user, request):
if not settings.SECURITY_CHECK_DIFFERENT_CITY_LOGIN:
return
ip = get_request_ip(request) or '0.0.0.0'
if not (ip and validate_ip(ip)):
city = DEFAULT_CITY
else:

View File

@@ -9,8 +9,9 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView
from users.utils import is_auth_password_time_valid
from users.utils import is_auth_confirm_time_valid
from users.models import User
from users.permissions import IsAuthConfirmTimeValid
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
@@ -21,6 +22,7 @@ from authentication.mixins import AuthMixin
from common.sdk.im.dingtalk import DingTalk
from common.utils.common import get_request_ip
from authentication.notifications import OAuthBindMessage
from .mixins import METAMixin
logger = get_logger(__file__)
@@ -28,7 +30,7 @@ logger = get_logger(__file__)
DINGTALK_STATE_SESSION_KEY = '_dingtalk_state'
class DingTalkQRMixin(PermissionsMixin, View):
class DingTalkBaseMixin(PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
@@ -54,20 +56,6 @@ class DingTalkQRMixin(PermissionsMixin, View):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
def get_qr_url(self, redirect_uri):
state = random_string(16)
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
params = {
'appid': settings.DINGTALK_APPKEY,
'response_type': 'code',
'scope': 'snsapi_login',
'state': state,
'redirect_uri': redirect_uri,
}
url = URL.QR_CONNECT + '?' + urlencode(params)
return url
@staticmethod
def get_success_response(redirect_url, title, msg):
message_data = {
@@ -94,18 +82,49 @@ class DingTalkQRMixin(PermissionsMixin, View):
return response
class DingTalkQRMixin(DingTalkBaseMixin, View):
def get_qr_url(self, redirect_uri):
state = random_string(16)
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
params = {
'appid': settings.DINGTALK_APPKEY,
'response_type': 'code',
'scope': 'snsapi_login',
'state': state,
'redirect_uri': redirect_uri,
}
url = URL.QR_CONNECT + '?' + urlencode(params)
return url
class DingTalkOAuthMixin(DingTalkBaseMixin, View):
def get_oauth_url(self, redirect_uri):
if not settings.AUTH_DINGTALK:
return reverse('authentication:login')
state = random_string(16)
self.request.session[DINGTALK_STATE_SESSION_KEY] = state
params = {
'appid': settings.DINGTALK_APPKEY,
'response_type': 'code',
'scope': 'snsapi_auth',
'state': state,
'redirect_uri': redirect_uri,
}
url = URL.OAUTH_CONNECT + '?' + urlencode(params)
return url
class DingTalkQRBindView(DingTalkQRMixin, View):
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
def get(self, request: HttpRequest):
user = request.user
redirect_url = request.GET.get('redirect_url')
if not is_auth_password_time_valid(request.session):
msg = _('Please verify your password first')
response = self.get_failed_response(redirect_url, msg, msg)
return response
redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
@@ -178,14 +197,18 @@ class DingTalkEnableStartView(UserVerifyPasswordView):
return success_url
class DingTalkQRLoginView(DingTalkQRMixin, View):
class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View):
permission_classes = (AllowAny,)
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url')
redirect_url = request.GET.get('redirect_url') or reverse('index')
next_url = self.get_next_url_from_meta() or reverse('index')
redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
redirect_uri += '?' + urlencode({
'redirect_url': redirect_url,
'next': next_url,
})
url = self.get_qr_url(redirect_uri)
return HttpResponseRedirect(url)
@@ -230,3 +253,57 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View):
return response
return self.redirect_to_guard_view()
class DingTalkOAuthLoginView(DingTalkOAuthMixin, View):
permission_classes = (AllowAny,)
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url')
redirect_uri = reverse('authentication:dingtalk-oauth-login-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
url = self.get_oauth_url(redirect_uri)
return HttpResponseRedirect(url)
class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View):
permission_classes = (AllowAny,)
def get(self, request: HttpRequest):
code = request.GET.get('code')
redirect_url = request.GET.get('redirect_url')
login_url = reverse('authentication:login')
if not self.verify_state():
return self.get_verify_state_failed_response(redirect_url)
dingtalk = DingTalk(
appid=settings.DINGTALK_APPKEY,
appsecret=settings.DINGTALK_APPSECRET,
agentid=settings.DINGTALK_AGENTID
)
userid = dingtalk.get_userid_by_code(code)
if not userid:
# 正常流程不会出这个错误hack 行为
msg = _('Failed to get user from DingTalk')
response = self.get_failed_response(login_url, title=msg, msg=msg)
return response
user = get_object_or_none(User, dingtalk_id=userid)
if user is None:
title = _('DingTalk is not bound')
msg = _('Please login with a password and then bind the DingTalk')
response = self.get_failed_response(login_url, title=title, msg=msg)
return response
try:
self.check_oauth2_auth(user, settings.AUTH_BACKEND_DINGTALK)
except errors.AuthFailedError as e:
self.set_login_failed_mark()
msg = e.msg
response = self.get_failed_response(login_url, title=msg, msg=msg)
return response
return self.redirect_to_guard_view()

View File

@@ -8,7 +8,7 @@ from django.db.utils import IntegrityError
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.utils import is_auth_password_time_valid
from users.permissions import IsAuthConfirmTimeValid
from users.views import UserVerifyPasswordView
from users.models import User
from common.utils import get_logger, FlashMessageUtil
@@ -89,17 +89,12 @@ class FeiShuQRMixin(PermissionsMixin, View):
class FeiShuQRBindView(FeiShuQRMixin, View):
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
def get(self, request: HttpRequest):
user = request.user
redirect_url = request.GET.get('redirect_url')
if not is_auth_password_time_valid(request.session):
msg = _('Please verify your password first')
response = self.get_failed_response(redirect_url, msg, msg)
return response
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
@@ -170,10 +165,11 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
permission_classes = (AllowAny,)
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url')
redirect_url = request.GET.get('redirect_url') or reverse('index')
redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
redirect_uri += '?' + urlencode({
'redirect_url': redirect_url,
})
url = self.get_qr_url(redirect_uri)
return HttpResponseRedirect(url)

View File

@@ -96,7 +96,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
self.request.session.delete_test_cookie()
try:
self.check_user_auth(decrypt_passwd=True)
self.check_user_auth(form.cleaned_data)
except errors.AuthFailedError as e:
form.add_error(None, e.msg)
self.set_login_failed_mark()
@@ -219,7 +219,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs):
try:
user = self.check_user_auth_if_need()
user = self.get_user_from_session()
self.check_user_mfa_if_need(user)
self.check_user_login_confirm_if_need(user)
except (errors.CredentialError, errors.SessionEmptyError) as e:

View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
#
class METAMixin:
def get_next_url_from_meta(self):
request_meta = self.request.META or {}
next_url = None
referer = request_meta.get('HTTP_REFERER', '')
next_url_item = referer.rsplit('next=', 1)
if len(next_url_item) > 1:
next_url = next_url_item[-1]
return next_url

View File

@@ -9,8 +9,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.exceptions import APIException
from users.views import UserVerifyPasswordView
from users.utils import is_auth_password_time_valid
from users.models import User
from users.permissions import IsAuthConfirmTimeValid
from common.utils import get_logger, FlashMessageUtil
from common.utils.random import random_string
from common.utils.django import reverse, get_object_or_none
@@ -21,6 +21,7 @@ from common.utils.common import get_request_ip
from authentication import errors
from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage
from .mixins import METAMixin
logger = get_logger(__file__)
@@ -28,7 +29,7 @@ logger = get_logger(__file__)
WECOM_STATE_SESSION_KEY = '_wecom_state'
class WeComQRMixin(PermissionsMixin, View):
class WeComBaseMixin(PermissionsMixin, View):
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
@@ -54,19 +55,6 @@ class WeComQRMixin(PermissionsMixin, View):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
def get_qr_url(self, redirect_uri):
state = random_string(16)
self.request.session[WECOM_STATE_SESSION_KEY] = state
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': state,
'redirect_uri': redirect_uri,
}
url = URL.QR_CONNECT + '?' + urlencode(params)
return url
@staticmethod
def get_success_response(redirect_url, title, msg):
message_data = {
@@ -93,18 +81,49 @@ class WeComQRMixin(PermissionsMixin, View):
return response
class WeComQRMixin(WeComBaseMixin, View):
def get_qr_url(self, redirect_uri):
state = random_string(16)
self.request.session[WECOM_STATE_SESSION_KEY] = state
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': state,
'redirect_uri': redirect_uri,
}
url = URL.QR_CONNECT + '?' + urlencode(params)
return url
class WeComOAuthMixin(WeComBaseMixin, View):
def get_oauth_url(self, redirect_uri):
if not settings.AUTH_WECOM:
return reverse('authentication:login')
state = random_string(16)
self.request.session[WECOM_STATE_SESSION_KEY] = state
params = {
'appid': settings.WECOM_CORPID,
'agentid': settings.WECOM_AGENTID,
'state': state,
'redirect_uri': redirect_uri,
'response_type': 'code',
'scope': 'snsapi_base',
}
url = URL.OAUTH_CONNECT + '?' + urlencode(params) + '#wechat_redirect'
return url
class WeComQRBindView(WeComQRMixin, View):
permission_classes = (IsAuthenticated,)
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
def get(self, request: HttpRequest):
user = request.user
redirect_url = request.GET.get('redirect_url')
if not is_auth_password_time_valid(request.session):
msg = _('Please verify your password first')
response = self.get_failed_response(redirect_url, msg, msg)
return response
redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
@@ -173,14 +192,17 @@ class WeComEnableStartView(UserVerifyPasswordView):
return success_url
class WeComQRLoginView(WeComQRMixin, View):
class WeComQRLoginView(WeComQRMixin, METAMixin, View):
permission_classes = (AllowAny,)
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url')
redirect_url = request.GET.get('redirect_url') or reverse('index')
next_url = self.get_next_url_from_meta() or reverse('index')
redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
redirect_uri += '?' + urlencode({
'redirect_url': redirect_url,
'next': next_url,
})
url = self.get_qr_url(redirect_uri)
return HttpResponseRedirect(url)
@@ -225,3 +247,57 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View):
return response
return self.redirect_to_guard_view()
class WeComOAuthLoginView(WeComOAuthMixin, View):
permission_classes = (AllowAny,)
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url')
redirect_uri = reverse('authentication:wecom-oauth-login-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
url = self.get_oauth_url(redirect_uri)
return HttpResponseRedirect(url)
class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View):
permission_classes = (AllowAny,)
def get(self, request: HttpRequest):
code = request.GET.get('code')
redirect_url = request.GET.get('redirect_url')
login_url = reverse('authentication:login')
if not self.verify_state():
return self.get_verify_state_failed_response(redirect_url)
wecom = WeCom(
corpid=settings.WECOM_CORPID,
corpsecret=settings.WECOM_SECRET,
agentid=settings.WECOM_AGENTID
)
wecom_userid, __ = wecom.get_user_id_by_code(code)
if not wecom_userid:
# 正常流程不会出这个错误hack 行为
msg = _('Failed to get user from WeCom')
response = self.get_failed_response(login_url, title=msg, msg=msg)
return response
user = get_object_or_none(User, wecom_id=wecom_userid)
if user is None:
title = _('WeCom is not bound')
msg = _('Please login with a password and then bind the WeCom')
response = self.get_failed_response(login_url, title=title, msg=msg)
return response
try:
self.check_oauth2_auth(user, settings.AUTH_BACKEND_WECOM)
except errors.AuthFailedError as e:
self.set_login_failed_mark()
msg = e.msg
response = self.get_failed_response(login_url, title=msg, msg=msg)
return response
return self.redirect_to_guard_view()

View File

@@ -1,11 +1,11 @@
import time
from redis import Redis
from channels_redis.core import RedisChannelLayer as _RedisChannelLayer
from common.utils.lock import DistributedLock
from common.utils.connection import get_redis_client
from common.utils import lazyproperty
from common.utils import get_logger
from jumpserver.const import CONFIG
logger = get_logger(__file__)
@@ -58,7 +58,7 @@ class Cache(metaclass=CacheType):
def __init__(self):
self._data = None
self.redis = Redis(host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, password=CONFIG.REDIS_PASSWORD)
self.redis = get_redis_client()
def __getitem__(self, item):
return self.field_desc_mapper[item]
@@ -218,3 +218,29 @@ class CacheValueDesc:
def to_internal_value(self, value):
return self.field_type.field_type(value)
class RedisChannelLayer(_RedisChannelLayer):
async def _brpop_with_clean(self, index, channel, timeout):
cleanup_script = """
local backed_up = redis.call('ZRANGE', ARGV[2], 0, -1, 'WITHSCORES')
for i = #backed_up, 1, -2 do
redis.call('ZADD', ARGV[1], backed_up[i], backed_up[i - 1])
end
redis.call('DEL', ARGV[2])
"""
backup_queue = self._backup_channel_name(channel)
async with self.connection(index) as connection:
# 部分云厂商的 Redis 此操作会报错(不支持,比如阿里云有限制)
try:
await connection.eval(cleanup_script, keys=[], args=[channel, backup_queue])
except:
pass
result = await connection.bzpopmin(channel, timeout=timeout)
if result is not None:
_, member, timestamp = result
await connection.zadd(backup_queue, float(timestamp), member)
else:
member = None
return member

View File

@@ -4,8 +4,8 @@ import json
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text
from ..utils import signer, crypto
from django.core.validators import MinValueValidator, MaxValueValidator
from common.utils import signer, crypto
__all__ = [
@@ -13,7 +13,7 @@ __all__ = [
'JsonCharField', 'JsonTextField', 'JsonListCharField', 'JsonListTextField',
'JsonDictCharField', 'JsonDictTextField', 'EncryptCharField',
'EncryptTextField', 'EncryptMixin', 'EncryptJsonDictTextField',
'EncryptJsonDictCharField',
'EncryptJsonDictCharField', 'PortField'
]
@@ -180,3 +180,13 @@ class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField):
pass
class PortField(models.IntegerField):
def __init__(self, *args, **kwargs):
kwargs.update({
'blank': False,
'null': False,
'validators': [MinValueValidator(0), MaxValueValidator(65535)]
})
super().__init__(*args, **kwargs)

View File

@@ -3,9 +3,10 @@
from rest_framework import serializers
from common.utils import decrypt_password
__all__ = [
'ReadableHiddenField',
'ReadableHiddenField', 'EncryptedField'
]
@@ -23,3 +24,15 @@ class ReadableHiddenField(serializers.HiddenField):
if hasattr(value, 'id'):
return getattr(value, 'id')
return value
class EncryptedField(serializers.CharField):
def __init__(self, write_only=None, **kwargs):
if write_only is None:
write_only = True
kwargs['write_only'] = write_only
super().__init__(**kwargs)
def to_internal_value(self, value):
value = super().to_internal_value(value)
return decrypt_password(value)

View File

@@ -8,10 +8,12 @@ from common.mixins import BulkListSerializerMixin
from django.utils.functional import cached_property
from rest_framework.utils.serializer_helpers import BindingDict
from common.mixins.serializers import BulkSerializerMixin
from common.drf.fields import EncryptedField
__all__ = [
'MethodSerializer',
'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer'
'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer',
'SecretReadableMixin'
]
@@ -83,3 +85,20 @@ class CeleryTaskSerializer(serializers.Serializer):
task = serializers.CharField(read_only=True)
class SecretReadableMixin(serializers.Serializer):
""" 加密字段 (EncryptedField) 可读性 """
def __init__(self, *args, **kwargs):
super(SecretReadableMixin, self).__init__(*args, **kwargs)
if not hasattr(self, 'Meta') or not hasattr(self.Meta, 'extra_kwargs'):
return
extra_kwargs = self.Meta.extra_kwargs
for field_name, serializer_field in self.fields.items():
if not isinstance(serializer_field, EncryptedField):
continue
if field_name not in extra_kwargs:
continue
field_extra_kwargs = extra_kwargs[field_name]
if 'write_only' not in field_extra_kwargs:
continue
serializer_field.write_only = field_extra_kwargs['write_only']

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.mixins import UserPassesTestMixin
from rest_framework import permissions
from rest_framework.decorators import action
@@ -7,8 +8,10 @@ from rest_framework.request import Request
from rest_framework.response import Response
from common.permissions import IsValidUser
from audits.utils import create_operate_log
from audits.models import OperateLog
__all__ = ["PermissionsMixin"]
__all__ = ["PermissionsMixin", "RecordViewLogMixin"]
class PermissionsMixin(UserPassesTestMixin):
@@ -24,3 +27,35 @@ class PermissionsMixin(UserPassesTestMixin):
if not permission_class().has_permission(self.request, self):
return False
return True
class RecordViewLogMixin:
ACTION = OperateLog.ACTION_VIEW
@staticmethod
def get_resource_display(request):
query_params = dict(request.query_params)
if query_params.get('format'):
query_params.pop('format')
spm_filter = query_params.pop('spm') if query_params.get('spm') else None
if not query_params and not spm_filter:
display_message = _('Export all')
elif spm_filter:
display_message = _('Export only selected items')
else:
query = ','.join(
['%s=%s' % (key, value) for key, value in query_params.items()]
)
display_message = _('Export filtered: %s') % query
return display_message
def list(self, request, *args, **kwargs):
response = super().list(request, *args, **kwargs)
resource = self.get_resource_display(request)
create_operate_log(self.ACTION, self.model, resource)
return response
def retrieve(self, request, *args, **kwargs):
response = super().retrieve(request, *args, **kwargs)
create_operate_log(self.ACTION, self.model, self.get_object())
return response

View File

@@ -28,6 +28,7 @@ class ErrorCode:
class URL:
QR_CONNECT = 'https://oapi.dingtalk.com/connect/qrconnect'
OAUTH_CONNECT = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize'
GET_USER_INFO_BY_CODE = 'https://oapi.dingtalk.com/sns/getuserinfo_bycode'
GET_TOKEN = 'https://oapi.dingtalk.com/gettoken'
SEND_MESSAGE_BY_TEMPLATE = 'https://oapi.dingtalk.com/topapi/message/corpconversation/sendbytemplate'

View File

@@ -19,6 +19,7 @@ class URL:
GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect'
OAUTH_CONNECT = 'https://open.weixin.qq.com/connect/oauth2/authorize'
# https://open.work.weixin.qq.com/api/doc/90000/90135/91437
GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'

View File

@@ -9,4 +9,3 @@ from .crypto import *
from .random import *
from .jumpserver import *
from .ip import *
from .geoip import *

View File

@@ -31,6 +31,8 @@ def combine_seq(s1, s2, callback=None):
def get_logger(name=''):
if '/' in name:
name = os.path.basename(name).replace('.py', '')
return logging.getLogger('jumpserver.%s' % name)
@@ -338,3 +340,24 @@ def get_file_by_arch(dir, filename):
settings.BASE_DIR, dir, platform_name, arch, filename
)
return file_path
def pretty_string(data: str, max_length=128, ellipsis_str='...'):
"""
params:
data: abcdefgh
max_length: 7
ellipsis_str: ...
return:
ab...gh
"""
if len(data) < max_length:
return data
remain_length = max_length - len(ellipsis_str)
half = remain_length // 2
if half <= 1:
return data[:max_length]
start = data[:half]
end = data[-half:]
data = f'{start}{ellipsis_str}{end}'
return data

View File

@@ -1,23 +1,30 @@
import json
import threading
import redis
from redis import Redis
from django.conf import settings
from jumpserver.const import CONFIG
from common.http import is_true
from common.db.utils import safe_db_connection
from common.utils import get_logger
logger = get_logger(__name__)
def get_redis_client(db):
rc = redis.StrictRedis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
password=settings.REDIS_PASSWORD,
db=db
)
return rc
def get_redis_client(db=0):
params = {
'host': CONFIG.REDIS_HOST,
'port': CONFIG.REDIS_PORT,
'password': CONFIG.REDIS_PASSWORD,
'db': db,
"ssl": is_true(CONFIG.REDIS_USE_SSL),
'ssl_cert_reqs': getattr(settings, 'REDIS_SSL_REQUIRED'),
'ssl_keyfile': getattr(settings, 'REDIS_SSL_KEYFILE'),
'ssl_certfile': getattr(settings, 'REDIS_SSL_CERTFILE'),
'ssl_ca_certs': getattr(settings, 'REDIS_SSL_CA_CERTS'),
}
return Redis(**params)
class Subscription:
@@ -99,5 +106,3 @@ class RedisPubSub:
data_json = json.dumps(data)
self.redis.publish(self.ch, data_json)
return True

View File

@@ -1,7 +1,10 @@
import base64
from Cryptodome.Cipher import AES
import logging
from Cryptodome.Cipher import AES, PKCS1_v1_5
from Cryptodome.Util.Padding import pad
from Cryptodome.Random import get_random_bytes
from Cryptodome.PublicKey import RSA
from Cryptodome import Random
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
from django.conf import settings
@@ -88,12 +91,13 @@ class AESCrypto:
def encrypt(self, text):
aes = self.aes()
return str(base64.encodebytes(aes.encrypt(self.to_16(text))),
encoding='utf8').replace('\n', '') # 加密
cipher = base64.encodebytes(aes.encrypt(self.to_16(text)))
return str(cipher, encoding='utf8').replace('\n', '') # 加密
def decrypt(self, text):
aes = self.aes()
return str(aes.decrypt(base64.decodebytes(bytes(text, encoding='utf8'))).rstrip(b'\0').decode("utf8")) # 解密
text_decoded = base64.decodebytes(bytes(text, encoding='utf8'))
return str(aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
class AESCryptoGCM:
@@ -193,4 +197,72 @@ class Crypto:
continue
def gen_key_pair(length=1024):
""" 生成加密key
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
"""
random_generator = Random.new().read
rsa = RSA.generate(length, random_generator)
rsa_private_key = rsa.exportKey().decode()
rsa_public_key = rsa.publickey().exportKey().decode()
return rsa_private_key, rsa_public_key
def rsa_encrypt(message, rsa_public_key):
""" 加密登录密码 """
key = RSA.importKey(rsa_public_key)
cipher = PKCS1_v1_5.new(key)
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
return cipher_text
def rsa_decrypt(cipher_text, rsa_private_key=None):
""" 解密登录密码 """
if rsa_private_key is None:
# rsa_private_key 为 None可以能是API请求认证不需要解密
return cipher_text
key = RSA.importKey(rsa_private_key)
cipher = PKCS1_v1_5.new(key)
cipher_decoded = base64.b64decode(cipher_text.encode())
# Todo: 弄明白为何要以下这么写https://xbuba.com/questions/57035263
if len(cipher_decoded) == 127:
hex_fixed = '00' + cipher_decoded.hex()
cipher_decoded = base64.b16decode(hex_fixed.upper())
message = cipher.decrypt(cipher_decoded, b'error').decode()
return message
def rsa_decrypt_by_session_pkey(value):
from jumpserver.utils import current_request
if not current_request:
return value
private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
private_key = current_request.session.get(private_key_name)
if not private_key or not value:
return value
try:
value = rsa_decrypt(value, private_key)
except Exception as e:
logging.error('Decrypt field error: {}'.format(e))
return value
def decrypt_password(value):
cipher = value.split(':')
if len(cipher) != 2:
return value
key_cipher, password_cipher = cipher
aes_key = rsa_decrypt_by_session_pkey(key_cipher)
aes = get_aes_crypto(aes_key, 'ECB')
try:
password = aes.decrypt(password_cipher)
except UnicodeDecodeError as e:
logging.error("Decript password error: {}, {}".format(password_cipher, e))
return value
return password
crypto = Crypto()

View File

@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
#
import re
from django.shortcuts import reverse as dj_reverse
from django.conf import settings
from django.utils import timezone
from django.db import models
from django.db.models.signals import post_save, pre_save
UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}')
@@ -58,3 +61,12 @@ def get_log_keep_day(s, defaults=200):
except ValueError:
days = defaults
return days
def bulk_create_with_signal(cls: models.Model, items, **kwargs):
for i in items:
pre_save.send(sender=cls, instance=i)
result = cls.objects.bulk_create(items, **kwargs)
for i in items:
post_save.send(sender=cls, instance=i, created=True)
return result

View File

@@ -186,10 +186,27 @@ def make_signature(access_key_secret, date=None):
return content_md5(data)
def encrypt_password(password, salt=None):
from passlib.hash import sha512_crypt
if password:
def encrypt_password(password, salt=None, algorithm='sha512'):
from passlib.hash import sha512_crypt, des_crypt
def sha512():
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
def des():
return des_crypt.hash(password, salt=salt[:2])
support_algorithm = {
'sha512': sha512, 'des': des
}
if isinstance(algorithm, str):
algorithm = algorithm.lower()
if algorithm not in support_algorithm.keys():
algorithm = 'sha512'
if password and support_algorithm[algorithm]:
return support_algorithm[algorithm]()
return None

View File

@@ -1,6 +1,8 @@
import os
import csv
import pyzipper
import requests
def create_csv_file(filename, headers, rows, ):
@@ -18,3 +20,11 @@ def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filenames
for encrypted_filename in encrypted_filenames:
with open(encrypted_filename, 'rb') as f:
zf.writestr(os.path.basename(encrypted_filename), f.read())
def download_file(src, path):
with requests.get(src, stream=True) as r:
r.raise_for_status()
with open(path, 'wb') as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)

View File

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

View File

@@ -8,15 +8,11 @@ from geoip2.errors import GeoIP2Error
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
__all__ = ['get_ip_city']
__all__ = ['get_ip_city_by_geoip']
reader = None
def get_ip_city(ip):
if not ip or '.' not in ip or not isinstance(ip, str):
return _("Invalid ip")
if ':' in ip:
return 'IPv6'
def get_ip_city_by_geoip(ip):
global reader
if reader is None:
path = os.path.join(os.path.dirname(__file__), 'GeoLite2-City.mmdb')
@@ -32,15 +28,13 @@ def get_ip_city(ip):
try:
response = reader.city(ip)
except GeoIP2Error:
return _("Unknown ip")
return _("Unknown")
names = response.city.names
if not names:
names = response.country.names
city_names = response.city.names or {}
lang = settings.LANGUAGE_CODE[:2]
if lang == 'zh':
lang = 'zh-CN'
city = city_names.get(lang, _("Unknown"))
return city
if 'en' in settings.LANGUAGE_CODE and 'en' in names:
return names['en']
elif 'zh-CN' in names:
return names['zh-CN']
return _("Unknown ip")

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
from .model import *
from .utils import *

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