Merge pull request #13988 from jumpserver/dev

v4.1.0
pull/14052/head
Bryan 2024-08-16 18:40:35 +08:00 committed by GitHub
commit 56373e362b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
203 changed files with 7994 additions and 5027 deletions

View File

@ -8,3 +8,4 @@ celerybeat.pid
.vagrant/ .vagrant/
apps/xpack/.git apps/xpack/.git
.history/ .history/
.idea

60
.github/workflows/build-base-image.yml vendored Normal file
View File

@ -0,0 +1,60 @@
name: Build and Push Base Image
on:
push:
branches:
- 'pr*'
paths:
- 'poetry.lock'
- 'pyproject.toml'
- 'Dockerfile-base'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract date
id: vars
run: echo "IMAGE_TAG=$(date +'%Y%m%d_%H%M%S')" >> $GITHUB_ENV
- name: Extract repository name
id: repo
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
file: Dockerfile-base
tags: jumpserver/core-base:${{ env.IMAGE_TAG }}
- name: Update Dockerfile
run: |
sed -i 's|-base:.* AS stage-build|-base:${{ env.IMAGE_TAG }} AS stage-build|' Dockerfile
- name: Commit changes
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add Dockerfile
git commit -m "perf: Update Dockerfile with new base image tag"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,3 +1,4 @@
[settings] [settings]
line_length=120 line_length=120
known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets

View File

@ -1,101 +1,25 @@
FROM debian:bullseye-slim as stage-1 FROM jumpserver/core-base:20240808_054051 AS stage-build
ARG TARGETARCH
ARG DEPENDENCIES=" \
ca-certificates \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt
ARG CHECK_VERSION=v1.0.2
RUN set -ex \
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& mv check /usr/local/bin/ \
&& chown root:root /usr/local/bin/check \
&& chmod 755 /usr/local/bin/check \
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
ARG RECEPTOR_VERSION=v1.4.5
RUN set -ex \
&& wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
&& tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
&& chown root:root /usr/local/bin/receptor \
&& chmod 755 /usr/local/bin/receptor \
&& rm -f /opt/receptor.tar.gz
ARG VERSION ARG VERSION
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
ADD . . ADD . .
RUN echo > /opt/jumpserver/config.yml \ RUN echo > /opt/jumpserver/config.yml \
&& \ && \
if [ -n "${VERSION}" ]; then \ if [ -n "${VERSION}" ]; then \
sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" apps/jumpserver/const.py; \ sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" apps/jumpserver/const.py; \
fi fi
FROM python:3.11-slim-bullseye as stage-2
ARG TARGETARCH
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
default-libmysqlclient-dev \
freetds-dev \
gettext \
libkrb5-dev \
libldap2-dev \
libsasl2-dev"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
--mount=type=bind,source=poetry.lock,target=poetry.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install --only main
COPY --from=stage-1 /opt/jumpserver /opt/jumpserver
RUN set -ex \ RUN set -ex \
&& export SECRET_KEY=$(head -c100 < /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 48) \ && export SECRET_KEY=$(head -c100 < /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 48) \
&& . /opt/py3/bin/activate \ && . /opt/py3/bin/activate \
&& cd apps \ && cd apps \
&& python manage.py compilemessages && python manage.py compilemessages
FROM python:3.11-slim-bullseye FROM python:3.11-slim-bullseye
ARG TARGETARCH
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH PATH=/opt/py3/bin:$PATH
@ -110,32 +34,27 @@ ARG TOOLS=" \
sshpass \ sshpass \
bubblewrap" bubblewrap"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn ARG APT_MIRROR=http://deb.debian.org
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ RUN set -ex \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \ && rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ && sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \ && apt-get update > /dev/null \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& apt-get -y install --no-install-recommends ${TOOLS} \ && apt-get -y install --no-install-recommends ${TOOLS} \
&& apt-get clean \
&& mkdir -p /root/.ssh/ \ && mkdir -p /root/.ssh/ \
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
&& echo "no" | dpkg-reconfigure dash \ && echo "no" | dpkg-reconfigure dash \
&& sed -i "s@# export @export @g" ~/.bashrc \ && sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc && sed -i "s@# alias @alias @g" ~/.bashrc
COPY --from=stage-2 /opt /opt COPY --from=stage-build /opt /opt
COPY --from=stage-1 /usr/local/bin /usr/local/bin COPY --from=stage-build /usr/local/bin /usr/local/bin
COPY --from=stage-1 /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/ COPY --from=stage-build /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
ARG VERSION
ENV VERSION=$VERSION
VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/data
ENTRYPOINT ["./entrypoint.sh"] ENTRYPOINT ["./entrypoint.sh"]

54
Dockerfile-base Normal file
View File

@ -0,0 +1,54 @@
FROM python:3.11-slim-bullseye
ARG TARGETARCH
# Install APT dependencies
ARG DEPENDENCIES=" \
ca-certificates \
wget \
g++ \
make \
pkg-config \
default-libmysqlclient-dev \
freetds-dev \
gettext \
libkrb5-dev \
libldap2-dev \
libsasl2-dev"
ARG APT_MIRROR=http://deb.debian.org
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update > /dev/null \
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
# Install bin tools
ARG CHECK_VERSION=v1.0.3
RUN set -ex \
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& mv check /usr/local/bin/ \
&& chown root:root /usr/local/bin/check \
&& chmod 755 /usr/local/bin/check \
&& rm -f check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz
# Install Python dependencies
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
--mount=type=bind,source=poetry.lock,target=poetry.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install --only main

View File

@ -1,38 +1,12 @@
ARG VERSION ARG VERSION=dev
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
FROM python:3.11-slim-bullseye as build-core FROM jumpserver/core:${VERSION}-ce
ARG BUILD_DEPENDENCIES=" \
g++"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
set -ex \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
RUN --mount=type=cache,target=/root/.cache,sharing=locked,id=core \
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install --only xpack
FROM registry.fit2cloud.com/jumpserver/core:${VERSION}-ce
ARG TARGETARCH
ARG TOOLS=" \ ARG TOOLS=" \
g++ \
curl \ curl \
iputils-ping \ iputils-ping \
netcat-openbsd \ netcat-openbsd \
@ -41,12 +15,21 @@ ARG TOOLS=" \
vim \ vim \
wget" wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn ARG APT_MIRROR=http://deb.debian.org
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ RUN set -ex \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \ && rm -f /etc/apt/apt.conf.d/docker-clean \
set -ex \ && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& apt-get update \ && apt-get update \
&& apt-get -y install --no-install-recommends ${TOOLS} && apt-get -y install --no-install-recommends ${TOOLS} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
COPY poetry.lock pyproject.toml ./
RUN set -ex \
&& . /opt/py3/bin/activate \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry install --only xpack
COPY --from=build-core /opt/py3 /opt/py3
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack

View File

