Compare commits

..

60 Commits

Author SHA1 Message Date
老广
a025930957 Merge pull request #2642 from jumpserver/dev
Dev
2019-04-30 10:46:28 +08:00
老广
990c78e7cc Merge branch 'master' into dev 2019-04-30 10:46:10 +08:00
BaiJiangJie
0ef12906d3 [Update] 修改翻译信息 (#2643)
* [Update] 更新翻译信息

* [Update] 更新翻译信息
2019-04-30 10:30:58 +08:00
ibuler
61a37731ec Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-04-30 10:20:57 +08:00
ibuler
d3217b6a67 [Update] 修改版本号 2019-04-30 10:20:48 +08:00
BaiJiangJie
04266cc20b [Update] 修改Copyright (#2649) 2019-04-30 10:07:57 +08:00
ibuler
4f36cf7dd1 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-04-29 10:39:51 +08:00
ibuler
490041587b [Update] 修改copyright 2019-04-29 10:39:31 +08:00
BaiJiangJie
3a3da94468 [Update] 修改预留/auth/login/页面方式(admin=1);解决luna页面刷新不跳转openid认证的问题 (#2634) 2019-04-25 18:55:48 +08:00
BaiJiangJie
b7ad6cfe62 [Update] 防止 XSS (#2633)
* [Bugfix] 修改管理用户列表显示bug

* [Bugfix] 修复刷新批量命令页面的bug

* [Update] 防止 XSS
2019-04-25 18:16:41 +08:00
ibuler
4463e7545d [Update] 去掉多余的 2019-04-25 14:34:47 +08:00
ibuler
d0eafc8b8e [Update] Merge 2019-04-25 14:32:53 +08:00
ibuler
8b98c20d68 [Update] xss 2019-04-25 14:31:34 +08:00
BaiJiangJie
caa5060ecd [Update] 控制组织管理员不允许更新、删除超级用户;修复ViewSet API批量更新的bug (#2629)
* [Update] 控制组织管理员不允许编辑(更新、删除)超级用户 - 待续(控制批量更新API)

* [Update] 修改方法名称

* [Update] 控制组织管理员不允许批量更新包含超级用户的用户列表

* [Bugfix] 修复所有ViewSet API进行批量更新时rest_framework_bulk库内部的bug

* [Update] 修改 OpenID Middleware 日志输出模式 info => debug
2019-04-25 10:11:50 +08:00
BaiJiangJie
aabcf7f31c [Update] 授权列表下拉信息添加action字段 (#2618) 2019-04-22 12:31:40 +08:00
ibuler
40d48cdfe4 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-04-22 12:07:59 +08:00
ibuler
8196537878 [Update] 去掉logger.info openid msg 2019-04-22 12:07:48 +08:00
BaiJiangJie
33a00f043b [Bugfix] 修复创建azure类型的录像存储时前端的bug (#2617) 2019-04-22 11:55:14 +08:00
BaiJiangJie
f235e20153 [Feature] 授权规则添加 actions 选项,控制用户对资产的操作行为 (#2610)
* [Feature] 1. perms actions - 添加 Action Model

* [Feature] 2. perms actions - 添加 Action API

* [Feature] 3. perms actions - 授权规则: 添加actions字段

* [Feature] 4. perms actions - 授权规则创建页面: 设置 actions 默认 all

* [Feature] 5. perms actions - 资产授权工具: 动态给system_user设置actions属性; 修改授权相关的API-serializer类: 添加actions字段值

* [Feature] 6. perms actions - 更新API(用户使用系统用户连接资产时权限校验): 添加actions校验

* [Feature] 7. perms actions - 迁移文件中为已经存在的perms添加默认的action

* [Feature] 8. perms actions - 创建授权规则时设置默认action(如果actions字段值为空)

* [Feature] 9. check actions - 修改校验用户资产权限API逻辑(添加actions校验)

* [Feature] 10. check actions - 修改注释

* [Feature] 11. check actions - 添加API: 获取用户指定资产和系统用户被授权的actions

* [Feature] 12. check actions - 添加翻译信息
2019-04-22 11:42:20 +08:00
BaiJiangJie
cf2455c084 [Bugfix] 修复创建azure类型的录像存储时前端的bug (#2608) 2019-04-18 11:50:30 +08:00
BaiJiangJie
fc1068a9dc [Update] 更新翻译 (#2599) 2019-04-15 16:34:56 +08:00
zhnxin
35a0ca1875 Update node.py (#2527)
bugfix: node get_all_children逻辑
2019-04-15 14:43:49 +08:00
八千流
56519354b6 Email reset password (#2547)
* [Update]更改英文登录界面标题,可两行

* [Update] 更改用户通过邮箱修改密码后,该链接就失效

* [Update]更改页面左上角logo_text图片

* [Update] 优化发送邮箱改密连接失效代码

* [Update] 优化发送邮箱改密连接失效代码(2)

* [Update] 优化发送邮箱改密连接失效代码(3)

* [Update] 更新interface一键恢复默认的翻译

* [Update] 更改登录失败 用户名密码错误信息的翻译

* [Update] 优化生成token并设置缓存的代码
2019-04-15 14:37:45 +08:00
jokimina
78e4e13fb9 [Bugfix] IsSuperUserOrAppUser deny app user (#2558) 2019-04-15 14:33:40 +08:00
八千流
699b8d9980 [Update] 修改批量更新资产url过长导致的错误 (#2571) 2019-04-15 14:33:16 +08:00
zbhlove100
ba9581801c Update README.md 2019-04-09 16:57:01 +08:00
zbhlove100
0a5fdf4ea1 功能转为表格 2019-04-09 16:53:54 +08:00
zhangbohan
3849fa2b15 update readme 2019-04-09 15:27:37 +08:00
zhangbohan
0952cbc7c6 英文readme 2019-04-09 12:21:13 +08:00
zhangbohan
bb06c39dd4 更新readme 2019-04-09 12:09:11 +08:00
zhangbohan
d60dc31443 更新readme 2019-04-08 18:30:39 +08:00
老广
76b3cd8edd Merge pull request #2583 from jumpserver/dev
Dev
2019-04-04 12:33:16 +08:00
老广
638ba31694 Merge pull request #2582 from jumpserver/dev_bugfix_assets
[Bugfix] 修复资产对象获取所有节点时的bug
2019-04-04 12:21:42 +08:00
BaiJiangJie
c31b169cae [Bugfix] 修复资产对象获取所有节点时的bug 2019-04-04 12:17:53 +08:00
老广
fc167526ae Merge pull request #2575 from jumpserver/dev
[Bugfix] 修复OpenID判断退出登录的中间件逻辑的bug (#2574)
2019-04-03 12:19:23 +08:00
BaiJiangJie
55eff5eab9 [Bugfix] 修复OpenID判断退出登录的中间件逻辑的bug (#2574) 2019-04-03 12:18:58 +08:00
老广
f5a7f4e086 Merge pull request #2570 from jumpserver/dev
Bugfix (#2569)
2019-04-02 16:26:28 +08:00
老广
f7b0932cdd Bugfix (#2569)
* [Update] 修改Logo 的位置

* [Update] 修改镜像build
2019-04-02 16:25:39 +08:00
老广
ba89ce8fb9 Merge pull request #2560 from jumpserver/dev
[Update] 修复小问题 (#2557)
2019-03-29 19:34:28 +08:00
BaiJiangJie
9d62deeabe [Update] 修复小问题 (#2557)
* [Update] 修复小问题

* [Bugfix] 修复操作日志动作、资源类型过滤不成功的bug
2019-03-29 19:14:18 +08:00
老广
459b41f327 Merge pull request #2559 from jumpserver/dev
[Update] 修复重定向的问题
2019-03-29 15:54:20 +08:00
ibuler
3062e3f64a [Update] 修复重定向的问题 2019-03-29 15:53:31 +08:00
老广
c1362ca4e2 Merge pull request #2552 from jumpserver/dev
Dev
2019-03-28 13:13:36 +08:00
ibuler
9d24912ad9 [Update] Merge 2019-03-28 13:12:19 +08:00
老广
db290609a8 Merge pull request #2551 from jumpserver/dev
Dev
2019-03-28 13:07:34 +08:00
ibuler
4bc5eced6c Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2019-03-28 13:07:04 +08:00
BaiJiangJie
b82a66c83d [Update] 优化LDAP用户导入功能 (#2550) 2019-03-28 12:50:04 +08:00
ibuler
bf7079df9e [Update] 修改logo 2019-03-28 12:35:16 +08:00
ibuler
f137c5740e [Update] 添加openldap client 2019-03-28 11:53:52 +08:00
ibuler
ee47905966 [Update] 修改超时时间 2019-03-27 20:39:31 +08:00
fit2cloudrd
f6cd193f9e Update README.md (#2548)
* Dev (#2544)

* [Update] 更新翻译 (#2541)

* [Update] 更新缓存机制

* [Update] 增加task最大允许事件,并设置命令最大运行时间为60s

* Update README.md
2019-03-27 18:47:38 +08:00
ibuler
a31775dd23 [Update] 整理 copyright 和 版本 2019-03-27 17:46:35 +08:00
老广
30ba1e5886 Merge pull request #2545 from jumpserver/dev
Dev
2019-03-27 13:04:58 +08:00
ibuler
f97bfa7bf1 [Update] 修复s3创建时端点不含协议的提示 2019-03-27 12:49:46 +08:00
ibuler
ace028fa7f [Update] 统一可更改的interface 2019-03-27 12:20:43 +08:00
老广
69f6401e87 Dev (#2544)
* [Update] 更新翻译 (#2541)

* [Update] 更新缓存机制

* [Update] 增加task最大允许事件,并设置命令最大运行时间为60s
2019-03-27 11:21:21 +08:00
ibuler
bd4d974df1 [Update] merge 2019-03-27 11:13:35 +08:00
ibuler
6e7446f530 [Update] 增加task最大允许事件,并设置命令最大运行时间为60s 2019-03-27 11:12:34 +08:00
ibuler
afe9471aa2 [Update] 更新缓存机制 2019-03-26 19:46:04 +08:00
BaiJiangJie
4d56b84861 [Update] 更新翻译 (#2541) 2019-03-26 15:47:24 +08:00
98 changed files with 1934 additions and 997 deletions

View File

@@ -6,11 +6,11 @@ RUN useradd jumpserver
COPY ./requirements /tmp/requirements
RUN rpm -ivh https://repo.mysql.com/mysql57-community-release-el6.rpm
RUN yum -y install epel-release openldap-clients telnet && cd /tmp/requirements && \
yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt
RUN yum -y install epel-release && rpm -ivh https://repo.mysql.com/mysql57-community-release-el6.rpm
RUN cd /tmp/requirements && yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install --upgrade pip setuptools && \
pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt || pip install -r requirements.txt
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
COPY . /opt/jumpserver
RUN echo > config.yml

189
README.md
View File

@@ -1,4 +1,4 @@
## Jumpserver
## Jumpserver 多云环境下更好用的堡垒机
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
@@ -8,45 +8,200 @@
----
Jumpserver是全球首款完全开源的堡垒机使用GNU GPL v2.0开源协议,是符合 4A 的专业运维审计系统。
Jumpserver 是全球首款完全开源的堡垒机,使用 GNU GPL v2.0 开源协议,是符合 4A 的专业运维审计系统。
Jumpserver使用Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
Jumpserver 使用 Python / Django 进行开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 解决方案,交互界面美观、用户体验好。
Jumpserver采纳分布式架构支持多机房跨区域部署中心节点提供 API各机房部署登录节点可横向扩展、无并发限制。
Jumpserver 采纳分布式架构,支持多机房跨区域部署,中心节点提供 API各机房部署登录节点可横向扩展、无并发限制。
改变世界,从一点点开始。
----
- [English Version](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
### 功能
----
<table class="subscription-level-table">
<tr class="subscription-level-tr-border">
<th style="background-color: #1ab394;color: #ffffff;" colspan="3">Jumpserver提供的堡垒机必备功能</th>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="4">身份验证 Authentication</td>
<td class="features-second-td-background-style" rowspan="3" >登录认证
</td>
<td class="features-third-td-background-style">资源统一登录和认证
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">LDAP认证
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">支持OpenID实现单点登录
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">多因子认证
</td>
<td class="features-third-td-background-style">MFAGoogle Authenticator
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="9">账号管理 Account</td>
<td class="features-second-td-background-style" rowspan="2">集中账号管理
</td>
<td class="features-third-td-background-style">管理用户管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">系统用户管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style" rowspan="4">统一密码管理
</td>
<td class="features-third-td-background-style">资产密码托管
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">自动生成密码
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">密码自动推送
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">密码过期设置
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style" rowspan="2">批量密码变更(X-PACK)
</td>
<td class="features-outline-td-background-style">定期批量修改密码
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style">生成随机密码
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style">多云环境的资产纳管(X-PACK)
</td>
<td class="features-outline-td-background-style">对私有云、公有云资产统一纳管
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="8">授权控制 Authorization</td>
<td class="features-second-td-background-style" rowspan="3">资产授权管理
</td>
<td class="features-third-td-background-style">资产树
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">资产或资产组灵活授权
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">节点内资产自动继承授权
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-outline-td-background-style">组织管理(X-PACK)
</td>
<td class="features-outline-td-background-style">实现多租户管理,权限隔离
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">多维度授权
</td>
<td class="features-third-td-background-style">可对用户、用户组或系统角色授权
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">指令限制
</td>
<td class="features-third-td-background-style">限制特权指令使用,支持黑白名单
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">统一文件传输
</td>
<td class="features-third-td-background-style">SFTP 文件上传/下载
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">文件管理
</td>
<td class="features-third-td-background-style">Web SFTP 文件管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-first-td-background-style" rowspan="6">安全审计 Audit</td>
<td class="features-second-td-background-style" rowspan="2">会话管理
</td>
<td class="features-third-td-background-style">在线会话管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">历史会话管理
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style" rowspan="2">录像管理
</td>
<td class="features-third-td-background-style">Linux 录像支持
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-third-td-background-style">Windows 录像支持
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">指令审计
</td>
<td class="features-third-td-background-style">指令记录
</td>
</tr>
<tr class="subscription-level-tr-border">
<td class="features-second-td-background-style">文件传输审计
</td>
<td class="features-third-td-background-style">上传/下载记录审计
</td>
</tr>
</table>
![Jumpserver功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver-14.png "Jumpserver功能")
### 开始使用
----
快速开始文档 [Docker安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
- 快速开始文档 [Docker 安装](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
一步一步安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
- Step by Step 安装文档 [详细部署](http://docs.jumpserver.org/zh/docs/step_by_step.html)
也可以查看我们完整文档包括了使用和开发 [文档](http://docs.jumpserver.org)
- 也可以查看我们完整文档 [文档](http://docs.jumpserver.org)
### Demo 和 截图
### Demo、视频 和 截图
----
我们提供了DEMO和截图可以让你快速了解Jumpserver
我们提供了 Demo 、演示视频和截图可以让你快速了解 Jumpserver
[DEMO](https://demo.jumpserver.org)
[截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
- [Demo](https://demo.jumpserver.org/auth/login/?next=/)
- [视频](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
- [截图](http://docs.jumpserver.org/zh/docs/snapshot.html)
### SDK
----
我们还编写了一些SDK供你其它系统快速和Jumpserver APi交互
我们还编写了一些SDK供你其它系统快速和 Jumpserver API 交互
- [python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver其它组件使用这个SDK完成交互
- [java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver其它组件使用这个SDK完成交互
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK
### License & Copyright
Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.
Copyright (c) 2014-2019 飞致云 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

58
README_EN.md Normal file
View File

@@ -0,0 +1,58 @@
## Jumpserver
[![Python3](https://img.shields.io/badge/python-3.6-green.svg?style=plastic)](https://www.python.org/)
[![Django](https://img.shields.io/badge/django-2.1-brightgreen.svg?style=plastic)](https://www.djangoproject.com/)
[![Ansible](https://img.shields.io/badge/ansible-2.4.2.0-blue.svg?style=plastic)](https://www.ansible.com/)
[![Paramiko](https://img.shields.io/badge/paramiko-2.4.1-green.svg?style=plastic)](http://www.paramiko.org/)
----
- [中文版](https://github.com/jumpserver/jumpserver/blob/master/README_EN.md)
Jumpserver is the first fully open source bastion in the world, based on the GNU GPL v2.0 open source protocol. Jumpserver is a professional operation and maintenance audit system conforms to 4A specifications.
Jumpserver is developed using Python / Django, conforms to the Web 2.0 specification, and is equipped with the industry-leading Web Terminal solution which have beautiful interface and great user experience.
Jumpserver adopts a distributed architecture to support multi-branch deployment across multiple areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
Change the world, starting from little things.
----
### Features
![Jumpserver 功能](https://jumpserver-release.oss-cn-hangzhou.aliyuncs.com/Jumpserver148.jpeg "Jumpserver 功能")
### Start
Quick start [Docker Install](http://docs.jumpserver.org/zh/docs/dockerinstall.html)
Step by Step deployment. [Docs](http://docs.jumpserver.org/zh/docs/step_by_step.html)
Full documentation [Docs](http://docs.jumpserver.org)
### Demo、Video 和 Snapshot
We provide online demo, demo video and screenshots to get you started quickly.
[Demo](https://demo.jumpserver.org/auth/login/?next=/)
[Video](https://fit2cloud2-offline-installer.oss-cn-beijing.aliyuncs.com/tools/Jumpserver%20%E4%BB%8B%E7%BB%8Dv1.4.mp4)
[Snapshot](http://docs.jumpserver.org/zh/docs/snapshot.html)
### SDK
We provide the SDK for your other systems to quickly interact with the Jumpserver API.
- [Python](https://github.com/jumpserver/jumpserver-python-sdk) Jumpserver other components use this SDK to complete the interaction.
- [Java](https://github.com/KaiJunYan/jumpserver-java-sdk.git) 恺珺同学提供的Java版本的SDK thanks to 恺珺 for provide Java SDK
### License & Copyright
Copyright (c) 2014-2019 Beijing Duizhan Tech, Inc., All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-2.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

@@ -1,4 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
__version__ = "1.4.9"

View File

@@ -1,19 +1,25 @@
# -*- coding: utf-8 -*-
#
import uuid
import random
from rest_framework import generics
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
from rest_framework.pagination import LimitOffsetPagination
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from django.core.cache import cache
from django.db.models import Q
from common.mixins import IDInFilterMixin
from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from ..models import Asset, AdminUser, Node
from .. import serializers
from ..tasks import update_asset_hardware_info_manual, \
@@ -25,7 +31,7 @@ logger = get_logger(__file__)
__all__ = [
'AssetViewSet', 'AssetListUpdateApi',
'AssetRefreshHardwareApi', 'AssetAdminUserTestApi',
'AssetGatewayApi'
'AssetGatewayApi', 'AssetBulkUpdateSelectAPI'
]
@@ -92,6 +98,21 @@ class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView):
permission_classes = (IsOrgAdmin,)
class AssetBulkUpdateSelectAPI(APIView):
permission_classes = (IsOrgAdmin,)
def post(self, request, *args, **kwargs):
assets_id = request.data.get('assets_id', '')
if assets_id:
spm = uuid.uuid4().hex
key = CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm)
cache.set(key, assets_id, 300)
url = reverse_lazy('assets:asset-bulk-update') + '?spm=%s' % spm
return Response({'url': url})
error = _('Please select assets that need to be updated')
return Response({'error': error}, status=400)
class AssetRefreshHardwareApi(generics.RetrieveAPIView):
"""
Refresh asset hardware info

View File

@@ -48,3 +48,6 @@ TASK_OPTIONS = {
'timeout': 10,
'forks': 10,
}
CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}'

View File

@@ -153,7 +153,7 @@ class Asset(OrgModelMixin):
nodes = []
for node in self.get_nodes():
_nodes = node.get_ancestor(with_self=True)
_nodes.append(_nodes)
nodes.append(_nodes)
if flat:
nodes = list(reduce(lambda x, y: set(x) | set(y), nodes))
return nodes

View File

@@ -148,7 +148,7 @@ class Node(OrgModelMixin):
)
def get_all_children(self, with_self=False):
pattern = r'^{0}$|^{0}:' if with_self else r'^{0}'
pattern = r'^{0}$|^{0}:' if with_self else r'^{0}:'
return self.__class__.objects.filter(
key__regex=pattern.format(self.key)
)

View File

@@ -3,6 +3,8 @@
from django.core.cache import cache
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from ..models import Node, AdminUser
from ..const import ADMIN_USER_CONN_CACHE_KEY
@@ -18,6 +20,7 @@ class AdminUserSerializer(serializers.ModelSerializer):
reachable_amount = serializers.SerializerMethodField()
class Meta:
list_serializer_class = AdaptedBulkListSerializer
model = AdminUser
fields = '__all__'

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
from ..models import Asset
from .system_user import AssetSystemUserSerializer
@@ -19,7 +19,7 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
"""
class Meta:
model = Asset
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
validators = []

View File

@@ -3,6 +3,7 @@
from rest_framework import serializers
from common.fields import ChoiceDisplayField
from common.serializers import AdaptedBulkListSerializer
from ..models import CommandFilter, CommandFilterRule, SystemUser
@@ -12,6 +13,7 @@ class CommandFilterSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilter
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
@@ -21,3 +23,4 @@ class CommandFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = CommandFilterRule
fields = '__all__'
list_serializer_class = AdaptedBulkListSerializer

View File

@@ -2,6 +2,8 @@
#
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from ..models import Domain, Gateway
@@ -12,6 +14,7 @@ class DomainSerializer(serializers.ModelSerializer):
class Meta:
model = Domain
fields = '__all__'
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
def get_asset_count(obj):
@@ -25,6 +28,7 @@ class DomainSerializer(serializers.ModelSerializer):
class GatewaySerializer(serializers.ModelSerializer):
class Meta:
model = Gateway
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'ip', 'port', 'protocol', 'username',
'domain', 'is_active', 'date_created', 'date_updated',

View File

@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.serializers import AdaptedBulkListSerializer
from ..models import Label
@@ -12,7 +13,7 @@ class LabelSerializer(serializers.ModelSerializer):
class Meta:
model = Label
fields = '__all__'
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
@staticmethod
def get_asset_count(obj):

View File

@@ -1,5 +1,7 @@
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from ..models import SystemUser, Asset
from .base import AuthSerializer
@@ -17,6 +19,7 @@ class SystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
exclude = ('_password', '_private_key', '_public_key')
list_serializer_class = AdaptedBulkListSerializer
def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
@@ -61,13 +64,19 @@ class AssetSystemUserSerializer(serializers.ModelSerializer):
"""
查看授权的资产系统用户的数据结构这个和AssetSerializer不同字段少
"""
actions = serializers.SerializerMethodField()
class Meta:
model = SystemUser
fields = (
'id', 'name', 'username', 'priority',
'protocol', 'comment', 'login_mode'
'protocol', 'comment', 'login_mode', 'actions',
)
@staticmethod
def get_actions(obj):
return [action.name for action in obj.actions]
class SystemUserSimpleSerializer(serializers.ModelSerializer):
"""

View File

@@ -98,6 +98,7 @@ function initTable() {
order: [],
columnDefs: [
{targets: 0, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -44,9 +44,10 @@ $(document).ready(function(){
var options = {
ele: $('#admin_user_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{targets: 1, render: function (cellData, tp, rowData, meta) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:admin-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
return detail_btn.replace('{{ DEFAULT_PK }}', rowData.id);
}},
{targets: 4, createdCell: function (td, cellData) {
var innerHtml = "";
@@ -82,7 +83,6 @@ $(document).ready(function(){
innerHtml = "<span class='text-danger'>" + num.toFixed(1) + "% </span>";
}
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}},
{targets: 8, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "assets:admin-user-update" pk=DEFAULT_PK %}" class="btn btn-xs m-l-xs btn-info">{% trans "Update" %}</a>'.replace('{{ DEFAULT_PK }}', cellData);
@@ -90,8 +90,8 @@ $(document).ready(function(){
$(td).html(update_btn + del_btn)
}}],
ajax_url: '{% url "api-assets:admin-user-list" %}',
columns: [{data: function(){return ""}}, {data: "name" }, {data: "username" }, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment" }, {data: "id" }]
columns: [{data: function(){return ""}}, {data: "name"}, {data: "username" }, {data: "assets_amount" },
{data: "reachable_amount"}, {data: "unreachable_amount"}, {data: "id"}, {data: "comment"}, {data: "id"}]
};
jumpserver.initServerSideDataTable(options)
})

View File

@@ -46,7 +46,7 @@
<tr>
<th class="text-center"><input type="checkbox" class="ipt_check_all"></th>
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Version' %}</th>
<th class="text-center">{% trans 'Password version' %}</th>
<th class="text-center">{% trans 'Reachable' %}</th>
<th class="text-center">{% trans 'Date updated' %}</th>
<th class="text-center">{% trans 'Action' %}</th>

View File

@@ -20,7 +20,7 @@
<a href="{% url 'assets:asset-detail' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset detail' %} </a>
</li>
<li>
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Asset user list' %} </a>
<a href="{% url 'assets:asset-user-list' pk=asset.id %}" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Asset user list' %} </a>
</li>
{% if user.is_superuser %}
<li class="pull-right">

View File

@@ -156,6 +156,7 @@ function initTable() {
ele: $('#asset_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
@@ -657,9 +658,23 @@ $(document).ready(function(){
});
}
function doUpdate() {
var id_list_string = id_list.join(',');
var url = "{% url 'assets:asset-bulk-update' %}?assets_id=" + id_list_string;
location.href = url
var data = {
'assets_id':id_list
};
function error(data) {
toastr.error(JSON.parse(data).error)
}
function success(data) {
location.href = data.url;
}
APIUpdateAttr({
'url': "{% url 'api-assets:asset-bulk-update-select' %}",
'method': 'POST',
'body': JSON.stringify(data),
'flash_message': false,
'success': success,
'error': error,
})
}
function doRemove() {

View File

@@ -40,6 +40,7 @@ function initTable() {
ele: $('#cmd_filter_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url 'assets:cmd-filter-detail' pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -41,6 +41,7 @@ function initTable() {
ele: $('#domain_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:domain-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -30,6 +30,7 @@ function initTable() {
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
{# var detail_btn = '<a href="{% url "assets:label-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';#}
cellData = htmlEscape(cellData);
var detail_btn = '<a>' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -144,6 +144,7 @@ function initAssetsTable() {
order: [],
columnDefs: [
{targets: 0, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:asset-detail" pk=DEFAULT_PK %}" data-aid="'+rowData.id+'">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -49,6 +49,7 @@ function initTable() {
ele: $('#system_user_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "assets:system-user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -25,6 +25,8 @@ cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-r
urlpatterns = [
path('assets-bulk/', api.AssetListUpdateApi.as_view(), name='asset-bulk-update'),
path('asset/update/select/',
api.AssetBulkUpdateSelectAPI.as_view(), name='asset-bulk-update-select'),
path('assets/<uuid:pk>/refresh/',
api.AssetRefreshHardwareApi.as_view(), name='asset-refresh'),
path('assets/<uuid:pk>/alive/',

View File

@@ -28,6 +28,7 @@ from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger
from common.permissions import AdminUserRequiredMixin
from common.const import create_success_msg, update_success_msg
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from orgs.utils import current_org
from .. import forms
from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
@@ -120,15 +121,12 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
form = None
def get(self, request, *args, **kwargs):
assets_id = self.request.GET.get('assets_id', '')
self.id_list = [i for i in assets_id.split(',')]
spm = request.GET.get('spm', '')
assets_id = cache.get(CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm))
if kwargs.get('form'):
self.form = kwargs['form']
elif assets_id:
self.form = self.form_class(
initial={'assets': self.id_list}
)
self.form = self.form_class(initial={'assets': assets_id})
else:
self.form = self.form_class()
return super().get(request, *args, **kwargs)

View File

@@ -37,7 +37,7 @@
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="asset">
<select class="select2 form-control" name="action">
<option value="">{% trans 'Action' %}</option>
{% for k, v in actions.items %}
<option value="{{ k }}" {% if k == action %} selected {% endif %}>{{ v }}</option>
@@ -45,7 +45,7 @@
</select>
</div>
<div class="input-group">
<select class="select2 form-control" name="system_user">
<select class="select2 form-control" name="resource_type">
<option value="">{% trans 'Resource Type' %}</option>
{% for r in resource_type_list %}
<option value="{{ r }}" {% if r == resource_type %} selected {% endif %}>{{ r }}</option>

View File

@@ -23,12 +23,15 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
# Don't need openid auth if AUTH_OPENID is False
if not settings.AUTH_OPENID:
logger.debug("Not settings.AUTH_OPENID")
return
# Don't need check single logout if user not authenticated
if not request.user.is_authenticated:
logger.debug("User is not authenticated")
return
elif request.session[BACKEND_SESSION_KEY].endswith(
elif not request.session[BACKEND_SESSION_KEY].endswith(
BACKEND_OPENID_AUTH_CODE):
logger.debug("BACKEND_SESSION_KEY is not BACKEND_OPENID_AUTH_CODE")
return
# Check openid user single logout or not with access_token
@@ -37,7 +40,6 @@ class OpenIDAuthenticationMiddleware(MiddlewareMixin):
client.openid_connect_client.userinfo(
token=request.session.get(OIDT_ACCESS_TOKEN)
)
except Exception as e:
logout(request)
logger.error(e)

View File

@@ -14,6 +14,14 @@ class UserLoginForm(AuthenticationForm):
max_length=128, strip=False
)
error_messages = {
'invalid_login': _(
"Please enter a correct username and password. Note that both "
"fields may be case-sensitive."
),
'inactive': _("This account is inactive."),
}
def confirm_login_allowed(self, user):
if not user.is_staff:
raise forms.ValidationError(

View File

@@ -7,7 +7,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jumpserver</title>
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
@@ -42,7 +42,7 @@
<div class="col-md-6">
<div class="ibox-content">
<div>
<img src="{% static 'img/logo.png' %}" width="60" height="60">
<img src="{{ LOGO_URL }}" width="60" height="60">
<span class="font-bold text-center" style="font-size: 24px; font-family: inherit; margin-left: 20px">{% trans 'Login' %}</span>
</div>
<form class="m-t" role="form" method="post" action="">

View File

@@ -6,8 +6,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Jumpserver </title>
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<title> {{ JMS_TITLE }} </title>
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>

View File

@@ -6,18 +6,9 @@
<!--/*@thymesVar id="message" type="java.lang.String"*/-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
{% if interface and interface.favicon %}
<link rel="shortcut icon" href="{{ MEDIA_URL }}{{ interface.favicon }}" type="image/x-icon">
{% else %}
<link rel="shortcut icon" href="{% static 'img/facio.ico' %}" type="image/x-icon">
{% endif %}
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<title>
{% if interface and interface.login_title %}
{{ interface.login_title }}
{% else %}
Jumpserver
{% endif %}
{{ JMS_TITLE }}
</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Stylesheets -->
@@ -61,38 +52,27 @@
</style>
</head>
<body style="height: 100%">
<body style="height: 100%;font-size: 13px">
<div>
<div class="box-1">
<div class="box-2">
{% if interface.login_image %}
<img src="{{ MEDIA_URL }}{{ interface.login_image }}" style="height: 100%; width: 100%"/>
{% else %}
<img src="{% static 'img/login/login_image_1.png' %}" style=" height: 100%; width: 100%"/>
{% endif %}
<img src="{{ LOGIN_IMAGE_URL }}" style="height: 100%; width: 100%"/>
</div>
<div class="box-3">
<div style="background-color: white">
{% if interface.login_title %}
<div style="margin-top: 40px;padding-top: 50px;">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{{ interface.login_title }}</span>
</div>
{% else %}
<div style="margin-top: 40px;padding-top: 50px;">
<span style="font-size: 24px;font-weight:400;color: #151515;letter-spacing: 0;">{% trans 'Welcome to the Jumpserver open source fortress' %}</span>
</div>
{% endif %}
<div style="font-size: 12px;color: #999999;letter-spacing: 0;line-height: 18px;margin-top: 10px">
<div style="margin-top: 30px;padding-top: 40px;padding-left: 20px;padding-right: 20px;height: 80px">
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
</div>
<div style="font-size: 12px;color: #999999;letter-spacing: 0;line-height: 18px;margin-top: 18px">
{% trans 'Welcome back, please enter username and password to login' %}
</div>
<div style="margin-bottom: 10px">
<div>
<div class="col-md-1"></div>
<div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
<div class="contact-form col-md-10" style="margin-top: 10px;height: 35px">
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
{% csrf_token %}
<div style="height: 48px;color: red">
<div style="height: 45px;color: red;line-height: 17px;">
{% if block_login %}
<p class="red-fonts">{% trans 'Log in frequently and try again later' %}</p>
{% elif password_expired %}
@@ -112,7 +92,7 @@
<div class="form-group">
<input type="password" class="form-control" name="{{ form.password.html_name }}" placeholder="{% trans 'Password' %}" required="">
</div>
<div class="form-group" style="height: 50px;margin-bottom: 0">
<div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px">
{{ form.captcha }}
</div>
<div class="form-group" style="margin-top: 10px">

View File

@@ -43,7 +43,7 @@ class UserLoginView(FormView):
key_prefix_captcha = "_LOGIN_INVALID_{}"
def get_template_names(self):
template_name = 'users/login.html'
template_name = 'authentication/login.html'
if not settings.XPACK_ENABLED:
return template_name
@@ -51,7 +51,7 @@ class UserLoginView(FormView):
if not License.has_valid_license():
return template_name
template_name = 'users/new_login.html'
template_name = 'authentication/new_login.html'
return template_name
def get(self, request, *args, **kwargs):
@@ -59,6 +59,11 @@ class UserLoginView(FormView):
return redirect(redirect_user_first_login_or_index(
request, self.redirect_field_name)
)
# show jumpserver login page if request http://{JUMP-SERVER}/?admin=1
if settings.AUTH_OPENID and not self.request.GET.get('admin', 0):
query_string = request.GET.urlencode()
login_url = "{}?{}".format(settings.LOGIN_URL, query_string)
return redirect(login_url)
request.session.set_test_cookie()
return super().get(request, *args, **kwargs)
@@ -145,7 +150,7 @@ class UserLoginView(FormView):
class UserLoginOtpView(FormView):
template_name = 'users/login_otp.html'
template_name = 'authentication/login_otp.html'
form_class = forms.UserCheckOtpCodeForm
redirect_field_name = 'next'

View File

@@ -3,7 +3,7 @@
from django.utils.translation import ugettext_lazy as _
create_success_msg = _("<b>%(name)s</b> was created successfully")
update_success_msg = _("<b>%(name)s</b> was updated successfully")
create_success_msg = _("%(name)s was created successfully")
update_success_msg = _("%(name)s was updated successfully")
FILE_END_GUARD = ">>> Content End <<<"
celery_task_pre_key = "CELERY_"

View File

@@ -4,6 +4,10 @@ from django.db import models
from django.http import JsonResponse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from rest_framework.utils import html
from rest_framework.settings import api_settings
from rest_framework.exceptions import ValidationError
from rest_framework.fields import SkipField
class NoDeleteQuerySet(models.query.QuerySet):
@@ -89,6 +93,60 @@ class BulkSerializerMixin(object):
return ret
class BulkListSerializerMixin(object):
"""
Become rest_framework_bulk doing bulk update raise Exception:
'QuerySet' object has no attribute 'pk' when doing bulk update
so rewrite it .
https://github.com/miki725/django-rest-framework-bulk/issues/68
"""
def to_internal_value(self, data):
"""
List of dicts of native values <- List of dicts of primitive datatypes.
"""
if html.is_html_input(data):
data = html.parse_html_list(data)
if not isinstance(data, list):
message = self.error_messages['not_a_list'].format(
input_type=type(data).__name__
)
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
}, code='not_a_list')
if not self.allow_empty and len(data) == 0:
if self.parent and self.partial:
raise SkipField()
message = self.error_messages['empty']
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: [message]
}, code='empty')
ret = []
errors = []
for item in data:
try:
# prepare child serializer to only handle one instance
self.child.instance = self.instance.get(id=item['id']) if self.instance else None
self.child.initial_data = item
# raw
validated = self.child.run_validation(item)
except ValidationError as exc:
errors.append(exc.detail)
else:
ret.append(validated)
errors.append({})
if any(errors):
raise ValidationError(errors)
return ret
class DatetimeSearchMixin:
date_format = '%Y-%m-%d'
date_from = date_to = None

View File

@@ -35,7 +35,7 @@ class IsSuperUser(IsValidUser):
class IsSuperUserOrAppUser(IsSuperUser):
def has_permission(self, request, view):
return super(IsSuperUserOrAppUser, self).has_permission(request, view) \
and (request.user.is_superuser or request.user.is_app)
or request.user.is_app
class IsOrgAdmin(IsValidUser):
@@ -70,6 +70,14 @@ class IsCurrentUserOrReadOnly(permissions.BasePermission):
return obj == request.user
class LoginRequiredMixin(UserPassesTestMixin):
def test_func(self):
if self.request.user.is_authenticated:
return True
else:
return False
class AdminUserRequiredMixin(UserPassesTestMixin):
def test_func(self):
if not self.request.user.is_authenticated:

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
from .mixins import BulkListSerializerMixin
from rest_framework_bulk.serializers import BulkListSerializer
class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer):
pass

3
apps/jumpserver/const.py Normal file
View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
#
VERSION = '1.4.10'

View File

@@ -1,14 +1,22 @@
# -*- coding: utf-8 -*-
#
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.conf import settings
from django.utils.translation import gettext_lazy as _
def jumpserver_processor(request):
context = {}
# Setting default pk
context.update(
{'DEFAULT_PK': '00000000-0000-0000-0000-000000000000'}
)
context = {
'DEFAULT_PK': '00000000-0000-0000-0000-000000000000',
'LOGO_URL': static('img/logo.png'),
'LOGO_TEXT_URL': static('img/logo_text.png'),
'LOGIN_IMAGE_URL': static('img/login_image.png'),
'FAVICON_URL': static('img/facio.ico'),
'JMS_TITLE': 'Jumpserver',
'VERSION': settings.VERSION,
'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2019'
}
return context

View File

@@ -12,25 +12,24 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
import os
import sys
import socket
import ldap
from django.urls import reverse_lazy
from . import const
from .conf import load_user_config
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(BASE_DIR)
sys.path.append(PROJECT_DIR)
from apps import __version__
VERSION = __version__
CONFIG = load_user_config()
LOG_DIR = os.path.join(PROJECT_DIR, 'logs')
JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log')
ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible.log')
GUNICORN_LOG_FILE = os.path.join(LOG_DIR, 'gunicorn.log')
VERSION = const.VERSION
if not os.path.isdir(LOG_DIR):
os.makedirs(LOG_DIR)
@@ -163,7 +162,7 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
DB_OPTIONS = {}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE),
'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE.lower()),
'NAME': CONFIG.DB_NAME,
'HOST': CONFIG.DB_HOST,
'PORT': CONFIG.DB_PORT,
@@ -174,8 +173,10 @@ DATABASES = {
}
}
DB_CA_PATH = os.path.join(PROJECT_DIR, 'data', 'ca.pem')
if CONFIG.DB_ENGINE == 'mysql' and os.path.isfile(DB_CA_PATH):
DB_OPTIONS['ssl'] = {'ca': DB_CA_PATH}
if CONFIG.DB_ENGINE.lower() == 'mysql':
DB_OPTIONS['init_command'] = "SET sql_mode='STRICT_TRANS_TABLES'"
if os.path.isfile(DB_CA_PATH):
DB_OPTIONS['ssl'] = {'ca': DB_CA_PATH}
# Password validation
@@ -411,7 +412,7 @@ AUTH_LDAP_USER_ATTR_MAP = {"username": "cn", "name": "sn", "email": "mail"}
# AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER
# )
AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: 5
ldap.OPT_TIMEOUT: 30
}
AUTH_LDAP_GROUP_CACHE_TIMEOUT = 1
AUTH_LDAP_ALWAYS_UPDATE_USER = True
@@ -475,6 +476,7 @@ CELERY_WORKER_REDIRECT_STDOUTS = True
CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
# CELERY_WORKER_HIJACK_ROOT_LOGGER = True
CELERY_WORKER_MAX_TASKS_PER_CHILD = 40
CELERY_TASK_SOFT_TIME_LIMIT = 3600
# Cache use redis
CACHES = {

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import subprocess
from django.conf import settings
from celery import shared_task, subtask
from celery.exceptions import SoftTimeLimitExceeded
from django.utils import timezone
from common.utils import get_logger, get_object_or_none
@@ -38,11 +39,14 @@ def run_ansible_task(tid, callback=None, **kwargs):
logger.error("No task found")
@shared_task
@shared_task(soft_time_limit=60)
def run_command_execution(cid, **kwargs):
execution = get_object_or_none(CommandExecution, id=cid)
if execution:
execution.run()
try:
execution.run()
except SoftTimeLimitExceeded:
print("HLLL")
else:
logger.error("Not found the execution id: {}".format(cid))

View File

@@ -5,17 +5,17 @@
{% block custom_head_css_js %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
<link href="{% static 'css/plugins/codemirror/codemirror.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/codemirror/ambiance.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.exhide.min.js' %}"></script>
<script src="{% static 'js/jquery.form.min.js' %}"></script>
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
<script src="{% static 'js/plugins/xterm/addons/fit/fit.js' %}"></script>
<script src="{% static 'js/plugins/codemirror/codemirror.js' %}"></script>
<script src="{% static 'js/plugins/codemirror/mode/shell/shell.js' %}"></script>
<link href="{% static 'css/plugins/codemirror/codemirror.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/codemirror/ambiance.css' %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<style type="text/css">
.xterm .xterm-screen canvas {
@@ -82,6 +82,8 @@
<script>
var zTree, show = 0;
var systemUserId = null;
var url = null;
var treeUrl = "{% url 'api-perms:my-nodes-assets-as-tree' %}?cache_policy=1";
function initTree() {
var setting = {
@@ -110,14 +112,20 @@ function initTree() {
onCheck: onCheck
}
};
var url = "{% url 'api-perms:my-nodes-assets-as-tree' %}?cache_policy=1";
if (systemUserId) {
url += '&system_user=' + systemUserId
url = treeUrl + '&system_user=' + systemUserId
}
else{
url = treeUrl
}
$.get(url, function(data, status){
$.fn.zTree.init($("#assetTree"), setting, data);
zTree = $.fn.zTree.getZTreeObj("assetTree");
rootNodeAddDom(zTree, function () {
treeUrl = treeUrl.replace('cache_policy=1', 'cache_policy=2');
initTree();
});
});
}
@@ -250,7 +258,8 @@ function execute() {
method: 'POST',
flash_message: false,
success: function (resp) {
term.write("{% trans 'Pending' %}" + "...\r\n");
var msg = "{% trans 'Pending' %}";
term.write(msg + "...\r\n");
log_url = resp.log_url;
int = setInterval(function () {
writeExecutionOutput()

View File

@@ -5,6 +5,7 @@ from django.utils.translation import ugettext as _
from django.conf import settings
from django.views.generic import ListView, TemplateView
from common.permissions import AdminUserRequiredMixin, LoginRequiredMixin
from common.mixins import DatetimeSearchMixin
from ..models import CommandExecution
from ..forms import CommandExecutionForm
@@ -15,7 +16,7 @@ __all__ = [
]
class CommandExecutionListView(DatetimeSearchMixin, ListView):
class CommandExecutionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView):
template_name = 'ops/command_execution_list.html'
model = CommandExecution
paginate_by = settings.DISPLAY_PER_PAGE
@@ -50,7 +51,7 @@ class CommandExecutionListView(DatetimeSearchMixin, ListView):
return super().get_context_data(**kwargs)
class CommandExecutionStartView(TemplateView):
class CommandExecutionStartView(LoginRequiredMixin, TemplateView):
template_name = 'ops/command_execution_create.html'
form_class = CommandExecutionForm

View File

@@ -1,11 +1,11 @@
from rest_framework.serializers import ModelSerializer
from rest_framework import serializers
from rest_framework_bulk import BulkListSerializer
from users.models import User, UserGroup
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
from perms.models import AssetPermission
from common.serializers import AdaptedBulkListSerializer
from .utils import set_current_org, get_current_org
from .models import Organization
from .mixins import OrgMembershipSerializerMixin
@@ -14,7 +14,7 @@ from .mixins import OrgMembershipSerializerMixin
class OrgSerializer(ModelSerializer):
class Meta:
model = Organization
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
read_only_fields = ['created_by', 'date_created']
@@ -70,12 +70,12 @@ class OrgReadSerializer(ModelSerializer):
class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer):
class Meta:
model = Organization.admins.through
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer):
class Meta:
model = Organization.users.through
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'

View File

@@ -10,7 +10,7 @@ from rest_framework.pagination import LimitOffsetPagination
from common.permissions import IsOrgAdmin
from common.utils import get_object_or_none
from ..models import AssetPermission
from ..models import AssetPermission, Action
from ..hands import (
User, UserGroup, Asset, Node, SystemUser,
)
@@ -20,10 +20,16 @@ from .. import serializers
__all__ = [
'AssetPermissionViewSet', 'AssetPermissionRemoveUserApi',
'AssetPermissionAddUserApi', 'AssetPermissionRemoveAssetApi',
'AssetPermissionAddAssetApi',
'AssetPermissionAddAssetApi', 'ActionViewSet',
]
class ActionViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Action.objects.all()
serializer_class = serializers.ActionSerializer
permission_classes = (IsOrgAdmin,)
class AssetPermissionViewSet(viewsets.ModelViewSet):
"""
资产授权列表的增删改查api

View File

@@ -108,7 +108,7 @@ class UserGroupGrantedNodesWithAssetsAsTreeApi(ListAPIView):
group = get_object_or_404(UserGroup, id=user_group_id)
util = AssetPermissionUtil(group)
if self.system_user_id:
util.filter_permission_with_system_user(system_user=self.system_user_id)
util.filter_permissions(system_users=self.system_user_id)
nodes = util.get_nodes_with_assets()
for node, assets in nodes.items():
data = parse_node_to_tree_node(node)

View File

@@ -16,7 +16,8 @@ from common.tree import TreeNodeSerializer
from common.utils import get_logger
from orgs.utils import set_to_root_org
from ..utils import (
AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node
AssetPermissionUtil, parse_asset_to_tree_node, parse_node_to_tree_node,
check_system_user_action
)
from ..hands import (
AssetGrantedSerializer, User, Asset, Node,
@@ -24,6 +25,7 @@ from ..hands import (
)
from .. import serializers
from ..mixins import AssetsFilterMixin
from ..models import Action
logger = get_logger(__name__)
@@ -31,14 +33,15 @@ __all__ = [
'UserGrantedAssetsApi', 'UserGrantedNodesApi',
'UserGrantedNodesWithAssetsApi', 'UserGrantedNodeAssetsApi',
'ValidateUserAssetPermissionApi', 'UserGrantedNodeChildrenApi',
'UserGrantedNodesWithAssetsAsTreeApi',
'UserGrantedNodesWithAssetsAsTreeApi', 'GetUserAssetPermissionActionsApi',
]
class UserPermissionMixin:
class UserPermissionCacheMixin:
cache_policy = '0'
RESP_CACHE_KEY = '_PERMISSION_RESPONSE_CACHE_{}'
CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
_object = None
@staticmethod
def change_org_if_need(request, kwargs):
@@ -51,56 +54,82 @@ class UserPermissionMixin:
def get_object(self):
return None
# 内部使用可控制缓存
def _get_object(self):
if not self._object:
self._object = self.get_object()
return self._object
def get_object_id(self):
obj = self._get_object()
if obj:
return str(obj.id)
return None
def get_request_md5(self):
full_path = self.request.get_full_path()
return md5(full_path.encode()).hexdigest()
def get_meta_cache_id(self):
obj = self._get_object()
util = AssetPermissionUtil(obj, cache_policy=self.cache_policy)
meta_cache_id = util.cache_meta.get('id')
return meta_cache_id
def get_response_cache_id(self):
obj_id = self.get_object_id()
request_md5 = self.get_request_md5()
meta_cache_id = self.get_meta_cache_id()
resp_cache_id = '{}_{}_{}'.format(obj_id, request_md5, meta_cache_id)
return resp_cache_id
def get_response_from_cache(self):
resp_cache_id = self.get_response_cache_id()
# 没有数据缓冲
meta_cache_id = self.get_meta_cache_id()
if not meta_cache_id:
return None
# 从响应缓冲里获取响应
key = self.RESP_CACHE_KEY.format(resp_cache_id)
data = cache.get(key)
if not data:
return None
logger.debug("Get user permission from cache: {}".format(self.get_object()))
response = Response(data)
return response
def expire_response_cache(self):
obj_id = self.get_object_id()
expire_cache_id = '{}_{}'.format(obj_id, '*')
key = self.RESP_CACHE_KEY.format(expire_cache_id)
cache.delete_pattern(key)
def set_response_to_cache(self, response):
resp_cache_id = self.get_response_cache_id()
key = self.RESP_CACHE_KEY.format(resp_cache_id)
cache.set(key, response.data, self.CACHE_TIME)
def get(self, request, *args, **kwargs):
self.change_org_if_need(request, kwargs)
self.cache_policy = request.GET.get('cache_policy', '0')
obj = self.get_object()
obj = self._get_object()
if obj is None:
return super().get(request, *args, **kwargs)
request_path_md5 = md5(request.get_full_path().encode()).hexdigest()
obj_id = str(obj.id)
expire_cache_key = '{}_{}'.format(obj_id, '*')
if self.CACHE_TIME <= 0 or \
self.cache_policy in AssetPermissionUtil.CACHE_POLICY_MAP[0]:
if AssetPermissionUtil.is_not_using_cache(self.cache_policy):
return super().get(request, *args, **kwargs)
elif self.cache_policy in AssetPermissionUtil.CACHE_POLICY_MAP[2]:
self.expire_cache_response(expire_cache_key)
elif AssetPermissionUtil.is_refresh_cache(self.cache_policy):
self.expire_response_cache()
util = AssetPermissionUtil(obj, cache_policy=self.cache_policy)
meta_cache_id = util.cache_meta.get('id')
cache_id = '{}_{}_{}'.format(obj_id, request_path_md5, meta_cache_id)
# 没有数据缓冲
if not meta_cache_id:
response = super().get(request, *args, **kwargs)
self.set_cache_response(cache_id, response)
return response
# 从响应缓冲里获取响应
response = self.get_cache_response(cache_id)
if not response:
response = super().get(request, *args, **kwargs)
self.set_cache_response(cache_id, response)
return response
def get_cache_response(self, _id):
if not _id:
return None
key = self.RESP_CACHE_KEY.format(_id)
data = cache.get(key)
if not data:
return None
return Response(data)
def expire_cache_response(self, _id):
key = self.RESP_CACHE_KEY.format(_id)
cache.delete(key)
def set_cache_response(self, _id, response):
key = self.RESP_CACHE_KEY.format(_id)
cache.set(key, response.data, self.CACHE_TIME)
resp = self.get_response_from_cache()
if not resp:
resp = super().get(request, *args, **kwargs)
self.set_response_to_cache(resp)
return resp
class UserGrantedAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIView):
"""
用户授权的所有资产
"""
@@ -110,7 +139,6 @@ class UserGrantedAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
def get_object(self):
user_id = self.kwargs.get('pk', '')
if user_id:
user = get_object_or_404(User, id=user_id)
else:
@@ -134,7 +162,7 @@ class UserGrantedAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
return super().get_permissions()
class UserGrantedNodesApi(UserPermissionMixin, ListAPIView):
class UserGrantedNodesApi(UserPermissionCacheMixin, ListAPIView):
"""
查询用户授权的所有节点的API, 如果是超级用户或者是 app切换到root org
"""
@@ -161,7 +189,7 @@ class UserGrantedNodesApi(UserPermissionMixin, ListAPIView):
return super().get_permissions()
class UserGrantedNodesWithAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
class UserGrantedNodesWithAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIView):
"""
用户授权的节点并带着节点下资产的api
"""
@@ -202,17 +230,12 @@ class UserGrantedNodesWithAssetsApi(UserPermissionMixin, AssetsFilterMixin, List
return super().get_permissions()
class UserGrantedNodesWithAssetsAsTreeApi(UserPermissionMixin, ListAPIView):
class UserGrantedNodesWithAssetsAsTreeApi(UserPermissionCacheMixin, ListAPIView):
serializer_class = TreeNodeSerializer
permission_classes = (IsOrgAdminOrAppUser,)
show_assets = True
system_user_id = None
def get(self, request, *args, **kwargs):
self.show_assets = request.query_params.get('show_assets', '1') == '1'
self.system_user_id = request.query_params.get('system_user')
return super().get(request, *args, **kwargs)
def get_permissions(self):
if self.kwargs.get('pk') is None:
self.permission_classes = (IsValidUser,)
@@ -226,13 +249,19 @@ class UserGrantedNodesWithAssetsAsTreeApi(UserPermissionMixin, ListAPIView):
user = get_object_or_404(User, id=user_id)
return user
def list(self, request, *args, **kwargs):
resp = super().list(request, *args, **kwargs)
return resp
def get_queryset(self):
queryset = []
self.show_assets = self.request.query_params.get('show_assets', '1') == '1'
self.system_user_id = self.request.query_params.get('system_user')
user = self.get_object()
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
if self.system_user_id:
util.filter_permission_with_system_user(
system_user=self.system_user_id
util.filter_permissions(
system_users=self.system_user_id
)
nodes = util.get_nodes_with_assets()
for node, assets in nodes.items():
@@ -247,7 +276,7 @@ class UserGrantedNodesWithAssetsAsTreeApi(UserPermissionMixin, ListAPIView):
return queryset
class UserGrantedNodeAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIView):
class UserGrantedNodeAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIView):
"""
查询用户授权的节点下的资产的api, 与上面api不同的是只返回某个节点下的资产
"""
@@ -283,7 +312,7 @@ class UserGrantedNodeAssetsApi(UserPermissionMixin, AssetsFilterMixin, ListAPIVi
return super().get_permissions()
class UserGrantedNodeChildrenApi(UserPermissionMixin, ListAPIView):
class UserGrantedNodeChildrenApi(UserPermissionCacheMixin, ListAPIView):
"""
获取用户自己授权节点下子节点的api
"""
@@ -369,7 +398,35 @@ class UserGrantedNodeChildrenApi(UserPermissionMixin, ListAPIView):
return self.get_children_queryset()
class ValidateUserAssetPermissionApi(UserPermissionMixin, APIView):
class ValidateUserAssetPermissionApi(UserPermissionCacheMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get(self, request, *args, **kwargs):
user_id = request.query_params.get('user_id', '')
asset_id = request.query_params.get('asset_id', '')
system_id = request.query_params.get('system_user_id', '')
action_name = request.query_params.get('action_name', '')
user = get_object_or_404(User, id=user_id)
asset = get_object_or_404(Asset, id=asset_id)
su = get_object_or_404(SystemUser, id=system_id)
action = get_object_or_404(Action, name=action_name)
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
granted_assets = util.get_assets()
granted_system_users = granted_assets.get(asset, [])
if su not in granted_system_users:
return Response({'msg': False}, status=403)
_su = next((s for s in granted_system_users if s.id == su.id), None)
if not check_system_user_action(_su, action):
return Response({'msg': False}, status=403)
return Response({'msg': True}, status=200)
class GetUserAssetPermissionActionsApi(UserPermissionCacheMixin, APIView):
permission_classes = (IsOrgAdminOrAppUser,)
def get(self, request, *args, **kwargs):
@@ -379,13 +436,14 @@ class ValidateUserAssetPermissionApi(UserPermissionMixin, APIView):
user = get_object_or_404(User, id=user_id)
asset = get_object_or_404(Asset, id=asset_id)
system_user = get_object_or_404(SystemUser, id=system_id)
su = get_object_or_404(SystemUser, id=system_id)
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
assets_granted = util.get_assets()
if system_user in assets_granted.get(asset, []):
return Response({'msg': True}, status=200)
else:
return Response({'msg': False}, status=403)
granted_assets = util.get_assets()
granted_system_users = granted_assets.get(asset, [])
_su = next((s for s in granted_system_users if s.id == su.id), None)
if not _su:
return Response({'actions': []}, status=403)
actions = [action.name for action in getattr(_su, 'actions', [])]
return Response({'actions': actions}, status=200)

22
apps/perms/const.py Normal file
View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
__all__ = [
'PERMS_ACTION_NAME_ALL', 'PERMS_ACTION_NAME_CONNECT',
'PERMS_ACTION_NAME_DOWNLOAD_FILE', 'PERMS_ACTION_NAME_UPLOAD_FILE',
'PERMS_ACTION_NAME_CHOICES'
]
PERMS_ACTION_NAME_ALL = 'all'
PERMS_ACTION_NAME_CONNECT = 'connect'
PERMS_ACTION_NAME_UPLOAD_FILE = 'upload_file'
PERMS_ACTION_NAME_DOWNLOAD_FILE = 'download_file'
PERMS_ACTION_NAME_CHOICES = (
(PERMS_ACTION_NAME_ALL, _('All')),
(PERMS_ACTION_NAME_CONNECT, _('Connect')),
(PERMS_ACTION_NAME_UPLOAD_FILE, _('Upload file')),
(PERMS_ACTION_NAME_DOWNLOAD_FILE, _('Download file')),
)

View File

@@ -47,10 +47,17 @@ class AssetPermissionForm(OrgModelForm):
'system_users': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('System user')}
),
'actions': forms.SelectMultiple(
attrs={'class': 'select2', 'data-placeholder': _('Action')}
)
}
labels = {
'nodes': _("Node"),
}
help_texts = {
'actions': _('Tips: The RDP protocol does not support separate '
'controls for uploading or downloading files')
}
def clean_user_groups(self):
users = self.cleaned_data.get('users')

View File

@@ -0,0 +1,33 @@
# Generated by Django 2.1.7 on 2019-04-12 07:00
from django.db import migrations, models
import uuid
def add_default_actions(apps, schema_editor):
from ..const import PERMS_ACTION_NAME_CHOICES
action_model = apps.get_model('perms', 'Action')
db_alias = schema_editor.connection.alias
for action, _ in PERMS_ACTION_NAME_CHOICES:
action_model.objects.using(db_alias).update_or_create(name=action)
class Migration(migrations.Migration):
dependencies = [
('perms', '0002_auto_20171228_0025_squashed_0009_auto_20180903_1132'),
]
operations = [
migrations.CreateModel(
name='Action',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(choices=[('all', 'All'), ('connect', 'Connect'), ('upload_file', 'Upload file'), ('download_file', 'Download file')], max_length=128, unique=True, verbose_name='Name')),
],
options={
'verbose_name': 'Action',
},
),
migrations.RunPython(add_default_actions)
]

View File

@@ -0,0 +1,31 @@
# Generated by Django 2.1.7 on 2019-04-12 09:17
from django.db import migrations, models
def set_default_action_to_existing_perms(apps, schema_editor):
from orgs.utils import set_to_root_org
from ..models import Action
set_to_root_org()
perm_model = apps.get_model('perms', 'AssetPermission')
db_alias = schema_editor.connection.alias
perms = perm_model.objects.using(db_alias).all()
default_action = Action.get_action_all()
for perm in perms:
perm.actions.add(default_action.id)
class Migration(migrations.Migration):
dependencies = [
('perms', '0003_action'),
]
operations = [
migrations.AddField(
model_name='assetpermission',
name='actions',
field=models.ManyToManyField(blank=True, related_name='permissions', to='perms.Action', verbose_name='Action'),
),
migrations.RunPython(set_default_action_to_existing_perms)
]

View File

@@ -7,6 +7,26 @@ from django.utils import timezone
from common.utils import date_expired_default, set_or_append_attr_bulk
from orgs.mixins import OrgModelMixin, OrgManager
from .const import PERMS_ACTION_NAME_CHOICES, PERMS_ACTION_NAME_ALL
class Action(models.Model):
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
name = models.CharField(
max_length=128, unique=True, choices=PERMS_ACTION_NAME_CHOICES,
verbose_name=_('Name')
)
class Meta:
verbose_name = _('Action')
def __str__(self):
return self.get_name_display()
@classmethod
def get_action_all(cls):
return cls.objects.get(name=PERMS_ACTION_NAME_ALL)
class AssetPermissionQuerySet(models.QuerySet):
def active(self):
@@ -30,6 +50,7 @@ class AssetPermission(OrgModelMixin):
assets = models.ManyToManyField('assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset"))
nodes = models.ManyToManyField('assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes"))
system_users = models.ManyToManyField('assets.SystemUser', related_name='granted_by_permissions', verbose_name=_("System user"))
actions = models.ManyToManyField('Action', related_name='permissions', blank=True, verbose_name=_('Action'))
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
date_start = models.DateTimeField(default=timezone.now, db_index=True, verbose_name=_("Date start"))
date_expired = models.DateTimeField(default=date_expired_default, db_index=True, verbose_name=_('Date expired'))

View File

@@ -4,7 +4,7 @@
from rest_framework import serializers
from common.fields import StringManyToManyField
from .models import AssetPermission
from .models import AssetPermission, Action
from assets.models import Node, Asset, SystemUser
from assets.serializers import AssetGrantedSerializer
@@ -13,9 +13,16 @@ __all__ = [
'AssetPermissionUpdateUserSerializer', 'AssetPermissionUpdateAssetSerializer',
'AssetPermissionNodeSerializer', 'GrantedNodeSerializer',
'GrantedAssetSerializer', 'GrantedSystemUserSerializer',
'ActionSerializer',
]
class ActionSerializer(serializers.ModelSerializer):
class Meta:
model = Action
fields = '__all__'
class AssetPermissionCreateUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = AssetPermission
@@ -28,6 +35,7 @@ class AssetPermissionListSerializer(serializers.ModelSerializer):
assets = StringManyToManyField(many=True, read_only=True)
nodes = StringManyToManyField(many=True, read_only=True)
system_users = StringManyToManyField(many=True, read_only=True)
actions = StringManyToManyField(many=True, read_only=True)
is_valid = serializers.BooleanField()
is_expired = serializers.BooleanField()

View File

@@ -2,15 +2,37 @@
#
from django.db.models.signals import m2m_changed, post_save, post_delete
from django.dispatch import receiver
from django.db import transaction
from common.utils import get_logger
from .utils import AssetPermissionUtil
from .models import AssetPermission
from .models import AssetPermission, Action
logger = get_logger(__file__)
def on_transaction_commit(func):
"""
如果不调用on_commit, 对象创建时添加多对多字段值失败
"""
def inner(*args, **kwargs):
transaction.on_commit(lambda: func(*args, **kwargs))
return inner
@receiver(post_save, sender=AssetPermission, dispatch_uid="my_unique_identifier")
@on_transaction_commit
def on_permission_created(sender, instance=None, created=False, **kwargs):
actions = instance.actions.all()
if created and not actions:
default_action = Action.get_action_all()
instance.actions.add(default_action)
logger.debug(
"Set default action to perms: {}".format(default_action, instance)
)
@receiver(post_save, sender=AssetPermission)
def on_permission_update(sender, **kwargs):
AssetPermissionUtil.expire_all_cache()

View File

@@ -47,6 +47,9 @@
{% bootstrap_field form.nodes layout="horizontal" %}
{% bootstrap_field form.system_users layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Action' %}</h3>
{% bootstrap_field form.actions layout="horizontal" %}
<div class="hr-line-dashed"></div>
<h3>{% trans 'Other' %}</h3>
<div class="form-group">
<label for="{{ form.is_active.id_for_label }}" class="col-sm-2 control-label">{% trans 'Active' %}</label>

View File

@@ -130,6 +130,9 @@ function format(d) {
if (d.system_users.length > 0) {
data += makeLabel(["{% trans 'System user' %}", d.system_users.join(", ")])
}
if (d.actions.length > 0) {
data += makeLabel(["{% trans 'Action' %}", d.actions.join(", ")])
}
return data
}
@@ -143,6 +146,7 @@ function initTable() {
$(td).html("<i class='fa fa-angle-right'></i>");
}},
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "perms:asset-permission-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -7,6 +7,7 @@ from .. import api
app_name = 'perms'
router = routers.DefaultRouter()
router.register('actions', api.ActionViewSet, 'action')
router.register('asset-permissions', api.AssetPermissionViewSet, 'asset-permission')
urlpatterns = [
@@ -67,6 +68,8 @@ urlpatterns = [
# 验证用户是否有某个资产和系统用户的权限
path('asset-permission/user/validate/', api.ValidateUserAssetPermissionApi.as_view(),
name='validate-user-asset-permission'),
path('asset-permission/user/actions/', api.GetUserAssetPermissionActionsApi.as_view(),
name='get-user-asset-permission-actions'),
]
urlpatterns += router.urls

View File

@@ -1,8 +1,10 @@
# coding: utf-8
from __future__ import absolute_import, unicode_literals
import uuid
from collections import defaultdict
import json
from hashlib import md5
from django.utils import timezone
from django.db.models import Q
from django.core.cache import cache
@@ -10,7 +12,7 @@ from django.conf import settings
from common.utils import get_logger
from common.tree import TreeNode
from .models import AssetPermission
from .models import AssetPermission, Action
from .hands import Node
logger = get_logger(__file__)
@@ -98,11 +100,11 @@ class AssetPermissionUtil:
"UserGroup": get_user_group_permissions,
"Asset": get_asset_permissions,
"Node": get_node_permissions,
"SystemUser": get_node_permissions,
"SystemUser": get_system_user_permissions,
}
CACHE_KEY = '_ASSET_PERM_CACHE_{}_{}'
CACHE_META_KEY = '_ASSET_PERM_META_KEY_{}'
CACHE_KEY_PREFIX = '_ASSET_PERM_CACHE_'
CACHE_META_KEY_PREFIX = '_ASSET_PERM_META_KEY_'
CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
CACHE_POLICY_MAP = (('0', 'never'), ('1', 'using'), ('2', 'refresh'))
@@ -110,11 +112,31 @@ class AssetPermissionUtil:
self.object = obj
self.obj_id = str(obj.id)
self._permissions = None
self._permissions_id = None # 标记_permission的唯一值
self._assets = None
self._filter_id = 'None' # 当通过filter更改 permission是标记
self.cache_policy = cache_policy
self.node_key = self.CACHE_KEY.format(self.obj_id, 'NODES_WITH_ASSETS')
self.asset_key = self.CACHE_KEY.format(self.obj_id, 'ASSETS')
self.system_key = self.CACHE_KEY.format(self.obj_id, 'SYSTEM_USER')
@classmethod
def is_not_using_cache(cls, cache_policy):
return cls.CACHE_TIME == 0 or cache_policy in cls.CACHE_POLICY_MAP[0]
@classmethod
def is_using_cache(cls, cache_policy):
return cls.CACHE_TIME != 0 and cache_policy in cls.CACHE_POLICY_MAP[1]
@classmethod
def is_refresh_cache(cls, cache_policy):
return cache_policy in cls.CACHE_POLICY_MAP[2]
def _is_not_using_cache(self):
return self.is_not_using_cache(self.cache_policy)
def _is_using_cache(self):
return self.is_using_cache(self.cache_policy)
def _is_refresh_cache(self):
return self.is_refresh_cache(self.cache_policy)
@property
def permissions(self):
@@ -126,8 +148,10 @@ class AssetPermissionUtil:
self._permissions = permissions
return permissions
def filter_permission_with_system_user(self, system_user):
self._permissions = self.permissions.filter(system_users=system_user)
def filter_permissions(self, **filters):
filters_json = json.dumps(filters, sort_keys=True)
self._permissions = self.permissions.filter(**filters)
self._filter_id = md5(filters_json.encode()).hexdigest()
def get_nodes_direct(self):
"""
@@ -155,6 +179,24 @@ class AssetPermissionUtil:
)
return assets
def _setattr_actions_to_system_user(self):
"""
动态给system_use设置属性actions
"""
for asset, system_users in self._assets.items():
# 获取资产和资产的祖先节点的所有授权规则
perms = get_asset_permissions(asset, include_node=True)
# 过滤当前self.permission的授权规则
perms = perms.filter(id__in=[perm.id for perm in self.permissions])
for system_user in system_users:
actions = set()
_perms = perms.filter(system_users=system_user).\
prefetch_related('actions')
for _perm in _perms:
actions.update(_perm.actions.all())
setattr(system_user, 'actions', actions)
def get_assets_without_cache(self):
if self._assets:
return self._assets
@@ -167,8 +209,28 @@ class AssetPermissionUtil:
[s for s in system_users if s.protocol == asset.protocol]
)
self._assets = assets
self._setattr_actions_to_system_user()
return self._assets
def get_cache_key(self, resource):
cache_key = self.CACHE_KEY_PREFIX + '{obj_id}_{filter_id}_{resource}'
return cache_key.format(
obj_id=self.obj_id, filter_id=self._filter_id,
resource=resource
)
@property
def node_key(self):
return self.get_cache_key('NODES_WITH_ASSETS')
@property
def asset_key(self):
return self.get_cache_key('ASSETS')
@property
def system_key(self):
return self.get_cache_key('SYSTEM_USER')
def get_assets_from_cache(self):
cached = cache.get(self.asset_key)
if not cached:
@@ -177,9 +239,9 @@ class AssetPermissionUtil:
return cached
def get_assets(self):
if self.CACHE_TIME <= 0 or self.cache_policy in self.CACHE_POLICY_MAP[1]:
if self._is_not_using_cache():
return self.get_assets_from_cache()
elif self.cache_policy in self.CACHE_POLICY_MAP[2]:
elif self._is_refresh_cache():
self.expire_cache()
return self.get_assets_from_cache()
else:
@@ -206,9 +268,9 @@ class AssetPermissionUtil:
return cached
def get_nodes_with_assets(self):
if self.CACHE_TIME <= 0 or self.cache_policy in self.CACHE_POLICY_MAP[1]:
if self._is_using_cache():
return self.get_nodes_with_assets_from_cache()
elif self.cache_policy in self.CACHE_POLICY_MAP[2]:
elif self._is_refresh_cache():
self.expire_cache()
return self.get_nodes_with_assets_from_cache()
else:
@@ -229,21 +291,28 @@ class AssetPermissionUtil:
return cached
def get_system_users(self):
if self.CACHE_TIME <= 0 or self.cache_policy in self.CACHE_POLICY_MAP[1]:
if self._is_using_cache():
return self.get_system_user_from_cache()
elif self.cache_policy in self.CACHE_POLICY_MAP[2]:
elif self._is_refresh_cache():
self.expire_cache()
return self.get_system_user_from_cache()
else:
return self.get_system_user_without_cache()
def get_meta_cache_key(self):
cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_{filter_id}'
key = cache_key.format(
obj_id=str(self.object.id), filter_id=self._filter_id
)
return key
@property
def cache_meta(self):
key = self.CACHE_META_KEY.format(str(self.object.id))
key = self.get_meta_cache_key()
return cache.get(key) or {}
def set_cache_meta(self):
key = self.CACHE_META_KEY.format(str(self.object.id))
def set_meta_to_cache(self):
key = self.get_meta_cache_key()
meta = {
'id': str(uuid.uuid4()),
'datetime': timezone.now(),
@@ -252,8 +321,9 @@ class AssetPermissionUtil:
cache.set(key, meta, self.CACHE_TIME)
def expire_cache_meta(self):
key = self.CACHE_META_KEY.format(str(self.object.id))
cache.delete(key)
cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_*'
key = cache_key.format(obj_id=str(self.object.id))
cache.delete_pattern(key)
def update_cache(self):
assets = self.get_assets_without_cache()
@@ -262,7 +332,7 @@ class AssetPermissionUtil:
cache.set(self.asset_key, assets, self.CACHE_TIME)
cache.set(self.node_key, nodes, self.CACHE_TIME)
cache.set(self.system_key, system_users, self.CACHE_TIME)
self.set_cache_meta()
self.set_meta_to_cache()
def expire_cache(self):
"""
@@ -270,17 +340,19 @@ class AssetPermissionUtil:
缓存,以免造成不统一的情况
:return:
"""
key = self.CACHE_KEY.format(str(self.object.id), '*')
cache_key = self.CACHE_KEY_PREFIX + '{obj_id}_*'
key = cache_key.format(obj_id='*')
cache.delete_pattern(key)
self.expire_cache_meta()
def expire_all_cache_meta(self):
key = self.CACHE_META_KEY.format('*')
@classmethod
def expire_all_cache_meta(cls):
key = cls.CACHE_META_KEY_PREFIX + '*'
cache.delete_pattern(key)
@classmethod
def expire_all_cache(cls):
key = cls.CACHE_KEY.format('*', '*')
key = cls.CACHE_KEY_PREFIX + '*'
cache.delete_pattern(key)
@@ -341,6 +413,7 @@ def parse_asset_to_tree_node(node, asset, system_users):
'protocol': system_user.protocol,
'priority': system_user.priority,
'login_mode': system_user.login_mode,
'actions': [action.name for action in system_user.actions],
'comment': system_user.comment,
})
data = {
@@ -369,3 +442,21 @@ def parse_asset_to_tree_node(node, asset, system_users):
}
tree_node = TreeNode(**data)
return tree_node
#
# actions
#
def check_system_user_action(system_user, action):
"""
:param system_user: SystemUser object (包含动态属性: actions)
:param action: Action object
:return: bool
"""
check_actions = [Action.get_action_all(), action]
granted_actions = getattr(system_user, 'actions', [])
actions = list(set(granted_actions).intersection(set(check_actions)))
return bool(actions)

View File

@@ -11,8 +11,9 @@ from django.conf import settings
from common.permissions import AdminUserRequiredMixin
from orgs.utils import current_org
from .hands import Node, Asset, SystemUser, User, UserGroup
from .models import AssetPermission
from .models import AssetPermission, Action
from .forms import AssetPermissionForm
from .const import PERMS_ACTION_NAME_ALL
class AssetPermissionListView(AdminUserRequiredMixin, TemplateView):
@@ -46,6 +47,8 @@ class AssetPermissionCreateView(AdminUserRequiredMixin, CreateView):
assets_id = assets_id.split(",")
assets = Asset.objects.filter(id__in=assets_id)
form['assets'].initial = assets
form['actions'].initial = Action.objects.get(name=PERMS_ACTION_NAME_ALL)
return form
def get_context_data(self, **kwargs):

View File

@@ -5,18 +5,21 @@ import os
import json
import jms_storage
from ldap3 import Server, Connection
from rest_framework.views import Response, APIView
from django.conf import settings
from django.core.mail import send_mail
from django.utils.translation import ugettext_lazy as _
from .models import Setting
from .utils import get_ldap_users_list, save_user
from .utils import LDAPUtil
from common.permissions import IsOrgAdmin, IsSuperUser
from common.utils import get_logger
from .serializers import MailTestSerializer, LDAPTestSerializer
logger = get_logger(__file__)
class MailTestingAPI(APIView):
permission_classes = (IsOrgAdmin,)
serializer_class = MailTestSerializer
@@ -46,78 +49,78 @@ class LDAPTestingAPI(APIView):
serializer_class = LDAPTestSerializer
success_message = _("Test ldap success")
@staticmethod
def get_ldap_util(serializer):
host = serializer.validated_data["AUTH_LDAP_SERVER_URI"]
bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"]
password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"]
use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False)
search_ougroup = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
try:
attr_map = json.loads(attr_map)
except json.JSONDecodeError:
return Response({"error": "AUTH_LDAP_USER_ATTR_MAP not valid"}, status=401)
util = LDAPUtil(
use_settings_config=False, server_uri=host, bind_dn=bind_dn,
password=password, use_ssl=use_ssl,
search_ougroup=search_ougroup, search_filter=search_filter,
attr_map=attr_map
)
return util
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
host = serializer.validated_data["AUTH_LDAP_SERVER_URI"]
bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"]
password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"]
use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False)
search_ougroup = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
try:
attr_map = json.loads(attr_map)
except json.JSONDecodeError:
return Response({"error": "AUTH_LDAP_USER_ATTR_MAP not valid"}, status=401)
server = Server(host, use_ssl=use_ssl)
conn = Connection(server, bind_dn, password)
try:
conn.bind()
except Exception as e:
return Response({"error": str(e)}, status=401)
users = []
for search_ou in str(search_ougroup).split("|"):
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
return Response({"error": _("Search no entry matched in ou {}").format(search_ou)}, status=401)
for entry in conn.entries:
user = {}
for attr, mapping in attr_map.items():
if hasattr(entry, mapping):
user[attr] = getattr(entry, mapping)
users.append(user)
if len(users) > 0:
return Response({"msg": _("Match {} s users").format(len(users))})
else:
return Response({"error": "Have user but attr mapping error"}, status=401)
else:
if not serializer.is_valid():
return Response({"error": str(serializer.errors)}, status=401)
util = self.get_ldap_util(serializer)
class LDAPSyncAPI(APIView):
try:
users = util.get_search_user_items()
except Exception as e:
return Response({"error": str(e)}, status=401)
if len(users) > 0:
return Response({"msg": _("Match {} s users").format(len(users))})
else:
return Response({"error": "Have user but attr mapping error"}, status=401)
class LDAPUserListApi(APIView):
permission_classes = (IsOrgAdmin,)
def get(self, request):
ldap_users_list = get_ldap_users_list()
if not isinstance(ldap_users_list, list):
return Response(ldap_users_list, status=401)
return Response(ldap_users_list)
util = LDAPUtil()
try:
users = util.get_search_user_items()
except Exception as e:
users = []
logger.error(e, exc_info=True)
else:
users = sorted(users, key=lambda u: (u['existing'], u['username']))
return Response(users)
class LDAPConfirmSyncAPI(APIView):
class LDAPUserSyncAPI(APIView):
permission_classes = (IsOrgAdmin,)
def post(self, request):
user_names = request.data.get('user_names', '')
if not user_names:
error = _('User is not currently selected, please check the user '
'you want to import')
return Response({'error': error}, status=401)
ldap_users_list = get_ldap_users_list(user_names=user_names)
if not isinstance(ldap_users_list, list):
return Response(ldap_users_list, status=401)
save_result = save_user(ldap_users_list)
if 'error' in save_result.keys():
return Response(save_result, status=401)
return Response(save_result)
util = LDAPUtil()
try:
result = util.sync_users(username_set=user_names)
except Exception as e:
logger.error(e, exc_info=True)
return Response({'error': str(e)}, status=401)
else:
msg = _("succeed: {} failed: {} total: {}").format(
result['succeed'], result['failed'], result['total']
)
return Response({'msg': msg})
class ReplayStorageCreateAPI(APIView):

View File

@@ -79,6 +79,8 @@ class Setting(models.Model):
obj.cleaned_value = data
else:
value = obj.cleaned_value
if value is None:
value = {}
value.update(data)
obj.cleaned_value = value
obj.save()

View File

@@ -4,7 +4,10 @@
{% block modal_class %}modal-lg{% endblock %}
{% block modal_id %}ldap_list_users_modal{% endblock %}
{% block modal_title%}{% trans "Ldap users" %}{% endblock %}
{% block modal_title%}{% trans "LDAP user list" %}{% endblock %}
{% block modal_help_message%} <div class="alert alert-info help-message" style="width: 838px; margin-left: 30px">{% trans 'Please submit the LDAP configuration before import' %}</div>{% endblock %}
{% block modal_body %}
<link href="{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}" rel="stylesheet">
<script type="text/javascript" src="{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"></script>
@@ -34,7 +37,7 @@
<th class="text-center">{% trans 'Username' %}</th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Email' %}</th>
<th class="text-center">{% trans 'Is imported' %}</th>
<th class="text-center">{% trans 'Existing' %}</th>
</tr>
</thead>
<tbody>
@@ -47,16 +50,25 @@
<script>
var ldap_users_table = 0;
function initLdapTable() {
function initLdapUsersTable() {
if(ldap_users_table){
return
}
var options = {
ele: $('#ldap_list_users_table'),
ajax_url: '{% url "api-settings:ldap-sync" %}',
ajax_url: '{% url "api-settings:ldap-user-list" %}',
columnDefs: [
{targets: 4, createdCell: function (td, cellData, rowData) {
if(cellData){
$(td).html('<i class="fa fa-check text-navy"></i>')
}else{
$(td).html('<i class="fa fa-times text-danger"></i>')
}
}}
],
columns: [
{data: "username" },{data: "username" }, {data: "name" },
{data:"email"}, {data:'is_imported'}
{data:"email"}, {data:'existing'}
],
pageLength: 10
};
@@ -68,8 +80,7 @@ function initLdapTable() {
$(document).ready(function(){
}).on('show.bs.modal', function () {
initLdapTable();
initLdapUsersTable();
})
.on('click','.close_btn1',function () {
window.location.reload()
@@ -82,9 +93,9 @@ $(document).ready(function(){
{% endblock %}
{% block modal_button %}
{{ block.super }}
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
<button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}btn_ldap_modal_confirm{% endblock %}">{% trans 'Import' %}</button>
{% endblock %}
{% block modal_confirm_id %}btn_ldap_modal_confirm{% endblock %}

View File

@@ -58,11 +58,11 @@
<div class="hr-line-dashed"></div>
<div class="form-group">
<div class="col-sm-4 col-sm-offset-2">
<button class="btn btn-default btn-test" type="button"> {% trans 'Test connection' %}</button>
<button class="btn btn-default" type="reset"> {% trans 'Reset' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
<button class="btn btn-default btn-test" type="button"> {% trans 'Test connection' %}</button>
{# <button class="btn btn-primary sync_button " data-toggle="modal" data-target="#sync_users_modal" type="button">{% trans 'Synchronization' %}</button>#}
<button class="btn btn-primary sync_button " data-toggle="modal" data-target="#ldap_list_users_modal" type="button">{% trans 'Sync User' %}</button>
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
<button class="btn btn-default sync_button " data-toggle="modal" data-target="#ldap_list_users_modal" type="button">{% trans 'Bulk import' %}</button>
</div>
</div>
</form>
@@ -108,11 +108,17 @@ $(document).ready(function () {
})
.on("click","#btn_ldap_modal_confirm",function () {
var user_names=[];
var cheked = $("tbody input[type='checkbox']:checked").each(function () {
$("tbody input[type='checkbox']:checked").each(function () {
user_names.push($(this).attr('id'));
});
var the_url = "{% url "api-settings:ldap-comfirm-sync" %}";
if (user_names.length === 0){
var msg = "{% trans 'User is not currently selected, please check the user you want to import'%}"
toastr.error(msg);
return
}
var the_url = "{% url "api-settings:ldap-user-sync" %}";
function error(message) {
toastr.error(message)

View File

@@ -108,6 +108,7 @@
<label class="col-md-2 control-label" for="id_endpoint">{% trans "Endpoint" %}</label>
<div class="col-md-9">
<input id="id_endpoint" class="form-control" type="text" name="ENDPOINT" value="" placeholder="Endpoint">
<div id="endpoint_error" style="color: red;"></div>
<div class="help-block">
<span class="oss">
{% trans 'OSS: http://{REGION_NAME}.aliyuncs.com' %}
@@ -251,6 +252,13 @@ $(document).ready(function() {
var name = $(id_field).attr('name');
data[name] = $(id_field).val();
});
if (data['ENDPOINT'] && data['ENDPOINT'].indexOf('http') === -1) {
var msg = "{% trans 'Endpoint need contain protocol, ex: http' %}";
$("#endpoint_error").html(msg);
submitBtn.removeClass('disabled');
submitBtn.html(origin_text);
return
}
var url = "{% url 'api-settings:replay-storage-create' %}";
var success = function(data, textStatus) {
location = "{% url 'settings:terminal-setting' %}";

View File

@@ -9,8 +9,8 @@ app_name = 'common'
urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
path('ldap/testing/', api.LDAPTestingAPI.as_view(), name='ldap-testing'),
path('ldap/sync/', api.LDAPSyncAPI.as_view(), name='ldap-sync'),
path('ldap/comfirm/sync/', api.LDAPConfirmSyncAPI.as_view(), name='ldap-comfirm-sync'),
path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'),
path('ldap/users/sync/', api.LDAPUserSyncAPI.as_view(), name='ldap-user-sync'),
path('terminal/replay-storage/create/', api.ReplayStorageCreateAPI.as_view(), name='replay-storage-create'),
path('terminal/replay-storage/delete/', api.ReplayStorageDeleteAPI.as_view(), name='replay-storage-delete'),
path('terminal/command-storage/create/', api.CommandStorageCreateAPI.as_view(), name='command-storage-create'),

View File

@@ -4,151 +4,159 @@
from ldap3 import Server, Connection
from django.utils.translation import ugettext_lazy as _
from .models import settings
from users.models import User
from common.utils import get_logger
from .models import settings
def ldap_conn(host, use_ssl, bind_dn, password):
server = Server(host, use_ssl=use_ssl)
conn = Connection(server, bind_dn, password)
return conn
logger = get_logger(__file__)
def ldap_search(conn, search_ougroup, search_filter, attr_map, user_names=None):
users_list = []
for search_ou in str(search_ougroup).split("|"):
ok = conn.search(search_ou, search_filter % ({"user": "*"}),
attributes=list(attr_map.values()))
if not ok:
error = _("Search no entry matched in ou {}").format(search_ou)
return {"error": error}
ldap_map_users(conn, attr_map, users_list, user_names)
if len(users_list) > 0:
return users_list
return {"error": _("Have user but attr mapping error")}
class LDAPOUGroupException(Exception):
pass
def get_ldap_users_list(user_names=None):
ldap_setting = get_ldap_setting()
conn = ldap_conn(ldap_setting['host'], ldap_setting['use_ssl'],
ldap_setting['bind_dn'], ldap_setting['password'])
try:
conn.bind()
except Exception as e:
return {"error": str(e)}
class LDAPUtil:
result_search = ldap_search(conn, ldap_setting['search_ougroup'],
ldap_setting['search_filter'],
ldap_setting['attr_map'], user_names=user_names)
return result_search
def __init__(self, use_settings_config=True, server_uri=None, bind_dn=None,
password=None, use_ssl=None, search_ougroup=None,
search_filter=None, attr_map=None, auth_ldap=None):
def ldap_map_users(conn, attr_map, users, user_names=None):
for entry in conn.entries:
user = entry_user(entry, attr_map)
if user_names:
if user.get('username', '') in user_names:
users.append(user)
# config
if use_settings_config:
self._load_config_from_settings()
else:
users.append(user)
self.server_uri = server_uri
self.bind_dn = bind_dn
self.password = password
self.use_ssl = use_ssl
self.search_ougroup = search_ougroup
self.search_filter = search_filter
self.attr_map = attr_map
self.auth_ldap = auth_ldap
def _load_config_from_settings(self):
self.server_uri = settings.AUTH_LDAP_SERVER_URI
self.bind_dn = settings.AUTH_LDAP_BIND_DN
self.password = settings.AUTH_LDAP_BIND_PASSWORD
self.use_ssl = settings.AUTH_LDAP_START_TLS
self.search_ougroup = settings.AUTH_LDAP_SEARCH_OU
self.search_filter = settings.AUTH_LDAP_SEARCH_FILTER
self.attr_map = settings.AUTH_LDAP_USER_ATTR_MAP
self.auth_ldap = settings.AUTH_LDAP
def entry_user(entry, attr_map):
user = {}
user['is_imported'] = _('No')
for attr, mapping in attr_map.items():
if not hasattr(entry, mapping):
continue
value = getattr(entry, mapping).value
user[attr] = value if value else ''
if attr != 'username':
continue
if User.objects.filter(username=user[attr]):
user['is_imported'] = _('Yes')
return user
@staticmethod
def get_user_by_username(username):
try:
user = User.objects.get(username=username)
except Exception as e:
logger.info(e)
return None
else:
return user
def get_ldap_setting():
host = settings.AUTH_LDAP_SERVER_URI
bind_dn = settings.AUTH_LDAP_BIND_DN
password = settings.AUTH_LDAP_BIND_PASSWORD
use_ssl = settings.AUTH_LDAP_START_TLS
search_ougroup = settings.AUTH_LDAP_SEARCH_OU
search_filter = settings.AUTH_LDAP_SEARCH_FILTER
attr_map = settings.AUTH_LDAP_USER_ATTR_MAP
auth_ldap = settings.AUTH_LDAP
ldap_setting = {
'host': host, 'bind_dn': bind_dn, 'password': password,
'search_ougroup': search_ougroup, 'search_filter': search_filter,
'attr_map': attr_map, 'auth_ldap': auth_ldap, 'use_ssl': use_ssl,
}
return ldap_setting
def save_user(users):
exist = []
username_list = [item.get('username') for item in users]
for name in username_list:
if User.objects.filter(username=name).exclude(source='ldap'):
exist.append(name)
users = [user for user in users if (user.get('username') not in exist)]
result_save = save(users, exist)
return result_save
def save(users, exist):
fail_user = []
for item in users:
item = set_default_item(item)
user = User.objects.filter(username=item['username'], source='ldap')
user = user.first()
if not user:
try:
user = User.objects.create(**item)
except Exception as e:
fail_user.append(item.get('username'))
@staticmethod
def _update_user(user, user_item):
for field, value in user_item.items():
if not hasattr(user, field):
continue
for key, value in item.items():
user.key = value
user.save()
setattr(user, field, value)
user.save()
get_msg = get_messages(users, exist, fail_user)
return get_msg
def update_user(self, user_item):
user = self.get_user_by_username(user_item['username'])
if not user:
msg = _('User does not exist')
return False, msg
if user.source != User.SOURCE_LDAP:
msg = _('The user source is not LDAP')
return False, msg
def set_default_item(item):
item['source'] = 'ldap'
if not item.get('email', ''):
if '@' in item['username']:
item['email'] = item['username']
try:
self._update_user(user, user_item)
except Exception as e:
logger.error(e, exc_info=True)
return False, str(e)
else:
item['email'] = item['username'] + '@' + settings.EMAIL_SUFFIX
if 'is_imported' in item.keys():
item.pop('is_imported')
return item
return True, None
@staticmethod
def create_user(user_item):
user_item['source'] = User.SOURCE_LDAP
try:
User.objects.create(**user_item)
except Exception as e:
logger.error(e, exc_info=True)
return False, str(e)
else:
return True, None
def get_messages(users, exist, fail_user):
if exist:
info = _("Import {} users successfully; import {} users failed, the "
"database already exists with the same name")
msg = info.format(len(users), str(exist))
@staticmethod
def get_or_construct_email(user_item):
if not user_item.get('email', None):
if '@' in user_item['username']:
email = user_item['username']
else:
email = '{}@{}'.format(
user_item['username'], settings.EMAIL_SUFFIX)
else:
email = user_item['email']
return email
if fail_user:
info = _("Import {} users successfully; import {} users failed, "
"the database already exists with the same name; import {}"
"users failed, BecauseTypeError' object has no attribute "
"'keys'")
msg = info.format(len(users)-len(fail_user), str(exist), str(fail_user))
else:
msg = _("Import {} users successfully").format(len(users))
def create_or_update_users(self, user_items, force_update=True):
succeed = failed = 0
for user_item in user_items:
user_item['email'] = self.get_or_construct_email(user_item)
exist = user_item.pop('existing', None)
if exist:
ok, error = self.update_user(user_item)
else:
ok, error = self.create_user(user_item)
if not ok:
failed += 1
else:
succeed += 1
result = {'total': len(user_items), 'succeed': succeed, 'failed': failed}
return result
if fail_user:
info = _("Import {} users successfully;import {} users failed, "
"BecauseTypeError' object has no attribute 'keys'")
msg = info.format(len(users)-len(fail_user), str(fail_user))
return {'msg': msg}
def _ldap_entry_to_user_item(self, entry):
user_item = {}
for attr, mapping in self.attr_map.items():
if not hasattr(entry, mapping):
continue
user_item[attr] = getattr(entry, mapping).value or ''
return user_item
def get_connection(self):
server = Server(self.server_uri, use_ssl=self.use_ssl)
conn = Connection(server, self.bind_dn, self.password)
conn.bind()
return conn
def get_search_user_items(self):
conn = self.get_connection()
user_items = []
search_ougroup = str(self.search_ougroup).split("|")
for search_ou in search_ougroup:
ok = conn.search(
search_ou, self.search_filter % ({"user": "*"}),
attributes=list(self.attr_map.values())
)
if not ok:
error = _("Search no entry matched in ou {}".format(search_ou))
raise LDAPOUGroupException(error)
for entry in conn.entries:
user_item = self._ldap_entry_to_user_item(entry)
user = self.get_user_by_username(user_item['username'])
user_item['existing'] = bool(user)
user_items.append(user_item)
return user_items
def sync_users(self, username_set):
user_items = self.get_search_user_items()
if username_set:
user_items = [u for u in user_items if u['username'] in username_set]
result = self.create_or_update_users(user_items)
return result

View File

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -538,7 +538,11 @@ jumpserver.initServerSideDataTable = function (options) {
$(td).html('<input type="checkbox" class="text-center ipt_check" id=99991937>'.replace('99991937', cellData));
}
},
{className: 'text-center', targets: '_all'}
{
targets: '_all',
className: 'text-center',
render: $.fn.dataTable.render.text()
}
];
columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs;
var select = {
@@ -945,4 +949,11 @@ function rootNodeAddDom(ztree, callback) {
ztree.destroy();
callback()
})
}
function htmlEscape ( d ) {
return typeof d === 'string' ?
d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') :
d;
}

View File

@@ -2,10 +2,8 @@
{% load static %}
{% load i18n %}
{% block custom_head_css_js %}
<link href="{% static "css/plugins/datatables/datatables.min.css" %}" rel="stylesheet">
<link href="{% static 'css/plugins/select2/select2.min.css' %}" rel="stylesheet">
<script src="{% static 'js/plugins/select2/select2.full.min.js' %}"></script>
<script src="{% static "js/plugins/datatables/datatables.min.js" %}"></script>
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">

View File

@@ -1,2 +1,2 @@
{% load i18n %}
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} &copy; 2014-2019
<strong>Copyright</strong> {{ COPYRIGHT }}

View File

@@ -1,10 +1,10 @@
{% load i18n %}
<div class="footer fixed">
<div class="pull-right">
Version <strong>1.4.8-{% include '_build.html' %}</strong> GPLv2.
Version <strong>{{ VERSION }}-{% include '_build.html' %}</strong> GPLv2.
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div>
<div>
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %}&copy; 2014-2019
{% include '_copyright.html' %}
</div>
</div>

View File

@@ -47,7 +47,8 @@
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} help-message" >
{{ message|safe }}
{# {{ message|safe }}#}
{{ message }}
<button aria-hidden="true" data-dismiss="alert" class="close" type="button" style="outline: none;">×</button>
</div>

View File

@@ -12,6 +12,7 @@
<h4 class="modal-title">{% block modal_title %}{% endblock %}</h4>
<small>{% block modal_comment %}{% endblock %}</small>
</div>
{% block modal_help_message %}{% endblock %}
<div class="modal-body">
{% block modal_body %}
{% endblock %}

View File

@@ -3,15 +3,11 @@
<li class="nav-header">
<div class="profile-element" style="height: 65px">
<div href="http://www.jumpserver.org" target="_blank" style="width: 100%; background-image: url({% static 'img/header-profile.png' %})">
{% if interface and interface.logo_index %}
<img alt="logo" height="55" width="185" src="{{ MEDIA_URL }}{{ interface.logo_index }}"/>
{% else %}
<img alt="logo" height="55" width="185" src="{% static 'img/logo-text.png' %}"/>
{% endif %}
<img alt="logo" height="55" width="185" style="margin-right: 5px" src="{{ LOGO_TEXT_URL }}"/>
</div>
</div>
<div class="logo-element">
<img alt="image" height="40" src="/static/img/logo.png"/>
<img alt="image" height="40" src="{{ LOGO_URL }}"/>
</div>
{% if ADMIN_ORGS and request.COOKIES.IN_ADMIN_PAGE != 'No' %}
{% if ADMIN_ORGS|length > 1 or not CURRENT_ORG.is_default %}

View File

@@ -5,18 +5,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="renderer" content="webkit">
<title>
{% if interface and interface.login_title %}
{{ interface.login_title }}
{% else %}
Jumpserver
{% endif %}
</title>
{% if interface and interface.favicon %}
<link rel="shortcut icon" href="{{ MEDIA_URL }}{{ interface.favicon }}" type="image/x-icon">
{% else %}
<link rel="shortcut icon" href="{% static 'img/facio.ico' %}" type="image/x-icon">
{% endif %}
<title>{{ JMS_TITLE }}</title>
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static 'css/jumpserver.css' %}" rel="stylesheet">
{% block custom_head_css_js %} {% endblock %}

View File

@@ -22,18 +22,9 @@
<div class="col-md-12">
<div class="ibox-content">
<div>
{% if interface and interface.logo_logout %}
<img src="{{ MEDIA_URL }}{{ interface.logo_logout }}" style="margin: auto" width="82" height="82">
{% else %}
<img src="{% static 'img/logo.png' %}" style="margin: auto" width="82" height="82">
{% endif %}
<img src="{{ LOGO_URL }}" style="margin: auto" width="82" height="82">
<h2 style="display: inline">
{% if interface and interface.login_title %}
{{ interface.login_title }}
{% else %}
{% trans 'Welcome to the Jumpserver open source fortress' %}
{% endif %}
{{ JMS_TITLE }}
</h2>
</div>
{% if errors %}

View File

@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
from ..models import Terminal, Status, Session, Task
@@ -29,7 +29,7 @@ class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class Meta:
model = Session
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
@@ -44,7 +44,7 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = Task
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
class ReplaySerializer(serializers.Serializer):

View File

@@ -50,6 +50,7 @@ function initTable() {
buttons: [],
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "terminal:terminal-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},

View File

@@ -5,6 +5,7 @@ from django.core.cache import cache
from django.contrib.auth import logout
from django.utils.translation import ugettext as _
from rest_framework import status
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
@@ -52,9 +53,72 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet):
self.permission_classes = (IsOrgAdminOrAppUser,)
return super().get_permissions()
def _deny_permission(self, instance):
"""
check current user has permission to handle instance
(update, destroy, bulk_update, bulk destroy)
"""
return not self.request.user.is_superuser and instance.is_superuser
def destroy(self, request, *args, **kwargs):
"""
rewrite because limit org_admin destroy superuser
"""
instance = self.get_object()
if self._deny_permission(instance):
data = {'msg': _("You do not have permission.")}
return Response(data=data, status=status.HTTP_403_FORBIDDEN)
return super().destroy(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
"""
rewrite because limit org_admin update superuser
"""
instance = self.get_object()
if self._deny_permission(instance):
data = {'msg': _("You do not have permission.")}
return Response(data=data, status=status.HTTP_403_FORBIDDEN)
return super().update(request, *args, **kwargs)
def _bulk_deny_permission(self, instances):
deny_instances = [i for i in instances if self._deny_permission(i)]
if len(deny_instances) > 0:
return True
else:
return False
def allow_bulk_destroy(self, qs, filtered):
if self._bulk_deny_permission(filtered):
return False
return qs.count() != filtered.count()
def bulk_update(self, request, *args, **kwargs):
"""
rewrite because limit org_admin update superuser
"""
partial = kwargs.pop('partial', False)
# restrict the update to the filtered queryset
queryset = self.filter_queryset(self.get_queryset())
if self._bulk_deny_permission(queryset):
data = {'msg': _("You do not have permission.")}
return Response(data=data, status=status.HTTP_403_FORBIDDEN)
serializer = self.get_serializer(
queryset, data=request.data, many=True, partial=partial,
)
try:
serializer.is_valid(raise_exception=True)
except Exception as e:
data = {'error': str(e)}
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
self.perform_bulk_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
permission_classes = (IsOrgAdmin,)

View File

@@ -3,24 +3,28 @@
#
import uuid
import base64
import string
import random
from collections import OrderedDict
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.contrib.auth.models import AbstractUser
from django.core import signing
from django.core.cache import cache
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.shortcuts import reverse
from common.utils import get_signer, date_expired_default
from common.utils import get_signer, date_expired_default, get_logger
__all__ = ['User']
signer = get_signer()
logger = get_logger(__file__)
class User(AbstractUser):
ROLE_ADMIN = 'Admin'
@@ -47,6 +51,9 @@ class User(AbstractUser):
(SOURCE_OPENID, 'OpenID'),
(SOURCE_RADIUS, 'Radius'),
)
CACHE_KEY_USER_RESET_PASSWORD_PREFIX = "_KEY_USER_RESET_PASSWORD_{}"
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
username = models.CharField(
max_length=128, unique=True, verbose_name=_('Username')
@@ -346,9 +353,32 @@ class User(AbstractUser):
return user_default
def generate_reset_token(self):
return signer.sign_t(
{'reset': str(self.id), 'email': self.email}, expires_in=3600
)
letter = string.ascii_letters + string.digits
token =''.join([random.choice(letter) for _ in range(50)])
self.set_cache(token)
return token
def set_cache(self, token):
key = self.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
cache.set(key, {'id': self.id, 'email': self.email}, 3600)
@classmethod
def validate_reset_password_token(cls, token):
try:
key = cls.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
value = cache.get(key)
user_id = value.get('id', '')
email = value.get('email', '')
user = cls.objects.get(id=user_id, email=email)
except (AttributeError, cls.DoesNotExist) as e:
logger.error(e, exc_info=True)
user = None
return user
@classmethod
def expired_reset_password_token(cls, token):
key = cls.CACHE_KEY_USER_RESET_PASSWORD_PREFIX.format(token)
cache.delete(key)
@property
def otp_enabled(self):
@@ -400,18 +430,6 @@ class User(AbstractUser):
access_key = app.create_access_key()
return app, access_key
@classmethod
def validate_reset_token(cls, token):
try:
data = signer.unsign_t(token)
user_id = data.get('reset', None)
user_email = data.get('email', '')
user = cls.objects.get(id=user_id, email=user_email)
except (signing.BadSignature, cls.DoesNotExist):
user = None
return user
def reset_password(self, new_password):
self.set_password(new_password)
self.date_password_last_updated = timezone.now()

View File

@@ -3,10 +3,10 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework_bulk import BulkListSerializer
from common.utils import get_signer, validate_ssh_public_key
from common.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer
from ..models import User, UserGroup
signer = get_signer()
@@ -16,7 +16,7 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class Meta:
model = User
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = [
'id', 'name', 'username', 'email', 'groups', 'groups_display',
'role', 'role_display', 'avatar_url', 'wechat', 'phone',
@@ -52,7 +52,7 @@ class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class Meta:
model = UserGroup
list_serializer_class = BulkListSerializer
list_serializer_class = AdaptedBulkListSerializer
fields = '__all__'
read_only_fields = ['created_by']

View File

@@ -5,8 +5,8 @@
<html>
<head>
<meta charset="UTF-8">
<title> Jumpserver </title>
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<title> {{ JMS_TITLE }} </title>
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<link rel="stylesheet" href="{% static 'fonts/font_otp/iconfont.css' %}" />
<link rel="stylesheet" href="{% static 'css/otp.css' %}" />
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
@@ -19,9 +19,9 @@
<header>
<div class="logo">
<a href="{% url 'index' %}">
<img src="{% static 'img/logo.png' %}" alt="" width="50px" height="50px"/>
<img src="{{ LOGO_URL }}" alt="" width="50px" height="50px"/>
</a>
<a href="{% url 'index' %}">Jumpserver</a>
<a href="{% url 'index' %}">{{ JMS_TITLE }}</a>
</div>
<div>
<a href="{% url 'index' %}">{% trans 'Home page' %}</a>

View File

@@ -4,10 +4,9 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
<title>{% trans 'Forgot password' %}</title>
{% include '_head_css_js.html' %}
@@ -22,14 +21,8 @@
<div class="col-md-12">
<div class="ibox-content">
{% if interface.logout_logo %}
<img src="{{ MEDIA_URL }}{{ interface.logout_logo }}" style="margin: auto" width="82" height="82">
{% else %}
<img src="{% static 'img/logo.png' %}" style="margin: auto" width="82" height="82">
{% endif %}
<img src="{{ LOGO_URL }}" style="margin: auto" width="82" height="82">
<h2 class="font-bold" style="display: inline">{% trans 'Forgot password' %} ?</h2>
<h1></h1>
{% if errors %}
<p class="red-fonts">{{ errors }}</p>

View File

@@ -6,8 +6,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Jumpserver </title>
<link rel="shortcut icon" href="{% static "img/facio.ico" %}" type="image/x-icon">
<title> {{ JMS_TITLE }} </title>
<link rel="shortcut icon" href="{{ FAVICON_URL }}" type="image/x-icon">
{% include '_head_css_js.html' %}
<link href="{% static "css/jumpserver.css" %}" rel="stylesheet">
<script type="text/javascript" src="{% url 'javascript-catalog' %}"></script>
@@ -43,7 +43,7 @@
</div>
<div class="col-md-6">
<div class="ibox-content">
<div><img src="{% static 'img/logo.png' %}" width="82" height="82"> <span class="font-bold text-center" style="font-size: 32px; font-family: inherit">{% trans 'Reset password' %}</span></div>
<div><img src="{{ LOGO_URL }}" width="82" height="82"> <span class="font-bold text-center" style="font-size: 32px; font-family: inherit">{% trans 'Reset password' %}</span></div>
<form class="m-t" role="form" method="post" action="">
{% csrf_token %}
{% if errors %}

View File

@@ -22,11 +22,11 @@
<a href="{% url 'users:user-granted-asset' pk=user_object.id %}" class="text-center"><i class="fa fa-cubes"></i> {% trans 'Asset granted' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline btn-default" href="{% url 'users:user-update' pk=user_object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
<a class="btn btn-outline {% if user_object.is_superuser and not request.user.is_superuser %} disabled {% else %} btn-default {% endif %}" href="{% url 'users:user-update' pk=user_object.id %}"><i class="fa fa-edit"></i>{% trans 'Update' %}</a>
</li>
<li class="pull-right">
<a class="btn btn-outline {% if request.user != user_object and user_object.username != "admin" %} btn-danger btn-delete-user {% else %} disabled {% endif %}">
<a class="btn btn-outline {% if request.user == user_object or user_object.username == "admin" or user_object.is_superuser and not request.user.is_superuser %} disabled {% else %} btn-danger btn-delete-user {% endif %}">
<i class="fa fa-trash-o"></i>{% trans 'Delete' %}
</a>
</li>

View File

@@ -77,6 +77,7 @@ function initTable() {
ele: $('#user_assets_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
@@ -91,7 +92,8 @@ function initTable() {
{targets: 4, createdCell: function (td, cellData) {
var users = [];
$.each(cellData, function (id, data) {
users.push(data.name);
var name = htmlEscape(data.name);
users.push(name);
});
$(td).html(users.join(', '))
}}

View File

@@ -77,6 +77,7 @@ function initTable() {
ele: $('#user_assets_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
{% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %}
var detail_btn = '<a href="{{ the_url }}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
@@ -91,7 +92,8 @@ function initTable() {
{targets: 4, createdCell: function (td, cellData) {
var users = [];
$.each(cellData, function (id, data) {
users.push(data.name);
var name = htmlEscape(data.name);
users.push(name);
});
$(td).html(users.join(', '))
}}

View File

@@ -28,6 +28,7 @@ $(document).ready(function() {
buttons: [],
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "users:user-group-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id));
}},
@@ -36,6 +37,7 @@ $(document).ready(function() {
$(td).html(html);
}},
{targets: 3, createdCell: function (td, cellData) {
cellData = htmlEscape(cellData);
var innerHtml = cellData.length > 30 ? cellData.substring(0, 30) + '...': cellData;
$(td).html('<span href="javascript:void(0);" data-toggle="tooltip" title="' + cellData + '">' + innerHtml + '</span>');
}},

View File

@@ -59,6 +59,7 @@ function initTable() {
ele: $('#user_list_table'),
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detail_btn = '<a href="{% url "users:user-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detail_btn.replace("{{ DEFAULT_PK }}", rowData.id));
}},
@@ -77,10 +78,16 @@ function initTable() {
}
}},
{targets: 7, createdCell: function (td, cellData, rowData) {
var update_btn = '<a href="{% url "users:user-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('00000000-0000-0000-0000-000000000000', cellData);
var update_btn = "";
if (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin')) {
update_btn = '<a class="btn btn-xs disabled btn-info">{% trans "Update" %}</a>';
}
else{
update_btn = '<a href="{% url "users:user-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('00000000-0000-0000-0000-000000000000', cellData);
}
var del_btn = "";
if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}") {
if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}" || (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin'))) {
del_btn = '<a class="btn btn-xs btn-danger m-l-xs" disabled>{% trans "Delete" %}</a>'
.replace('{{ DEFAULT_PK }}', cellData)
.replace('99991938', rowData.name);

View File

@@ -1,6 +1,7 @@
# ~*~ coding: utf-8 ~*~
from __future__ import unicode_literals
from django.core.cache import cache
from django.shortcuts import render
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import RedirectView
@@ -29,7 +30,8 @@ __all__ = [
class UserLoginView(RedirectView):
urls = reverse_lazy('authentication:login')
url = reverse_lazy('authentication:login')
query_string = True
class UserForgotPasswordView(TemplateView):
@@ -83,7 +85,7 @@ class UserResetPasswordView(TemplateView):
def get(self, request, *args, **kwargs):
token = request.GET.get('token', '')
user = User.validate_reset_token(token)
user = User.validate_reset_password_token(token)
if not user:
kwargs.update({'errors': _('Token invalid or expired')})
else:
@@ -99,12 +101,12 @@ class UserResetPasswordView(TemplateView):
if password != password_confirm:
return self.get(request, errors=_('Password not same'))
user = User.validate_reset_token(token)
user = User.validate_reset_password_token(token)
if not user:
return self.get(request, errors=_('Token invalid or expired'))
if not user.can_update_password():
error = _('User auth from {}, go there change password'.format(user.source))
return self.get(request, errors=error)
if not user:
return self.get(request, errors=_('Token invalid or expired'))
is_ok = check_password_rules(password)
if not is_ok:
@@ -114,6 +116,7 @@ class UserResetPasswordView(TemplateView):
)
user.reset_password(password)
User.expired_reset_password_token(token)
return HttpResponseRedirect(reverse('users:reset-password-success'))

View File

@@ -107,6 +107,15 @@ class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView):
success_url = reverse_lazy('users:user-list')
success_message = update_success_msg
def _deny_permission(self):
obj = self.get_object()
return not self.request.user.is_superuser and obj.is_superuser
def get(self, request, *args, **kwargs):
if self._deny_permission():
return redirect(self.success_url)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
check_rules = get_password_check_rules()
context = {

3
jms
View File

@@ -13,7 +13,8 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)
try:
from apps import __version__
from apps.jumpserver import const
__version__ = const.VERSION
except ImportError as e:
print("Not found __version__: {}".format(e))
print("Sys path: {}".format(sys.path))

View File

@@ -1 +1 @@
libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-devel libffi-devel openssh-clients
libtiff-devel libjpeg-devel libzip-devel freetype-devel lcms2-devel libwebp-devel tcl-devel tk-devel sshpass openldap-devel mariadb-devel mysql-devel libffi-devel openssh-clients telnet openldap-clients

View File

@@ -7,4 +7,4 @@ if [ ! -d $backup_dir ];then
mkdir $backup_dir
fi
mysqldump -uroot -h127.0.0.1 -p jumpserver > ${backup_dir}/jumpserver_$(date +'%Y-%m-%d_%H:%M:%S').sql
mysqldump -uroot -h127.0.0.1 -p jumpserver -P3307 > ${backup_dir}/jumpserver_$(date +'%Y-%m-%d_%H:%M:%S').sql