mirror of https://github.com/jumpserver/jumpserver
commit
56373e362b
|
@ -8,3 +8,4 @@ celerybeat.pid
|
|||
.vagrant/
|
||||
apps/xpack/.git
|
||||
.history/
|
||||
.idea
|
|
@ -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 }}
|
|
@ -1,3 +1,4 @@
|
|||
[settings]
|
||||
line_length=120
|
||||
known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets
|
||||
|
||||
|
|
103
Dockerfile
103
Dockerfile
|
@ -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"]
|
||||
|
|
|
@ -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
|
|
@ -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
|
41
README.md
41
README.md
|
@ -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-->
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: 'ホームディレクトリ'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: 'ホームディレクトリ'
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: 'ホームディレクトリ'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: 'ホームディレクトリ'
|
||||
|
|
|
@ -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'},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -7,3 +7,4 @@ from .node import *
|
|||
from .platform import *
|
||||
from .protocol import *
|
||||
from .tree import *
|
||||
from .my_asset import *
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 }}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
]
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -7,3 +7,4 @@ from .domain import *
|
|||
from .node import *
|
||||
from .favorite_asset import *
|
||||
from .automations import *
|
||||
from .my_asset import *
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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}'
|
|
@ -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
|
||||
|
||||
|
|
|
@ -9,3 +9,4 @@ from .favorite_asset import *
|
|||
from .gateway import *
|
||||
from .node import *
|
||||
from .platform import *
|
||||
from .my_asset import *
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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"))
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 *
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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()
|
|
@ -0,0 +1 @@
|
|||
from .backends import *
|
|
@ -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):
|
|
@ -0,0 +1,3 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
radius_create_user = Signal()
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
]
|
|
@ -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 *
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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 *
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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">
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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", "")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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', '-'
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue