mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3355ab0ec | ||
|
|
81598a5264 | ||
|
|
298f6ba41d | ||
|
|
8e43e9ee2b | ||
|
|
adc8a8f7d3 | ||
|
|
1e3da50979 | ||
|
|
7ac385d64c | ||
|
|
2be74c4b84 | ||
|
|
75a72fb182 | ||
|
|
4c2274b14e | ||
|
|
a024f26768 | ||
|
|
2898c35970 | ||
|
|
62f5662bd0 | ||
|
|
0fe221019a | ||
|
|
d745314aa1 | ||
|
|
153fad9ac7 | ||
|
|
0792c7ec49 | ||
|
|
e617697553 | ||
|
|
9dc7da3595 | ||
|
|
f7f4d3a42e | ||
|
|
70fcbfe883 | ||
|
|
9e16b79abe | ||
|
|
8c839784fb | ||
|
|
10adb4e6b7 | ||
|
|
75c011f1c5 | ||
|
|
a882ca0d51 | ||
|
|
e0a2d03f44 | ||
|
|
2414f34a5a | ||
|
|
2aebfa51b2 | ||
|
|
f91bfedc50 | ||
|
|
68aad56bad | ||
|
|
556ce0a146 | ||
|
|
95f8b12912 | ||
|
|
25ae790f7d | ||
|
|
0464b1a9e6 | ||
|
|
3755f8f33a | ||
|
|
85b2ec2e6a | ||
|
|
9d1e94d3c2 | ||
|
|
be75edcb41 | ||
|
|
a5c6ba6cd6 | ||
|
|
81ef614820 | ||
|
|
c6949b4f68 | ||
|
|
a5acdb9f60 | ||
|
|
2366f02d10 | ||
|
|
dade0cadda | ||
|
|
e096244e75 | ||
|
|
3bc307d666 | ||
|
|
810c500402 | ||
|
|
6c0d0c3e92 | ||
|
|
af1150bb86 | ||
|
|
f7cbcc46f4 | ||
|
|
327c6beab4 | ||
|
|
196663f205 | ||
|
|
15423291cc | ||
|
|
021635b850 | ||
|
|
992c1407b6 | ||
|
|
1322106c91 | ||
|
|
42202bd528 | ||
|
|
b24d2f628a | ||
|
|
041302d5d2 | ||
|
|
a08dd5ee72 | ||
|
|
09ef72a4a8 | ||
|
|
26cf64ad2d | ||
|
|
0a04f0f351 | ||
|
|
1029556902 | ||
|
|
c41fc54380 | ||
|
|
c2fbe5c75a | ||
|
|
99e1b2cf92 | ||
|
|
33090c4cdf | ||
|
|
c8d7c7c56f | ||
|
|
aa7540045b | ||
|
|
e5f4b8000e | ||
|
|
44ffd09924 | ||
|
|
fe3059c1fd | ||
|
|
b76920a4bf | ||
|
|
b5ac5c5670 | ||
|
|
c3c0f87c01 | ||
|
|
d672122c79 | ||
|
|
0c71190337 | ||
|
|
14710e9c9e | ||
|
|
7eec50804c | ||
|
|
0fc5a33983 | ||
|
|
07779c5a7a | ||
|
|
d675b1d4fc | ||
|
|
514fa9cf0a | ||
|
|
2c73611cb4 | ||
|
|
83571718e9 | ||
|
|
521ec0245b | ||
|
|
e80b6936a2 | ||
|
|
2c4f937e0b | ||
|
|
2a5497de14 | ||
|
|
d87dc7cbd6 | ||
|
|
3b253e276c | ||
|
|
525538e775 | ||
|
|
2a8f8dd709 | ||
|
|
1e6e59d815 | ||
|
|
475678e29b | ||
|
|
7f52675bd3 | ||
|
|
6409b7deee | ||
|
|
4f37b2b920 | ||
|
|
c692eed3c6 | ||
|
|
dab8828b03 | ||
|
|
d692188a34 | ||
|
|
bc8df72603 | ||
|
|
bf466a1ba2 | ||
|
|
aff5b0035d | ||
|
|
b44fa64994 | ||
|
|
094446c548 | ||
|
|
64eda5f28b | ||
|
|
ab737ae09b | ||
|
|
55e04e8e9f | ||
|
|
5e70a8af15 | ||
|
|
031077c298 | ||
|
|
3f856e68f0 | ||
|
|
56862a965d | ||
|
|
e151548701 | ||
|
|
c56179e9e4 | ||
|
|
d23953932f | ||
|
|
2493647e5c | ||
|
|
00ed7bb025 | ||
|
|
b1aadf1ee9 | ||
|
|
86e6982383 | ||
|
|
dc42d1caa2 | ||
|
|
cb5d8fa13f | ||
|
|
3a3f7eaf71 | ||
|
|
9804ca5dd0 | ||
|
|
034d0e285c | ||
|
|
104d672634 | ||
|
|
529e3d12e0 | ||
|
|
978c1f6363 | ||
|
|
d25cde1bd5 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
18
.github/workflows/lgtm.yml
vendored
18
.github/workflows/lgtm.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Send LGTM reaction
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@1.0.0
|
||||
- uses: micnncim/action-lgtm-reaction@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
trigger: '["^.?lgtm$"]'
|
||||
59
Dockerfile
59
Dockerfile
@@ -1,20 +1,5 @@
|
||||
# 编译代码
|
||||
FROM python:3.8-slim as stage-build
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.8-slim
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
@@ -44,11 +29,12 @@ ARG TOOLS=" \
|
||||
redis-tools \
|
||||
telnet \
|
||||
vim \
|
||||
unzip \
|
||||
wget"
|
||||
|
||||
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& apt update \
|
||||
&& apt update && sleep 1 && apt update \
|
||||
&& apt -y install ${BUILD_DEPENDENCIES} \
|
||||
&& apt -y install ${DEPENDENCIES} \
|
||||
&& apt -y install ${TOOLS} \
|
||||
@@ -62,21 +48,44 @@ RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
|
||||
&& mv /bin/sh /bin/sh.bak \
|
||||
&& ln -s /bin/bash /bin/sh
|
||||
|
||||
RUN mkdir -p /opt/jumpserver/oracle/ \
|
||||
&& wget https://download.jumpserver.org/public/instantclient-basiclite-linux.x64-21.1.0.0.0.tar \
|
||||
&& tar xf instantclient-basiclite-linux.x64-21.1.0.0.0.tar -C /opt/jumpserver/oracle/ \
|
||||
&& echo "/opt/jumpserver/oracle/instantclient_21_1" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
ARG TARGETARCH
|
||||
ARG ORACLE_LIB_MAJOR=19
|
||||
ARG ORACLE_LIB_MINOR=10
|
||||
ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip"
|
||||
|
||||
RUN mkdir -p /opt/oracle/ \
|
||||
&& cd /opt/oracle/ \
|
||||
&& wget https://download.jumpserver.org/files/oracle/${ORACLE_FILE} \
|
||||
&& unzip instantclient-basiclite-linux.${TARGETARCH-amd64}-19.10.0.0.0dbru.zip \
|
||||
&& mv instantclient_${ORACLE_LIB_MAJOR}_${ORACLE_LIB_MINOR} instantclient \
|
||||
&& echo "/opt/oracle/instantclient" > /etc/ld.so.conf.d/oracle-instantclient.conf \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.x64-21.1.0.0.0.tar
|
||||
&& rm -f ${ORACLE_FILE}
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
RUN echo > config.yml \
|
||||
&& pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://mirrors.aliyun.com/pypi/simple/
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有
|
||||
RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \
|
||||
&& pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \
|
||||
&& rm -rf ~/.cache/pip
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
ADD . .
|
||||
RUN cd utils \
|
||||
&& bash -ixeu build.sh \
|
||||
&& mv ../release/jumpserver /opt/jumpserver \
|
||||
&& rm -rf /tmp/build \
|
||||
&& echo > /opt/jumpserver/config.yml
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
|
||||
61
README.md
61
README.md
@@ -1,10 +1,13 @@
|
||||
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
|
||||
<p align="center">
|
||||
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
|
||||
</p>
|
||||
<h3 align="center">多云环境下更好用的堡垒机</h3>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
|
||||
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
|
||||
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver/commits"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/jumpserver/jumpserver.svg" /></a>
|
||||
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
</p>
|
||||
|
||||
@@ -15,7 +18,7 @@
|
||||
|
||||
JumpServer 是全球首款开源的堡垒机,使用 GPLv3 开源协议,是符合 4A 规范的运维安全审计系统。
|
||||
|
||||
JumpServer 使用 Python 开发,遵循 Web 2.0 规范,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案,交互界面美观、用户体验好。
|
||||
|
||||
JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。
|
||||
|
||||
@@ -28,9 +31,9 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- 开源: 零门槛,线上快速获取和安装;
|
||||
- 分布式: 轻松支持大规模并发访问;
|
||||
- 无插件: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- 多租户: 一套系统,多个子公司或部门同时使用;
|
||||
- 多云支持: 一套系统,同时管理不同云上面的资产;
|
||||
- 云端存储: 审计录像云端存储,永不丢失;
|
||||
- 多租户: 一套系统,多个子公司和部门同时使用;
|
||||
- 多应用支持: 数据库,Windows远程应用,Kubernetes。
|
||||
|
||||
### UI 展示
|
||||
@@ -55,12 +58,15 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
- [手动安装](https://github.com/jumpserver/installer)
|
||||
|
||||
### 组件项目
|
||||
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI 项目
|
||||
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal 项目
|
||||
- [KoKo](https://github.com/jumpserver/koko) JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco)
|
||||
- [Lion](https://github.com/jumpserver/lion-release) JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/)
|
||||
- [Clients](https://github.com/jumpserver/clients) JumpServer 客户端 项目
|
||||
- [Installer](https://github.com/jumpserver/installer) JumpServer 安装包 项目
|
||||
| 项目 | 状态 | 描述 |
|
||||
| --------------------------------------------------------------------------- | ------------------- | ---------------------------------------- |
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer)| <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
|
||||
### 社区
|
||||
|
||||
@@ -75,27 +81,13 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
感谢以下贡献者,让 JumpServer 更加完善
|
||||
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/koko/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/lina/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/jumpserver/luna/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
|
||||
</a>
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
|
||||
|
||||
### 致谢
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web页面连接使用数据库,JumpServer Web数据库依赖
|
||||
- [Apache Guacamole](https://guacamole.apache.org/) Web页面连接 RDP, SSH, VNC 协议设备,JumpServer 图形化组件 Lion 依赖
|
||||
- [OmniDB](https://omnidb.org/) Web 页面连接使用数据库,JumpServer Web 数据库依赖
|
||||
|
||||
|
||||
### JumpServer 企业版
|
||||
@@ -103,14 +95,14 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向
|
||||
|
||||
### 案例研究
|
||||
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147);
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882);
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851);
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516);
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732);
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708);
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687);
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
|
||||
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
|
||||
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
|
||||
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
|
||||
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
|
||||
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
|
||||
- [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
|
||||
|
||||
### 安全说明
|
||||
|
||||
@@ -131,4 +123,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
|
||||
@@ -123,6 +123,8 @@ class LoginACL(BaseACL):
|
||||
'org_id': Organization.ROOT_ID,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(self.user)
|
||||
applicant = self.user
|
||||
assignees = self.reviewers.all()
|
||||
ticket.create_process_map_and_node(assignees, applicant)
|
||||
ticket.open(applicant)
|
||||
return ticket
|
||||
|
||||
@@ -97,7 +97,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(assignees)
|
||||
ticket.create_process_map_and_node(assignees, user)
|
||||
ticket.open(applicant=user)
|
||||
return ticket
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.db.models import F, Q
|
||||
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from rbac.permissions import RBACPermission
|
||||
from assets.models import SystemUser
|
||||
from ..models import Account
|
||||
@@ -54,7 +55,7 @@ class SystemUserAppRelationViewSet(ApplicationAccountViewSet):
|
||||
perm_model = SystemUser
|
||||
|
||||
|
||||
class ApplicationAccountSecretViewSet(ApplicationAccountViewSet):
|
||||
class ApplicationAccountSecretViewSet(RecordViewLogMixin, ApplicationAccountViewSet):
|
||||
serializer_class = serializers.AppAccountSecretSerializer
|
||||
permission_classes = [RBACPermission, NeedMFAVerify]
|
||||
http_method_names = ['get', 'options']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.1.7 on 2019-05-20 11:04
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('Browser', (('chrome', 'Chrome'),)), ('Database tools', (('mysql_workbench', 'MySQL Workbench'),)), ('Virtualization tools', (('vmware_client', 'vSphere Client'),)), ('custom', 'Custom')], default='chrome', max_length=128, verbose_name='App type')),
|
||||
('path', models.CharField(max_length=128, verbose_name='App path')),
|
||||
('params', common.fields.model.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
|
||||
('params', common.db.fields.EncryptJsonDictTextField(blank=True, default={}, max_length=4096, null=True, verbose_name='Parameters')),
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 3.1.12 on 2021-08-26 09:07
|
||||
|
||||
import assets.models.base
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
@@ -26,9 +26,9 @@ class Migration(migrations.Migration):
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
|
||||
@@ -56,9 +56,9 @@ class Migration(migrations.Migration):
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from assets.serializers.base import AuthSerializerMixin
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from common.drf.serializers import MethodSerializer, SecretReadableMixin
|
||||
from .attrs import (
|
||||
category_serializer_classes_mapping,
|
||||
type_serializer_classes_mapping,
|
||||
@@ -152,7 +152,7 @@ class AppAccountSerializer(AppSerializerMixin, AuthSerializerMixin, BulkOrgResou
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AppAccountSecretSerializer(AppAccountSerializer):
|
||||
class AppAccountSecretSerializer(SecretReadableMixin, AppAccountSerializer):
|
||||
class Meta(AppAccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'id', 'app_display', 'attrs', 'username', 'password', 'private_key',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['ChromeSerializer', 'ChromeSecretSerializer']
|
||||
@@ -13,19 +14,21 @@ class ChromeSerializer(RemoteAppSerializer):
|
||||
max_length=128, label=_('Application path'), default=CHROME_PATH, allow_null=True,
|
||||
)
|
||||
chrome_target = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Target URL'), allow_null=True,
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Target URL'), allow_null=True,
|
||||
)
|
||||
chrome_username = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, label=_('Chrome username'), allow_null=True,
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome username'), allow_null=True,
|
||||
)
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Chrome password'),
|
||||
allow_null=True
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class ChromeSecretSerializer(ChromeSerializer):
|
||||
chrome_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Chrome password'),
|
||||
allow_null=True
|
||||
chrome_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Chrome password'), allow_null=True, write_only=False
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['CustomSerializer', 'CustomSecretSerializer']
|
||||
@@ -19,14 +20,14 @@ class CustomSerializer(RemoteAppSerializer):
|
||||
max_length=128, allow_blank=True, required=False, label=_('Custom Username'),
|
||||
allow_null=True,
|
||||
)
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Custom password'),
|
||||
allow_null=True,
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class CustomSecretSerializer(RemoteAppSerializer):
|
||||
custom_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Custom password'),
|
||||
allow_null=True,
|
||||
custom_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Custom password'), allow_null=True,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['MySQLWorkbenchSerializer', 'MySQLWorkbenchSecretSerializer']
|
||||
@@ -29,14 +30,14 @@ class MySQLWorkbenchSerializer(RemoteAppSerializer):
|
||||
max_length=128, allow_blank=True, required=False, label=_('Mysql workbench username'),
|
||||
allow_null=True,
|
||||
)
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Mysql workbench password'),
|
||||
allow_null=True,
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
||||
|
||||
class MySQLWorkbenchSecretSerializer(RemoteAppSerializer):
|
||||
mysql_workbench_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Mysql workbench password'),
|
||||
allow_null=True,
|
||||
mysql_workbench_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Mysql workbench password'), allow_null=True,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..application_category import RemoteAppSerializer
|
||||
|
||||
__all__ = ['VMwareClientSerializer', 'VMwareClientSecretSerializer']
|
||||
@@ -25,14 +26,14 @@ class VMwareClientSerializer(RemoteAppSerializer):
|
||||
max_length=128, allow_blank=True, required=False, label=_('Vmware username'),
|
||||
allow_null=True
|
||||
)
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=True, label=_('Vmware password'),
|
||||
allow_null=True
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
||||
|
||||
class VMwareClientSecretSerializer(RemoteAppSerializer):
|
||||
vmware_password = serializers.CharField(
|
||||
max_length=128, allow_blank=True, required=False, read_only=True, label=_('Vmware password'),
|
||||
allow_null=True
|
||||
vmware_password = EncryptedField(
|
||||
max_length=128, allow_blank=True, required=False, write_only=False,
|
||||
label=_('Vmware password'), allow_null=True
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from rest_framework.generics import CreateAPIView
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from common.mixins import RecordViewLogMixin
|
||||
from common.permissions import NeedMFAVerify
|
||||
from ..tasks.account_connectivity import test_accounts_connectivity_manual
|
||||
from ..models import AuthBook, Node
|
||||
@@ -79,7 +80,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
return Response(data={'task': task.id})
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountViewSet):
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,6 @@ from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.utils.crypto import get_aes_crypto
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins.api import SuggestionMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
@@ -102,27 +101,17 @@ class SystemUserTempAuthInfoApi(generics.CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = SystemUserTempAuthSerializer
|
||||
|
||||
def decrypt_data_if_need(self, data):
|
||||
csrf_token = self.request.META.get('CSRF_COOKIE')
|
||||
aes = get_aes_crypto(csrf_token, 'ECB')
|
||||
password = data.get('password', '')
|
||||
try:
|
||||
data['password'] = aes.decrypt(password)
|
||||
except:
|
||||
pass
|
||||
return data
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = super().get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
pk = kwargs.get('pk')
|
||||
data = self.decrypt_data_if_need(serializer.validated_data)
|
||||
instance_id = data.get('instance_id')
|
||||
data = serializer.validated_data
|
||||
asset_or_app_id = data.get('instance_id')
|
||||
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_404(SystemUser, pk=pk)
|
||||
instance.set_temp_auth(instance_id, self.request.user.id, data)
|
||||
instance.set_temp_auth(asset_or_app_id, self.request.user.id, data)
|
||||
return Response(serializer.data, status=201)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Generated by Django 2.1.7 on 2019-06-24 13:08
|
||||
|
||||
import assets.models.utils
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -15,61 +15,61 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_password',
|
||||
field=common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, validators=[assets.models.utils.private_key_validator], verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='_public_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.1.7 on 2019-07-11 12:18
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -14,21 +14,21 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='private_key',
|
||||
field=common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-06 07:26
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Migration(migrations.Migration):
|
||||
('name', models.SlugField(allow_unicode=True, unique=True, verbose_name='Name')),
|
||||
('base', models.CharField(choices=[('Linux', 'Linux'), ('Unix', 'Unix'), ('MacOS', 'MacOS'), ('BSD', 'BSD'), ('Windows', 'Windows'), ('Other', 'Other')], default='Linux', max_length=16, verbose_name='Base')),
|
||||
('charset', models.CharField(choices=[('utf8', 'UTF-8'), ('gbk', 'GBK')], default='utf8', max_length=8, verbose_name='Charset')),
|
||||
('meta', common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
|
||||
('meta', common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Meta')),
|
||||
('internal', models.BooleanField(default=False, verbose_name='Internal')),
|
||||
('comment', models.TextField(blank=True, null=True, verbose_name='Comment')),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 3.1.6 on 2021-06-05 16:10
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
@@ -58,9 +58,9 @@ class Migration(migrations.Migration):
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('username', models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username')),
|
||||
('password', common.fields.model.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.fields.model.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('password', common.db.fields.EncryptCharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
||||
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH private key')),
|
||||
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='SSH public key')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('date_created', models.DateTimeField(blank=True, editable=False, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')),
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def create_internal_platform(apps, schema_editor):
|
||||
model = apps.get_model("assets", "Platform")
|
||||
db_alias = schema_editor.connection.alias
|
||||
type_platforms = (
|
||||
('AIX', 'Unix', None),
|
||||
)
|
||||
for name, base, meta in type_platforms:
|
||||
defaults = {'name': name, 'base': base, 'meta': meta, 'internal': True}
|
||||
model.objects.using(db_alias).update_or_create(
|
||||
name=name, defaults=defaults
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
@@ -15,4 +28,5 @@ class Migration(migrations.Migration):
|
||||
name='number',
|
||||
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Asset number'),
|
||||
),
|
||||
migrations.RunPython(create_internal_platform)
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from common.fields.model import JsonDictTextField
|
||||
from common.db.fields import JsonDictTextField
|
||||
from common.utils import lazyproperty
|
||||
from orgs.mixins.models import OrgModelMixin, OrgManager
|
||||
|
||||
@@ -301,7 +301,7 @@ class Asset(AbsConnectivity, AbsHardwareInfo, ProtocolsMixin, NodesRelationMixin
|
||||
'private_key': auth_user.private_key_file
|
||||
}
|
||||
|
||||
if not with_become:
|
||||
if not with_become or self.is_windows():
|
||||
return info
|
||||
|
||||
if become_user:
|
||||
|
||||
@@ -19,7 +19,7 @@ from common.utils import (
|
||||
)
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.validators import alphanumeric
|
||||
from common import fields
|
||||
from common.db import fields
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
|
||||
@@ -181,8 +181,10 @@ class CommandFilterRule(OrgModelMixin):
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(self.reviewers.all())
|
||||
ticket.open(applicant=session.user_obj)
|
||||
applicant = session.user_obj
|
||||
assignees = self.reviewers.all()
|
||||
ticket.create_process_map_and_node(assignees, applicant)
|
||||
ticket.open(applicant)
|
||||
return ticket
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -133,6 +133,15 @@ class AuthMixin:
|
||||
self.password = password
|
||||
|
||||
def load_app_more_auth(self, app_id=None, username=None, user_id=None):
|
||||
# 清除认证信息
|
||||
self._clean_auth_info_if_manual_login_mode()
|
||||
|
||||
# 先加载临时认证信息
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self._load_tmp_auth_if_has(app_id, user_id)
|
||||
return
|
||||
|
||||
# Remote app
|
||||
from applications.models import Application
|
||||
app = get_object_or_none(Application, pk=app_id)
|
||||
if app and app.category_remote_app:
|
||||
@@ -141,11 +150,6 @@ class AuthMixin:
|
||||
return
|
||||
|
||||
# Other app
|
||||
self._clean_auth_info_if_manual_login_mode()
|
||||
# 加载临时认证信息
|
||||
if self.login_mode == self.LOGIN_MANUAL:
|
||||
self._load_tmp_auth_if_has(app_id, user_id)
|
||||
return
|
||||
# 更新用户名
|
||||
from users.models import User
|
||||
user = get_object_or_none(User, pk=user_id) if user_id else None
|
||||
|
||||
@@ -5,8 +5,8 @@ from assets.models import AuthBook
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
from .base import AuthSerializerMixin
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
from common.utils.encode import ssh_pubkey_gen
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
|
||||
|
||||
class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
@@ -31,10 +31,6 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
fields = fields_small + fields_fk
|
||||
extra_kwargs = {
|
||||
'username': {'required': True},
|
||||
'password': {
|
||||
'write_only': True,
|
||||
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||
},
|
||||
'private_key': {'write_only': True},
|
||||
'public_key': {'write_only': True},
|
||||
'systemuser_display': {'label': _('System user display')}
|
||||
@@ -70,7 +66,7 @@ class AccountSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
return super().to_representation(instance)
|
||||
|
||||
|
||||
class AccountSecretSerializer(AccountSerializer):
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
fields_backup = [
|
||||
'hostname', 'ip', 'platform', 'protocols', 'username', 'password',
|
||||
|
||||
@@ -6,12 +6,14 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key
|
||||
from common.drf.fields import EncryptedField
|
||||
from assets.models import Type
|
||||
from .utils import validate_password_for_ansible
|
||||
|
||||
|
||||
class AuthSerializer(serializers.ModelSerializer):
|
||||
password = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=1024)
|
||||
private_key = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=4096)
|
||||
password = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=1024, label=_('Password'))
|
||||
private_key = EncryptedField(required=False, allow_blank=True, allow_null=True, max_length=4096, label=_('Private key'))
|
||||
|
||||
def gen_keys(self, private_key=None, password=None):
|
||||
if private_key is None:
|
||||
@@ -31,6 +33,13 @@ class AuthSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class AuthSerializerMixin(serializers.ModelSerializer):
|
||||
password = EncryptedField(
|
||||
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
|
||||
validators=[validate_password_for_ansible]
|
||||
)
|
||||
private_key = EncryptedField(
|
||||
label=_('SSH private key'), required=False, allow_blank=True, allow_null=True, max_length=4096
|
||||
)
|
||||
passphrase = serializers.CharField(
|
||||
allow_blank=True, allow_null=True, required=False, max_length=512,
|
||||
write_only=True, label=_('Key password')
|
||||
|
||||
@@ -5,6 +5,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.validators import alphanumeric
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
from ..models import Domain, Gateway
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
@@ -67,7 +68,7 @@ class GatewaySerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class GatewayWithAuthSerializer(GatewaySerializer):
|
||||
class GatewayWithAuthSerializer(SecretReadableMixin, GatewaySerializer):
|
||||
class Meta(GatewaySerializer.Meta):
|
||||
extra_kwargs = {
|
||||
'password': {'write_only': False},
|
||||
|
||||
@@ -4,10 +4,12 @@ from django.db.models import Count
|
||||
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
from common.utils import ssh_pubkey_gen
|
||||
from common.drf.fields import EncryptedField
|
||||
from common.drf.serializers import SecretReadableMixin
|
||||
from common.validators import alphanumeric_re, alphanumeric_cn_re, alphanumeric_win_re
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import SystemUser, Asset
|
||||
from .utils import validate_password_contains_left_double_curly_bracket
|
||||
from .utils import validate_password_for_ansible
|
||||
from .base import AuthSerializerMixin
|
||||
|
||||
__all__ = [
|
||||
@@ -23,9 +25,17 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
"""
|
||||
系统用户
|
||||
"""
|
||||
password = EncryptedField(
|
||||
label=_('Password'), required=False, allow_blank=True, allow_null=True, max_length=1024,
|
||||
trim_whitespace=False, validators=[validate_password_for_ansible],
|
||||
write_only=True
|
||||
)
|
||||
auto_generate_key = serializers.BooleanField(initial=True, required=False, write_only=True)
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
ssh_key_fingerprint = serializers.ReadOnlyField(label=_('SSH key fingerprint'))
|
||||
token = EncryptedField(
|
||||
label=_('Token'), required=False, write_only=True, style={'base_template': 'textarea.html'}
|
||||
)
|
||||
applications_amount = serializers.IntegerField(
|
||||
source='apps_amount', read_only=True, label=_('Apps amount')
|
||||
)
|
||||
@@ -46,15 +56,9 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
||||
fields_m2m = ['cmd_filters', 'assets_amount', 'applications_amount', 'nodes']
|
||||
fields = fields_small + fields_m2m
|
||||
extra_kwargs = {
|
||||
'password': {
|
||||
"write_only": True,
|
||||
'trim_whitespace': False,
|
||||
"validators": [validate_password_contains_left_double_curly_bracket]
|
||||
},
|
||||
'cmd_filters': {"required": False, 'label': _('Command filter')},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'token': {"write_only": True},
|
||||
'nodes_amount': {'label': _('Nodes amount')},
|
||||
'assets_amount': {'label': _('Assets amount')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
@@ -248,7 +252,7 @@ class MiniSystemUserSerializer(serializers.ModelSerializer):
|
||||
fields = SystemUserSerializer.Meta.fields_mini
|
||||
|
||||
|
||||
class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
class SystemUserWithAuthInfoSerializer(SecretReadableMixin, SystemUserSerializer):
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
fields_write_only = ['password', 'public_key', 'private_key']
|
||||
@@ -264,6 +268,9 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
'created_by': {'read_only': True},
|
||||
'password': {'write_only': False},
|
||||
'private_key': {'write_only': False},
|
||||
'token': {'write_only': False}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,16 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def validate_password_contains_left_double_curly_bracket(password):
|
||||
def validate_password_for_ansible(password):
|
||||
""" 校验 Ansible 不支持的特殊字符 """
|
||||
# validate password contains left double curly bracket
|
||||
# check password not contains `{{`
|
||||
# Ansible 推送的时候不支持
|
||||
if '{{' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
||||
# Ansible Windows 推送的时候不支持
|
||||
if "'" in password:
|
||||
raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||
if '"' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `"` '))
|
||||
|
||||
|
||||
@@ -32,17 +32,18 @@ def _dump_args(args: dict):
|
||||
return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty])
|
||||
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
comment = system_user.name
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None, **kwargs):
|
||||
algorithm = kwargs.get('algorithm')
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
|
||||
comment = system_user.name
|
||||
if system_user.username_same_with_user:
|
||||
from users.models import User
|
||||
user = User.objects.filter(username=username).only('name', 'username').first()
|
||||
if user:
|
||||
comment = f'{system_user.name}[{str(user)}]'
|
||||
comment = comment.replace(' ', '')
|
||||
|
||||
password = system_user.password
|
||||
public_key = system_user.public_key
|
||||
@@ -104,7 +105,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present password={}'.format(
|
||||
username, system_user.shell,
|
||||
encrypt_password(password, salt="K3mIlKK"),
|
||||
encrypt_password(password, salt="K3mIlKK", algorithm=algorithm),
|
||||
),
|
||||
}
|
||||
})
|
||||
@@ -138,7 +139,7 @@ def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
return tasks
|
||||
|
||||
|
||||
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
|
||||
def get_push_windows_system_user_tasks(system_user: SystemUser, username=None, **kwargs):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
@@ -176,7 +177,7 @@ def get_push_windows_system_user_tasks(system_user: SystemUser, username=None):
|
||||
return tasks
|
||||
|
||||
|
||||
def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
|
||||
def get_push_system_user_tasks(system_user, platform="unixlike", username=None, algorithm=None):
|
||||
"""
|
||||
获取推送系统用户的 ansible 命令,跟资产无关
|
||||
:param system_user:
|
||||
@@ -190,16 +191,16 @@ def get_push_system_user_tasks(system_user, platform="unixlike", username=None):
|
||||
}
|
||||
get_tasks = get_task_map.get(platform, get_push_unixlike_system_user_tasks)
|
||||
if not system_user.username_same_with_user:
|
||||
return get_tasks(system_user)
|
||||
return get_tasks(system_user, algorithm=algorithm)
|
||||
tasks = []
|
||||
# 仅推送这个username
|
||||
if username is not None:
|
||||
tasks.extend(get_tasks(system_user, username))
|
||||
tasks.extend(get_tasks(system_user, username, algorithm=algorithm))
|
||||
return tasks
|
||||
users = system_user.users.all().values_list('username', flat=True)
|
||||
print(_("System user is dynamic: {}").format(list(users)))
|
||||
for _username in users:
|
||||
tasks.extend(get_tasks(system_user, _username))
|
||||
tasks.extend(get_tasks(system_user, _username, algorithm=algorithm))
|
||||
return tasks
|
||||
|
||||
|
||||
@@ -244,7 +245,11 @@ def push_system_user_util(system_user, assets, task_name, username=None):
|
||||
for u in usernames:
|
||||
for a in _assets:
|
||||
system_user.load_asset_special_auth(a, u)
|
||||
tasks = get_push_system_user_tasks(system_user, platform, username=u)
|
||||
algorithm = 'des' if a.platform.name == 'AIX' else 'sha512'
|
||||
tasks = get_push_system_user_tasks(
|
||||
system_user, platform, username=u,
|
||||
algorithm=algorithm
|
||||
)
|
||||
run_task(tasks, [a])
|
||||
|
||||
|
||||
@@ -269,7 +274,7 @@ def push_system_user_a_asset_manual(system_user, asset, username=None):
|
||||
# if username is None:
|
||||
# username = system_user.username
|
||||
task_name = gettext_noop("Push system users to asset: ") + "{}({}) => {}".format(
|
||||
system_user.name, username, asset
|
||||
system_user.name, username or system_user.username, asset
|
||||
)
|
||||
return push_system_user_util(system_user, [asset], task_name=task_name, username=username)
|
||||
|
||||
|
||||
@@ -3,3 +3,23 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
DEFAULT_CITY = _("Unknown")
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform', 'AuthBook',
|
||||
# applications
|
||||
'Application',
|
||||
# orgs
|
||||
'Organization',
|
||||
# settings
|
||||
'Setting',
|
||||
# perms
|
||||
'AssetPermission', 'ApplicationPermission',
|
||||
# xpack
|
||||
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
)
|
||||
|
||||
18
apps/audits/migrations/0014_auto_20220505_1902.py
Normal file
18
apps/audits/migrations/0014_auto_20220505_1902.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.14 on 2022-05-05 11:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0013_auto_20211130_1037'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='operatelog',
|
||||
name='action',
|
||||
field=models.CharField(choices=[('create', 'Create'), ('view', 'View'), ('update', 'Update'), ('delete', 'Delete')], max_length=16, verbose_name='Action'),
|
||||
),
|
||||
]
|
||||
@@ -49,10 +49,12 @@ class FTPLog(OrgModelMixin):
|
||||
|
||||
class OperateLog(OrgModelMixin):
|
||||
ACTION_CREATE = 'create'
|
||||
ACTION_VIEW = 'view'
|
||||
ACTION_UPDATE = 'update'
|
||||
ACTION_DELETE = 'delete'
|
||||
ACTION_CHOICES = (
|
||||
(ACTION_CREATE, _("Create")),
|
||||
(ACTION_VIEW, _("View")),
|
||||
(ACTION_UPDATE, _("Update")),
|
||||
(ACTION_DELETE, _("Delete"))
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
|
||||
from django.db.models.signals import (
|
||||
post_save, m2m_changed, pre_delete
|
||||
)
|
||||
@@ -21,7 +23,7 @@ from jumpserver.utils import current_request
|
||||
from users.models import User
|
||||
from users.signals import post_user_change_password
|
||||
from terminal.models import Session, Command
|
||||
from .utils import write_login_log
|
||||
from .utils import write_login_log, create_operate_log
|
||||
from . import models, serializers
|
||||
from .models import OperateLog
|
||||
from orgs.utils import current_org
|
||||
@@ -36,26 +38,6 @@ logger = get_logger(__name__)
|
||||
sys_logger = get_syslogger(__name__)
|
||||
json_render = JSONRenderer()
|
||||
|
||||
MODELS_NEED_RECORD = (
|
||||
# users
|
||||
'User', 'UserGroup',
|
||||
# acls
|
||||
'LoginACL', 'LoginAssetACL', 'LoginConfirmSetting',
|
||||
# assets
|
||||
'Asset', 'Node', 'AdminUser', 'SystemUser', 'Domain', 'Gateway', 'CommandFilterRule',
|
||||
'CommandFilter', 'Platform', 'AuthBook',
|
||||
# applications
|
||||
'Application',
|
||||
# orgs
|
||||
'Organization',
|
||||
# settings
|
||||
'Setting',
|
||||
# perms
|
||||
'AssetPermission', 'ApplicationPermission',
|
||||
# xpack
|
||||
'License', 'Account', 'SyncInstanceTask', 'ChangeAuthPlan', 'GatherUserTask',
|
||||
)
|
||||
|
||||
|
||||
class AuthBackendLabelMapping(LazyObject):
|
||||
@staticmethod
|
||||
@@ -80,28 +62,6 @@ class AuthBackendLabelMapping(LazyObject):
|
||||
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
|
||||
|
||||
|
||||
def create_operate_log(action, sender, resource):
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
model_name = sender._meta.object_name
|
||||
if model_name not in MODELS_NEED_RECORD:
|
||||
return
|
||||
with translation.override('en'):
|
||||
resource_type = sender._meta.verbose_name
|
||||
remote_addr = get_request_ip(current_request)
|
||||
|
||||
data = {
|
||||
"user": str(user), 'action': action, 'resource_type': resource_type,
|
||||
'resource': str(resource), 'remote_addr': remote_addr,
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
models.OperateLog.objects.create(**data)
|
||||
except Exception as e:
|
||||
logger.error("Create operate log error: {}".format(e))
|
||||
|
||||
|
||||
M2M_NEED_RECORD = {
|
||||
User.groups.through._meta.object_name: (
|
||||
_('User and Group'),
|
||||
@@ -316,6 +276,8 @@ def on_user_auth_success(sender, user, request, login_type=None, **kwargs):
|
||||
logger.debug('User login success: {}'.format(user.username))
|
||||
check_different_city_login_if_need(user, request)
|
||||
data = generate_data(user.username, request, login_type=login_type)
|
||||
request.session['login_time'] = data['datetime'].strftime("%Y-%m-%d %H:%M:%S")
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
data.update({'mfa': int(user.mfa_enabled), 'status': True})
|
||||
write_login_log(**data)
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import csv
|
||||
import codecs
|
||||
from django.http import HttpResponse
|
||||
|
||||
from .const import DEFAULT_CITY
|
||||
from common.utils import validate_ip, get_ip_city
|
||||
from django.http import HttpResponse
|
||||
from django.db import transaction
|
||||
from django.utils import translation
|
||||
|
||||
from audits.models import OperateLog
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip, get_logger
|
||||
from jumpserver.utils import current_request
|
||||
from .const import DEFAULT_CITY, MODELS_NEED_RECORD
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_excel_response(filename):
|
||||
@@ -36,3 +44,25 @@ def write_login_log(*args, **kwargs):
|
||||
city = get_ip_city(ip) or DEFAULT_CITY
|
||||
kwargs.update({'ip': ip, 'city': city})
|
||||
UserLoginLog.objects.create(**kwargs)
|
||||
|
||||
|
||||
def create_operate_log(action, sender, resource):
|
||||
user = current_request.user if current_request else None
|
||||
if not user or not user.is_authenticated:
|
||||
return
|
||||
model_name = sender._meta.object_name
|
||||
if model_name not in MODELS_NEED_RECORD:
|
||||
return
|
||||
with translation.override('en'):
|
||||
resource_type = sender._meta.verbose_name
|
||||
remote_addr = get_request_ip(current_request)
|
||||
|
||||
data = {
|
||||
"user": str(user), 'action': action, 'resource_type': resource_type,
|
||||
'resource': str(resource), 'remote_addr': remote_addr,
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
OperateLog.objects.create(**data)
|
||||
except Exception as e:
|
||||
logger.error("Create operate log error: {}".format(e))
|
||||
|
||||
@@ -5,6 +5,7 @@ from .connection_token import *
|
||||
from .token import *
|
||||
from .mfa import *
|
||||
from .access_key import *
|
||||
from .confirm import *
|
||||
from .login_confirm import *
|
||||
from .sso import *
|
||||
from .wecom import *
|
||||
|
||||
85
apps/authentication/api/confirm.py
Normal file
85
apps/authentication/api/confirm.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.generics import ListCreateAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from ..mfa import MFAOtp
|
||||
from ..const import ConfirmType
|
||||
from ..mixins import authenticate
|
||||
from ..serializers import ConfirmSerializer
|
||||
|
||||
|
||||
class ConfirmViewSet(ListCreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = ConfirmSerializer
|
||||
|
||||
def check(self, confirm_type: str):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
return self.user.mfa_enabled
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
return self.user.is_password_authenticate()
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
return not self.user.is_password_authenticate()
|
||||
|
||||
def authenticate(self, confirm_type, secret_key):
|
||||
if confirm_type == ConfirmType.MFA:
|
||||
ok, msg = MFAOtp(self.user).check_code(secret_key)
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.PASSWORD:
|
||||
ok = authenticate(self.request, username=self.user.username, password=secret_key)
|
||||
msg = '' if ok else _('Authentication failed password incorrect')
|
||||
return ok, msg
|
||||
|
||||
if confirm_type == ConfirmType.RELOGIN:
|
||||
now = timezone.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
now = datetime.strptime(now, '%Y-%m-%d %H:%M:%S')
|
||||
login_time = self.request.session.get('login_time')
|
||||
SPECIFIED_TIME = 5
|
||||
msg = _('Login time has exceeded {} minutes, please login again').format(SPECIFIED_TIME)
|
||||
if not login_time:
|
||||
return False, msg
|
||||
login_time = datetime.strptime(login_time, '%Y-%m-%d %H:%M:%S')
|
||||
if (now - login_time).seconds >= SPECIFIED_TIME * 60:
|
||||
return False, msg
|
||||
return True, ''
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self.request.user
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
|
||||
return Response('ok')
|
||||
|
||||
mfa_verify_time = request.session.get('MFA_VERIFY_TIME', 0)
|
||||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return Response('ok')
|
||||
|
||||
data = []
|
||||
for i, confirm_type in enumerate(ConfirmType.values, 1):
|
||||
if self.check(confirm_type):
|
||||
data.append({'name': confirm_type, 'level': i})
|
||||
msg = _('This action require verify your MFA')
|
||||
return Response({'error': msg, 'backends': data}, status=400)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
confirm_type = validated_data.get('confirm_type')
|
||||
secret_key = validated_data.get('secret_key')
|
||||
ok, msg = self.authenticate(confirm_type, secret_key)
|
||||
if ok:
|
||||
request.session["MFA_VERIFY_TIME"] = int(time.time())
|
||||
return Response('ok')
|
||||
return Response({'error': msg}, status=400)
|
||||
@@ -7,7 +7,6 @@ import os
|
||||
import base64
|
||||
import ctypes
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
@@ -19,6 +18,7 @@ from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import serializers
|
||||
from django.conf import settings
|
||||
|
||||
from applications.models import Application
|
||||
from authentication.signals import post_auth_failed
|
||||
@@ -33,11 +33,11 @@ from perms.utils.asset.permission import get_asset_actions
|
||||
from common.const.http import PATCH
|
||||
from terminal.models import EndpointRule
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
__all__ = ['UserConnectionTokenViewSet', 'TokenCacheMixin']
|
||||
__all__ = ['UserConnectionTokenViewSet', 'UserSuperConnectionTokenViewSet', 'TokenCacheMixin']
|
||||
|
||||
|
||||
class ClientProtocolMixin:
|
||||
@@ -70,8 +70,7 @@ class ClientProtocolMixin:
|
||||
system_user = serializer.validated_data['system_user']
|
||||
|
||||
user = serializer.validated_data.get('user')
|
||||
if not user or not self.request.user.is_superuser:
|
||||
user = self.request.user
|
||||
user = user if user else self.request.user
|
||||
return asset, application, system_user, user
|
||||
|
||||
@staticmethod
|
||||
@@ -105,7 +104,7 @@ class ClientProtocolMixin:
|
||||
'bookmarktype:i': '3',
|
||||
'use redirection server name:i': '0',
|
||||
'smart sizing:i': '1',
|
||||
#'drivestoredirect:s': '*',
|
||||
# 'drivestoredirect:s': '*',
|
||||
# 'domain:s': ''
|
||||
# 'alternate shell:s:': '||MySQLWorkbench',
|
||||
# 'remoteapplicationname:s': 'Firefox',
|
||||
@@ -162,7 +161,6 @@ class ClientProtocolMixin:
|
||||
options['alternate shell:s'] = app
|
||||
options['remoteapplicationprogram:s'] = app
|
||||
options['remoteapplicationname:s'] = name
|
||||
options['remoteapplicationcmdline:s'] = '- ' + self.get_encrypt_cmdline(application)
|
||||
else:
|
||||
name = '*'
|
||||
|
||||
@@ -206,21 +204,6 @@ class ClientProtocolMixin:
|
||||
rst = rst.decode('ascii')
|
||||
return rst
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name, data = self.get_rdp_file_content(serializer)
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
def get_valid_serializer(self):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
@@ -252,6 +235,21 @@ class ClientProtocolMixin:
|
||||
}
|
||||
return data
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='rdp/file')
|
||||
def get_rdp_file(self, request, *args, **kwargs):
|
||||
if self.request.method == 'GET':
|
||||
data = self.request.query_params
|
||||
else:
|
||||
data = self.request.data
|
||||
serializer = self.get_serializer(data=data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name, data = self.get_rdp_file_content(serializer)
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
filename = "{}-{}-jumpserver.rdp".format(self.request.user.username, name)
|
||||
filename = urllib.parse.quote(filename)
|
||||
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
|
||||
return response
|
||||
|
||||
@action(methods=['POST', 'GET'], detail=False, url_path='client-url')
|
||||
def get_client_protocol_url(self, request, *args, **kwargs):
|
||||
serializer = self.get_valid_serializer()
|
||||
@@ -363,23 +361,7 @@ class TokenCacheMixin:
|
||||
""" endpoint smart view 用到此类来解析token中的资产、应用 """
|
||||
CACHE_KEY_PREFIX = 'CONNECTION_TOKEN_{}'
|
||||
|
||||
def get_token_cache_key(self, token):
|
||||
return self.CACHE_KEY_PREFIX.format(token)
|
||||
|
||||
def get_token_ttl(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
return cache.ttl(key)
|
||||
|
||||
def set_token_to_cache(self, token, value, ttl=5*60):
|
||||
key = self.get_token_cache_key(token)
|
||||
cache.set(key, value, timeout=ttl)
|
||||
|
||||
def get_token_from_cache(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
value = cache.get(key, None)
|
||||
return value
|
||||
|
||||
def renewal_token(self, token, ttl=5*60):
|
||||
def renewal_token(self, token, ttl=None):
|
||||
value = self.get_token_from_cache(token)
|
||||
if value:
|
||||
pre_ttl = self.get_token_ttl(token)
|
||||
@@ -396,23 +378,28 @@ class TokenCacheMixin:
|
||||
}
|
||||
return data
|
||||
|
||||
def get_token_ttl(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
return cache.ttl(key)
|
||||
|
||||
class UserConnectionTokenViewSet(
|
||||
def set_token_to_cache(self, token, value, ttl=None):
|
||||
key = self.get_token_cache_key(token)
|
||||
ttl = ttl or settings.CONNECTION_TOKEN_EXPIRATION
|
||||
cache.set(key, value, timeout=ttl)
|
||||
|
||||
def get_token_from_cache(self, token):
|
||||
key = self.get_token_cache_key(token)
|
||||
value = cache.get(key, None)
|
||||
return value
|
||||
|
||||
def get_token_cache_key(self, token):
|
||||
return self.CACHE_KEY_PREFIX.format(token)
|
||||
|
||||
|
||||
class BaseUserConnectionTokenViewSet(
|
||||
RootOrgViewMixin, SerializerMixin, ClientProtocolMixin,
|
||||
SecretDetailMixin, TokenCacheMixin, GenericViewSet
|
||||
TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'GET': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def check_resource_permission(user, asset, application, system_user):
|
||||
@@ -429,22 +416,7 @@ class UserConnectionTokenViewSet(
|
||||
raise PermissionDenied(error)
|
||||
return True
|
||||
|
||||
@action(methods=[PATCH], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
""" 续期 Token """
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
if not request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('No permissions for authentication.add_superconnectiontoken')
|
||||
token = request.data.get('token', '')
|
||||
data = self.renewal_token(token)
|
||||
status_code = 200 if data.get('ok') else 404
|
||||
return Response(data=data, status=status_code)
|
||||
|
||||
def create_token(self, user, asset, application, system_user, ttl=5*60):
|
||||
# 再次强调一下权限
|
||||
perm_required = 'authentication.add_superconnectiontoken'
|
||||
if user != self.request.user and not self.request.user.has_perm(perm_required):
|
||||
raise PermissionDenied('Only can create user token')
|
||||
def create_token(self, user, asset, application, system_user, ttl=None):
|
||||
self.check_resource_permission(user, asset, application, system_user)
|
||||
token = random_string(36)
|
||||
secret = random_string(16)
|
||||
@@ -489,6 +461,20 @@ class UserConnectionTokenViewSet(
|
||||
}
|
||||
return Response(data, status=201)
|
||||
|
||||
|
||||
class UserConnectionTokenViewSet(BaseUserConnectionTokenViewSet, SecretDetailMixin):
|
||||
serializer_classes = {
|
||||
'default': ConnectionTokenSerializer,
|
||||
'get_secret_detail': ConnectionTokenSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'GET': 'authentication.view_connectiontoken',
|
||||
'create': 'authentication.add_connectiontoken',
|
||||
'get_secret_detail': 'authentication.view_connectiontokensecret',
|
||||
'get_rdp_file': 'authentication.add_connectiontoken',
|
||||
'get_client_protocol_url': 'authentication.add_connectiontoken',
|
||||
}
|
||||
|
||||
def valid_token(self, token):
|
||||
from users.models import User
|
||||
from assets.models import SystemUser, Asset
|
||||
@@ -526,3 +512,23 @@ class UserConnectionTokenViewSet(
|
||||
if not value:
|
||||
return Response('', status=404)
|
||||
return Response(value)
|
||||
|
||||
|
||||
class UserSuperConnectionTokenViewSet(
|
||||
BaseUserConnectionTokenViewSet, TokenCacheMixin, GenericViewSet
|
||||
):
|
||||
serializer_classes = {
|
||||
'default': SuperConnectionTokenSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'create': 'authentication.add_superconnectiontoken',
|
||||
'renewal': 'authentication.add_superconnectiontoken'
|
||||
}
|
||||
|
||||
@action(methods=[PATCH], detail=False)
|
||||
def renewal(self, request, *args, **kwargs):
|
||||
""" 续期 Token """
|
||||
token = request.data.get('token', '')
|
||||
data = self.renewal_token(token)
|
||||
status_code = 200 if data.get('ok') else 404
|
||||
return Response(data=data, status=status_code)
|
||||
|
||||
@@ -2,7 +2,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
@@ -26,9 +26,8 @@ class DingTalkQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
||||
@@ -2,7 +2,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
@@ -26,7 +26,7 @@ class FeiShuQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
|
||||
@@ -27,8 +27,10 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
||||
def create(self, request, *args, **kwargs):
|
||||
self.create_session_if_need()
|
||||
# 如果认证没有过,检查账号密码
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
user = self.check_user_auth_if_need()
|
||||
user = self.get_user_or_auth(serializer.validated_data)
|
||||
self.check_user_mfa_if_need(user)
|
||||
self.check_user_login_confirm_if_need(user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
|
||||
@@ -2,7 +2,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from users.permissions import IsAuthPasswdTimeValid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.mixins.api import RoleUserMixin, RoleAdminMixin
|
||||
@@ -26,9 +26,8 @@ class WeComQRUnBindBase(APIView):
|
||||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsAuthPasswdTimeValid,)
|
||||
permission_classes = (IsAuthConfirmTimeValid,)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
||||
@@ -198,6 +198,6 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
||||
return None, None
|
||||
user, secret = key.user, str(key.secret)
|
||||
return user, secret
|
||||
except AccessKey.DoesNotExist:
|
||||
except (AccessKey.DoesNotExist, exceptions.ValidationError):
|
||||
return None, None
|
||||
|
||||
|
||||
@@ -157,6 +157,8 @@ class LDAPUser(_LDAPUser):
|
||||
|
||||
def _populate_user_from_attributes(self):
|
||||
for field, attr in self.settings.USER_ATTR_MAP.items():
|
||||
if field in ['groups']:
|
||||
continue
|
||||
try:
|
||||
value = self.attrs[attr][0]
|
||||
value = value.strip()
|
||||
|
||||
@@ -18,6 +18,7 @@ from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from users.utils import construct_user_email
|
||||
|
||||
from ..base import JMSBaseAuthBackend
|
||||
from .utils import validate_and_return_id_token, build_absolute_uri
|
||||
@@ -39,17 +40,22 @@ class UserMixin:
|
||||
logger.debug(log_prompt.format('start'))
|
||||
|
||||
sub = claims['sub']
|
||||
name = claims.get('name', sub)
|
||||
username = claims.get('preferred_username', sub)
|
||||
email = claims.get('email', "{}@{}".format(username, 'jumpserver.openid'))
|
||||
logger.debug(
|
||||
log_prompt.format(
|
||||
"sub: {}|name: {}|username: {}|email: {}".format(sub, name, username, email)
|
||||
)
|
||||
)
|
||||
|
||||
# Construct user attrs value
|
||||
user_attrs = {}
|
||||
for field, attr in settings.AUTH_OPENID_USER_ATTR_MAP.items():
|
||||
user_attrs[field] = claims.get(attr, sub)
|
||||
email = user_attrs.get('email', '')
|
||||
email = construct_user_email(user_attrs.get('username'), email)
|
||||
user_attrs.update({'email': email})
|
||||
|
||||
logger.debug(log_prompt.format(user_attrs))
|
||||
|
||||
username = user_attrs.get('username')
|
||||
name = user_attrs.get('name')
|
||||
|
||||
user, created = get_user_model().objects.get_or_create(
|
||||
username=username, defaults={"name": name, "email": email}
|
||||
username=username, defaults=user_attrs
|
||||
)
|
||||
logger.debug(log_prompt.format("user: {}|created: {}".format(user, created)))
|
||||
logger.debug(log_prompt.format("Send signal => openid create or update user"))
|
||||
@@ -103,21 +109,44 @@ class OIDCAuthCodeBackend(OIDCBaseBackend):
|
||||
# Prepares the token payload that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token payload'))
|
||||
"""
|
||||
The reason for need not client_id and client_secret in token_payload.
|
||||
OIDC protocol indicate client's token_endpoint_auth_method only accept one type in
|
||||
- client_secret_basic
|
||||
- client_secret_post
|
||||
- client_secret_jwt
|
||||
- private_key_jwt
|
||||
- none
|
||||
If the client offer more than one auth method type to OIDC, OIDC will auth client failed.
|
||||
OIDC default use client_secret_basic,
|
||||
this type only need in headers add Authorization=Basic xxx.
|
||||
|
||||
More info see: https://github.com/jumpserver/jumpserver/issues/8165
|
||||
More info see: https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
||||
"""
|
||||
token_payload = {
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': build_absolute_uri(
|
||||
request, path=reverse(settings.AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME)
|
||||
)
|
||||
}
|
||||
|
||||
# Prepares the token headers that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token headers'))
|
||||
basic_token = "{}:{}".format(settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET)
|
||||
headers = {"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())}
|
||||
if settings.AUTH_OPENID_CLIENT_AUTH_METHOD == 'client_secret_post':
|
||||
token_payload.update({
|
||||
'client_id': settings.AUTH_OPENID_CLIENT_ID,
|
||||
'client_secret': settings.AUTH_OPENID_CLIENT_SECRET,
|
||||
})
|
||||
headers = None
|
||||
else:
|
||||
# Prepares the token headers that will be used to request an authentication token to the
|
||||
# token endpoint of the OIDC provider.
|
||||
logger.debug(log_prompt.format('Prepares token headers'))
|
||||
basic_token = "{}:{}".format(
|
||||
settings.AUTH_OPENID_CLIENT_ID, settings.AUTH_OPENID_CLIENT_SECRET
|
||||
)
|
||||
headers = {
|
||||
"Authorization": "Basic {}".format(base64.b64encode(basic_token.encode()).decode())
|
||||
}
|
||||
|
||||
# Calls the token endpoint.
|
||||
logger.debug(log_prompt.format('Call the token endpoint'))
|
||||
@@ -258,6 +287,11 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
try:
|
||||
claims_response.raise_for_status()
|
||||
claims = claims_response.json()
|
||||
preferred_username = claims.get('preferred_username')
|
||||
if preferred_username and \
|
||||
preferred_username.lower() == username.lower() and \
|
||||
preferred_username != username:
|
||||
return
|
||||
except Exception as e:
|
||||
error = "Json claims response error, claims response " \
|
||||
"content is: {}, error is: {}".format(claims_response.content, str(e))
|
||||
@@ -286,5 +320,3 @@ class OIDCAuthPasswordBackend(OIDCBaseBackend):
|
||||
openid_user_login_failed.send(
|
||||
sender=self.__class__, request=request, username=username, reason="User is invalid"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -74,27 +74,37 @@ class PrepareRequestMixin:
|
||||
return idp_settings
|
||||
|
||||
@staticmethod
|
||||
def get_attribute_consuming_service():
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
|
||||
if attr_mapping and isinstance(attr_mapping, dict):
|
||||
attr_list = [
|
||||
{
|
||||
"name": sp_key,
|
||||
"friendlyName": idp_key, "isRequired": True
|
||||
}
|
||||
for idp_key, sp_key in attr_mapping.items()
|
||||
]
|
||||
request_attribute_template = {
|
||||
"attributeConsumingService": {
|
||||
"isDefault": False,
|
||||
"serviceName": "JumpServer",
|
||||
"serviceDescription": "JumpServer",
|
||||
"requestedAttributes": attr_list
|
||||
}
|
||||
def get_request_attributes():
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES or {}
|
||||
attr_map_reverse = {v: k for k, v in attr_mapping.items()}
|
||||
need_attrs = (
|
||||
('username', 'username', True),
|
||||
('email', 'email', True),
|
||||
('name', 'name', False),
|
||||
('phone', 'phone', False),
|
||||
('comment', 'comment', False),
|
||||
)
|
||||
attr_list = []
|
||||
for name, friend_name, is_required in need_attrs:
|
||||
rename_name = attr_map_reverse.get(friend_name)
|
||||
name = rename_name if rename_name else name
|
||||
attr_list.append({
|
||||
"name": name, "isRequired": is_required,
|
||||
"friendlyName": friend_name,
|
||||
})
|
||||
return attr_list
|
||||
|
||||
def get_attribute_consuming_service(self):
|
||||
attr_list = self.get_request_attributes()
|
||||
request_attribute_template = {
|
||||
"attributeConsumingService": {
|
||||
"isDefault": False,
|
||||
"serviceName": "JumpServer",
|
||||
"serviceDescription": "JumpServer",
|
||||
"requestedAttributes": attr_list
|
||||
}
|
||||
return request_attribute_template
|
||||
else:
|
||||
return {}
|
||||
}
|
||||
return request_attribute_template
|
||||
|
||||
@staticmethod
|
||||
def get_advanced_settings():
|
||||
@@ -167,11 +177,14 @@ class PrepareRequestMixin:
|
||||
|
||||
def get_attributes(self, saml_instance):
|
||||
user_attrs = {}
|
||||
attr_mapping = settings.SAML2_RENAME_ATTRIBUTES
|
||||
attrs = saml_instance.get_attributes()
|
||||
valid_attrs = ['username', 'name', 'email', 'comment', 'phone']
|
||||
|
||||
for attr, value in attrs.items():
|
||||
attr = attr.rsplit('/', 1)[-1]
|
||||
if attr_mapping and attr_mapping.get(attr):
|
||||
attr = attr_mapping.get(attr)
|
||||
if attr not in valid_attrs:
|
||||
continue
|
||||
user_attrs[attr] = self.value_to_str(value)
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
from django.db.models import TextChoices
|
||||
|
||||
RSA_PRIVATE_KEY = 'rsa_private_key'
|
||||
RSA_PUBLIC_KEY = 'rsa_public_key'
|
||||
|
||||
|
||||
class ConfirmType(TextChoices):
|
||||
RELOGIN = 'relogin', 'Re-Login'
|
||||
PASSWORD = 'password', 'Password'
|
||||
MFA = 'mfa', 'MFA'
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from captcha.fields import CaptchaField, CaptchaTextInput
|
||||
|
||||
from common.utils import get_logger, decrypt_password
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class EncryptedField(forms.CharField):
|
||||
def to_python(self, value):
|
||||
value = super().to_python(value)
|
||||
return decrypt_password(value)
|
||||
|
||||
|
||||
class UserLoginForm(forms.Form):
|
||||
days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24)
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or days_auto_login < 1
|
||||
disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE \
|
||||
or days_auto_login < 1
|
||||
|
||||
username = forms.CharField(
|
||||
label=_('Username'), max_length=100,
|
||||
@@ -18,7 +28,7 @@ class UserLoginForm(forms.Form):
|
||||
'autofocus': 'autofocus'
|
||||
})
|
||||
)
|
||||
password = forms.CharField(
|
||||
password = EncryptedField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=1024, strip=False
|
||||
)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import base64
|
||||
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import gen_key_pair
|
||||
|
||||
|
||||
class MFAMiddleware:
|
||||
"""
|
||||
@@ -41,10 +45,43 @@ class MFAMiddleware:
|
||||
class SessionCookieMiddleware(MiddlewareMixin):
|
||||
|
||||
@staticmethod
|
||||
def process_response(request, response: HttpResponse):
|
||||
def set_cookie_public_key(request, response):
|
||||
pub_key_name = settings.SESSION_RSA_PUBLIC_KEY_NAME
|
||||
public_key = request.session.get(pub_key_name)
|
||||
cookie_key = request.COOKIES.get(pub_key_name)
|
||||
if public_key and public_key == cookie_key:
|
||||
return
|
||||
|
||||
pri_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
|
||||
private_key, public_key = gen_key_pair()
|
||||
public_key_decode = base64.b64encode(public_key.encode()).decode()
|
||||
request.session[pub_key_name] = public_key_decode
|
||||
request.session[pri_key_name] = private_key
|
||||
response.set_cookie(pub_key_name, public_key_decode)
|
||||
|
||||
@staticmethod
|
||||
def set_cookie_session_prefix(request, response):
|
||||
key = settings.SESSION_COOKIE_NAME_PREFIX_KEY
|
||||
value = settings.SESSION_COOKIE_NAME_PREFIX
|
||||
if request.COOKIES.get(key) == value:
|
||||
return response
|
||||
response.set_cookie(key, value)
|
||||
|
||||
@staticmethod
|
||||
def set_cookie_session_expire(request, response):
|
||||
if not request.session.get('auth_session_expiration_required'):
|
||||
return
|
||||
value = 'age'
|
||||
if settings.SESSION_EXPIRE_AT_BROWSER_CLOSE_FORCE or \
|
||||
not request.session.get('auto_login', False):
|
||||
value = 'close'
|
||||
|
||||
age = request.session.get_expiry_age()
|
||||
response.set_cookie('jms_session_expire', value, max_age=age)
|
||||
request.session.pop('auth_session_expiration_required', None)
|
||||
|
||||
def process_response(self, request, response: HttpResponse):
|
||||
self.set_cookie_session_prefix(request, response)
|
||||
self.set_cookie_public_key(request, response)
|
||||
self.set_cookie_session_expire(request, response)
|
||||
return response
|
||||
|
||||
@@ -23,9 +23,7 @@ from acls.models import LoginACL
|
||||
from users.models import User
|
||||
from users.utils import LoginBlockUtil, MFABlockUtils, LoginIpBlockUtil
|
||||
from . import errors
|
||||
from .utils import rsa_decrypt, gen_key_pair
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
from .const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -58,6 +56,7 @@ def authenticate(request=None, **credentials):
|
||||
|
||||
for backend, backend_path in _get_backends(return_tuples=True):
|
||||
# 检查用户名是否允许认证 (预先检查,不浪费认证时间)
|
||||
logger.info('Try using auth backend: {}'.format(str(backend)))
|
||||
if not backend.username_allow_authenticate(username):
|
||||
continue
|
||||
|
||||
@@ -91,46 +90,8 @@ def authenticate(request=None, **credentials):
|
||||
auth.authenticate = authenticate
|
||||
|
||||
|
||||
class PasswordEncryptionViewMixin:
|
||||
request = None
|
||||
|
||||
def get_decrypted_password(self, password=None, username=None):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
|
||||
username = username or data.get('username')
|
||||
password = password or data.get('password')
|
||||
|
||||
password = self.decrypt_passwd(password)
|
||||
if not password:
|
||||
self.raise_password_decrypt_failed(username=username)
|
||||
return password
|
||||
|
||||
def raise_password_decrypt_failed(self, username):
|
||||
ip = self.get_request_ip()
|
||||
raise errors.CredentialError(
|
||||
error=errors.reason_password_decrypt_failed,
|
||||
username=username, ip=ip, request=self.request
|
||||
)
|
||||
|
||||
def decrypt_passwd(self, raw_passwd):
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if rsa_private_key is None:
|
||||
return raw_passwd
|
||||
|
||||
try:
|
||||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error(
|
||||
f'Decrypt password failed: password[{raw_passwd}] '
|
||||
f'rsa_private_key[{rsa_private_key}]'
|
||||
)
|
||||
return None
|
||||
class CommonMixin:
|
||||
request: Request
|
||||
|
||||
def get_request_ip(self):
|
||||
ip = ''
|
||||
@@ -139,26 +100,6 @@ class PasswordEncryptionViewMixin:
|
||||
ip = ip or get_request_ip(self.request)
|
||||
return ip
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
||||
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if not all([rsa_private_key, rsa_public_key]):
|
||||
rsa_private_key, rsa_public_key = gen_key_pair()
|
||||
rsa_public_key = rsa_public_key.replace('\n', '\\n')
|
||||
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
|
||||
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
|
||||
|
||||
kwargs.update({
|
||||
'rsa_public_key': rsa_public_key,
|
||||
})
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class CommonMixin(PasswordEncryptionViewMixin):
|
||||
request: Request
|
||||
get_request_ip: Callable
|
||||
|
||||
def raise_credential_error(self, error):
|
||||
raise self.partial_credential_error(error=error)
|
||||
|
||||
@@ -193,20 +134,13 @@ class CommonMixin(PasswordEncryptionViewMixin):
|
||||
user.backend = self.request.session.get("auth_backend")
|
||||
return user
|
||||
|
||||
def get_auth_data(self, decrypt_passwd=False):
|
||||
def get_auth_data(self, data):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
|
||||
items = ['username', 'password', 'challenge', 'public_key', 'auto_login']
|
||||
username, password, challenge, public_key, auto_login = bulk_get(data, items, default='')
|
||||
ip = self.get_request_ip()
|
||||
self._set_partial_credential_error(username=username, ip=ip, request=request)
|
||||
|
||||
if decrypt_passwd:
|
||||
password = self.get_decrypted_password()
|
||||
password = password + challenge.strip()
|
||||
return username, password, public_key, ip, auto_login
|
||||
|
||||
@@ -482,10 +416,10 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||
need = cache.get(self.key_prefix_captcha.format(ip))
|
||||
return need
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
def check_user_auth(self, valid_data=None):
|
||||
# pre check
|
||||
self.check_is_block()
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(decrypt_passwd)
|
||||
username, password, public_key, ip, auto_login = self.get_auth_data(valid_data)
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
|
||||
# check auth
|
||||
@@ -537,11 +471,12 @@ class AuthMixin(CommonMixin, AuthPreCheckMixin, AuthACLMixin, MFAMixin, AuthPost
|
||||
self.mark_password_ok(user, False)
|
||||
return user
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
def get_user_or_auth(self, valid_data):
|
||||
request = self.request
|
||||
if not request.session.get('auth_password'):
|
||||
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
|
||||
return self.get_user_from_session()
|
||||
if request.session.get('auth_password'):
|
||||
return self.get_user_from_session()
|
||||
else:
|
||||
return self.check_user_auth(valid_data)
|
||||
|
||||
def clear_auth_mark(self):
|
||||
keys = ['auth_password', 'user_id', 'auth_confirm', 'auth_ticket_id']
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .token import *
|
||||
from .connect_token import *
|
||||
from .password_mfa import *
|
||||
from .confirm import *
|
||||
|
||||
11
apps/authentication/serializers/confirm.py
Normal file
11
apps/authentication/serializers/confirm.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
from ..const import ConfirmType
|
||||
|
||||
|
||||
class ConfirmSerializer(serializers.Serializer):
|
||||
confirm_type = serializers.ChoiceField(
|
||||
required=True, choices=ConfirmType.choices
|
||||
)
|
||||
secret_key = EncryptedField()
|
||||
@@ -13,24 +13,16 @@ __all__ = [
|
||||
'ConnectionTokenUserSerializer', 'ConnectionTokenFilterRuleSerializer',
|
||||
'ConnectionTokenAssetSerializer', 'ConnectionTokenSystemUserSerializer',
|
||||
'ConnectionTokenDomainSerializer', 'ConnectionTokenRemoteAppSerializer',
|
||||
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer'
|
||||
'ConnectionTokenGatewaySerializer', 'ConnectionTokenSecretSerializer',
|
||||
'SuperConnectionTokenSerializer'
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenSerializer(serializers.Serializer):
|
||||
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
|
||||
system_user = serializers.CharField(max_length=128, required=True)
|
||||
asset = serializers.CharField(max_length=128, required=False)
|
||||
application = serializers.CharField(max_length=128, required=False)
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user_id):
|
||||
from users.models import User
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if user is None:
|
||||
raise serializers.ValidationError('user id not exist')
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
def validate_system_user(system_user_id):
|
||||
from assets.models import SystemUser
|
||||
@@ -65,6 +57,18 @@ class ConnectionTokenSerializer(serializers.Serializer):
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class SuperConnectionTokenSerializer(ConnectionTokenSerializer):
|
||||
user = serializers.CharField(max_length=128, required=False, allow_blank=True)
|
||||
|
||||
@staticmethod
|
||||
def validate_user(user_id):
|
||||
from users.models import User
|
||||
user = User.objects.filter(id=user_id).first()
|
||||
if user is None:
|
||||
raise serializers.ValidationError('user id not exist')
|
||||
return user
|
||||
|
||||
|
||||
class ConnectionTokenUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
@@ -114,7 +118,6 @@ class ConnectionTokenDomainSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CommandFilterRule
|
||||
fields = [
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
|
||||
__all__ = [
|
||||
'OtpVerifySerializer', 'MFAChallengeSerializer', 'MFASelectTypeSerializer',
|
||||
@@ -10,7 +12,7 @@ __all__ = [
|
||||
|
||||
|
||||
class PasswordVerifySerializer(serializers.Serializer):
|
||||
password = serializers.CharField()
|
||||
password = EncryptedField()
|
||||
|
||||
|
||||
class MFASelectTypeSerializer(serializers.Serializer):
|
||||
|
||||
@@ -35,6 +35,9 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
||||
session.delete()
|
||||
cache.set(lock_key, request.session.session_key, None)
|
||||
|
||||
# 标记登录,设置 cookie,前端可以控制刷新, Middleware 会拦截这个生成 cookie
|
||||
request.session['auth_session_expiration_required'] = 1
|
||||
|
||||
|
||||
@receiver(openid_user_login_success)
|
||||
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
<div class="contact-form col-md-10 col-md-offset-1">
|
||||
|
||||
<form id="login-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
<div style="line-height: 17px;margin-bottom: 20px;color: #999999;">
|
||||
@@ -240,21 +241,13 @@
|
||||
</body>
|
||||
{% include '_foot_js.html' %}
|
||||
<script type="text/javascript" src="/static/js/plugins/jsencrypt/jsencrypt.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/plugins/cryptojs/crypto-js.min.js"></script>
|
||||
<script type="text/javascript" src="/static/js/plugins/buffer/buffer.min.js"></script>
|
||||
<script>
|
||||
function encryptLoginPassword(password, rsaPublicKey) {
|
||||
if (!password) {
|
||||
return ''
|
||||
}
|
||||
var jsencrypt = new JSEncrypt(); //加密对象
|
||||
jsencrypt.setPublicKey(rsaPublicKey); // 设置密钥
|
||||
return jsencrypt.encrypt(password); //加密
|
||||
}
|
||||
|
||||
function doLogin() {
|
||||
//公钥加密
|
||||
var rsaPublicKey = "{{ rsa_public_key }}"
|
||||
var password = $('#password').val(); //明文密码
|
||||
var passwordEncrypted = encryptLoginPassword(password, rsaPublicKey)
|
||||
var passwordEncrypted = encryptPassword(password)
|
||||
$('#password-hidden').val(passwordEncrypted); //返回给密码输入input
|
||||
$('#login-form').submit(); //post提交
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
||||
router.register('sso', api.SSOViewSet, 'sso')
|
||||
router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
|
||||
router.register('connection-token', api.UserConnectionTokenViewSet, 'connection-token')
|
||||
router.register('super-connection-token', api.UserSuperConnectionTokenViewSet, 'super-connection-token')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -25,6 +26,7 @@ urlpatterns = [
|
||||
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'),
|
||||
|
||||
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
|
||||
path('confirm/', api.ConfirmViewSet.as_view(), name='user-confirm'),
|
||||
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
|
||||
path('mfa/verify/', api.MFAChallengeVerifyApi.as_view(), name='mfa-verify'),
|
||||
path('mfa/challenge/', api.MFAChallengeVerifyApi.as_view(), name='mfa-challenge'),
|
||||
|
||||
@@ -1,62 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import base64
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5
|
||||
from Cryptodome import Random
|
||||
|
||||
from django.conf import settings
|
||||
from .notifications import DifferentCityLoginMessage
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
|
||||
from common.utils import validate_ip, get_ip_city, get_request_ip
|
||||
from common.utils import get_logger
|
||||
from audits.models import UserLoginLog
|
||||
from audits.const import DEFAULT_CITY
|
||||
from .notifications import DifferentCityLoginMessage
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def gen_key_pair():
|
||||
""" 生成加密key
|
||||
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
|
||||
"""
|
||||
random_generator = Random.new().read
|
||||
rsa = RSA.generate(1024, random_generator)
|
||||
rsa_private_key = rsa.exportKey().decode()
|
||||
rsa_public_key = rsa.publickey().exportKey().decode()
|
||||
return rsa_private_key, rsa_public_key
|
||||
|
||||
|
||||
def rsa_encrypt(message, rsa_public_key):
|
||||
""" 加密登录密码 """
|
||||
key = RSA.importKey(rsa_public_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
|
||||
return cipher_text
|
||||
|
||||
|
||||
def rsa_decrypt(cipher_text, rsa_private_key=None):
|
||||
""" 解密登录密码 """
|
||||
if rsa_private_key is None:
|
||||
# rsa_private_key 为 None,可以能是API请求认证,不需要解密
|
||||
return cipher_text
|
||||
|
||||
key = RSA.importKey(rsa_private_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_decoded = base64.b64decode(cipher_text.encode())
|
||||
# Todo: 弄明白为何要以下这么写,https://xbuba.com/questions/57035263
|
||||
if len(cipher_decoded) == 127:
|
||||
hex_fixed = '00' + cipher_decoded.hex()
|
||||
cipher_decoded = base64.b16decode(hex_fixed.upper())
|
||||
message = cipher.decrypt(cipher_decoded, b'error').decode()
|
||||
return message
|
||||
|
||||
|
||||
def check_different_city_login_if_need(user, request):
|
||||
if not settings.SECURITY_CHECK_DIFFERENT_CITY_LOGIN:
|
||||
return
|
||||
|
||||
ip = get_request_ip(request) or '0.0.0.0'
|
||||
|
||||
if not (ip and validate_ip(ip)):
|
||||
city = DEFAULT_CITY
|
||||
else:
|
||||
|
||||
@@ -9,8 +9,9 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.utils import is_auth_confirm_time_valid
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
@@ -21,6 +22,7 @@ from authentication.mixins import AuthMixin
|
||||
from common.sdk.im.dingtalk import DingTalk
|
||||
from common.utils.common import get_request_ip
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
from .mixins import METAMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -117,17 +119,12 @@ class DingTalkOAuthMixin(DingTalkBaseMixin, View):
|
||||
|
||||
|
||||
class DingTalkQRBindView(DingTalkQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
@@ -200,14 +197,18 @@ class DingTalkEnableStartView(UserVerifyPasswordView):
|
||||
return success_url
|
||||
|
||||
|
||||
class DingTalkQRLoginView(DingTalkQRMixin, View):
|
||||
class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
redirect_url = request.GET.get('redirect_url') or reverse('index')
|
||||
next_url = self.get_next_url_from_meta() or reverse('index')
|
||||
|
||||
redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'next': next_url,
|
||||
})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -305,4 +306,4 @@ class DingTalkOAuthLoginCallbackView(AuthMixin, DingTalkOAuthMixin, View):
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
@@ -8,7 +8,7 @@ from django.db.utils import IntegrityError
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.models import User
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
@@ -89,17 +89,12 @@ class FeiShuQRMixin(PermissionsMixin, View):
|
||||
|
||||
|
||||
class FeiShuQRBindView(FeiShuQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
@@ -170,10 +165,11 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_url = request.GET.get('redirect_url') or reverse('index')
|
||||
redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -96,7 +96,7 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
||||
self.request.session.delete_test_cookie()
|
||||
|
||||
try:
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
self.check_user_auth(form.cleaned_data)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
self.set_login_failed_mark()
|
||||
@@ -219,7 +219,7 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
||||
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
try:
|
||||
user = self.check_user_auth_if_need()
|
||||
user = self.get_user_from_session()
|
||||
self.check_user_mfa_if_need(user)
|
||||
self.check_user_login_confirm_if_need(user)
|
||||
except (errors.CredentialError, errors.SessionEmptyError) as e:
|
||||
|
||||
12
apps/authentication/views/mixins.py
Normal file
12
apps/authentication/views/mixins.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
class METAMixin:
|
||||
def get_next_url_from_meta(self):
|
||||
request_meta = self.request.META or {}
|
||||
next_url = None
|
||||
referer = request_meta.get('HTTP_REFERER', '')
|
||||
next_url_item = referer.rsplit('next=', 1)
|
||||
if len(next_url_item) > 1:
|
||||
next_url = next_url_item[-1]
|
||||
return next_url
|
||||
@@ -9,8 +9,8 @@ from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.exceptions import APIException
|
||||
|
||||
from users.views import UserVerifyPasswordView
|
||||
from users.utils import is_auth_password_time_valid
|
||||
from users.models import User
|
||||
from users.permissions import IsAuthConfirmTimeValid
|
||||
from common.utils import get_logger, FlashMessageUtil
|
||||
from common.utils.random import random_string
|
||||
from common.utils.django import reverse, get_object_or_none
|
||||
@@ -21,6 +21,7 @@ from common.utils.common import get_request_ip
|
||||
from authentication import errors
|
||||
from authentication.mixins import AuthMixin
|
||||
from authentication.notifications import OAuthBindMessage
|
||||
from .mixins import METAMixin
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -117,17 +118,12 @@ class WeComOAuthMixin(WeComBaseMixin, View):
|
||||
|
||||
|
||||
class WeComQRBindView(WeComQRMixin, View):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
permission_classes = (IsAuthenticated, IsAuthConfirmTimeValid)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
user = request.user
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
if not is_auth_password_time_valid(request.session):
|
||||
msg = _('Please verify your password first')
|
||||
response = self.get_failed_response(redirect_url, msg, msg)
|
||||
return response
|
||||
|
||||
redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
|
||||
@@ -196,14 +192,17 @@ class WeComEnableStartView(UserVerifyPasswordView):
|
||||
return success_url
|
||||
|
||||
|
||||
class WeComQRLoginView(WeComQRMixin, View):
|
||||
class WeComQRLoginView(WeComQRMixin, METAMixin, View):
|
||||
permission_classes = (AllowAny,)
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
redirect_url = request.GET.get('redirect_url')
|
||||
|
||||
redirect_url = request.GET.get('redirect_url') or reverse('index')
|
||||
next_url = self.get_next_url_from_meta() or reverse('index')
|
||||
redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True)
|
||||
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
|
||||
redirect_uri += '?' + urlencode({
|
||||
'redirect_url': redirect_url,
|
||||
'next': next_url,
|
||||
})
|
||||
|
||||
url = self.get_qr_url(redirect_uri)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -301,4 +300,4 @@ class WeComOAuthLoginCallbackView(AuthMixin, WeComOAuthMixin, View):
|
||||
response = self.get_failed_response(login_url, title=msg, msg=msg)
|
||||
return response
|
||||
|
||||
return self.redirect_to_guard_view()
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.encoding import force_text
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from ..utils import signer, crypto
|
||||
from common.utils import signer, crypto
|
||||
|
||||
|
||||
__all__ = [
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import decrypt_password
|
||||
|
||||
__all__ = [
|
||||
'ReadableHiddenField',
|
||||
'ReadableHiddenField', 'EncryptedField'
|
||||
]
|
||||
|
||||
|
||||
@@ -23,3 +24,15 @@ class ReadableHiddenField(serializers.HiddenField):
|
||||
if hasattr(value, 'id'):
|
||||
return getattr(value, 'id')
|
||||
return value
|
||||
|
||||
|
||||
class EncryptedField(serializers.CharField):
|
||||
def __init__(self, write_only=None, **kwargs):
|
||||
if write_only is None:
|
||||
write_only = True
|
||||
kwargs['write_only'] = write_only
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def to_internal_value(self, value):
|
||||
value = super().to_internal_value(value)
|
||||
return decrypt_password(value)
|
||||
|
||||
@@ -8,10 +8,12 @@ from common.mixins import BulkListSerializerMixin
|
||||
from django.utils.functional import cached_property
|
||||
from rest_framework.utils.serializer_helpers import BindingDict
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
from common.drf.fields import EncryptedField
|
||||
|
||||
__all__ = [
|
||||
'MethodSerializer',
|
||||
'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer'
|
||||
'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer',
|
||||
'SecretReadableMixin'
|
||||
]
|
||||
|
||||
|
||||
@@ -83,3 +85,20 @@ class CeleryTaskSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
||||
|
||||
class SecretReadableMixin(serializers.Serializer):
|
||||
""" 加密字段 (EncryptedField) 可读性 """
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SecretReadableMixin, self).__init__(*args, **kwargs)
|
||||
if not hasattr(self, 'Meta') or not hasattr(self.Meta, 'extra_kwargs'):
|
||||
return
|
||||
extra_kwargs = self.Meta.extra_kwargs
|
||||
for field_name, serializer_field in self.fields.items():
|
||||
if not isinstance(serializer_field, EncryptedField):
|
||||
continue
|
||||
if field_name not in extra_kwargs:
|
||||
continue
|
||||
field_extra_kwargs = extra_kwargs[field_name]
|
||||
if 'write_only' not in field_extra_kwargs:
|
||||
continue
|
||||
serializer_field.write_only = field_extra_kwargs['write_only']
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from rest_framework import permissions
|
||||
from rest_framework.decorators import action
|
||||
@@ -7,8 +8,10 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from audits.utils import create_operate_log
|
||||
from audits.models import OperateLog
|
||||
|
||||
__all__ = ["PermissionsMixin"]
|
||||
__all__ = ["PermissionsMixin", "RecordViewLogMixin"]
|
||||
|
||||
|
||||
class PermissionsMixin(UserPassesTestMixin):
|
||||
@@ -24,3 +27,35 @@ class PermissionsMixin(UserPassesTestMixin):
|
||||
if not permission_class().has_permission(self.request, self):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class RecordViewLogMixin:
|
||||
ACTION = OperateLog.ACTION_VIEW
|
||||
|
||||
@staticmethod
|
||||
def get_resource_display(request):
|
||||
query_params = dict(request.query_params)
|
||||
if query_params.get('format'):
|
||||
query_params.pop('format')
|
||||
spm_filter = query_params.pop('spm') if query_params.get('spm') else None
|
||||
if not query_params and not spm_filter:
|
||||
display_message = _('Export all')
|
||||
elif spm_filter:
|
||||
display_message = _('Export only selected items')
|
||||
else:
|
||||
query = ','.join(
|
||||
['%s=%s' % (key, value) for key, value in query_params.items()]
|
||||
)
|
||||
display_message = _('Export filtered: %s') % query
|
||||
return display_message
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
response = super().list(request, *args, **kwargs)
|
||||
resource = self.get_resource_display(request)
|
||||
create_operate_log(self.ACTION, self.model, resource)
|
||||
return response
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
response = super().retrieve(request, *args, **kwargs)
|
||||
create_operate_log(self.ACTION, self.model, self.get_object())
|
||||
return response
|
||||
|
||||
@@ -9,4 +9,3 @@ from .crypto import *
|
||||
from .random import *
|
||||
from .jumpserver import *
|
||||
from .ip import *
|
||||
from .geoip import *
|
||||
|
||||
@@ -19,7 +19,7 @@ def get_redis_client(db=0):
|
||||
'password': CONFIG.REDIS_PASSWORD,
|
||||
'db': db,
|
||||
"ssl": is_true(CONFIG.REDIS_USE_SSL),
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
'ssl_cert_reqs': getattr(settings, 'REDIS_SSL_REQUIRED'),
|
||||
'ssl_keyfile': getattr(settings, 'REDIS_SSL_KEYFILE'),
|
||||
'ssl_certfile': getattr(settings, 'REDIS_SSL_CERTFILE'),
|
||||
'ssl_ca_certs': getattr(settings, 'REDIS_SSL_CA_CERTS'),
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import base64
|
||||
from Cryptodome.Cipher import AES
|
||||
import logging
|
||||
from Cryptodome.Cipher import AES, PKCS1_v1_5
|
||||
from Cryptodome.Util.Padding import pad
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome import Random
|
||||
from gmssl.sm4 import CryptSM4, SM4_ENCRYPT, SM4_DECRYPT
|
||||
|
||||
from django.conf import settings
|
||||
@@ -88,12 +91,13 @@ class AESCrypto:
|
||||
|
||||
def encrypt(self, text):
|
||||
aes = self.aes()
|
||||
return str(base64.encodebytes(aes.encrypt(self.to_16(text))),
|
||||
encoding='utf8').replace('\n', '') # 加密
|
||||
cipher = base64.encodebytes(aes.encrypt(self.to_16(text)))
|
||||
return str(cipher, encoding='utf8').replace('\n', '') # 加密
|
||||
|
||||
def decrypt(self, text):
|
||||
aes = self.aes()
|
||||
return str(aes.decrypt(base64.decodebytes(bytes(text, encoding='utf8'))).rstrip(b'\0').decode("utf8")) # 解密
|
||||
text_decoded = base64.decodebytes(bytes(text, encoding='utf8'))
|
||||
return str(aes.decrypt(text_decoded).rstrip(b'\0').decode("utf8"))
|
||||
|
||||
|
||||
class AESCryptoGCM:
|
||||
@@ -193,4 +197,72 @@ class Crypto:
|
||||
continue
|
||||
|
||||
|
||||
def gen_key_pair(length=1024):
|
||||
""" 生成加密key
|
||||
用于登录页面提交用户名/密码时,对密码进行加密(前端)/解密(后端)
|
||||
"""
|
||||
random_generator = Random.new().read
|
||||
rsa = RSA.generate(length, random_generator)
|
||||
rsa_private_key = rsa.exportKey().decode()
|
||||
rsa_public_key = rsa.publickey().exportKey().decode()
|
||||
return rsa_private_key, rsa_public_key
|
||||
|
||||
|
||||
def rsa_encrypt(message, rsa_public_key):
|
||||
""" 加密登录密码 """
|
||||
key = RSA.importKey(rsa_public_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_text = base64.b64encode(cipher.encrypt(message.encode())).decode()
|
||||
return cipher_text
|
||||
|
||||
|
||||
def rsa_decrypt(cipher_text, rsa_private_key=None):
|
||||
""" 解密登录密码 """
|
||||
if rsa_private_key is None:
|
||||
# rsa_private_key 为 None,可以能是API请求认证,不需要解密
|
||||
return cipher_text
|
||||
|
||||
key = RSA.importKey(rsa_private_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
cipher_decoded = base64.b64decode(cipher_text.encode())
|
||||
# Todo: 弄明白为何要以下这么写,https://xbuba.com/questions/57035263
|
||||
if len(cipher_decoded) == 127:
|
||||
hex_fixed = '00' + cipher_decoded.hex()
|
||||
cipher_decoded = base64.b16decode(hex_fixed.upper())
|
||||
message = cipher.decrypt(cipher_decoded, b'error').decode()
|
||||
return message
|
||||
|
||||
|
||||
def rsa_decrypt_by_session_pkey(value):
|
||||
from jumpserver.utils import current_request
|
||||
if not current_request:
|
||||
return value
|
||||
private_key_name = settings.SESSION_RSA_PRIVATE_KEY_NAME
|
||||
private_key = current_request.session.get(private_key_name)
|
||||
|
||||
if not private_key or not value:
|
||||
return value
|
||||
|
||||
try:
|
||||
value = rsa_decrypt(value, private_key)
|
||||
except Exception as e:
|
||||
logging.error('Decrypt field error: {}'.format(e))
|
||||
return value
|
||||
|
||||
|
||||
def decrypt_password(value):
|
||||
cipher = value.split(':')
|
||||
if len(cipher) != 2:
|
||||
return value
|
||||
key_cipher, password_cipher = cipher
|
||||
aes_key = rsa_decrypt_by_session_pkey(key_cipher)
|
||||
aes = get_aes_crypto(aes_key, 'ECB')
|
||||
try:
|
||||
password = aes.decrypt(password_cipher)
|
||||
except UnicodeDecodeError as e:
|
||||
logging.error("Decript password error: {}, {}".format(password_cipher, e))
|
||||
return value
|
||||
return password
|
||||
|
||||
|
||||
crypto = Crypto()
|
||||
|
||||
@@ -186,10 +186,27 @@ def make_signature(access_key_secret, date=None):
|
||||
return content_md5(data)
|
||||
|
||||
|
||||
def encrypt_password(password, salt=None):
|
||||
from passlib.hash import sha512_crypt
|
||||
if password:
|
||||
def encrypt_password(password, salt=None, algorithm='sha512'):
|
||||
from passlib.hash import sha512_crypt, des_crypt
|
||||
|
||||
def sha512():
|
||||
return sha512_crypt.using(rounds=5000).hash(password, salt=salt)
|
||||
|
||||
def des():
|
||||
return des_crypt.hash(password, salt=salt[:2])
|
||||
|
||||
support_algorithm = {
|
||||
'sha512': sha512, 'des': des
|
||||
}
|
||||
|
||||
if isinstance(algorithm, str):
|
||||
algorithm = algorithm.lower()
|
||||
|
||||
if algorithm not in support_algorithm.keys():
|
||||
algorithm = 'sha512'
|
||||
|
||||
if password and support_algorithm[algorithm]:
|
||||
return support_algorithm[algorithm]()
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
import csv
|
||||
|
||||
import pyzipper
|
||||
import requests
|
||||
|
||||
|
||||
def create_csv_file(filename, headers, rows, ):
|
||||
@@ -18,3 +20,11 @@ def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filenames
|
||||
for encrypted_filename in encrypted_filenames:
|
||||
with open(encrypted_filename, 'rb') as f:
|
||||
zf.writestr(os.path.basename(encrypted_filename), f.read())
|
||||
|
||||
|
||||
def download_file(src, path):
|
||||
with requests.get(src, stream=True) as r:
|
||||
r.raise_for_status()
|
||||
with open(path, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
1
apps/common/utils/ip/geoip/__init__.py
Normal file
1
apps/common/utils/ip/geoip/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .utils import *
|
||||
@@ -8,15 +8,11 @@ from geoip2.errors import GeoIP2Error
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
|
||||
__all__ = ['get_ip_city']
|
||||
__all__ = ['get_ip_city_by_geoip']
|
||||
reader = None
|
||||
|
||||
|
||||
def get_ip_city(ip):
|
||||
if not ip or '.' not in ip or not isinstance(ip, str):
|
||||
return _("Invalid ip")
|
||||
if ':' in ip:
|
||||
return 'IPv6'
|
||||
def get_ip_city_by_geoip(ip):
|
||||
global reader
|
||||
if reader is None:
|
||||
path = os.path.join(os.path.dirname(__file__), 'GeoLite2-City.mmdb')
|
||||
@@ -32,15 +28,13 @@ def get_ip_city(ip):
|
||||
try:
|
||||
response = reader.city(ip)
|
||||
except GeoIP2Error:
|
||||
return _("Unknown ip")
|
||||
return _("Unknown")
|
||||
|
||||
names = response.city.names
|
||||
if not names:
|
||||
names = response.country.names
|
||||
city_names = response.city.names or {}
|
||||
lang = settings.LANGUAGE_CODE[:2]
|
||||
if lang == 'zh':
|
||||
lang = 'zh-CN'
|
||||
city = city_names.get(lang, _("Unknown"))
|
||||
return city
|
||||
|
||||
if 'en' in settings.LANGUAGE_CODE and 'en' in names:
|
||||
return names['en']
|
||||
elif 'zh-CN' in names:
|
||||
return names['zh-CN']
|
||||
return _("Unknown ip")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from .model import *
|
||||
from .utils import *
|
||||
3
apps/common/utils/ip/ipip/ipipfree.ipdb
Normal file
3
apps/common/utils/ip/ipip/ipipfree.ipdb
Normal file
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b82b874152c798dda407ffe7544e1f5ec67efa1f5c334efc0d3893b8053b4be1
|
||||
size 3649897
|
||||
22
apps/common/utils/ip/ipip/utils.py
Normal file
22
apps/common/utils/ip/ipip/utils.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import os
|
||||
|
||||
import ipdb
|
||||
|
||||
__all__ = ['get_ip_city_by_ipip']
|
||||
ipip_db = None
|
||||
|
||||
|
||||
def get_ip_city_by_ipip(ip):
|
||||
global ipip_db
|
||||
if ipip_db is None:
|
||||
ipip_db_path = os.path.join(os.path.dirname(__file__), 'ipipfree.ipdb')
|
||||
ipip_db = ipdb.City(ipip_db_path)
|
||||
try:
|
||||
info = ipip_db.find_info(ip, 'CN')
|
||||
except ValueError:
|
||||
return None
|
||||
if not info:
|
||||
raise None
|
||||
return {'city': info.city_name, 'country': info.country_name}
|
||||
@@ -1,4 +1,9 @@
|
||||
from ipaddress import ip_network, ip_address
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .ipip import get_ip_city_by_ipip
|
||||
from .geoip import get_ip_city_by_geoip
|
||||
|
||||
|
||||
def is_ip_address(address):
|
||||
@@ -66,3 +71,21 @@ def contains_ip(ip, ip_group):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_ip_city(ip):
|
||||
if not ip or not isinstance(ip, str):
|
||||
return _("Invalid ip")
|
||||
if ':' in ip:
|
||||
return 'IPv6'
|
||||
|
||||
info = get_ip_city_by_ipip(ip)
|
||||
if info:
|
||||
city = info.get('city', _("Unknown"))
|
||||
country = info.get('country')
|
||||
|
||||
# 国内城市 并且 语言是中文就使用国内
|
||||
is_zh = settings.LANGUAGE_CODE.startswith('zh')
|
||||
if country == '中国' and is_zh:
|
||||
return city
|
||||
return get_ip_city_by_geoip(ip)
|
||||
@@ -32,6 +32,10 @@ def local_now_display(fmt='%Y-%m-%d %H:%M:%S'):
|
||||
return local_now().strftime(fmt)
|
||||
|
||||
|
||||
def local_now_date_display(fmt='%Y-%m-%d'):
|
||||
return local_now().strftime(fmt)
|
||||
|
||||
|
||||
_rest_dt_field = DateTimeField()
|
||||
dt_parser = _rest_dt_field.to_internal_value
|
||||
dt_formatter = _rest_dt_field.to_representation
|
||||
|
||||
@@ -161,6 +161,7 @@ class Config(dict):
|
||||
'SESSION_COOKIE_AGE': 3600 * 24,
|
||||
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
|
||||
'LOGIN_URL': reverse_lazy('authentication:login'),
|
||||
'CONNECTION_TOKEN_EXPIRATION': 5 * 60,
|
||||
|
||||
# Custom Config
|
||||
# Auth LDAP settings
|
||||
@@ -187,8 +188,13 @@ class Config(dict):
|
||||
'BASE_SITE_URL': None,
|
||||
'AUTH_OPENID_CLIENT_ID': 'client-id',
|
||||
'AUTH_OPENID_CLIENT_SECRET': 'client-secret',
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
|
||||
'AUTH_OPENID_CLIENT_AUTH_METHOD': 'client_secret_basic',
|
||||
'AUTH_OPENID_SHARE_SESSION': True,
|
||||
'AUTH_OPENID_IGNORE_SSL_VERIFICATION': True,
|
||||
'AUTH_OPENID_USER_ATTR_MAP': {
|
||||
'name': 'name', 'username': 'preferred_username', 'email': 'email'
|
||||
},
|
||||
|
||||
# OpenID 新配置参数 (version >= 1.5.9)
|
||||
'AUTH_OPENID_PROVIDER_ENDPOINT': 'https://oidc.example.com/',
|
||||
@@ -318,8 +324,7 @@ class Config(dict):
|
||||
# 保留(Luna还在用)
|
||||
'TERMINAL_MAGNUS_ENABLED': True,
|
||||
'TERMINAL_KOKO_SSH_ENABLED': True,
|
||||
# 保留(Luna还在用)
|
||||
'XRDP_ENABLED': True,
|
||||
'TERMINAL_RAZOR_ENABLED': True,
|
||||
|
||||
# 安全配置
|
||||
'SECURITY_MFA_AUTH': 0, # 0 不开启 1 全局开启 2 管理员开启
|
||||
@@ -387,6 +392,8 @@ class Config(dict):
|
||||
'FTP_LOG_KEEP_DAYS': 200,
|
||||
'CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS': 30,
|
||||
|
||||
'TICKETS_ENABLED': True,
|
||||
|
||||
# 废弃的
|
||||
'DEFAULT_ORG_SHOW_ALL_USERS': True,
|
||||
'ORG_CHANGE_TO_URL': '',
|
||||
|
||||
@@ -18,7 +18,7 @@ class RedisServer(_RedisServer):
|
||||
ssl_params = {}
|
||||
if CONFIG.REDIS_USE_SSL:
|
||||
ssl_params = {
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
'ssl_cert_reqs': getattr(settings, 'REDIS_SSL_REQUIRED'),
|
||||
'ssl_keyfile': getattr(settings, 'REDIS_SSL_KEYFILE'),
|
||||
'ssl_certfile': getattr(settings, 'REDIS_SSL_CERTFILE'),
|
||||
'ssl_ca_certs': getattr(settings, 'REDIS_SSL_CA_CERTS'),
|
||||
|
||||
@@ -55,6 +55,7 @@ AUTH_OPENID = CONFIG.AUTH_OPENID
|
||||
BASE_SITE_URL = CONFIG.BASE_SITE_URL
|
||||
AUTH_OPENID_CLIENT_ID = CONFIG.AUTH_OPENID_CLIENT_ID
|
||||
AUTH_OPENID_CLIENT_SECRET = CONFIG.AUTH_OPENID_CLIENT_SECRET
|
||||
AUTH_OPENID_CLIENT_AUTH_METHOD = CONFIG.AUTH_OPENID_CLIENT_AUTH_METHOD
|
||||
AUTH_OPENID_PROVIDER_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_AUTHORIZATION_ENDPOINT
|
||||
AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT = CONFIG.AUTH_OPENID_PROVIDER_TOKEN_ENDPOINT
|
||||
@@ -72,6 +73,7 @@ AUTH_OPENID_USE_NONCE = CONFIG.AUTH_OPENID_USE_NONCE
|
||||
AUTH_OPENID_SHARE_SESSION = CONFIG.AUTH_OPENID_SHARE_SESSION
|
||||
AUTH_OPENID_IGNORE_SSL_VERIFICATION = CONFIG.AUTH_OPENID_IGNORE_SSL_VERIFICATION
|
||||
AUTH_OPENID_ALWAYS_UPDATE_USER = CONFIG.AUTH_OPENID_ALWAYS_UPDATE_USER
|
||||
AUTH_OPENID_USER_ATTR_MAP = CONFIG.AUTH_OPENID_USER_ATTR_MAP
|
||||
AUTH_OPENID_AUTH_LOGIN_URL_NAME = 'authentication:openid:login'
|
||||
AUTH_OPENID_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:openid:login-callback'
|
||||
AUTH_OPENID_AUTH_LOGOUT_URL_NAME = 'authentication:openid:logout'
|
||||
@@ -147,6 +149,11 @@ AUTH_TEMP_TOKEN = CONFIG.AUTH_TEMP_TOKEN
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
||||
# Connection token
|
||||
CONNECTION_TOKEN_EXPIRATION = CONFIG.CONNECTION_TOKEN_EXPIRATION
|
||||
if CONNECTION_TOKEN_EXPIRATION < 5 * 60:
|
||||
# 最少5分钟
|
||||
CONNECTION_TOKEN_EXPIRATION = 5 * 60
|
||||
|
||||
|
||||
RBAC_BACKEND = 'rbac.backends.RBACBackend'
|
||||
|
||||
@@ -276,6 +276,11 @@ REDIS_SSL_CA_CERTS = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_ca.crt')
|
||||
if not os.path.exists(REDIS_SSL_CA_CERTS):
|
||||
REDIS_SSL_CA_CERTS = os.path.join(PROJECT_DIR, 'data', 'certs', 'redis_ca.pem')
|
||||
|
||||
if not os.path.exists(REDIS_SSL_CA_CERTS):
|
||||
REDIS_SSL_CA_CERTS = None
|
||||
|
||||
REDIS_SSL_REQUIRED = CONFIG.REDIS_SSL_REQUIRED or 'none'
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
# 'BACKEND': 'redis_cache.RedisCache',
|
||||
@@ -290,7 +295,7 @@ CACHES = {
|
||||
'OPTIONS': {
|
||||
"REDIS_CLIENT_KWARGS": {"health_check_interval": 30},
|
||||
"CONNECTION_POOL_KWARGS": {
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
|
||||
"ssl_keyfile": REDIS_SSL_KEYFILE,
|
||||
"ssl_certfile": REDIS_SSL_CERTFILE,
|
||||
"ssl_ca_certs": REDIS_SSL_CA_CERTS
|
||||
|
||||
@@ -119,6 +119,7 @@ CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABL
|
||||
|
||||
DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
TICKETS_ENABLED = CONFIG.TICKETS_ENABLED
|
||||
REFERER_CHECK_ENABLED = CONFIG.REFERER_CHECK_ENABLED
|
||||
|
||||
CONNECTION_TOKEN_ENABLED = CONFIG.CONNECTION_TOKEN_ENABLED
|
||||
@@ -138,7 +139,7 @@ LOGIN_REDIRECT_MSG_ENABLED = CONFIG.LOGIN_REDIRECT_MSG_ENABLED
|
||||
|
||||
CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS = CONFIG.CLOUD_SYNC_TASK_EXECUTION_KEEP_DAYS
|
||||
|
||||
XRDP_ENABLED = CONFIG.XRDP_ENABLED
|
||||
TERMINAL_RAZOR_ENABLED = CONFIG.TERMINAL_RAZOR_ENABLED
|
||||
TERMINAL_MAGNUS_ENABLED = CONFIG.TERMINAL_MAGNUS_ENABLED
|
||||
TERMINAL_KOKO_SSH_ENABLED = CONFIG.TERMINAL_KOKO_SSH_ENABLED
|
||||
|
||||
@@ -169,3 +170,6 @@ ANNOUNCEMENT = CONFIG.ANNOUNCEMENT
|
||||
# help
|
||||
HELP_DOCUMENT_URL = CONFIG.HELP_DOCUMENT_URL
|
||||
HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL
|
||||
|
||||
SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key'
|
||||
SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import os
|
||||
import ssl
|
||||
|
||||
from .base import REDIS_SSL_CA_CERTS, REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE
|
||||
from .base import REDIS_SSL_CA_CERTS, REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE, REDIS_SSL_REQUIRED
|
||||
from ..const import CONFIG, PROJECT_DIR
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
@@ -90,7 +90,8 @@ if not CONFIG.REDIS_USE_SSL:
|
||||
else:
|
||||
context = ssl.SSLContext()
|
||||
context.check_hostname = bool(CONFIG.REDIS_SSL_REQUIRED)
|
||||
context.load_verify_locations(REDIS_SSL_CA_CERTS)
|
||||
if REDIS_SSL_CA_CERTS:
|
||||
context.load_verify_locations(REDIS_SSL_CA_CERTS)
|
||||
if REDIS_SSL_CERTFILE and REDIS_SSL_KEYFILE:
|
||||
context.load_cert_chain(REDIS_SSL_CERTFILE, REDIS_SSL_KEYFILE)
|
||||
|
||||
@@ -140,7 +141,7 @@ CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = 3600
|
||||
if CONFIG.REDIS_USE_SSL:
|
||||
CELERY_BROKER_USE_SSL = CELERY_REDIS_BACKEND_USE_SSL = {
|
||||
'ssl_cert_reqs': CONFIG.REDIS_SSL_REQUIRED,
|
||||
'ssl_cert_reqs': REDIS_SSL_REQUIRED,
|
||||
'ssl_ca_certs': REDIS_SSL_CA_CERTS,
|
||||
'ssl_certfile': REDIS_SSL_CERTFILE,
|
||||
'ssl_keyfile': REDIS_SSL_KEYFILE
|
||||
|
||||
@@ -31,6 +31,7 @@ api_v1 = [
|
||||
app_view_patterns = [
|
||||
path('auth/', include('authentication.urls.view_urls'), name='auth'),
|
||||
path('ops/', include('ops.urls.view_urls'), name='ops'),
|
||||
path('tickets/', include('tickets.urls.view_urls'), name='tickets'),
|
||||
path('common/', include('common.urls.view_urls'), name='common'),
|
||||
re_path(r'flower/(?P<path>.*)', views.celery_flower_view, name='flower-view'),
|
||||
path('download/', views.ResourceDownload.as_view(), name='download'),
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f2c88ade4bfae213bdcdafad656af73f764e3b1b3f2b0c59aa39626e967730ca
|
||||
size 125911
|
||||
oid sha256:132e7f59a56d1cf5b2358b21b547861e872fa456164f2e0809120fb2b13f0ec1
|
||||
size 128122
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c75e0a1f2a047dac1374916c630bc0e8ef5ad5eea7518ffc21e93f747fc1235e
|
||||
size 104165
|
||||
oid sha256:002f6953ebbe368642f0ea3c383f617b5f998edf2238341be63393123d4be8a9
|
||||
size 105894
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.2.7 on 2019-12-17 09:58
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -18,17 +18,17 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='_become',
|
||||
field=common.fields.model.EncryptJsonDictCharField(blank=True, null=True, default='', max_length=1024, verbose_name='Become'),
|
||||
field=common.db.fields.EncryptJsonDictCharField(blank=True, null=True, default='', max_length=1024, verbose_name='Become'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='_options',
|
||||
field=common.fields.model.JsonDictCharField(default='', max_length=1024, verbose_name='Options'),
|
||||
field=common.db.fields.JsonDictCharField(default='', max_length=1024, verbose_name='Options'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='_tasks',
|
||||
field=common.fields.model.JsonListTextField(verbose_name='Tasks'),
|
||||
field=common.db.fields.JsonListTextField(verbose_name='Tasks'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='adhoc',
|
||||
@@ -48,12 +48,12 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adhocrunhistory',
|
||||
name='_result',
|
||||
field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc raw result'),
|
||||
field=common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc raw result'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='adhocrunhistory',
|
||||
name='_summary',
|
||||
field=common.fields.model.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc result summary'),
|
||||
field=common.db.fields.JsonDictTextField(blank=True, null=True, verbose_name='Adhoc result summary'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='adhocrunhistory',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 2.2.7 on 2020-01-06 07:34
|
||||
|
||||
import common.fields.model
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='adhoc',
|
||||
name='become',
|
||||
field=common.fields.model.EncryptJsonDictCharField(blank=True, default='', max_length=1024, null=True, verbose_name='Become'),
|
||||
field=common.db.fields.EncryptJsonDictCharField(blank=True, default='', max_length=1024, null=True, verbose_name='Become'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,11 +9,11 @@ from celery import current_task
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _, gettext
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.utils.translate import translate_value
|
||||
from common.fields.model import (
|
||||
from common.db.fields import (
|
||||
JsonListTextField, JsonDictCharField, EncryptJsonDictCharField,
|
||||
JsonDictTextField,
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from users.models import UserGroup, User
|
||||
from users.signals import pre_user_leave_org
|
||||
from applications.models import Application
|
||||
from terminal.models import Session
|
||||
from rbac.models import OrgRoleBinding, SystemRoleBinding
|
||||
from rbac.models import OrgRoleBinding, SystemRoleBinding, RoleBinding
|
||||
from assets.models import Asset, SystemUser, Domain, Gateway
|
||||
from orgs.caches import OrgResourceStatisticsCache
|
||||
from orgs.utils import current_org
|
||||
@@ -85,15 +85,17 @@ class OrgResourceStatisticsRefreshUtil:
|
||||
Node: ['nodes_amount'],
|
||||
Asset: ['assets_amount'],
|
||||
UserGroup: ['groups_amount'],
|
||||
RoleBinding: ['users_amount']
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def refresh_if_need(cls, instance):
|
||||
cache_field_name = cls.model_cache_field_mapper.get(type(instance))
|
||||
if cache_field_name:
|
||||
org_cache = OrgResourceStatisticsCache(instance.org)
|
||||
org_cache.expire(*cache_field_name)
|
||||
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
|
||||
if not cache_field_name:
|
||||
return
|
||||
OrgResourceStatisticsCache(Organization.root()).expire(*cache_field_name)
|
||||
if instance.org:
|
||||
OrgResourceStatisticsCache(instance.org).expire(*cache_field_name)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
|
||||
@@ -128,6 +128,7 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin,
|
||||
|
||||
nodes = PermNode.objects.none()
|
||||
assets = Asset.objects.none()
|
||||
all_tree_nodes = []
|
||||
|
||||
if not key:
|
||||
nodes = nodes_query_utils.get_top_level_nodes()
|
||||
@@ -142,7 +143,9 @@ class GrantedNodeChildrenWithAssetsAsTreeApiMixin(SerializeToTreeNodeMixin,
|
||||
|
||||
tree_nodes = self.serialize_nodes(nodes, with_asset_amount=True)
|
||||
tree_assets = self.serialize_assets(assets, key)
|
||||
return Response(data=[*tree_nodes, *tree_assets])
|
||||
all_tree_nodes.extend(tree_nodes)
|
||||
all_tree_nodes.extend(tree_assets)
|
||||
return Response(data=all_tree_nodes)
|
||||
|
||||
|
||||
class UserGrantedNodeChildrenWithAssetsAsTreeApi(AssetRoleAdminMixin, GrantedNodeChildrenWithAssetsAsTreeApiMixin):
|
||||
|
||||
@@ -9,14 +9,16 @@ from notifications.notifications import UserMessage
|
||||
|
||||
|
||||
class PermedAssetsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, assets):
|
||||
def __init__(self, user, assets, day_count=0):
|
||||
super().__init__(user)
|
||||
self.assets = assets
|
||||
self.day_count = day_count
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("You permed assets is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'items': [str(asset) for asset in self.assets],
|
||||
'item_type': _("permed assets"),
|
||||
'show_help': True
|
||||
@@ -38,10 +40,11 @@ class PermedAssetsWillExpireUserMsg(UserMessage):
|
||||
|
||||
class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
|
||||
def __init__(self, user, perms, org):
|
||||
def __init__(self, user, perms, org, day_count=0):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
self.day_count = day_count
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
@@ -59,6 +62,7 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
subject = _("Asset permissions is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'items_with_url': items_with_url,
|
||||
'item_type': _('asset permissions of organization {}').format(self.org)
|
||||
}
|
||||
@@ -81,14 +85,16 @@ class AssetPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
|
||||
|
||||
class PermedAppsWillExpireUserMsg(UserMessage):
|
||||
def __init__(self, user, apps):
|
||||
def __init__(self, user, apps, day_count=0):
|
||||
super().__init__(user)
|
||||
self.apps = apps
|
||||
self.day_count = day_count
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
subject = _("Your permed applications is about to expire")
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'item_type': _('permed applications'),
|
||||
'items': [str(app) for app in self.apps]
|
||||
}
|
||||
@@ -109,10 +115,11 @@ class PermedAppsWillExpireUserMsg(UserMessage):
|
||||
|
||||
|
||||
class AppPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
def __init__(self, user, perms, org):
|
||||
def __init__(self, user, perms, org, day_count=0):
|
||||
super().__init__(user)
|
||||
self.perms = perms
|
||||
self.org = org
|
||||
self.day_count = day_count
|
||||
|
||||
def get_items_with_url(self):
|
||||
items_with_url = []
|
||||
@@ -127,6 +134,7 @@ class AppPermsWillExpireForOrgAdminMsg(UserMessage):
|
||||
subject = _('Application permissions is about to expire')
|
||||
context = {
|
||||
'name': self.user.name,
|
||||
'count': self.day_count,
|
||||
'item_type': _('application permissions of organization {}').format(self.org),
|
||||
'items_with_url': items
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user