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/
apps/xpack/.git
.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]
line_length=120
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
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
FROM jumpserver/core-base:20240808_054051 AS stage-build
ARG VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN echo > /opt/jumpserver/config.yml \
&& \
if [ -n "${VERSION}" ]; then \
sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" apps/jumpserver/const.py; \
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 \
&& export SECRET_KEY=$(head -c100 < /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 48) \
&& . /opt/py3/bin/activate \
&& cd apps \
&& python manage.py compilemessages
FROM python:3.11-slim-bullseye
ARG TARGETARCH
ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH
@ -110,32 +34,27 @@ ARG TOOLS=" \
sshpass \
bubblewrap"
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 \
ARG APT_MIRROR=http://deb.debian.org
RUN 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 \
&& 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 ${TOOLS} \
&& apt-get clean \
&& 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 "no" | dpkg-reconfigure dash \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc
COPY --from=stage-2 /opt /opt
COPY --from=stage-1 /usr/local/bin /usr/local/bin
COPY --from=stage-1 /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
COPY --from=stage-build /opt /opt
COPY --from=stage-build /usr/local/bin /usr/local/bin
COPY --from=stage-build /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
WORKDIR /opt/jumpserver
ARG VERSION
ENV VERSION=$VERSION
VOLUME /opt/jumpserver/data
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 python:3.11-slim-bullseye as build-core
ARG BUILD_DEPENDENCIES=" \
g++"
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
FROM jumpserver/core:${VERSION}-ce
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} \
&& 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
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
ARG TOOLS=" \
g++ \
curl \
iputils-ping \
netcat-openbsd \
@ -41,12 +15,21 @@ ARG TOOLS=" \
vim \
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 \
ARG APT_MIRROR=http://deb.debian.org
RUN 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 ${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-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>
<br/>
## 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
@ -43,15 +55,6 @@ JumpServer is an open-source Privileged Access Management (PAM) tool that provid
</tr>
</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
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 |
| [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 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) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer 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 |
| [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 |
## 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.
<!-- JumpServer official link -->
[docs-link]: https://en-docs.jumpserver.org/
[discord-link]: https://discord.com/invite/jcM5tKWJ
[docs-link]: https://jumpserver.com/docs
[discord-link]: https://discord.com/invite/W6vYXmAQG2
[contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md
<!-- JumpServer Other link-->

View File

@ -12,13 +12,13 @@
<p align="center">
9 年时间,倾情投入,用心做好一款开源堡垒机。
10 年时间,倾情投入,用心做好一款开源堡垒机。
</p>
------------------------------
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
## JumpServer 是什么?
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
- **SSH**: Linux / Unix / 网络设备 等;
- **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展示](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)
@ -81,28 +81,24 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
### 参与贡献
## 参与贡献
欢迎提交 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/)

View File

@ -35,6 +35,17 @@
- user_info.failed
- 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"
ansible.builtin.user:
name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
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
ansible.builtin.meta: reset_connection

View File

@ -5,6 +5,12 @@ type:
- AIX
method: change_secret
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (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:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
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:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@ -35,6 +35,17 @@
- user_info.failed
- 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"
ansible.builtin.user:
name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
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
ansible.builtin.meta: reset_connection

View File

@ -6,6 +6,12 @@ type:
- linux
method: change_secret
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (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:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
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:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

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

View File

@ -31,7 +31,7 @@ class GatherAccountsFilter:
def posix_filter(info):
username_pattern = re.compile(r'^(\S+)')
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 = {}
for line in info:
usernames = username_pattern.findall(line)
@ -46,7 +46,8 @@ class GatherAccountsFilter:
result[username].update({'address': ip_addr})
login_times = login_time_pattern.findall(line)
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})
return result

View File

@ -35,6 +35,17 @@
- user_info.failed
- 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"
ansible.builtin.user:
name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
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
ansible.builtin.meta: reset_connection

View File

@ -5,6 +5,12 @@ type:
- AIX
method: push_account
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@ -34,6 +40,11 @@ i18n:
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (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:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -49,6 +60,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
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:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@ -35,6 +35,17 @@
- user_info.failed
- 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"
ansible.builtin.user:
name: "{{ account.username }}"
@ -59,17 +70,6 @@
exclusive: "{{ ssh_params.exclusive }}"
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
ansible.builtin.meta: reset_connection

View File

@ -6,6 +6,12 @@ type:
- linux
method: push_account
params:
- name: modify_sudo
type: bool
label: "{{ 'Modify sudo label' | trans }}"
default: False
help_text: "{{ 'Modify params sudo help text' | trans }}"
- name: sudo
type: str
label: 'Sudo'
@ -36,6 +42,11 @@ i18n:
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (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:
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
@ -51,6 +62,11 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
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:
zh: '家目录'
ja: 'ホームディレクトリ'

View File

@ -10,7 +10,6 @@ import common.db.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
@ -26,13 +25,19 @@ class Migration(migrations.Migration):
('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)),
('org_id', 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')),
('org_id',
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')),
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('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')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('version', models.IntegerField(default=0, verbose_name='Version')),
@ -41,7 +46,11 @@ class Migration(migrations.Migration):
],
options={
'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(
@ -53,16 +62,21 @@ class Migration(migrations.Migration):
('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)),
('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')),
('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')),
('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')),
('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_obj_storage', 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')),
('is_password_divided_by_obj_storage',
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={
'verbose_name': 'Account backup plan',
@ -72,12 +86,16 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='AccountBackupExecution',
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)),
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
('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')),
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')),
('snapshot',
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')),
('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')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('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_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')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('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')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('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')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('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')),
('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')),
@ -158,12 +183,16 @@ class Migration(migrations.Migration):
fields=[
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('_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')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=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={
'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_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('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')),
('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'), ('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')),
('org_id',
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')),
],
options={'verbose_name': 'Virtual account'},
),
]

View File

@ -119,7 +119,8 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount):
return auth
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
auth['ansible_become'] = True
auth['ansible_become_method'] = become_method

View File

@ -81,21 +81,28 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
@staticmethod
def get_template_attr_for_account(template):
# Set initial data from template
field_names = [
'name', 'username', 'secret', 'push_params',
'secret_type', 'privileged', 'is_active'
'name', 'username',
'secret_type', 'secret',
'privileged', 'is_active'
]
field_map = {
'push_params': 'params',
'auto_push': 'push_now'
}
field_names.extend(field_map.keys())
attrs = {}
for name in field_names:
value = getattr(template, name, None)
if value is None:
continue
if name == 'push_params':
attrs['params'] = value
else:
attrs[name] = value
attr_name = field_map.get(name, name)
attrs[attr_name] = value
attrs['secret'] = template.get_secret()
return attrs
@ -178,7 +185,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
params = validated_data.pop('params', None)
self.clean_auth_fields(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
def update(self, instance, validated_data):
@ -280,8 +288,8 @@ class AssetAccountBulkSerializer(
fields = [
'name', 'username', 'secret', 'secret_type', 'passphrase',
'privileged', 'is_active', 'comment', 'template',
'on_invalid', 'push_now', 'assets', 'su_from_username',
'source', 'source_id',
'on_invalid', 'push_now', 'params', 'assets',
'su_from_username', 'source', 'source_id',
]
extra_kwargs = {
'name': {'required': False},
@ -419,16 +427,23 @@ class AssetAccountBulkSerializer(
return results
@staticmethod
def push_accounts_if_need(results, push_now):
def push_accounts_if_need(results, push_now, params):
if not push_now:
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):
params = validated_data.pop('params', None)
push_now = validated_data.pop('push_now', False)
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:
res['asset'] = str(res['asset'])
return results

View File

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

View File

@ -7,3 +7,4 @@ from .node import *
from .platform import *
from .protocol 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 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 common.utils import lazyproperty, timeit
@ -82,6 +82,7 @@ class SerializeToTreeNodeMixin:
data = []
root_assets_count = 0
MyAsset.set_asset_custom_value(assets, self.request.user)
for asset in assets:
platform = platform_map.get(asset.platform_id)
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 serializers
from rest_framework.decorators import action
@ -5,7 +6,7 @@ from rest_framework.response import Response
from assets.const import AllTypes
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.permissions import IsValidUser
from common.serializers import GroupedChoiceSerializer
@ -17,6 +18,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
queryset = Platform.objects.all()
serializer_classes = {
'default': PlatformSerializer,
'list': PlatformListSerializer,
'categories': GroupedChoiceSerializer,
}
filterset_fields = ['name', 'category', 'type']
@ -31,8 +33,8 @@ class AssetPlatformViewSet(JMSModelViewSet):
def get_queryset(self):
# 因为没有走分页逻辑,所以需要这里 prefetch
queryset = super().get_queryset().prefetch_related(
'protocols', 'automation', 'labels', 'labels__label',
queryset = super().get_queryset().annotate(assets_amount=Count('assets')).prefetch_related(
'protocols', 'automation', 'labels', 'labels__label'
)
queryset = queryset.filter(type__in=AllTypes.get_types_values())
return queryset

View File

@ -39,16 +39,16 @@ class NodeChildrenApi(generics.ListCreateAPIView):
self.instance = self.get_object()
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):
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)
# 避免查询 full value
node._full_value = node.value

View File

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

View File

@ -12,7 +12,12 @@
cpu_cores: "{{ ansible_processor_cores }}"
cpu_vcpus: "{{ ansible_processor_vcpus }}"
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_version: "{{ ansible_distribution_version }}"
arch: "{{ ansible_architecture }}"

View File

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

View File

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

View File

@ -117,5 +117,6 @@ class DatabaseTypes(BaseType):
@classmethod
def get_community_types(cls):
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
'domain_enabled': True,
'su_enabled': True,
'su_methods': ['sudo', 'su'],
'su_methods': ['sudo', 'su', 'only_sudo', 'only_su'],
},
cls.WINDOWS: {
'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')],
'default': 'any',
'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': {
'type': 'str',
@ -208,6 +219,12 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': 'admin',
'label': _('Auth source'),
'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': {
'api_mode': {
'type': 'choice',
'default': 'gpt-3.5-turbo',
'default': 'gpt-4o-mini',
'label': _('API mode'),
'choices': [
('gpt-3.5-turbo', 'GPT-3.5 Turbo'),
('gpt-3.5-turbo-1106', 'GPT-3.5 Turbo 1106'),
('gpt-4o-mini', 'GPT-4o-mini'),
('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
@classmethod

View File

@ -171,12 +171,9 @@ class AllTypes(ChoicesMixin):
(Category.DEVICE, DeviceTypes),
(Category.DATABASE, DatabaseTypes),
(Category.WEB, WebTypes),
(Category.CLOUD, CloudTypes),
(Category.CUSTOM, CustomTypes)
]
if settings.XPACK_ENABLED:
types.extend([
(Category.CLOUD, CloudTypes),
(Category.CUSTOM, CustomTypes),
])
return types
@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 .favorite_asset import *
from .automations import *
from .my_asset import *

View File

@ -173,7 +173,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
def get_labels(self):
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) \
.values_list('label_id', flat=True)
return Label.objects.filter(id__in=label_ids)

View File

@ -31,7 +31,7 @@ class Domain(LabeledMixin, JMSOrgBaseModel):
@lazyproperty
def assets_amount(self):
return self.assets.count()
return self.assets.exclude(platform__name='Gateway').count()
def random_gateway(self):
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.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.models import JMSBaseModel
@ -111,6 +111,10 @@ class Platform(LabeledMixin, JMSBaseModel):
def type_constraints(self):
return AllTypes.get_constraints(self.category, self.type)
@lazyproperty
def assets_amount(self):
return self.assets.count()
@classmethod
def default(cls):
linux, created = cls.objects.get_or_create(
@ -127,6 +131,17 @@ class Platform(LabeledMixin, JMSBaseModel):
return True
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):
return self.name

View File

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

View File

@ -36,6 +36,7 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe
class AutomationExecutionSerializer(serializers.ModelSerializer):
snapshot = serializers.SerializerMethodField(label=_('Automation snapshot'))
status = serializers.SerializerMethodField(label=_("Status"))
trigger = LabeledChoiceField(choices=Trigger.choices, read_only=True, label=_("Trigger mode"))
class Meta:
@ -45,6 +46,14 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
]
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
def get_snapshot(obj):
from assets.const import AutomationTypes as AssetTypes

View File

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

View File

@ -3,7 +3,6 @@
from rest_framework import serializers
from orgs.utils import tmp_to_root_org
from common.serializers import BulkSerializerMixin
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.validators import UniqueValidator
from assets.models import Asset
from common.serializers import (
WritableNestedModelSerializer, type_field_map, MethodSerializer,
DictSerializer, create_serializer_class, ResourceLabelsMixin
)
from common.serializers.fields import LabeledChoiceField
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import lazyproperty
from ..const import Category, AllTypes, Protocol
from ..const import Category, AllTypes, Protocol, SuMethodChoices
from ..models import Platform, PlatformProtocol, PlatformAutomation
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer"]
__all__ = ["PlatformSerializer", "PlatformOpsMethodSerializer", "PlatformProtocolSerializer", "PlatformListSerializer"]
class PlatformAutomationSerializer(serializers.ModelSerializer):
@ -158,13 +159,6 @@ class PlatformCustomField(serializers.Serializer):
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(
label='ID', required=False,
validators=[UniqueValidator(queryset=Platform.objects.all())]
@ -175,10 +169,12 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
protocols = PlatformProtocolSerializer(label=_("Protocols"), many=True, required=False)
automation = PlatformAutomationSerializer(label=_("Automation"), required=False, default=dict)
su_method = LabeledChoiceField(
choices=SU_METHOD_CHOICES, label=_("Su method"),
required=False, default="sudo", allow_null=True
choices=SuMethodChoices.choices, label=_("Su method"),
required=False, default=SuMethodChoices.sudo, allow_null=True
)
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:
model = Platform
@ -191,7 +187,8 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
'internal', 'date_created', 'date_updated',
'created_by', 'updated_by'
]
fields = fields_small + [
fields_m2m = ['assets', 'assets_amount']
fields = fields_small + fields_m2m + [
"protocols", "domain_enabled", "su_enabled", "su_method",
"automation", "comment", "custom_fields", "labels"
] + read_only_fields
@ -208,6 +205,7 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
"help_text": _("Assets can be connected using a zone gateway")
},
"domain_default": {"label": _('Default Domain')},
'assets': {'required': False, 'label': _('Assets')},
}
def __init__(self, *args, **kwargs):
@ -265,6 +263,11 @@ class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
return automation
class PlatformListSerializer(PlatformSerializer):
class Meta(PlatformSerializer.Meta):
fields = list(set(PlatformSerializer.Meta.fields + ['assets_amount']) - {'assets'})
class PlatformOpsMethodSerializer(serializers.Serializer):
id = serializers.CharField(read_only=True)
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'protocol-settings', api.PlatformProtocolViewSet, 'protocol-setting')
router.register(r'labels', LabelViewSet, 'label')
router.register(r'my-asset', api.MyAssetViewSet, 'my-asset')
urlpatterns = [
# 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.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.encode import Singleton
from common.utils.timezone import as_current_tz
@ -109,19 +109,31 @@ class OperatorLogHandler(metaclass=Singleton):
return ','.join(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):
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):
value = _('Yes') if value else _('No')
elif isinstance(value, (list, tuple)):
value = self.serialized_value(value)
elif isinstance(value, dict) and loop:
self.__data_processing(value, loop=False)
if key in encrypted_field_set:
if self.__similar_check(field_name):
value = encrypt_value
dict_item[key] = value
return dict_item
new_data[label] = value
return new_data
def data_processing(self, before, after):
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 ops.celery.decorator import register_as_period_task
from ops.models import CeleryTaskExecution
from orgs.utils import tmp_to_root_org
from terminal.backends import server_replay_storage
from terminal.models import Session, Command
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)
def clean_audits_log_period():
print("Start clean audit session task log")
clean_login_log_period()
clean_operation_log_period()
clean_ftp_log_period()
clean_activity_log_period()
clean_celery_tasks_period()
clean_expired_session_period()
clean_password_change_log_period()
with tmp_to_root_org():
clean_login_log_period()
clean_operation_log_period()
clean_ftp_log_period()
clean_activity_log_period()
clean_celery_tasks_period()
clean_expired_session_period()
clean_password_change_log_period()
@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):
continue
try:
data.setdefault(str(f.verbose_name), value)
data.setdefault(
str(f.verbose_name), {'name': getattr(f, 'column', ''), 'value': value}
)
except Exception as e:
print(f.__dict__)
raise e
@ -106,7 +108,9 @@ def model_to_dict_for_operate_log(
return
try:
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:
pass

View File

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

View File

@ -55,14 +55,14 @@ class UserSessionApi(generics.RetrieveDestroyAPIView):
def retrieve(self, request, *args, **kwargs):
if isinstance(request.user, AnonymousUser):
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_403_FORBIDDEN)
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):
if isinstance(request.user, AnonymousUser):
return Response(status=status.HTTP_200_OK)
return Response(status=status.HTTP_403_FORBIDDEN)
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
from django.conf import settings
from django.contrib.auth import get_user_model
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()
@ -28,8 +28,8 @@ class CreateUserMixin:
email = '{}@{}'.format(username, email_suffix)
user = User(username=username, name=username, email=email)
user.source = user.Source.radius.value
user.save()
radius_create_user.send(sender=user.__class__, user=user)
return user
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
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.views.decorators.csrf import csrf_exempt
from django.contrib import auth
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.errors import OneLogin_Saml2_Error
from onelogin.saml2.idp_metadata_parser import (
@ -16,23 +14,29 @@ from onelogin.saml2.idp_metadata_parser import (
dict_deep_merge
)
from .settings import JmsSaml2Settings
from common.utils import get_logger
from .settings import JmsSaml2Settings
logger = get_logger(__file__)
class PrepareRequestMixin:
@staticmethod
def is_secure():
url_result = parse.urlparse(settings.SITE_URL)
return 'on' if url_result.scheme == 'https' else 'off'
@property
def parsed_url(self):
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):
result = {
'https': self.is_secure(),
'http_host': request.META['HTTP_HOST'],
'http_host': self.http_host(),
'script_name': request.META['PATH_INFO'],
'get_data': request.GET.copy(),
'post_data': request.POST.copy()
@ -275,7 +279,7 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin):
logger.debug(log_prompt.format('Redirect'))
redir = post_data.get('RelayState')
if not redir or len(redir) == 0:
redir = "/"
redir = "/"
next_url = saml_instance.redirect_to(redir)
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 .connection_token import *
from .private_token import *
from .ssh_key import *
from .sso_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)
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'))
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 .connection_token import *
from .password_mfa import *
from .ssh_key import *
from .token import *

