mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-24 13:02:37 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a025930957 | ||
|
|
990c78e7cc | ||
|
|
0ef12906d3 | ||
|
|
61a37731ec | ||
|
|
d3217b6a67 | ||
|
|
04266cc20b | ||
|
|
4f36cf7dd1 | ||
|
|
490041587b | ||
|
|
3a3da94468 | ||
|
|
b7ad6cfe62 | ||
|
|
4463e7545d | ||
|
|
d0eafc8b8e | ||
|
|
8b98c20d68 | ||
|
|
caa5060ecd | ||
|
|
aabcf7f31c | ||
|
|
40d48cdfe4 | ||
|
|
8196537878 | ||
|
|
33a00f043b | ||
|
|
f235e20153 | ||
|
|
cf2455c084 | ||
|
|
fc1068a9dc | ||
|
|
35a0ca1875 | ||
|
|
56519354b6 | ||
|
|
78e4e13fb9 | ||
|
|
699b8d9980 | ||
|
|
ba9581801c | ||
|
|
0a5fdf4ea1 | ||
|
|
3849fa2b15 | ||
|
|
0952cbc7c6 | ||
|
|
bb06c39dd4 | ||
|
|
d60dc31443 | ||
|
|
76b3cd8edd | ||
|
|
638ba31694 | ||
|
|
c31b169cae | ||
|
|
fc167526ae | ||
|
|
55eff5eab9 | ||
|
|
f5a7f4e086 | ||
|
|
f7b0932cdd | ||
|
|
ba89ce8fb9 | ||
|
|
9d62deeabe | ||
|
|
459b41f327 | ||
|
|
3062e3f64a | ||
|
|
c1362ca4e2 | ||
|
|
9d24912ad9 | ||
|
|
db290609a8 | ||
|
|
4bc5eced6c | ||
|
|
b82a66c83d | ||
|
|
bf7079df9e | ||
|
|
f137c5740e | ||
|
|
ee47905966 | ||
|
|
f6cd193f9e | ||
|
|
a31775dd23 | ||
|
|
30ba1e5886 | ||
|
|
f97bfa7bf1 | ||
|
|
ace028fa7f | ||
|
|
69f6401e87 | ||
|
|
bd4d974df1 | ||
|
|
6e7446f530 | ||
|
|
afe9471aa2 | ||
|
|
4d56b84861 |
10
Dockerfile
10
Dockerfile
@@ -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
189
README.md
@@ -1,4 +1,4 @@
|
||||
## Jumpserver
|
||||
## Jumpserver 多云环境下更好用的堡垒机
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](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">MFA(Google 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>
|
||||
|
||||

|
||||
|
||||
### 开始使用
|
||||
----
|
||||
|
||||
快速开始文档 [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
58
README_EN.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## Jumpserver
|
||||
|
||||
[](https://www.python.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://www.ansible.com/)
|
||||
[](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
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
@@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
__version__ = "1.4.9"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,3 +48,6 @@ TASK_OPTIONS = {
|
||||
'timeout': 10,
|
||||
'forks': 10,
|
||||
}
|
||||
|
||||
CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX = '_KEY_ASSET_BULK_UPDATE_ID_{}'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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__'
|
||||
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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="">
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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_"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
9
apps/common/serializers.py
Normal file
9
apps/common/serializers.py
Normal 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
3
apps/jumpserver/const.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
VERSION = '1.4.10'
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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__'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
22
apps/perms/const.py
Normal 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')),
|
||||
)
|
||||
@@ -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')
|
||||
|
||||
33
apps/perms/migrations/0003_action.py
Normal file
33
apps/perms/migrations/0003_action.py
Normal 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)
|
||||
]
|
||||
31
apps/perms/migrations/0004_assetpermission_actions.py
Normal file
31
apps/perms/migrations/0004_assetpermission_actions.py
Normal 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)
|
||||
]
|
||||
@@ -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'))
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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' %}";
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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, Because’TypeError' 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, "
|
||||
"Because’TypeError' 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
|
||||
|
||||
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
BIN
apps/static/img/logo_text.png
Normal file
BIN
apps/static/img/logo_text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -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, '<').replace(/>/g, '>').replace(/"/g, '"') :
|
||||
d;
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
{% load i18n %}
|
||||
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} © 2014-2019
|
||||
<strong>Copyright</strong> {{ COPYRIGHT }}
|
||||
@@ -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. ' %}© 2014-2019
|
||||
{% include '_copyright.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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));
|
||||
}},
|
||||
|
||||
@@ -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,)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(', '))
|
||||
}}
|
||||
|
||||
@@ -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(', '))
|
||||
}}
|
||||
|
||||
@@ -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>');
|
||||
}},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
|
||||
@@ -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
3
jms
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user