@ -10,15 +10,27 @@
[![][github-release-shield]][github-release-link] [![][github-release-shield]][github-release-link]
[![][github-stars-shield]][github-stars-link] [![][github-stars-shield]][github-stars-link]
**English** · [简体中文](./README.zh-CN.md) · [Documents][docs-link] · [Report Bug][github-issues-link] · [Request Feature][github-issues-link] **English** · [简体中文](./README.zh-CN.md)
</div> </div>
<br/> <br/>
## What is JumpServer? ## What is JumpServer?
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and Remote App endpoints through a web browser. JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
![JumpServer Overview](https://github.com/jumpserver/jumpserver/assets/41712985/eb9e6f39-3911-4d4a-bca9-aa50cc3b761d) ![JumpServer Overview](https://github.com/jumpserver/jumpserver/assets/32935519/35a371cb-8590-40ed-88ec-f351f8cf9045)
## Quickstart
Prepare a clean Linux Server ( 64 bit, >= 4c8g )
```sh
curl -sSL https://github.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash
```
Access JumpServer in your browser at `http://your-jumpserver-ip/`
- Username: `admin`
- Password: `ChangeMe`
## Screenshots ## Screenshots
@ -43,15 +55,6 @@ JumpServer is an open-source Privileged Access Management (PAM) tool that provid
</tr> </tr>
</table> </table>
## Quick Start
Prepare a clean Linux Server ( 64bit, >= 4c8g )
```
curl -sSL https://github.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash
```
Access JumpServer in your browser at `http://your-ip/`, `admin`/`admin`
## Components ## Components
JumpServer consists of multiple key components, which collectively form the functional framework of JumpServer, providing users with comprehensive capabilities for operations management and security control. JumpServer consists of multiple key components, which collectively form the functional framework of JumpServer, providing users with comprehensive capabilities for operations management and security control.
@ -63,12 +66,10 @@ JumpServer consists of multiple key components, which collectively form the func
| [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 Character Protocol Connector | | [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 Character Protocol Connector |
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector | | [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB | | [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer RDP Proxy Connector | | [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Remote Application Connector (Windows) | | [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Remote Application Connector (Linux) | | [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Database Proxy Connector | | [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
## Contributing ## Contributing
@ -91,8 +92,8 @@ 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. 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.
<!-- JumpServer official link --> <!-- JumpServer official link -->
[docs-link]: https://en-docs.jumpserver.org/ [docs-link]: https://jumpserver.com/docs
[discord-link]: https://discord.com/invite/jcM5tKWJ [discord-link]: https://discord.com/invite/W6vYXmAQG2
[contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md [contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md
<!-- JumpServer Other link--> <!-- JumpServer Other link-->

View File

@ -12,13 +12,13 @@
<p align="center"> <p align="center">
9 年时间,倾情投入,用心做好一款开源堡垒机。 10 年时间,倾情投入,用心做好一款开源堡垒机。
</p> </p>
------------------------------ ------------------------------
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。 ## JumpServer 是什么?
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括: JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
- **SSH**: Linux / Unix / 网络设备 等; - **SSH**: Linux / Unix / 网络设备 等;
- **Windows**: Web 方式连接 / 原生 RDP 连接; - **Windows**: Web 方式连接 / 原生 RDP 连接;
@ -38,6 +38,13 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
- **多租户**: 一套系统,多个子公司或部门同时使用; - **多租户**: 一套系统,多个子公司或部门同时使用;
- **云端存储**: 审计录像云端存储,永不丢失; - **云端存储**: 审计录像云端存储,永不丢失;
## 快速开始
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
- [产品文档](https://docs.jumpserver.org)
- [在线学习](https://edu.fit2cloud.com/page/2635362)
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
## UI 展示 ## UI 展示
![UI展示](https://docs.jumpserver.org/zh/v3/img/dashboard.png) ![UI展示](https://docs.jumpserver.org/zh/v3/img/dashboard.png)
@ -52,13 +59,6 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
| 请勿修改体验环境用户的密码! | | 请勿修改体验环境用户的密码! |
| 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! | | 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! |
## 快速开始
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
- [产品文档](https://docs.jumpserver.org)
- [在线学习](https://edu.fit2cloud.com/page/2635362)
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
## 案例研究 ## 案例研究
- [腾讯音乐娱乐集团基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2) - [腾讯音乐娱乐集团基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
@ -81,28 +81,24 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。 您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
### 参与贡献 ## 参与贡献
欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md) 欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
## 组件项目 ## 组件项目
| 项目 | 状态 | 描述 |
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [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 项目 |
| [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/) |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer RDP 代理 Connector 项目 |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Linux) |
| [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 项目 |
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core API 通信的组件项目 |
| [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 安装包 项目 |
| Project | Status | Description |
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| [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 Character Protocol Connector |
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
## 安全说明 ## 安全说明
JumpServer是一款安全产品请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/) JumpServer是一款安全产品请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/)

View File

@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

@ -5,6 +5,12 @@ type:
- AIX - AIX
method: change_secret method: change_secret
params: params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo - name: sudo
type: str type: str
label: 'Sudo' label: 'Sudo'
@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)' ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
en: 'Using Ansible module user to change account secret (DES)' en: 'Using Ansible module user to change account secret (DES)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text: Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label: Params home label:
zh: '家目录' zh: '家目录'
ja: 'ホームディレクトリ' ja: 'ホームディレクトリ'

View File

@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

@ -6,6 +6,12 @@ type:
- linux - linux
method: change_secret method: change_secret
params: params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo - name: sudo
type: str type: str
label: 'Sudo' label: 'Sudo'
@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)' ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
en: 'Using Ansible module user to change account secret (SHA512)' en: 'Using Ansible module user to change account secret (SHA512)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text: Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label: Params home label:
zh: '家目录' zh: '家目录'
ja: 'ホームディレクトリ' ja: 'ホームディレクトリ'

View File

@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _
from xlsxwriter import Workbook from xlsxwriter import Workbook
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
from accounts.models import ChangeSecretRecord from accounts.models import ChangeSecretRecord, BaseAccountQuerySet
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer from accounts.serializers import ChangeSecretRecordBackUpSerializer
from assets.const import HostTypes from assets.const import HostTypes
@ -68,10 +68,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
else: else:
return self.secret_generator(secret_type).get_secret() return self.secret_generator(secret_type).get_secret()
def get_accounts(self, privilege_account): def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
if not privilege_account: if not privilege_account:
print(f'not privilege account') print('Not privilege account')
return [] return
asset = privilege_account.asset asset = privilege_account.asset
accounts = asset.accounts.all() accounts = asset.accounts.all()
@ -108,6 +108,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
print(f'Windows {asset} does not support ssh key push') print(f'Windows {asset} does not support ssh key push')
return inventory_hosts return inventory_hosts
if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
host['ssh_params'] = {} host['ssh_params'] = {}
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
@ -226,6 +229,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
if self.secret_type and not self.check_secret(): if self.secret_type and not self.check_secret():
self.execution.status = 'success'
self.execution.date_finished = timezone.now()
self.execution.save()
return return
super().run(*args, **kwargs) super().run(*args, **kwargs)
recorders = list(self.name_recorder_mapper.values()) recorders = list(self.name_recorder_mapper.values())

View File

@ -31,7 +31,7 @@ class GatherAccountsFilter:
def posix_filter(info): def posix_filter(info):
username_pattern = re.compile(r'^(\S+)') username_pattern = re.compile(r'^(\S+)')
ip_pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})') ip_pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
login_time_pattern = re.compile(r'\w{3} \d{2} \d{2}:\d{2}:\d{2} \d{4}') login_time_pattern = re.compile(r'\w{3} \w{3}\s+\d{1,2} \d{2}:\d{2}:\d{2} \d{4}')
result = {} result = {}
for line in info: for line in info:
usernames = username_pattern.findall(line) usernames = username_pattern.findall(line)
@ -46,7 +46,8 @@ class GatherAccountsFilter:
result[username].update({'address': ip_addr}) result[username].update({'address': ip_addr})
login_times = login_time_pattern.findall(line) login_times = login_time_pattern.findall(line)
if login_times: if login_times:
date = timezone.datetime.strptime(f'{login_times[0]} +0800', '%b %d %H:%M:%S %Y %z') datetime_str = login_times[0].split(' ', 1)[1] + " +0800"
date = timezone.datetime.strptime(datetime_str, '%b %d %H:%M:%S %Y %z')
result[username].update({'date': date}) result[username].update({'date': date})
return result return result

View File

@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

@ -5,6 +5,12 @@ type:
- AIX - AIX
method: push_account method: push_account
params: params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo - name: sudo
type: str type: str
label: 'Sudo' label: 'Sudo'
@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)' ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
en: 'Using Ansible module user to push account (DES)' en: 'Using Ansible module user to push account (DES)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text: Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label: Params home label:
zh: '家目录' zh: '家目录'
ja: 'ホームディレクトリ' ja: 'ホームディレクトリ'

View File

@ -35,6 +35,17 @@
- user_info.failed - user_info.failed
- params.groups - params.groups
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed or params.modify_sudo
- params.sudo
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection

View File

@ -6,6 +6,12 @@ type:
- linux - linux
method: push_account method: push_account
params: params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo - name: sudo
type: str type: str
label: 'Sudo' label: 'Sudo'
@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)' ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
en: 'Using Ansible module user to push account (sha512)' en: 'Using Ansible module user to push account (sha512)'
Modify params sudo help text:
zh: '如果用户存在可以修改sudo权限'
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
en: 'If the user exists, sudo permissions can be modified'
Params sudo help text: Params sudo help text:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Modify sudo label:
zh: '修改 sudo 权限'
ja: 'sudo 権限を変更'
en: 'Modify sudo'
Params home label: Params home label:
zh: '家目录' zh: '家目录'
ja: 'ホームディレクトリ' ja: 'ホームディレクトリ'

View File

@ -10,7 +10,6 @@ import common.db.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
@ -26,13 +25,19 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('org_id',
('connectivity', models.CharField(choices=[('-', 'Unknown'), ('ok', 'OK'), ('err', 'Error')], default='-', max_length=16, verbose_name='Connectivity')), models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('connectivity',
models.CharField(choices=[('-', 'Unknown'), ('ok', 'OK'), ('err', 'Error')], default='-',
max_length=16, verbose_name='Connectivity')),
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, verbose_name='Secret type')), ('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
verbose_name='Secret type')),
('privileged', models.BooleanField(default=False, verbose_name='Privileged')), ('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('version', models.IntegerField(default=0, verbose_name='Version')), ('version', models.IntegerField(default=0, verbose_name='Version')),
@ -41,7 +46,11 @@ class Migration(migrations.Migration):
], ],
options={ options={
'verbose_name': 'Account', 'verbose_name': 'Account',
'permissions': [('view_accountsecret', 'Can view asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret'), ('verify_account', 'Can verify account'), ('push_account', 'Can push account'), ('remove_account', 'Can remove account')], 'permissions': [('view_accountsecret', 'Can view asset account secret'),
('view_historyaccount', 'Can view asset history account'),
('view_historyaccountsecret', 'Can view asset history account secret'),
('verify_account', 'Can verify account'), ('push_account', 'Can push account'),
('remove_account', 'Can remove account')],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -53,16 +62,21 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')),
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')), ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')),
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')), ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')),
('types', models.JSONField(default=list)), ('types', models.JSONField(default=list)),
('backup_type', models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email', max_length=128, verbose_name='Backup type')), ('backup_type',
models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email',
max_length=128, verbose_name='Backup type')),
('is_password_divided_by_email', models.BooleanField(default=True, verbose_name='Password divided')), ('is_password_divided_by_email', models.BooleanField(default=True, verbose_name='Password divided')),
('is_password_divided_by_obj_storage', models.BooleanField(default=True, verbose_name='Password divided')), ('is_password_divided_by_obj_storage',
('zip_encrypt_password', common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True, verbose_name='Zip encrypt password')), models.BooleanField(default=True, verbose_name='Password divided')),
('zip_encrypt_password', common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True,
verbose_name='Zip encrypt password')),
], ],
options={ options={
'verbose_name': 'Account backup plan', 'verbose_name': 'Account backup plan',
@ -72,12 +86,16 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='AccountBackupExecution', name='AccountBackupExecution',
fields=[ fields=[
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')), ('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')), ('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')),
('snapshot', models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True, verbose_name='Account backup snapshot')), ('snapshot',
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')), models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True,
verbose_name='Account backup snapshot')),
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')],
default='manual', max_length=128, verbose_name='Trigger mode')),
('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')), ('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')),
('is_success', models.BooleanField(default=False, verbose_name='Is success')), ('is_success', models.BooleanField(default=False, verbose_name='Is success')),
], ],
@ -95,13 +113,19 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')), ('secret_strategy',
models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')],
default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, verbose_name='Secret type')), ('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
verbose_name='Secret type')),
('privileged', models.BooleanField(default=False, verbose_name='Privileged')), ('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('auto_push', models.BooleanField(default=False, verbose_name='Auto push')), ('auto_push', models.BooleanField(default=False, verbose_name='Auto push')),
@ -142,7 +166,8 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('present', models.BooleanField(default=True, verbose_name='Present')), ('present', models.BooleanField(default=True, verbose_name='Present')),
('date_last_login', models.DateTimeField(null=True, verbose_name='Date login')), ('date_last_login', models.DateTimeField(null=True, verbose_name='Date login')),
('username', models.CharField(blank=True, db_index=True, max_length=32, verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=32, verbose_name='Username')),
@ -158,12 +183,16 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.UUIDField(db_index=True, default=uuid.uuid4)), ('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), ('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_type', models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, verbose_name='Secret type')), ('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
verbose_name='Secret type')),
('version', models.IntegerField(default=0, verbose_name='Version')), ('version', models.IntegerField(default=0, verbose_name='Version')),
('history_id', models.AutoField(primary_key=True, serialize=False)), ('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)), ('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)), ('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), ('history_type',
models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
], ],
options={ options={
'verbose_name': 'historical Account', 'verbose_name': 'historical Account',
@ -181,9 +210,13 @@ class Migration(migrations.Migration):
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('org_id',
('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'), ('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')), models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('alias', models.CharField(
choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'),
('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')),
('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')), ('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')),
], ],
options={'verbose_name': 'Virtual account'},
), ),
] ]

View File

@ -119,7 +119,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
return auth return auth
auth.update(self.make_account_ansible_vars(su_from)) auth.update(self.make_account_ansible_vars(su_from))
become_method = platform.su_method if platform.su_method else 'sudo'
become_method = platform.ansible_become_method
password = su_from.secret if become_method == 'sudo' else self.secret password = su_from.secret if become_method == 'sudo' else self.secret
auth['ansible_become'] = True auth['ansible_become'] = True
auth['ansible_become_method'] = become_method auth['ansible_become_method'] = become_method

View File