View File

@ -40,6 +40,7 @@ class ConnectionTokenSerializer(CommonModelSerializer):
'from_ticket': {'read_only': True},
'value': {'read_only': True},
'is_expired': {'read_only': True, 'label': _('Is expired')},
'org_name': {'label': _("Org name")},
}
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 {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 500px;
width: 1000px;
margin-right: auto;
margin-left: auto;
margin-top: calc((100vh - 470px) / 3);
}
body {
position: relative;
width: 100vw;
height: 100vh;
background-color: #f3f3f3;
height: calc(100vh - (100vh - 470px) / 3);
{#height: calc(100vh - (100vh - 470px) / 3);#}
}
.captcha {
@ -99,6 +104,27 @@
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 {
color: red;
}
@ -112,11 +138,11 @@
}
.jms-title {
padding: 22px 10px 10px;
{#padding: 22px 10px 10px;#}
}
.more-login-items {
margin-top: 10px;
margin-top: 15px;
}
.more-login-item {
@ -153,6 +179,9 @@
}
.jms-title {
display: flex;
justify-content: center;
align-items: center;
font-size: 21px;
font-weight: 400;
color: #151515;
@ -252,7 +281,7 @@
.mobile-logo {
display: block;
padding: 0 30px;
padding: 0 45px;
text-align: left;
}
@ -260,6 +289,15 @@
height: revert;
width: revert;
}
.left-form-box .form-panel {
transform: translateY(-65%);
}
.left-form-box .form-panel .form-mobile h2 {
padding: 0;
margin: 0;
}
}
</style>
</head>
@ -279,14 +317,15 @@
</a>
</div>
<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">
<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 style="position: relative;top: 50%;transform: translateY(-50%);">
<div style='padding: 15px 60px; text-align: left'>
<h2 style='font-weight: 400;display: inline'>
<div class="form-panel">
<div class="form-mobile">
<h2 style='font-weight: 400;'>
{% trans 'Login' %}
</h2>
<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('super-connection-token', api.SuperConnectionTokenViewSet, 'super-connection-token')
router.register('confirm', api.UserConfirmationViewSet, 'confirm')
router.register('ssh-key', api.SSHkeyViewSet, 'ssh-key')
urlpatterns = [
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'),
# 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/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'),
# OTP Setting
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.django import reverse, get_object_or_none
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
logger = get_logger(__file__)
@ -46,9 +46,6 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
def verify_state(self):
raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def create_user_if_not_exist(self, user_id, **kwargs):
user = None
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)
if create:
setattr(user, 'source', self.user_type)
bind_user_to_org_role(user)
user.save()
except IntegrityError as 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):
raise NotImplementedError
def get_verify_state_failed_response(self, redirect_uri):
raise NotImplementedError
def get_already_bound_response(self, redirect_uri):
raise NotImplementedError
@ -151,11 +146,9 @@ class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View):
setattr(user, f'{self.auth_type}_id', auth_user_id)
user.save()
except IntegrityError as e:
if e.args[0] == 1062:
msg = _('The %s is already bound to another user') % self.auth_type_label
response = self.get_failed_response(redirect_url, msg, msg)
return response
raise e
msg = _('The %s is already bound to another user') % self.auth_type_label
response = self.get_failed_response(redirect_url, msg, msg)
return response
ip = get_request_ip(request)
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):
state = self.request.GET.get('state')
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)
return self.verify_state_with_session_key(DINGTALK_STATE_SESSION_KEY)
def get_already_bound_response(self, redirect_url):
msg = _('DingTalk is already bound')