@ -81,21 +81,28 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
@staticmethod @staticmethod
def get_template_attr_for_account(template): def get_template_attr_for_account(template):
# Set initial data from template
field_names = [ field_names = [
'name', 'username', 'secret', 'push_params', 'name', 'username',
'secret_type', 'privileged', 'is_active' 'secret_type', 'secret',
'privileged', 'is_active'
] ]
field_map = {
'push_params': 'params',
'auto_push': 'push_now'
}
field_names.extend(field_map.keys())
attrs = {} attrs = {}
for name in field_names: for name in field_names:
value = getattr(template, name, None) value = getattr(template, name, None)
if value is None: if value is None:
continue continue
if name == 'push_params':
attrs['params'] = value attr_name = field_map.get(name, name)
else: attrs[attr_name] = value
attrs[name] = value
attrs['secret'] = template.get_secret() attrs['secret'] = template.get_secret()
return attrs return attrs
@ -178,7 +185,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
params = validated_data.pop('params', None) params = validated_data.pop('params', None)
self.clean_auth_fields(validated_data) self.clean_auth_fields(validated_data)
instance, stat = self.do_create(validated_data) instance, stat = self.do_create(validated_data)
self.push_account_if_need(instance, push_now, params, stat) if instance.source == Source.LOCAL:
self.push_account_if_need(instance, push_now, params, stat)
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -280,8 +288,8 @@ class AssetAccountBulkSerializer(
fields = [ fields = [
'name', 'username', 'secret', 'secret_type', 'passphrase', 'name', 'username', 'secret', 'secret_type', 'passphrase',
'privileged', 'is_active', 'comment', 'template', 'privileged', 'is_active', 'comment', 'template',
'on_invalid', 'push_now', 'assets', 'su_from_username', 'on_invalid', 'push_now', 'params', 'assets',
'source', 'source_id', 'su_from_username', 'source', 'source_id',
] ]
extra_kwargs = { extra_kwargs = {
'name': {'required': False}, 'name': {'required': False},
@ -419,16 +427,23 @@ class AssetAccountBulkSerializer(
return results return results
@staticmethod @staticmethod
def push_accounts_if_need(results, push_now): def push_accounts_if_need(results, push_now, params):
if not push_now: if not push_now:
return return
accounts = [str(v['instance']) for v in results if v.get('instance')]
push_accounts_to_assets_task.delay(accounts) account_ids = [v['instance'] for v in results if v.get('instance')]
accounts = Account.objects.filter(id__in=account_ids, source=Source.LOCAL)
if not accounts.exists():
return
account_ids = [str(_id) for _id in accounts.values_list('id', flat=True)]
push_accounts_to_assets_task.delay(account_ids, params)
def create(self, validated_data): def create(self, validated_data):
params = validated_data.pop('params', None)
push_now = validated_data.pop('push_now', False) push_now = validated_data.pop('push_now', False)
results = self.perform_bulk_create(validated_data) results = self.perform_bulk_create(validated_data)
self.push_accounts_if_need(results, push_now) self.push_accounts_if_need(results, push_now, params)
for res in results: for res in results:
res['asset'] = str(res['asset']) res['asset'] = str(res['asset'])
return results return results

View File

@ -6,6 +6,7 @@ from django.dispatch import receiver
from django.utils.translation import gettext_noop from django.utils.translation import gettext_noop
from accounts.backends import vault_client from accounts.backends import vault_client
from accounts.const import Source
from audits.const import ActivityChoices from audits.const import ActivityChoices
from audits.signal_handlers import create_activities from audits.signal_handlers import create_activities
from common.decorators import merge_delay_run from common.decorators import merge_delay_run
@ -32,7 +33,7 @@ def push_accounts_if_need(accounts=()):
template_accounts = defaultdict(list) template_accounts = defaultdict(list)
for ac in accounts: for ac in accounts:
# 再强调一次吧 # 再强调一次吧
if ac.source != 'template': if ac.source != Source.TEMPLATE:
continue continue
template_accounts[ac.source_id].append(ac) template_accounts[ac.source_id].append(ac)
@ -61,7 +62,7 @@ def create_accounts_activities(account, action='create'):
@receiver(post_save, sender=Account) @receiver(post_save, sender=Account)
def on_account_create_by_template(sender, instance, created=False, **kwargs): def on_account_create_by_template(sender, instance, created=False, **kwargs):
if not created or instance.source != 'template': if not created or instance.source != Source.TEMPLATE:
return return
push_accounts_if_need.delay(accounts=(instance,)) push_accounts_if_need.delay(accounts=(instance,))
create_accounts_activities(instance, action='create') create_accounts_activities(instance, action='create')

View File

@ -7,3 +7,4 @@ from .node import *
from .platform import * from .platform import *
from .protocol import * from .protocol import *
from .tree import * from .tree import *
from .my_asset import *

View File

@ -2,7 +2,7 @@ from typing import List
from rest_framework.request import Request from rest_framework.request import Request
from assets.models import Node, Platform, Protocol from assets.models import Node, Platform, Protocol, MyAsset
from assets.utils import get_node_from_request, is_query_node_all_assets from assets.utils import get_node_from_request, is_query_node_all_assets
from common.utils import lazyproperty, timeit from common.utils import lazyproperty, timeit
@ -82,6 +82,7 @@ class SerializeToTreeNodeMixin:
data = [] data = []
root_assets_count = 0 root_assets_count = 0
MyAsset.set_asset_custom_value(assets, self.request.user)
for asset in assets: for asset in assets:
platform = platform_map.get(asset.platform_id) platform = platform_map.get(asset.platform_id)
if not platform: if not platform:

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
#
from common.api import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from common.permissions import IsValidUser
from ..serializers import MyAssetSerializer
__all__ = ['MyAssetViewSet']
class MyAssetViewSet(CreateModelMixin, GenericViewSet):
serializer_class = MyAssetSerializer
permission_classes = (IsValidUser,)

View File

@ -1,3 +1,4 @@
from django.db.models import Count
from rest_framework import generics from rest_framework import generics
from rest_framework import serializers from rest_framework import serializers
from rest_framework.decorators import action from rest_framework.decorators import action
@ -5,7 +6,7 @@ from rest_framework.response import Response
from assets.const import AllTypes from assets.const import AllTypes
from assets.models import Platform, Node, Asset, PlatformProtocol from assets.models import Platform, Node, Asset, PlatformProtocol
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
from common.api import JMSModelViewSet from common.api import JMSModelViewSet
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.serializers import GroupedChoiceSerializer from common.serializers import GroupedChoiceSerializer
@ -17,6 +18,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
queryset = Platform.objects.all() queryset = Platform.objects.all()
serializer_classes = { serializer_classes = {
'default': PlatformSerializer, 'default': PlatformSerializer,
'list': PlatformListSerializer,
'categories': GroupedChoiceSerializer, 'categories': GroupedChoiceSerializer,
} }
filterset_fields = ['name', 'category', 'type'] filterset_fields = ['name', 'category', 'type']
@ -31,8 +33,8 @@ class AssetPlatformViewSet(JMSModelViewSet):
def get_queryset(self): def get_queryset(self):
# 因为没有走分页逻辑,所以需要这里 prefetch # 因为没有走分页逻辑,所以需要这里 prefetch
queryset = super().get_queryset().prefetch_related( queryset = super().get_queryset().annotate(assets_amount=Count('assets')).prefetch_related(
'protocols', 'automation', 'labels', 'labels__label', 'protocols', 'automation', 'labels', 'labels__label'
) )
queryset = queryset.filter(type__in=AllTypes.get_types_values()) queryset = queryset.filter(type__in=AllTypes.get_types_values())
return queryset return queryset

View File

@ -39,16 +39,16 @@ class NodeChildrenApi(generics.ListCreateAPIView):
self.instance = self.get_object() self.instance = self.get_object()
def perform_create(self, serializer): def perform_create(self, serializer):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if value:
children = self.instance.get_children()
if children.filter(value=value).exists():
raise JMSException(_('The same level node name cannot be the same'))
else:
value = self.instance.get_next_child_preset_name()
with NodeAddChildrenLock(self.instance): with NodeAddChildrenLock(self.instance):
data = serializer.validated_data
_id = data.get("id")
value = data.get("value")
if value:
children = self.instance.get_children()
if children.filter(value=value).exists():
raise JMSException(_('The same level node name cannot be the same'))
else:
value = self.instance.get_next_child_preset_name()
node = self.instance.create_child(value=value, _id=_id) node = self.instance.create_child(value=value, _id=_id)
# 避免查询 full value # 避免查询 full value
node._full_value = node.value node._full_value = node.value

View File

@ -113,11 +113,7 @@ class BasePlaybookManager:
if not data: if not data:
data = automation_params.get(method_id, {}) data = automation_params.get(method_id, {})
params = serializer(data).data params = serializer(data).data
return { return params
field_name: automation_params.get(field_name, '')
if not params[field_name] else params[field_name]
for field_name in params
}
@property @property
def platform_automation_methods(self): def platform_automation_methods(self):

View File

@ -12,7 +12,12 @@
cpu_cores: "{{ ansible_processor_cores }}" cpu_cores: "{{ ansible_processor_cores }}"
cpu_vcpus: "{{ ansible_processor_vcpus }}" cpu_vcpus: "{{ ansible_processor_vcpus }}"
memory: "{{ ansible_memtotal_mb / 1024 | round(2) }}" memory: "{{ ansible_memtotal_mb / 1024 | round(2) }}"
disk_total: "{{ (ansible_mounts | map(attribute='size_total') | sum / 1024 / 1024 / 1024) | round(2) }}" disk_total: |-
{% set ns = namespace(total=0) %}
{%- for name, dev in ansible_devices.items() if dev.removable == '0' and dev.host != '' -%}
{%- set ns.total = ns.total + ( dev.sectors | int * dev.sectorsize | int ) -%}
{%- endfor -%}
{{- (ns.total / 1024 / 1024 / 1024) | round(2) -}}
distribution: "{{ ansible_distribution }}" distribution: "{{ ansible_distribution }}"
distribution_version: "{{ ansible_distribution_version }}" distribution_version: "{{ ansible_distribution_version }}"
arch: "{{ ansible_architecture }}" arch: "{{ ansible_architecture }}"

View File

@ -11,8 +11,10 @@
vendor: "{{ ansible_system_vendor }}" vendor: "{{ ansible_system_vendor }}"
model: "{{ ansible_product_name }}" model: "{{ ansible_product_name }}"
sn: "{{ ansible_product_serial }}" sn: "{{ ansible_product_serial }}"
cpu_count: "{{ ansible_processor_count }}"
cpu_cores: "{{ ansible_processor_cores }}"
cpu_vcpus: "{{ ansible_processor_vcpus }}" cpu_vcpus: "{{ ansible_processor_vcpus }}"
memory: "{{ ansible_memtotal_mb }}" memory: "{{ (ansible_memtotal_mb / 1024) | round(2) }}"
- debug: - debug:
var: info var: info

View File

@ -2,5 +2,6 @@ from .automation import *
from .base import * from .base import *
from .category import * from .category import *
from .host import * from .host import *
from .platform import *
from .protocol import * from .protocol import *
from .types import * from .types import *

View File

@ -117,5 +117,6 @@ class DatabaseTypes(BaseType):
@classmethod @classmethod
def get_community_types(cls): def get_community_types(cls):
return [ return [
cls.MYSQL, cls.MARIADB, cls.MONGODB, cls.REDIS cls.MYSQL, cls.MARIADB, cls.POSTGRESQL,
cls.MONGODB, cls.REDIS,
] ]

View File

@ -19,7 +19,7 @@ class HostTypes(BaseType):
'charset': 'utf-8', # default 'charset': 'utf-8', # default
'domain_enabled': True, 'domain_enabled': True,
'su_enabled': True, 'su_enabled': True,
'su_methods': ['sudo', 'su'], 'su_methods': ['sudo', 'su', 'only_sudo', 'only_su'],
}, },
cls.WINDOWS: { cls.WINDOWS: {
'su_enabled': False, 'su_enabled': False,

View File

@ -0,0 +1,11 @@
from django.db.models import TextChoices
class SuMethodChoices(TextChoices):
sudo = "sudo", "sudo su -"
su = "su", "su - "
only_sudo = "only_sudo", "sudo su"
only_su = "only_su", "su"
enable = "enable", "enable"
super = "super", "super 15"
super_level = "super_level", "super level 15"

View File

@ -80,7 +80,18 @@ class Protocol(ChoicesMixin, models.TextChoices):
'choices': [('any', _('Any')), ('rdp', 'RDP'), ('tls', 'TLS'), ('nla', 'NLA')], 'choices': [('any', _('Any')), ('rdp', 'RDP'), ('tls', 'TLS'), ('nla', 'NLA')],
'default': 'any', 'default': 'any',
'label': _('Security'), 'label': _('Security'),
'help_text': _("Security layer to use for the connection") 'help_text': _("Security layer to use for the connection:<br>"
"Any<br>"
"Automatically select the security mode based on the security protocols "
"supported by both the client and the server<br>"
"RDP<br>"
"Legacy RDP encryption. This mode is generally only used for older Windows "
"servers or in cases where a standard Windows login screen is desired<br>"
"TLS<br>"
"RDP authentication and encryption implemented via TLS.<br>"
"NLA<br>"
"This mode uses TLS encryption and requires the username and password "
"to be given in advance")
}, },
'ad_domain': { 'ad_domain': {
'type': 'str', 'type': 'str',
@ -208,6 +219,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': 'admin', 'default': 'admin',
'label': _('Auth source'), 'label': _('Auth source'),
'help_text': _('The database to authenticate against') 'help_text': _('The database to authenticate against')
},
'connection_options': {
'type': 'str',
'default': '',
'label': _('Connect options'),
'help_text': _('The connection specific options eg. retryWrites=false&retryReads=false')
} }
} }
}, },
@ -289,23 +306,17 @@ class Protocol(ChoicesMixin, models.TextChoices):
'setting': { 'setting': {
'api_mode': { 'api_mode': {
'type': 'choice', 'type': 'choice',
'default': 'gpt-3.5-turbo', 'default': 'gpt-4o-mini',
'label': _('API mode'), 'label': _('API mode'),
'choices': [ 'choices': [
('gpt-3.5-turbo', 'GPT-3.5 Turbo'), ('gpt-4o-mini', 'GPT-4o-mini'),
('gpt-3.5-turbo-1106', 'GPT-3.5 Turbo 1106'), ('gpt-4o', 'GPT-4o'),
('gpt-4-turbo', 'GPT-4 Turbo'),
] ]
} }
} }
} }
} }
if settings.XPACK_LICENSE_IS_VALID:
choices = protocols[cls.chatgpt]['setting']['api_mode']['choices']
choices.extend([
('gpt-4', 'GPT-4'),
('gpt-4-turbo', 'GPT-4 Turbo'),
('gpt-4o', 'GPT-4o'),
])
return protocols return protocols
@classmethod @classmethod

View File

@ -171,12 +171,9 @@ class AllTypes(ChoicesMixin):
(Category.DEVICE, DeviceTypes), (Category.DEVICE, DeviceTypes),
(Category.DATABASE, DatabaseTypes), (Category.DATABASE, DatabaseTypes),
(Category.WEB, WebTypes), (Category.WEB, WebTypes),
(Category.CLOUD, CloudTypes),
(Category.CUSTOM, CustomTypes)
] ]
if settings.XPACK_ENABLED:
types.extend([
(Category.CLOUD, CloudTypes),
(Category.CUSTOM, CustomTypes),
])
return types return types
@classmethod @classmethod

View File

@ -0,0 +1,28 @@
# Generated by Django 4.1.13 on 2024-07-09 10:19
from django.db import migrations
def migrate_platform_protocol_primary(apps, schema_editor):
platform_model = apps.get_model('assets', 'Platform')
platforms = platform_model.objects.all()
for platform in platforms:
p = platform.protocols.filter(primary=True).first()
if p:
continue
p = platform.protocols.first()
if not p:
continue
p.primary = True
p.save()
class Migration(migrations.Migration):
dependencies = [
('assets', '0003_auto_20180109_2331'),
]
operations = [
migrations.RunPython(migrate_platform_protocol_primary)
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.1.13 on 2024-08-06 09:11
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0004_auto_20240709_1819'),
]
operations = [
migrations.CreateModel(
name='MyAsset',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(default='', max_length=128, verbose_name='Custom Name')),
('comment', models.CharField(default='', max_length=512, verbose_name='Custom Comment')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='my_assets', to='assets.asset')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'My assets',
'unique_together': {('user', 'asset')},
},
),
]

View File

@ -7,3 +7,4 @@ from .domain import *
from .node import * from .node import *
from .favorite_asset import * from .favorite_asset import *
from .automations import * from .automations import *
from .my_asset import *

View File

@ -173,7 +173,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
def get_labels(self): def get_labels(self):
from labels.models import Label, LabeledResource from labels.models import Label, LabeledResource
res_type = ContentType.objects.get_for_model(self.__class__) res_type = ContentType.objects.get_for_model(self.__class__.label_model())
label_ids = LabeledResource.objects.filter(res_type=res_type, res_id=self.id) \ label_ids = LabeledResource.objects.filter(res_type=res_type, res_id=self.id) \
.values_list('label_id', flat=True) .values_list('label_id', flat=True)
return Label.objects.filter(id__in=label_ids) return Label.objects.filter(id__in=label_ids)

View File

@ -31,7 +31,7 @@ class Domain(LabeledMixin, JMSOrgBaseModel):
@lazyproperty @lazyproperty
def assets_amount(self): def assets_amount(self):
return self.assets.count() return self.assets.exclude(platform__name='Gateway').count()
def random_gateway(self): def random_gateway(self):
gateways = [gw for gw in self.active_gateways if gw.is_connective] gateways = [gw for gw in self.active_gateways if gw.is_connective]

View File

@ -0,0 +1,43 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel
__all__ = ['MyAsset']
class MyAsset(JMSBaseModel):
user = models.ForeignKey('users.User', on_delete=models.CASCADE)
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, related_name='my_assets')
name = models.CharField(verbose_name=_("Custom Name"), max_length=128, default='')
comment = models.CharField(verbose_name=_("Custom Comment"), max_length=512, default='')
custom_fields = ['name', 'comment']
class Meta:
unique_together = ('user', 'asset')
verbose_name = _("My assets")
def custom_to_dict(self):
data = {}
for field in self.custom_fields:
value = getattr(self, field)
if value == "":
continue
data.update({field: value})
return data
@staticmethod
def set_asset_custom_value(assets, user):
my_assets = MyAsset.objects.filter(asset__in=assets, user=user).all()
customs = {my_asset.asset.id: my_asset.custom_to_dict() for my_asset in my_assets}
for asset in assets:
custom = customs.get(asset.id)
if not custom:
continue
for field, value in custom.items():
if not hasattr(asset, field):
continue
setattr(asset, field, value)
def __str__(self):
return f'{self.user}-{self.asset}'

View File

@ -1,7 +1,7 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from assets.const import AllTypes, Category, Protocol from assets.const import AllTypes, Category, Protocol, SuMethodChoices
from common.db.fields import JsonDictTextField from common.db.fields import JsonDictTextField
from common.db.models import JMSBaseModel from common.db.models import JMSBaseModel
@ -111,6 +111,10 @@ class Platform(LabeledMixin, JMSBaseModel):
def type_constraints(self): def type_constraints(self):
return AllTypes.get_constraints(self.category, self.type) return AllTypes.get_constraints(self.category, self.type)
@lazyproperty
def assets_amount(self):
return self.assets.count()
@classmethod @classmethod
def default(cls): def default(cls):
linux, created = cls.objects.get_or_create( linux, created = cls.objects.get_or_create(
@ -127,6 +131,17 @@ class Platform(LabeledMixin, JMSBaseModel):
return True return True
return False return False
@property
def ansible_become_method(self):
su_method = self.su_method or SuMethodChoices.sudo
if su_method in [SuMethodChoices.sudo, SuMethodChoices.only_sudo]:
method = SuMethodChoices.sudo
elif su_method in [SuMethodChoices.su, SuMethodChoices.only_su]:
method = SuMethodChoices.su
else:
method = su_method
return method
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -9,3 +9,4 @@ from .favorite_asset import *
from .gateway import * from .gateway import *
from .node import * from .node import *
from .platform import * from .platform import *
from .my_asset import *

View File

@ -36,6 +36,7 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe
class AutomationExecutionSerializer(serializers.ModelSerializer): class AutomationExecutionSerializer(serializers.ModelSerializer):
snapshot = serializers.SerializerMethodField(label=_('Automation snapshot')) snapshot = serializers.SerializerMethodField(label=_('Automation snapshot'))
status = serializers.SerializerMethodField(label=_("Status"))
trigger = LabeledChoiceField(choices=Trigger.choices, read_only=True, label=_("Trigger mode")) trigger = LabeledChoiceField(choices=Trigger.choices, read_only=True, label=_("Trigger mode"))
class Meta: class Meta:
@ -45,6 +46,14 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
] ]
fields = ['id', 'automation'] + read_only_fields fields = ['id', 'automation'] + read_only_fields
@staticmethod
def get_status(obj):
if obj.status == 'success':
return _("Success")
elif obj.status == 'pending':
return _("Pending")
return obj.status
@staticmethod @staticmethod
def get_snapshot(obj): def get_snapshot(obj):
from assets.const import AutomationTypes as AssetTypes from assets.const import AutomationTypes as AssetTypes

View File

@ -57,9 +57,7 @@ class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
queryset = queryset \ queryset = queryset.prefetch_related('labels', 'labels__label')
.annotate(assets_amount=Count('assets')) \
.prefetch_related('labels', 'labels__label')
return queryset return queryset

View File

@ -3,7 +3,6 @@
from rest_framework import serializers from rest_framework import serializers
from orgs.utils import tmp_to_root_org
from common.serializers import BulkSerializerMixin from common.serializers import BulkSerializerMixin
from ..models import FavoriteAsset from ..models import FavoriteAsset

View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from ..models import MyAsset
__all__ = ['MyAssetSerializer']
class MyAssetSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)
name = serializers.CharField(label=_("Custom Name"), max_length=128, allow_blank=True, required=False)
comment = serializers.CharField(label=_("Custom Comment"), max_length=512, allow_blank=True, required=False)
class Meta:
model = MyAsset
fields = ['user', 'asset', 'name', 'comment']
validators = []
def create(self, data):
custom_fields = MyAsset.custom_fields
asset = data['asset']
user = self.context['request'].user
defaults = {field: data.get(field, '') for field in custom_fields}
obj, created = MyAsset.objects.get_or_create(defaults=defaults, user=user, asset=asset)
if created:
return obj
for field in custom_fields:
value = data.get(field)
if value is None:
continue
setattr(obj, field, value)
obj.save()
return obj

View File

@ -3,16 +3,17 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from assets.models import Asset
from common.serializers import ( from common.serializers import (
WritableNestedModelSerializer, type_field_map, MethodSerializer, WritableNestedModelSerializer, type_field_map, MethodSerializer,
DictSerializer, create_serializer_class, ResourceLabelsMixin DictSerializer, create_serializer_class, ResourceLabelsMixin
) )
from common.serializers.fields import LabeledChoiceField from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import lazyproperty from common.utils import lazyproperty
from ..const import Category, AllTypes, Protocol from ..const import Category, AllTypes, Protocol, SuMethodChoices
from ..models import Platform, PlatformProtocol, PlatformAutomation from ..models import Platform, PlatformProtocol, PlatformAutomation
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"] __all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer", "PlatformListSerializer"]
class PlatformAutomationSerializer(serializers.ModelSerializer): class PlatformAutomationSerializer(serializers.ModelSerializer):
@ -158,13 +159,6 @@ class PlatformCustomField(serializers.Serializer):
class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer): class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
SU_METHOD_CHOICES = [
("sudo", "sudo su -"),
("su", "su - "),
("enable", "enable"),
("super", "super 15"),
("super_level", "super level 15")
]
id = serializers.IntegerField( id = serializers.IntegerField(
label='ID', required=False, label='ID', required=False,
validators=[UniqueValidator(queryset=Platform.objects.all())] validators=[UniqueValidator(queryset=Platform.objects.all())]
@ -175,10 +169,12 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False) protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict) automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField( su_method = LabeledChoiceField(
choices=SU_METHOD_CHOICES, label=_("Su method"), choices=SuMethodChoices.choices, label=_("Su method"),
required=False, default="sudo", allow_null=True required=False, default=SuMethodChoices.sudo, allow_null=True
) )
custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False) custom_fields = PlatformCustomField(label=_("Custom fields"), many=True, required=False)
assets = ObjectRelatedField(queryset=Asset.objects, many=True, required=False, label=_('Assets'))
assets_amount = serializers.IntegerField(label=_('Assets amount'), read_only=True)
class Meta: class Meta:
model = Platform model = Platform
@ -191,7 +187,8 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
'internal', 'date_created', 'date_updated', 'internal', 'date_created', 'date_updated',
'created_by', 'updated_by' 'created_by', 'updated_by'
] ]
fields = fields_small + [ fields_m2m = ['assets', 'assets_amount']
fields = fields_small + fields_m2m + [
"protocols", "domain_enabled", "su_enabled", "su_method", "protocols", "domain_enabled", "su_enabled", "su_method",
"automation", "comment", "custom_fields", "labels" "automation", "comment", "custom_fields", "labels"
] + read_only_fields ] + read_only_fields
@ -208,6 +205,7 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
"help_text": _("Assets can be connected using a zone gateway") "help_text": _("Assets can be connected using a zone gateway")
}, },
"domain_default": {"label": _('Default Domain')}, "domain_default": {"label": _('Default Domain')},
'assets': {'required': False, 'label': _('Assets')},
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -265,6 +263,11 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
return automation return automation
class PlatformListSerializer(PlatformSerializer):
class Meta(PlatformSerializer.Meta):
fields = list(set(PlatformSerializer.Meta.fields + ['assets_amount']) - {'assets'})
class PlatformOpsMethodSerializer(serializers.Serializer): class PlatformOpsMethodSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True) id = serializers.CharField(read_only=True)
name = serializers.CharField(max_length=50, label=_("Name")) name = serializers.CharField(max_length=50, label=_("Name"))

View File