View File

@ -58,15 +58,7 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
)
def verify_state(self):
state = self.request.GET.get('state')
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)
return self.verify_state_with_session_key(self.state_session_key)
def get_qr_url(self, redirect_uri):
state = random_string(16)

View File

@ -194,9 +194,6 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
if self.request.GET.get("admin", 0):
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')]
if not auth_types:
return None

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import gettext_lazy as _
from common.utils import FlashMessageUtil
@ -32,3 +33,12 @@ class FlashMessageMixin:
def get_failed_response(self, redirect_url, title, msg, interval=10):
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):
state = self.request.GET.get('state')
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)
return self.verify_state_with_session_key(SLACK_STATE_SESSION_KEY)
def get_qr_url(self, redirect_uri):
state = random_string(16)

View File

@ -45,15 +45,7 @@ class WeComBaseMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashM
)
def verify_state(self):
state = self.request.GET.get('state')
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)
return self.verify_state_with_session_key(WECOM_STATE_SESSION_KEY)
def get_already_bound_response(self, redirect_url):
msg = _('WeCom is already bound')

View File

@ -5,18 +5,18 @@ import uuid
from django.core.cache import cache
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.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.views.http import HttpResponseTemporaryRedirect
from common.utils import get_logger
from common.const import KEY_CACHE_RESOURCE_IDS
from common.views.http import HttpResponseTemporaryRedirect
__all__ = [
'LogTailApi', 'ResourcesIDCacheApi'
'LogTailApi', 'ResourcesIDCacheApi', 'CountryListApi'
]
logger = get_logger(__file__)
@ -96,6 +96,13 @@ class ResourcesIDCacheApi(APIView):
return Response({'spm': spm})
class CountryListApi(APIView):
permission_classes = (IsValidUser,)
def get(self, request, *args, **kwargs):
return Response(COUNTRY_CALLING_CODES)
@csrf_exempt
def redirect_plural_name_api(request, *args, **kwargs):
resource = kwargs.get("resource", "")