@ -24,6 +24,7 @@ router.register(r'gateways', api.GatewayViewSet, 'gateway')
router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset')
router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-setting') router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-setting')
router.register(r'labels', LabelViewSet, 'label') router.register(r'labels', LabelViewSet, 'label')
router.register(r'my-asset', api.MyAssetViewSet, 'my-asset')
urlpatterns = [ urlpatterns = [
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), # path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),

View File

@ -5,7 +5,7 @@ from django.core.cache import cache
from django.db import transaction from django.db import transaction
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from common.local import encrypted_field_set from common.local import similar_encrypted_pattern, exclude_encrypted_fields
from common.utils import get_request_ip, get_logger from common.utils import get_request_ip, get_logger
from common.utils.encode import Singleton from common.utils.encode import Singleton
from common.utils.timezone import as_current_tz from common.utils.timezone import as_current_tz
@ -109,19 +109,31 @@ class OperatorLogHandler(metaclass=Singleton):
return ','.join(value) return ','.join(value)
return json.dumps(value) return json.dumps(value)
@staticmethod
def __similar_check(key):
if not key or key in exclude_encrypted_fields:
return False
return bool(similar_encrypted_pattern.search(key))
def __data_processing(self, dict_item, loop=True): def __data_processing(self, dict_item, loop=True):
encrypt_value = '******' encrypt_value = '******'
for key, value in dict_item.items(): new_data = {}
for label, item in dict_item.items():
if not isinstance(item, (dict,)):
continue
value = item.get('value', '')
field_name = item.get('name', '')
if isinstance(value, bool): if isinstance(value, bool):
value = _('Yes') if value else _('No') value = _('Yes') if value else _('No')
elif isinstance(value, (list, tuple)): elif isinstance(value, (list, tuple)):
value = self.serialized_value(value) value = self.serialized_value(value)
elif isinstance(value, dict) and loop: elif isinstance(value, dict) and loop:
self.__data_processing(value, loop=False) self.__data_processing(value, loop=False)
if key in encrypted_field_set: if self.__similar_check(field_name):
value = encrypt_value value = encrypt_value
dict_item[key] = value new_data[label] = value
return dict_item return new_data
def data_processing(self, before, after): def data_processing(self, before, after):
if before: if before:

View File

@ -16,6 +16,7 @@ from common.storage.ftp_file import FTPFileStorageHandler
from common.utils import get_log_keep_day, get_logger from common.utils import get_log_keep_day, get_logger
from ops.celery.decorator import register_as_period_task from ops.celery.decorator import register_as_period_task
from ops.models import CeleryTaskExecution from ops.models import CeleryTaskExecution
from orgs.utils import tmp_to_root_org
from terminal.backends import server_replay_storage from terminal.backends import server_replay_storage
from terminal.models import Session, Command from terminal.models import Session, Command
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog, PasswordChangeLog from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog, PasswordChangeLog
@ -131,13 +132,14 @@ def clean_expired_session_period():
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO) @register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
def clean_audits_log_period(): def clean_audits_log_period():
print("Start clean audit session task log") print("Start clean audit session task log")
clean_login_log_period() with tmp_to_root_org():
clean_operation_log_period() clean_login_log_period()
clean_ftp_log_period() clean_operation_log_period()
clean_activity_log_period() clean_ftp_log_period()
clean_celery_tasks_period() clean_activity_log_period()
clean_expired_session_period() clean_celery_tasks_period()
clean_password_change_log_period() clean_expired_session_period()
clean_password_change_log_period()
@shared_task(verbose_name=_('Upload FTP file to external storage')) @shared_task(verbose_name=_('Upload FTP file to external storage'))

View File

@ -82,7 +82,9 @@ def _get_instance_field_value(
elif isinstance(f, GenericForeignKey): elif isinstance(f, GenericForeignKey):
continue continue
try: try:
data.setdefault(str(f.verbose_name), value) data.setdefault(
str(f.verbose_name), {'name': getattr(f, 'column', ''), 'value': value}
)
except Exception as e: except Exception as e:
print(f.__dict__) print(f.__dict__)
raise e raise e
@ -106,7 +108,9 @@ def model_to_dict_for_operate_log(
return return
try: try:
field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name
data.setdefault(str(field_key), value) data.setdefault(
str(field_key), {'name': getattr(f, 'column', ''), 'value': value}
)
except: except:
pass pass

View File

@ -11,6 +11,7 @@ from .login_confirm import *
from .mfa import * from .mfa import *
from .password import * from .password import *
from .session import * from .session import *
from .ssh_key import *
from .sso import * from .sso import *
from .temp_token import * from .temp_token import *
from .token import * from .token import *

View File

@ -55,14 +55,14 @@ class UserSessionApi(generics.RetrieveDestroyAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
if isinstance(request.user, AnonymousUser): if isinstance(request.user, AnonymousUser):
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_403_FORBIDDEN)
UserSessionManager(request).connect() UserSessionManager(request).connect()
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK, data={'ok': True})
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
if isinstance(request.user, AnonymousUser): if isinstance(request.user, AnonymousUser):
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_403_FORBIDDEN)
UserSessionManager(request).disconnect() UserSessionManager(request).disconnect()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_200_OK, data={'ok': True})

View File

@ -0,0 +1,19 @@
from common.api import JMSModelViewSet
from common.permissions import IsValidUser
from ..serializers import SSHKeySerializer
from users.notifications import ResetPublicKeySuccessMsg
class SSHkeyViewSet(JMSModelViewSet):
serializer_class = SSHKeySerializer
permission_classes = [IsValidUser]
filterset_fields = ('name', 'is_active')
search_fields = ('name',)
ordering = ('-date_last_used', '-date_created')
def get_queryset(self):
return self.request.user.ssh_keys.all()
def perform_update(self, serializer):
super().perform_update(serializer)
ResetPublicKeySuccessMsg(self.request.user, self.request).publish_async()

View File

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

View File

@ -2,12 +2,12 @@
# #
import traceback import traceback
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
from django.conf import settings
from .base import JMSBaseAuthBackend
from authentication.backends.base import JMSBaseAuthBackend
from .signals import radius_create_user
User = get_user_model() User = get_user_model()
@ -28,8 +28,8 @@ class CreateUserMixin:
email = '{}@{}'.format(username, email_suffix) email = '{}@{}'.format(username, email_suffix)
user = User(username=username, name=username, email=email) user = User(username=username, name=username, email=email)
user.source = user.Source.radius.value
user.save() user.save()
radius_create_user.send(sender=user.__class__, user=user)
return user return user
def _perform_radius_auth(self, client, packet): def _perform_radius_auth(self, client, packet):

View File

@ -0,0 +1,3 @@
from django.dispatch import Signal
radius_create_user = Signal()

View File

@ -1,14 +1,12 @@
import copy import copy
from urllib import parse from urllib import parse
from django.views import View
from django.contrib import auth
from django.urls import reverse
from django.conf import settings from django.conf import settings
from django.views.decorators.csrf import csrf_exempt from django.contrib import auth
from django.http import HttpResponseRedirect, HttpResponse, HttpResponseServerError from django.http import HttpResponseRedirect, HttpResponse, HttpResponseServerError
from django.urls import reverse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.errors import OneLogin_Saml2_Error from onelogin.saml2.errors import OneLogin_Saml2_Error
from onelogin.saml2.idp_metadata_parser import ( from onelogin.saml2.idp_metadata_parser import (
@ -16,23 +14,29 @@ from onelogin.saml2.idp_metadata_parser import (
dict_deep_merge dict_deep_merge
) )
from .settings import JmsSaml2Settings
from common.utils import get_logger from common.utils import get_logger
from .settings import JmsSaml2Settings
logger = get_logger(__file__) logger = get_logger(__file__)
class PrepareRequestMixin: class PrepareRequestMixin:
@staticmethod
def is_secure(): @property
url_result = parse.urlparse(settings.SITE_URL) def parsed_url(self):
return 'on' if url_result.scheme == 'https' else 'off' return parse.urlparse(settings.SITE_URL)
def is_secure(self):
return 'on' if self.parsed_url.scheme == 'https' else 'off'
def http_host(self):
return f"{self.parsed_url.hostname}:{self.parsed_url.port}" \
if self.parsed_url.port else self.parsed_url.hostname
def prepare_django_request(self, request): def prepare_django_request(self, request):
result = { result = {
'https': self.is_secure(), 'https': self.is_secure(),
'http_host': request.META['HTTP_HOST'], 'http_host': self.http_host(),
'script_name': request.META['PATH_INFO'], 'script_name': request.META['PATH_INFO'],
'get_data': request.GET.copy(), 'get_data': request.GET.copy(),
'post_data': request.POST.copy() 'post_data': request.POST.copy()
@ -275,7 +279,7 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
logger.debug(log_prompt.format('Redirect')) logger.debug(log_prompt.format('Redirect'))
redir = post_data.get('RelayState') redir = post_data.get('RelayState')
if not redir or len(redir) == 0: if not redir or len(redir) == 0:
redir = "/" redir = "/"
next_url = saml_instance.redirect_to(redir) next_url = saml_instance.redirect_to(redir)
return HttpResponseRedirect(next_url) return HttpResponseRedirect(next_url)

View File

@ -0,0 +1,51 @@
# Generated by Django 4.1.13 on 2024-07-29 02:25
import common.db.fields
import common.db.models
from django.conf import settings
from django.db import migrations, models
import uuid
def migrate_user_public_and_private_key(apps, schema_editor):
user_model = apps.get_model('users', 'User')
users = user_model.objects.all()
ssh_key_model = apps.get_model('authentication', 'SSHKey')
db_alias = schema_editor.connection.alias
for user in users:
if user.public_key:
ssh_key_model.objects.using(db_alias).create(
public_key=user.public_key, private_key=user.private_key, user=user
)
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('authentication', '0002_auto_20190729_1423'),
]
operations = [
migrations.CreateModel(
name='SSHKey',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('private_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Private key')),
('public_key', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Public key')),
('date_last_used', models.DateTimeField(blank=True, null=True, verbose_name='Date last used')),
('user', models.ForeignKey(db_constraint=False, on_delete=common.db.models.CASCADE_SIGNAL_SKIP,
related_name='ssh_keys', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'SSH key',
},
),
migrations.RunPython(migrate_user_public_and_private_key)
]

View File

@ -1,5 +1,7 @@
from .access_key import * from .access_key import *
from .connection_token import * from .connection_token import *
from .private_token import * from .private_token import *
from .ssh_key import *
from .sso_token import * from .sso_token import *
from .temp_token import * from .temp_token import *
from ..backends.passkey.models import *

View File

@ -200,7 +200,7 @@ class ConnectionToken(JMSOrgBaseModel):
host_account = applet.select_host_account(self.user, self.asset) host_account = applet.select_host_account(self.user, self.asset)
if not host_account: if not host_account:
raise JMSException({'error': 'No host account available'}) raise JMSException({'error': 'No host account available, please check the applet, host and account'})
host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key')) host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key'))
gateway = host.domain.select_gateway() if host.domain else None gateway = host.domain.select_gateway() if host.domain else None

View File

@ -0,0 +1,27 @@
import sshpubkeys
from django.db import models
from django.utils.translation import gettext_lazy as _
from common.db.models import JMSBaseModel, CASCADE_SIGNAL_SKIP
from users.models import AuthMixin
from common.db import fields
class SSHKey(JMSBaseModel, AuthMixin):
name = models.CharField(max_length=128, verbose_name=_("Name"))
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
private_key = fields.EncryptTextField(
blank=True, null=True, verbose_name=_("Private key")
)
public_key = fields.EncryptTextField(
blank=True, null=True, verbose_name=_("Public key")
)
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
user = models.ForeignKey(
'users.User', on_delete=CASCADE_SIGNAL_SKIP, verbose_name=_('User'), db_constraint=False,
related_name='ssh_keys'
)
class Meta:
verbose_name = _('SSH key')

View File

@ -2,4 +2,5 @@ from .confirm import *
from .connect_token_secret import * from .connect_token_secret import *
from .connection_token import * from .connection_token import *
from .password_mfa import * from .password_mfa import *
from .ssh_key import *
from .token import * from .token import *

View File

@ -40,6 +40,7 @@ class ConnectionTokenSerializer(CommonModelSerializer):
'from_ticket': {'read_only': True}, 'from_ticket': {'read_only': True},
'value': {'read_only': True}, 'value': {'read_only': True},
'is_expired': {'read_only': True, 'label': _('Is expired')}, 'is_expired': {'read_only': True, 'label': _('Is expired')},
'org_name': {'label': _("Org name")},
} }
def get_request_user(self): def get_request_user(self):

View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
#
from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.serializers.fields import ReadableHiddenField, LabeledChoiceField
from ..models import SSHKey
from common.utils import validate_ssh_public_key
from users.exceptions import CreateSSHKeyExceedLimit
__all__ = ['SSHKeySerializer', 'GenerateKeyType']
class GenerateKeyType(TextChoices):
auto = 'auto', _('Automatically Generate Key Pair')
# 目前只支持sftp方式
load = 'load', _('Import Existing Key Pair')
class SSHKeySerializer(serializers.ModelSerializer):
user = ReadableHiddenField(default=serializers.CurrentUserDefault())
public_key_comment = serializers.CharField(
source='get_public_key_comment', required=False, read_only=True, max_length=128
)
public_key_hash_md5 = serializers.CharField(
source='get_public_key_hash_md5', required=False, read_only=True, max_length=128
)
generate_key_type = LabeledChoiceField(
choices=GenerateKeyType.choices, label=_('Create Type'), default=GenerateKeyType.auto.value, required=False,
help_text=_(
'Please download the private key after creation. Each private key can only be downloaded once'
)
)
class Meta:
model = SSHKey
fields_mini = ['name']
fields_small = fields_mini + [
'public_key', 'is_active', 'comment'
]
read_only_fields = [
'id', 'user', 'public_key_comment', 'public_key_hash_md5',
'date_last_used', 'date_created', 'date_updated', 'generate_key_type',
]
fields = fields_small + read_only_fields
def to_representation(self, instance):
data = super().to_representation(instance)
data.pop('public_key', None)
return data
@staticmethod
def validate_public_key(value):
if not validate_ssh_public_key(value):
raise serializers.ValidationError(_('Not a valid ssh public key'))
return value
def create(self, validated_data):
if not self.context["request"].user.can_create_ssh_key():
raise CreateSSHKeyExceedLimit()
validated_data.pop('generate_key_type', None)
return super().create(validated_data)

View File

@ -69,16 +69,21 @@
} }
.login-content { .login-content {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 500px; height: 500px;
width: 1000px; width: 1000px;
margin-right: auto;
margin-left: auto;
margin-top: calc((100vh - 470px) / 3);
} }
body { body {
position: relative;
width: 100vw;
height: 100vh;
background-color: #f3f3f3; background-color: #f3f3f3;
height: calc(100vh - (100vh - 470px) / 3); {#height: calc(100vh - (100vh - 470px) / 3);#}
} }
.captcha { .captcha {
@ -99,6 +104,27 @@
border-right: 1px solid #EFF0F1; border-right: 1px solid #EFF0F1;
} }
.left-form-box .form-panel {
position: relative;
top: 50%;
transform: translateY(-50%);
}
.left-form-box .form-panel {
position: relative;
top: 50%;
transform: translateY(-50%);
}
.left-form-box .form-panel .form-mobile {
padding: 15px 60px;
text-align: left
}
.left-form-box .form-panel .form-mobile h2 {
display: inline
}
.red-fonts { .red-fonts {
color: red; color: red;
} }
@ -112,11 +138,11 @@
} }
.jms-title { .jms-title {
padding: 22px 10px 10px; {#padding: 22px 10px 10px;#}
} }
.more-login-items { .more-login-items {
margin-top: 10px; margin-top: 15px;
} }
.more-login-item { .more-login-item {
@ -153,6 +179,9 @@
} }
.jms-title { .jms-title {
display: flex;
justify-content: center;
align-items: center;
font-size: 21px; font-size: 21px;
font-weight: 400; font-weight: 400;
color: #151515; color: #151515;
@ -252,7 +281,7 @@
.mobile-logo { .mobile-logo {
display: block; display: block;
padding: 0 30px; padding: 0 45px;
text-align: left; text-align: left;
} }
@ -260,6 +289,15 @@
height: revert; height: revert;
width: revert; width: revert;
} }
.left-form-box .form-panel {
transform: translateY(-65%);
}
.left-form-box .form-panel .form-mobile h2 {
padding: 0;
margin: 0;
}
} }
</style> </style>
</head> </head>
@ -279,14 +317,15 @@
</a> </a>
</div> </div>
<div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}"> <div class="left-form-box {% if not form.challenge and not form.captcha %} no-captcha-challenge {% endif %}">
<div class="mobile-logo"> <div class="mobile-logo" style="padding-bottom: 45px; box-sizing: border-box">
<div class="jms-title"> <div class="jms-title">
<span style="">{{ INTERFACE.login_title }}</span> <img style="width: 60px; height: 60px" src="{{ INTERFACE.logo_logout }}" alt="Logo"/>
<span style="padding-left: 10px">{{ INTERFACE.login_title }}</span>
</div> </div>
</div> </div>
<div style="position: relative;top: 50%;transform: translateY(-50%);"> <div class="form-panel">
<div style='padding: 15px 60px; text-align: left'> <div class="form-mobile">
<h2 style='font-weight: 400;display: inline'> <h2 style='font-weight: 400;'>
{% trans 'Login' %} {% trans 'Login' %}
</h2> </h2>
<ul class=" nav navbar-top-links navbar-right"> <ul class=" nav navbar-top-links navbar-right">

View File

@ -14,6 +14,7 @@ router.register('temp-tokens', api.TempTokenViewSet, 'temp-token')
router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token') router.register('connection-token', api.ConnectionTokenViewSet, 'connection-token')
router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token') router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token')
router.register('confirm', api.UserConfirmationViewSet, 'confirm') router.register('confirm', api.UserConfirmationViewSet, 'confirm')
router.register('ssh-key', api.SSHkeyViewSet, 'ssh-key')
urlpatterns = [ urlpatterns = [
path('<str:backend>/qr/unbind/', api.QRUnBindForUserApi.as_view(), name='qr-unbind'), path('<str:backend>/qr/unbind/', api.QRUnBindForUserApi.as_view(), name='qr-unbind'),

View File

@ -62,8 +62,8 @@ urlpatterns = [
path('slack/qr/login/callback/', views.SlackQRLoginCallbackView.as_view(), name='slack-qr-login-callback'), path('slack/qr/login/callback/', views.SlackQRLoginCallbackView.as_view(), name='slack-qr-login-callback'),
# Profile # Profile
path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
path('profile/mfa/', users_view.MFASettingView.as_view(), name='user-mfa-setting'), path('profile/mfa/', users_view.MFASettingView.as_view(), name='user-mfa-setting'),
path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
# OTP Setting # OTP Setting
path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'), path('profile/otp/enable/start/', users_view.UserOtpEnableStartView.as_view(), name='user-otp-enable-start'),

View File

@ -15,7 +15,7 @@ from common.utils import get_logger
from common.utils.common import get_request_ip from common.utils.common import get_request_ip
from common.utils.django import reverse, get_object_or_none from common.utils.django import reverse, get_object_or_none
from users.models import User from users.models import User
from users.signal_handlers import check_only_allow_exist_user_auth from users.signal_handlers import check_only_allow_exist_user_auth, bind_user_to_org_role
from .mixins import FlashMessageMixin from .mixins import FlashMessageMixin
logger = get_logger(__file__) logger = get_logger(__file__)
@ -46,9 +46,6 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
def verify_state(self): def verify_state(self):
raise NotImplementedError raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def create_user_if_not_exist(self, user_id, **kwargs): def create_user_if_not_exist(self, user_id, **kwargs):
user = None user = None
user_attr = self.client.get_user_detail(user_id, **kwargs) user_attr = self.client.get_user_detail(user_id, **kwargs)
@ -64,6 +61,7 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
setattr(user, f'{self.user_type}_id', user_id) setattr(user, f'{self.user_type}_id', user_id)
if create: if create:
setattr(user, 'source', self.user_type) setattr(user, 'source', self.user_type)
bind_user_to_org_role(user)
user.save() user.save()
except IntegrityError as err: except IntegrityError as err:
logger.error(f'{self.msg_client_err}: create user error: {err}') logger.error(f'{self.msg_client_err}: create user error: {err}')
@ -122,9 +120,6 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
def verify_state(self): def verify_state(self):
raise NotImplementedError raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def get_already_bound_response(self, redirect_uri): def get_already_bound_response(self, redirect_uri):
raise NotImplementedError raise NotImplementedError
@ -151,11 +146,9 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
setattr(user, f'{self.auth_type}_id', auth_user_id) setattr(user, f'{self.auth_type}_id', auth_user_id)
user.save() user.save()
except IntegrityError as e: except IntegrityError as e:
if e.args[0] == 1062: msg = _('The %s is already bound to another user') % self.auth_type_label
msg = _('The %s is already bound to another user') % self.auth_type_label response = self.get_failed_response(redirect_url, msg, msg)
response = self.get_failed_response(redirect_url, msg, msg) return response
return response
raise e
ip = get_request_ip(request) ip = get_request_ip(request)
OAuthBindMessage(user, ip, self.auth_type_label, auth_user_id).publish_async() OAuthBindMessage(user, ip, self.auth_type_label, auth_user_id).publish_async()

View File

@ -47,15 +47,7 @@ class DingTalkBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, Fla
) )
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') return self.verify_state_with_session_key(DINGTALK_STATE_SESSION_KEY)
session_state = self.request.session.get(DINGTALK_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
def get_already_bound_response(self, redirect_url): def get_already_bound_response(self, redirect_url):
msg = _('DingTalk is already bound') msg = _('DingTalk is already bound')

View File

@ -58,15 +58,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
) )
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') return self.verify_state_with_session_key(self.state_session_key)
session_state = self.request.session.get(self.state_session_key)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
def get_qr_url(self, redirect_uri): def get_qr_url(self, redirect_uri):
state = random_string(16) state = random_string(16)

View File

@ -194,9 +194,6 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
if self.request.GET.get("admin", 0): if self.request.GET.get("admin", 0):
return None return None
if not settings.XPACK_ENABLED:
return None
auth_types = [m for m in self.get_support_auth_methods() if m.get('auto_redirect')] auth_types = [m for m in self.get_support_auth_methods() if m.get('auto_redirect')]
if not auth_types: if not auth_types:
return None return None

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import gettext_lazy as _
from common.utils import FlashMessageUtil from common.utils import FlashMessageUtil
@ -32,3 +33,12 @@ class FlashMessageMixin:
def get_failed_response(self, redirect_url, title, msg, interval=10): def get_failed_response(self, redirect_url, title, msg, interval=10):
return self.get_response(redirect_url, title, msg, 'error', interval) return self.get_response(redirect_url, title, msg, 'error', interval)
def get_verify_state_failed_response(self, redirect_uri):
msg = _(
"For your safety, automatic redirection login is not supported on the client."
" If you need to open it in the client, please log in again")
return self.get_failed_response(redirect_uri, msg, msg)
def verify_state_with_session_key(self, session_key):
return self.request.GET.get('state') == self.request.session.get(session_key)

View File

@ -37,15 +37,7 @@ class SlackMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessa
) )
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') return self.verify_state_with_session_key(SLACK_STATE_SESSION_KEY)
session_state = self.request.session.get(SLACK_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
def get_qr_url(self, redirect_uri): def get_qr_url(self, redirect_uri):
state = random_string(16) state = random_string(16)

View File

@ -45,15 +45,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
) )
def verify_state(self): def verify_state(self):
state = self.request.GET.get('state') return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
session_state = self.request.session.get(WECOM_STATE_SESSION_KEY)
if state != session_state:
return False
return True
def get_verify_state_failed_response(self, redirect_uri):
msg = _("The system configuration is incorrect. Please contact your administrator")
return self.get_failed_response(redirect_uri, msg, msg)
def get_already_bound_response(self, redirect_url): def get_already_bound_response(self, redirect_url):
msg = _('WeCom is already bound') msg = _('WeCom is already bound')