View File

@ -1,11 +1,58 @@
import phonenumbers
import pycountry
from django.db import models
from django.utils.translation import gettext_lazy as _
from phonenumbers import PhoneMetadata
ADMIN = 'Admin'
USER = 'User'
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):
manual = 'manual', _('Manual trigger')
timing = 'timing', _('Timing trigger')
@ -28,17 +75,4 @@ class Language(models.TextChoices):
jp = 'ja', '日本語',
COUNTRY_CALLING_CODES = [
{'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'}
]
COUNTRY_CALLING_CODES = get_country_phone_choices()

View File

@ -14,7 +14,6 @@ from django.db.models import Q, Manager, QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework.utils.encoders import JSONEncoder
from common.local import add_encrypted_field_set
from common.utils import contains_ip
from .utils import Encryptor
from .validators import PortRangeValidator
@ -168,7 +167,6 @@ class EncryptTextField(EncryptMixin, models.TextField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
class EncryptCharField(EncryptMixin, models.CharField):
@ -184,7 +182,6 @@ class EncryptCharField(EncryptMixin, models.CharField):
def __init__(self, *args, **kwargs):
self.change_max_length(kwargs)
super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
@ -198,13 +195,11 @@ class EncryptCharField(EncryptMixin, models.CharField):
class EncryptJsonDictTextField(EncryptMixin, JsonDictTextField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
class EncryptJsonDictCharField(EncryptMixin, JsonDictCharField):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
add_encrypted_field_set(self.verbose_name)
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()
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):
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<terminal>[/.]{36})/)?', 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.strip('$')
@ -70,7 +74,9 @@ known_unauth_urls = [
"/api/v1/authentication/login-confirm-ticket/status/",
"/api/v1/authentication/mfa/select/",
"/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 = [

View File

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

View File

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

View File

@ -2,16 +2,17 @@ import base64
import hmac
import time
from django.conf import settings
from common.sdk.im.mixin import BaseRequest
from common.sdk.im.utils import digest, as_request
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__)
def sign(secret, data):
digest = hmac.HMAC(
key=secret.encode('utf8'),
msg=data.encode('utf8'),
@ -115,6 +116,7 @@ class DingTalkRequests(BaseRequest):
class DingTalk:
def __init__(self, appid, appsecret, agentid, timeout=None):
self._appid = appid or ''
self._appsecret = appsecret or ''
@ -125,6 +127,10 @@ class DingTalk:
timeout=timeout
)
@property
def attributes(self):
return settings.DINGTALK_RENAME_ATTRIBUTES
def get_userinfo_bycode(self, code):
body = {
'clientId': self._appid,
@ -206,17 +212,24 @@ class DingTalk:
data = self._request.post(URL.GET_SEND_MSG_PROGRESS, json=body, with_token=True)
return data
def get_user_detail(self, user_id, **kwargs):
# https://open.dingtalk.com/document/orgapp/query-user-details
body = {'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
@staticmethod
def default_user_detail(data, user_id):
username = data.get('userid', user_id)
name = data.get('name', username)
email = data.get('email') or data.get('org_email')
email = construct_user_email(username, email)
return {
'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
from django.conf import settings
from rest_framework.exceptions import APIException
from common.sdk.im.mixin import RequestMixin, BaseRequest
from common.sdk.im.utils import digest
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__)
@ -53,6 +54,7 @@ class FeishuRequests(BaseRequest):
)
code_key = 'code'
msg_key = 'msg'
url_instance = URL()
def __init__(self, app_id, app_secret, timeout=None):
self._app_id = app_id
@ -65,7 +67,7 @@ class FeishuRequests(BaseRequest):
def request_access_token(self):
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)
access_token = response['tenant_access_token']
@ -92,6 +94,11 @@ class FeiShu(RequestMixin):
app_secret=app_secret,
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):
# https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN
@ -101,7 +108,7 @@ class FeiShu(RequestMixin):
'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)
return data['data']['user_id'], data['data']
@ -126,7 +133,7 @@ class FeiShu(RequestMixin):
try:
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:
# 只处理可预知的错误
logger.exception(e)
@ -134,13 +141,28 @@ class FeiShu(RequestMixin):
return invalid_users
@staticmethod
def get_user_detail(user_id, **kwargs):
# get_user_id_by_code 已经返回个人信息,这里直接解析
data = kwargs['other_info']
username = user_id
def default_user_detail(data, user_id):
username = data.get('user_id', user_id)
name = data.get('name', username)
email = data.get('email') or data.get('enterprise_email')
email = construct_user_email(username, email)
return {
'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 ..feishu import URL as FeiShuURL, FeishuRequests, FeiShu
@ -9,8 +11,12 @@ class URL(FeiShuURL):
class LarkRequests(FeishuRequests):
pass
url_instance = URL()
class Lark(FeiShu):
requests_cls = LarkRequests
@property
def attributes(self):
return settings.LARK_RENAME_ATTRIBUTES

View File

@ -1,11 +1,12 @@
import mistune
import requests
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException
from common.utils.common import get_logger
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__)
@ -93,12 +94,17 @@ class SlackRequests:
class Slack:
def __init__(self, client_id=None, client_secret=None, bot_token=None, **kwargs):
self._client = SlackRequests(
client_id=client_id, client_secret=client_secret, bot_token=bot_token
)
self.markdown = mistune.Markdown(renderer=SlackRenderer())
@property
def attributes(self):
return settings.SLACK_RENAME_ATTRIBUTES
def get_user_id_by_code(self, code):
self._client.request_access_token(code)
response = self._client.request(
@ -138,13 +144,22 @@ class Slack:
logger.exception(e)
@staticmethod
def get_user_detail(user_id, **kwargs):
# get_user_id_by_code 已经返回个人信息,这里直接解析
user_info = kwargs['other_info']
username = user_info.get('name') or user_id
name = user_info.get('real_name', username)
email = user_info.get('profile', {}).get('email')
def default_user_detail(data, user_id):
username = data.get('id', user_id)
username = data.get('name', username)
name = data.get('real_name', username)
email = data.get('profile.email')
email = construct_user_email(username, email)
return {
'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 django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import APIException
from common.sdk.im.mixin import RequestMixin, BaseRequest
from common.sdk.im.utils import digest, update_values
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__)
@ -92,6 +93,10 @@ class WeCom(RequestMixin):
timeout=timeout
)
@property
def attributes(self):
return settings.WECOM_RENAME_ATTRIBUTES
def send_markdown(self, users: Iterable, msg: AnyStr, **kwargs):
pass
@ -173,14 +178,20 @@ class WeCom(RequestMixin):
logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
raise WeComError
def get_user_detail(self, user_id, **kwargs):
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196
params = {'userid': user_id}
data = self._requests.get(URL.GET_USER_DETAIL, params)
username = data.get('userid')
@staticmethod
def default_user_detail(data, user_id):
username = data.get('userid', user_id)
name = data.get('name', username)
email = data.get('email') or data.get('biz_mail')
email = construct_user_email(username, email)
return {
'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}
try:
response = action(url=settings.CUSTOM_SMS_URL, verify=False, **kwargs)
if response.reason != 'OK':
raise JMSException(detail=response.text, code=response.status_code)
response.raise_for_status()
except Exception as exc:
logger.error('Custom sms error: {}'.format(exc))
raise JMSException(exc)

View File

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

View File

@ -9,4 +9,5 @@ app_name = 'common'
urlpatterns = [
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
args = [str(arg) for arg in args]
args = [arg.replace(', ', ' ') for arg in args]
try:
tpl % tuple(args)

View File

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

View File

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