View File

@ -5,18 +5,18 @@ import uuid
from django.core.cache import cache from django.core.cache import cache
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import generics, serializers from rest_framework import generics, serializers
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from common.const import KEY_CACHE_RESOURCE_IDS, COUNTRY_CALLING_CODES
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.views.http import HttpResponseTemporaryRedirect
from common.utils import get_logger from common.utils import get_logger
from common.const import KEY_CACHE_RESOURCE_IDS from common.views.http import HttpResponseTemporaryRedirect
__all__ = [ __all__ = [
'LogTailApi', 'ResourcesIDCacheApi' 'LogTailApi', 'ResourcesIDCacheApi', 'CountryListApi'
] ]
logger = get_logger(__file__) logger = get_logger(__file__)
@ -96,6 +96,13 @@ class ResourcesIDCacheApi(APIView):
return Response({'spm': spm}) return Response({'spm': spm})
class CountryListApi(APIView):
permission_classes = (IsValidUser,)
def get(self, request, *args, **kwargs):
return Response(COUNTRY_CALLING_CODES)
@csrf_exempt @csrf_exempt
def redirect_plural_name_api(request, *args, **kwargs): def redirect_plural_name_api(request, *args, **kwargs):
resource = kwargs.get("resource", "") resource = kwargs.get("resource", "")

View File

@ -1,11 +1,58 @@
import phonenumbers
import pycountry
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from phonenumbers import PhoneMetadata
ADMIN = 'Admin' ADMIN = 'Admin'
USER = 'User' USER = 'User'
AUDITOR = 'Auditor' AUDITOR = 'Auditor'
def get_country_phone_codes():
phone_codes = []
for region_code in phonenumbers.SUPPORTED_REGIONS:
phone_metadata = PhoneMetadata.metadata_for_region(region_code)
if phone_metadata:
phone_codes.append((region_code, phone_metadata.country_code))
return phone_codes
def get_country(region_code):
country = pycountry.countries.get(alpha_2=region_code)
if country:
return country
else:
return None
def get_country_phone_choices():
codes = get_country_phone_codes()
choices = []
for code, phone in codes:
country = get_country(code)
if not country:
continue
country_name = country.name
flag = country.flag
if country.name == 'China':
country_name = _('China')
if code == 'TW':
country_name = 'Taiwan'
flag = get_country('CN').flag
choices.append({
'name': country_name,
'phone_code': f'+{phone}',
'flag': flag,
'code': code,
})
choices.sort(key=lambda x: x['name'])
return choices
class Trigger(models.TextChoices): class Trigger(models.TextChoices):
manual = 'manual', _('Manual trigger') manual = 'manual', _('Manual trigger')
timing = 'timing', _('Timing trigger') timing = 'timing', _('Timing trigger')
@ -28,17 +75,4 @@ class Language(models.TextChoices):
jp = 'ja', '日本語', jp = 'ja', '日本語',
COUNTRY_CALLING_CODES = [ COUNTRY_CALLING_CODES = get_country_phone_choices()
{'name': 'China(中国)', 'value': '+86'},
{'name': 'HongKong(中国香港)', 'value': '+852'},
{'name': 'Macao(中国澳门)', 'value': '+853'},
{'name': 'Taiwan(中国台湾)', 'value': '+886'},
{'name': 'America(America)', 'value': '+1'},
{'name': 'Russia(Россия)', 'value': '+7'},
{'name': 'France(français)', 'value': '+33'},
{'name': 'Britain(Britain)', 'value': '+44'},
{'name': 'Germany(Deutschland)', 'value': '+49'},
{'name': 'Japan(日本)', 'value': '+81'},
{'name': 'Korea(한국)', 'value': '+82'},
{'name': 'India(भारत)', 'value': '+91'}
]

View File

@ -14,7 +14,6 @@ from django.db.models import Q, Manager, QuerySet
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder from rest_framework.utils.encoders import JSONEncoder
from common.local import add_encrypted_field_set
from common.utils import contains_ip from common.utils import contains_ip
from .utils import Encryptor from .utils import Encryptor
from .validators import PortRangeValidator from .validators import PortRangeValidator
@ -168,7 +167,6 @@ class EncryptTextField(EncryptMixin, models.TextField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
class EncryptCharField(EncryptMixin, models.CharField): class EncryptCharField(EncryptMixin, models.CharField):
@ -184,7 +182,6 @@ class EncryptCharField(EncryptMixin, models.CharField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.change_max_length(kwargs) self.change_max_length(kwargs)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
def deconstruct(self): def deconstruct(self):
name, path, args, kwargs = super().deconstruct() name, path, args, kwargs = super().deconstruct()
@ -198,13 +195,11 @@ class EncryptCharField(EncryptMixin, models.CharField):
class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField): class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField): class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
class PortField(models.IntegerField): class PortField(models.IntegerField):

View File

@ -1,18 +1,14 @@
from werkzeug.local import Local import re
from django.utils import translation from werkzeug.local import Local
thread_local = Local() thread_local = Local()
encrypted_field_set = {'password', 'secret'} exclude_encrypted_fields = ('secret_type', 'secret_strategy', 'password_rules')
similar_encrypted_pattern = re.compile(
'password|secret|token|passphrase|private|key|cert', re.IGNORECASE
)
def _find(attr): def _find(attr):
return getattr(thread_local, attr, None) return getattr(thread_local, attr, None)
def add_encrypted_field_set(label):
return
if label:
with translation.override('en'):
encrypted_field_set.add(str(label))

View File

@ -34,6 +34,10 @@ def parse_to_url(url):
url = url.replace('(?P<format>[a-z0-9]+)', '') url = url.replace('(?P<format>[a-z0-9]+)', '')
url = url.replace('((?P<terminal>[/.]{36})/)?', uid + '/') url = url.replace('((?P<terminal>[/.]{36})/)?', uid + '/')
url = url.replace('(?P<pk>[/.]+)', uid) url = url.replace('(?P<pk>[/.]+)', uid)
url = url.replace('(?P<label>.*)', uid)
url = url.replace('(?P<res_type>.*)', '1')
url = url.replace('(?P<name>[\\w.@]+)', '')
url = url.replace('<str:name>', 'zh-hans')
url = url.replace('\.', '') url = url.replace('\.', '')
url = url.replace('//', '/') url = url.replace('//', '/')
url = url.strip('$') url = url.strip('$')
@ -70,7 +74,9 @@ known_unauth_urls = [
"/api/v1/authentication/login-confirm-ticket/status/", "/api/v1/authentication/login-confirm-ticket/status/",
"/api/v1/authentication/mfa/select/", "/api/v1/authentication/mfa/select/",
"/api/v1/authentication/mfa/send-code/", "/api/v1/authentication/mfa/send-code/",
"/api/v1/authentication/sso/login/" "/api/v1/authentication/sso/login/",
"/api/v1/authentication/user-session/",
"/api/v1/settings/i18n/zh-hans/"
] ]
known_error_urls = [ known_error_urls = [

View File

@ -24,6 +24,7 @@ class GunicornService(BaseService):
'-w', str(self.worker), '-w', str(self.worker),
'--max-requests', '10240', '--max-requests', '10240',
'--max-requests-jitter', '2048', '--max-requests-jitter', '2048',
'--graceful-timeout', '30',
'--access-logformat', log_format, '--access-logformat', log_format,
'--access-logfile', '-' '--access-logfile', '-'
] ]

View File

@ -190,7 +190,8 @@ class ES(object):
mappings['aliases'] = { mappings['aliases'] = {
self.query_index: {} self.query_index: {}
} }
if self.es.indices.exists(index=self.index):
return
try: try:
self.es.indices.create(index=self.index, body=mappings) self.es.indices.create(index=self.index, body=mappings)
except (RequestError, BadRequestError) as e: except (RequestError, BadRequestError) as e:

View File

@ -2,16 +2,17 @@ import base64
import hmac import hmac
import time import time
from django.conf import settings
from common.sdk.im.mixin import BaseRequest from common.sdk.im.mixin import BaseRequest
from common.sdk.im.utils import digest, as_request from common.sdk.im.utils import digest, as_request
from common.utils import get_logger from common.utils import get_logger
from users.utils import construct_user_email from users.utils import construct_user_email, flatten_dict, map_attributes
logger = get_logger(__file__) logger = get_logger(__file__)
def sign(secret, data): def sign(secret, data):
digest = hmac.HMAC( digest = hmac.HMAC(
key=secret.encode('utf8'), key=secret.encode('utf8'),
msg=data.encode('utf8'), msg=data.encode('utf8'),
@ -115,6 +116,7 @@ class DingTalkRequests(BaseRequest):
class DingTalk: class DingTalk:
def __init__(self, appid, appsecret, agentid, timeout=None): def __init__(self, appid, appsecret, agentid, timeout=None):
self._appid = appid or '' self._appid = appid or ''
self._appsecret = appsecret or '' self._appsecret = appsecret or ''
@ -125,6 +127,10 @@ class DingTalk:
timeout=timeout timeout=timeout
) )
@property
def attributes(self):
return settings.DINGTALK_RENAME_ATTRIBUTES
def get_userinfo_bycode(self, code): def get_userinfo_bycode(self, code):
body = { body = {
'clientId': self._appid, 'clientId': self._appid,
@ -206,17 +212,24 @@ class DingTalk:
data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True) data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True)
return data return data
def get_user_detail(self, user_id, **kwargs): @staticmethod
# https://open.dingtalk.com/document/orgapp/query-user-details def default_user_detail(data, user_id):
body = {'userid': user_id} username = data.get('userid', user_id)
data = self._request.post(
URL.GET_USER_INFO_BY_USER_ID, json=body, with_token=True
)
data = data['result']
username = user_id
name = data.get('name', username) name = data.get('name', username)
email = data.get('email') or data.get('org_email') email = data.get('email') or data.get('org_email')
email = construct_user_email(username, email) email = construct_user_email(username, email)
return { return {
'username': username, 'name': name, 'email': email 'username': username, 'name': name, 'email': email
} }
def get_user_detail(self, user_id, **kwargs):
# https://open.dingtalk.com/document/orgapp/query-user-details
data = self._request.post(
URL.GET_USER_INFO_BY_USER_ID, json={'userid': user_id}, with_token=True
)
data = data['result']
data['user_id'] = user_id
info = flatten_dict(data)
default_detail = self.default_user_detail(data, user_id)
detail = map_attributes(default_detail, info, self.attributes)
return detail

View File

@ -1,11 +1,12 @@
import json import json
from django.conf import settings
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from common.sdk.im.mixin import RequestMixin, BaseRequest from common.sdk.im.mixin import RequestMixin, BaseRequest
from common.sdk.im.utils import digest from common.sdk.im.utils import digest
from common.utils.common import get_logger from common.utils.common import get_logger
from users.utils import construct_user_email from users.utils import construct_user_email, flatten_dict, map_attributes
logger = get_logger(__name__) logger = get_logger(__name__)
@ -53,6 +54,7 @@ class FeishuRequests(BaseRequest):
) )
code_key = 'code' code_key = 'code'
msg_key = 'msg' msg_key = 'msg'
url_instance = URL()
def __init__(self, app_id, app_secret, timeout=None): def __init__(self, app_id, app_secret, timeout=None):
self._app_id = app_id self._app_id = app_id
@ -65,7 +67,7 @@ class FeishuRequests(BaseRequest):
def request_access_token(self): def request_access_token(self):
data = {'app_id': self._app_id, 'app_secret': self._app_secret} data = {'app_id': self._app_id, 'app_secret': self._app_secret}
response = self.raw_request('post', url=URL().get_token, data=data) response = self.raw_request('post', url=self.url_instance.get_token, data=data)
self.check_errcode_is_0(response) self.check_errcode_is_0(response)
access_token = response['tenant_access_token'] access_token = response['tenant_access_token']
@ -92,6 +94,11 @@ class FeiShu(RequestMixin):
app_secret=app_secret, app_secret=app_secret,
timeout=timeout timeout=timeout
) )
self.url_instance = self._requests.url_instance
@property
def attributes(self):
return settings.FEISHU_RENAME_ATTRIBUTES
def get_user_id_by_code(self, code): def get_user_id_by_code(self, code):
# https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN # https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN
@ -101,7 +108,7 @@ class FeiShu(RequestMixin):
'code': code 'code': code
} }
data = self._requests.post(URL().get_user_info_by_code, json=body, check_errcode_is_0=False) data = self._requests.post(self.url_instance.get_user_info_by_code, json=body, check_errcode_is_0=False)
self._requests.check_errcode_is_0(data) self._requests.check_errcode_is_0(data)
return data['data']['user_id'], data['data'] return data['data']['user_id'], data['data']
@ -126,7 +133,7 @@ class FeiShu(RequestMixin):
try: try:
logger.info(f'{self.__class__.__name__} send text: user_ids={user_ids} msg={msg}') logger.info(f'{self.__class__.__name__} send text: user_ids={user_ids} msg={msg}')
self._requests.post(URL().send_message, params=params, json=body) self._requests.post(self.url_instance.send_message, params=params, json=body)
except APIException as e: except APIException as e:
# 只处理可预知的错误 # 只处理可预知的错误
logger.exception(e) logger.exception(e)
@ -134,13 +141,28 @@ class FeiShu(RequestMixin):
return invalid_users return invalid_users
@staticmethod @staticmethod
def get_user_detail(user_id, **kwargs): def default_user_detail(data, user_id):
# get_user_id_by_code 已经返回个人信息,这里直接解析 username = data.get('user_id', user_id)
data = kwargs['other_info']
username = user_id
name = data.get('name', username) name = data.get('name', username)
email = data.get('email') or data.get('enterprise_email') email = data.get('email') or data.get('enterprise_email')
email = construct_user_email(username, email) email = construct_user_email(username, email)
return { return {
'username': username, 'name': name, 'email': email 'username': username, 'name': name, 'email': email
} }
def get_user_detail(self, user_id, **kwargs):
# https://open.feishu.cn/document/server-docs/contact-v3/user/get
data = {}
try:
data = self._requests.get(
self.url_instance.get_user_detail(user_id),
{'user_id_type': 'user_id'}
)
data = data['data']['user']
except Exception as e:
logger.error(f'Get user detail error: {e} data={data}')
info = flatten_dict(data)
default_detail = self.default_user_detail(data, user_id)
detail = map_attributes(default_detail, info, self.attributes)
return detail

View File

@ -1,3 +1,5 @@
from django.conf import settings
from common.utils.common import get_logger from common.utils.common import get_logger
from ..feishu import URL as FeiShuURL, FeishuRequests, FeiShu from ..feishu import URL as FeiShuURL, FeishuRequests, FeiShu
@ -9,8 +11,12 @@ class URL(FeiShuURL):
class LarkRequests(FeishuRequests): class LarkRequests(FeishuRequests):
pass url_instance = URL()
class Lark(FeiShu): class Lark(FeiShu):
requests_cls = LarkRequests requests_cls = LarkRequests
@property
def attributes(self):
return settings.LARK_RENAME_ATTRIBUTES

View File

@ -1,11 +1,12 @@
import mistune import mistune
import requests import requests
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from common.utils.common import get_logger from common.utils.common import get_logger
from jumpserver.utils import get_current_request from jumpserver.utils import get_current_request
from users.utils import construct_user_email from users.utils import construct_user_email, flatten_dict, map_attributes
logger = get_logger(__name__) logger = get_logger(__name__)
@ -93,12 +94,17 @@ class SlackRequests:
class Slack: class Slack:
def __init__(self, client_id=None, client_secret=None, bot_token=None, **kwargs): def __init__(self, client_id=None, client_secret=None, bot_token=None, **kwargs):
self._client = SlackRequests( self._client = SlackRequests(
client_id=client_id, client_secret=client_secret, bot_token=bot_token client_id=client_id, client_secret=client_secret, bot_token=bot_token
) )
self.markdown = mistune.Markdown(renderer=SlackRenderer()) self.markdown = mistune.Markdown(renderer=SlackRenderer())
@property
def attributes(self):
return settings.SLACK_RENAME_ATTRIBUTES
def get_user_id_by_code(self, code): def get_user_id_by_code(self, code):
self._client.request_access_token(code) self._client.request_access_token(code)
response = self._client.request( response = self._client.request(
@ -138,13 +144,22 @@ class Slack:
logger.exception(e) logger.exception(e)
@staticmethod @staticmethod
def get_user_detail(user_id, **kwargs): def default_user_detail(data, user_id):
# get_user_id_by_code 已经返回个人信息,这里直接解析 username = data.get('id', user_id)
user_info = kwargs['other_info'] username = data.get('name', username)
username = user_info.get('name') or user_id name = data.get('real_name', username)
name = user_info.get('real_name', username) email = data.get('profile.email')
email = user_info.get('profile', {}).get('email')
email = construct_user_email(username, email) email = construct_user_email(username, email)
return { return {
'username': username, 'name': name, 'email': email 'username': username, 'name': name, 'email': email
} }
def get_user_detail(self, user_id, **kwargs):
# https://api.slack.com/methods/users.info
# get_user_id_by_code 已经返回个人信息,这里直接解析
data = kwargs['other_info']
data['user_id'] = user_id
info = flatten_dict(data)
default_detail = self.default_user_detail(data, user_id)
detail = map_attributes(default_detail, info, self.attributes)
return detail

View File

@ -1,12 +1,13 @@
from typing import Iterable, AnyStr from typing import Iterable, AnyStr
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from common.sdk.im.mixin import RequestMixin, BaseRequest from common.sdk.im.mixin import RequestMixin, BaseRequest
from common.sdk.im.utils import digest, update_values from common.sdk.im.utils import digest, update_values
from common.utils.common import get_logger from common.utils.common import get_logger
from users.utils import construct_user_email from users.utils import construct_user_email, flatten_dict, map_attributes
logger = get_logger(__name__) logger = get_logger(__name__)
@ -92,6 +93,10 @@ class WeCom(RequestMixin):
timeout=timeout timeout=timeout
) )
@property
def attributes(self):
return settings.WECOM_RENAME_ATTRIBUTES
def send_markdown(self, users: Iterable, msg: AnyStr, **kwargs): def send_markdown(self, users: Iterable, msg: AnyStr, **kwargs):
pass pass
@ -173,14 +178,20 @@ class WeCom(RequestMixin):
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId') logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
raise WeComError raise WeComError
def get_user_detail(self, user_id, **kwargs): @staticmethod
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196 def default_user_detail(data, user_id):
params = {'userid': user_id} username = data.get('userid', user_id)
data = self._requests.get(URL.GET_USER_DETAIL, params)
username = data.get('userid')
name = data.get('name', username) name = data.get('name', username)
email = data.get('email') or data.get('biz_mail') email = data.get('email') or data.get('biz_mail')
email = construct_user_email(username, email) email = construct_user_email(username, email)
return { return {
'username': username, 'name': name, 'email': email 'username': username, 'name': name, 'email': email
} }
def get_user_detail(self, user_id, **kwargs):
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196
data = self._requests.get(URL.GET_USER_DETAIL, {'userid': user_id})
info = flatten_dict(data)
default_detail = self.default_user_detail(data, user_id)
detail = map_attributes(default_detail, info, self.attributes)
return detail

View File

@ -39,8 +39,7 @@ class CustomSMS(BaseSMSClient):
kwargs = {'params': params} kwargs = {'params': params}
try: try:
response = action(url=settings.CUSTOM_SMS_URL, verify=False, **kwargs) response = action(url=settings.CUSTOM_SMS_URL, verify=False, **kwargs)
if response.reason != 'OK': response.raise_for_status()
raise JMSException(detail=response.text, code=response.status_code)
except Exception as exc: except Exception as exc:
logger.error('Custom sms error: {}'.format(exc)) logger.error('Custom sms error: {}'.format(exc))
raise JMSException(exc) raise JMSException(exc)

View File

@ -8,7 +8,6 @@ from rest_framework import serializers
from rest_framework.fields import ChoiceField, empty from rest_framework.fields import ChoiceField, empty
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
from common.local import add_encrypted_field_set
from common.utils import decrypt_password from common.utils import decrypt_password
__all__ = [ __all__ = [
@ -47,9 +46,7 @@ class EncryptedField(serializers.CharField):
if write_only is None: if write_only is None:
write_only = True write_only = True
kwargs["write_only"] = write_only kwargs["write_only"] = write_only
encrypted_key = kwargs.pop('encrypted_key', None)
super().__init__(**kwargs) super().__init__(**kwargs)
add_encrypted_field_set(encrypted_key or self.label)
def to_internal_value(self, value): def to_internal_value(self, value):
value = super().to_internal_value(value) value = super().to_internal_value(value)

View File

@ -9,4 +9,5 @@ app_name = 'common'
urlpatterns = [ urlpatterns = [
path('resources/cache/', api.ResourcesIDCacheApi.as_view(), name='resources-cache'), path('resources/cache/', api.ResourcesIDCacheApi.as_view(), name='resources-cache'),
path('countries/', api.CountryListApi.as_view(), name='resources-cache'),
] ]

View File

@ -21,6 +21,7 @@ def i18n_fmt(tpl, *args):
return tpl return tpl
args = [str(arg) for arg in args] args = [str(arg) for arg in args]
args = [arg.replace(', ', ' ') for arg in args]
try: try:
tpl % tuple(args) tpl % tuple(args)

View File

@ -13,9 +13,9 @@ from common.utils.random import random_string
logger = get_logger(__file__) logger = get_logger(__file__)
@shared_task(verbose_name=_('Send email')) @shared_task(verbose_name=_('Send SMS code'))
def send_async(sender): def send_sms_async(target, code):
sender.gen_and_send() SMS().send_verify_code(target, code)
class SendAndVerifyCodeUtil(object): class SendAndVerifyCodeUtil(object):
@ -35,7 +35,7 @@ class SendAndVerifyCodeUtil(object):
logger.warning('Send sms too frequently, delay {}'.format(ttl)) logger.warning('Send sms too frequently, delay {}'.format(ttl))
raise CodeSendTooFrequently(ttl) raise CodeSendTooFrequently(ttl)
return send_async.apply_async(kwargs={"sender": self}, priority=100) return self.gen_and_send()
def gen_and_send(self): def gen_and_send(self):
try: try:
@ -72,13 +72,15 @@ class SendAndVerifyCodeUtil(object):
return code return code
def __send_with_sms(self): def __send_with_sms(self):
sms = SMS() send_sms_async.apply_async(args=(self.target, self.code), priority=100)
sms.send_verify_code(self.target, self.code)
def __send_with_email(self): def __send_with_email(self):
subject = self.other_args.get('subject') subject = self.other_args.get('subject', '')
message = self.other_args.get('message') message = self.other_args.get('message', '')
send_mail_async(subject, message, [self.target], html_message=message) send_mail_async.apply_async(
args=(subject, message, [self.target]),
kwargs={'html_message': message}, priority=100
)
def __send(self, code): def __send(self, code):
""" """

View File

@ -53,7 +53,7 @@
"SaveSucceed": "保存成功", "SaveSucceed": "保存成功",
"SelectSQL": "选择 SQL", "SelectSQL": "选择 SQL",
"SessionClosedBy": "会话被 %s 关闭", "SessionClosedBy": "会话被 %s 关闭",
"SessionFinished": "会已结束", "SessionFinished": "会已结束",
"SessionLockedError": "当前会话已被锁定,无法继续执行命令", "SessionLockedError": "当前会话已被锁定,无法继续执行命令",
"SessionLockedMessage": "此会话已被 %s 锁定,无法继续执行命令", "SessionLockedMessage": "此会话已被 %s 锁定,无法继续执行命令",
"SessionUnlockedMessage": "此会话已被 %s 解锁,可以继续执行命令", "SessionUnlockedMessage": "此会话已被 %s 解锁,可以继续执行命令",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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