mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d3eb8c75d |
@@ -1,4 +1,5 @@
|
||||
.git
|
||||
logs/*
|
||||
data/*
|
||||
.github
|
||||
tmp/*
|
||||
@@ -7,4 +8,4 @@ celerybeat.pid
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
apps/xpack/.git
|
||||
.history/
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/----.md
vendored
6
.github/ISSUE_TEMPLATE/----.md
vendored
@@ -3,10 +3,8 @@ name: 需求建议
|
||||
about: 提出针对本项目的想法和建议
|
||||
title: "[Feature] "
|
||||
labels: 类型:需求
|
||||
assignees:
|
||||
- ibuler
|
||||
- baijiangjie
|
||||
- wojiushixiaobai
|
||||
assignees: ibuler
|
||||
|
||||
---
|
||||
|
||||
**请描述您的需求或者改进建议.**
|
||||
|
||||
12
.github/ISSUE_TEMPLATE/bug---.md
vendored
12
.github/ISSUE_TEMPLATE/bug---.md
vendored
@@ -3,13 +3,11 @@ name: Bug 提交
|
||||
about: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[Bug] "
|
||||
labels: 类型:bug
|
||||
assignees:
|
||||
- wojiushixiaobai
|
||||
- baijiangjie
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
**JumpServer 版本( v2.28 之前的版本不再支持 )**
|
||||
**JumpServer 版本(v1.5.9以下不再支持)**
|
||||
|
||||
|
||||
**浏览器版本**
|
||||
@@ -19,6 +17,6 @@ assignees:
|
||||
|
||||
|
||||
**Bug 重现步骤(有截图更好)**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/question.md
vendored
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -3,9 +3,7 @@ name: 问题咨询
|
||||
about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||
title: "[Question] "
|
||||
labels: 类型:提问
|
||||
assignees:
|
||||
- wojiushixiaobai
|
||||
- baijiangjie
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
|
||||
31
.github/workflows/issue-comment.yml
vendored
31
.github/workflows/issue-comment.yml
vendored
@@ -21,44 +21,17 @@ jobs:
|
||||
actions: 'remove-labels'
|
||||
labels: '状态:待反馈'
|
||||
|
||||
add-label-if-is-member:
|
||||
add-label-if-not-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: (github.event.issue.user.id != github.event.comment.user.id) && !github.event.issue.pull_request && (github.event.issue.state == 'open')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get Organization name
|
||||
id: org_name
|
||||
run: echo "data=$(echo '${{ github.repository }}' | cut -d '/' -f 1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Organization public members
|
||||
uses: octokit/request-action@v2.x
|
||||
id: members
|
||||
with:
|
||||
route: GET /orgs/${{ steps.org_name.outputs.data }}/public_members
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Process public members data
|
||||
# 将 members 中的数据转化为 login 字段的拼接字符串
|
||||
id: member_names
|
||||
run: echo "data=$(echo '${{ steps.members.outputs.data }}' | jq '[.[].login] | join(",")')" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- run: "echo members: '${{ steps.members.outputs.data }}'"
|
||||
- run: "echo member names: '${{ steps.member_names.outputs.data }}'"
|
||||
- run: "echo comment user: '${{ github.event.comment.user.login }}'"
|
||||
- run: "echo contains? : '${{ contains(steps.member_names.outputs.data, github.event.comment.user.login) }}'"
|
||||
|
||||
- name: Add require replay label
|
||||
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '状态:待反馈'
|
||||
|
||||
- name: Remove require handle label
|
||||
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
|
||||
1
.github/workflows/jms-build-test.yml
vendored
1
.github/workflows/jms-build-test.yml
vendored
@@ -24,7 +24,6 @@ jobs:
|
||||
build-args: |
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_MIRROR=https://pypi.org/simple
|
||||
PIP_JMS_MIRROR=https://pypi.org/simple
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
||||
2
.github/workflows/sync-gitee.yml
vendored
2
.github/workflows/sync-gitee.yml
vendored
@@ -20,4 +20,4 @@ jobs:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}
|
||||
with:
|
||||
source-repo: 'git@github.com:jumpserver/jumpserver.git'
|
||||
destination-repo: 'git@gitee.com:fit2cloud-feizhiyun/JumpServer.git'
|
||||
destination-repo: 'git@gitee.com:jumpserver/jumpserver.git'
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,6 +35,7 @@ celerybeat-schedule.db
|
||||
docs/_build/
|
||||
xpack
|
||||
xpack.bak
|
||||
logs/*
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
release/*
|
||||
@@ -42,4 +43,3 @@ releashe
|
||||
/apps/script.py
|
||||
data/*
|
||||
test.py
|
||||
.history/
|
||||
|
||||
35
Dockerfile
35
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM jumpserver/python:3.9-slim-buster as stage-build
|
||||
FROM python:3.9-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
@@ -8,7 +8,7 @@ WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM jumpserver/python:3.9-slim-buster
|
||||
FROM python:3.9-slim
|
||||
ARG TARGETARCH
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
@@ -24,11 +24,9 @@ ARG DEPENDENCIES=" \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
freerdp2-dev \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
@@ -57,7 +55,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
@@ -67,36 +65,27 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
|
||||
ARG DOWNLOAD_URL=https://download.jumpserver.org
|
||||
|
||||
RUN set -ex \
|
||||
&& \
|
||||
if [ "${TARGETARCH}" == "amd64" ] || [ "${TARGETARCH}" == "arm64" ]; then \
|
||||
mkdir -p /opt/oracle; \
|
||||
cd /opt/oracle; \
|
||||
wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \
|
||||
unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \
|
||||
echo "/opt/oracle/instantclient_19_10" > /etc/ld.so.conf.d/oracle-instantclient.conf; \
|
||||
ldconfig; \
|
||||
rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \
|
||||
fi
|
||||
RUN mkdir -p /opt/oracle/ \
|
||||
&& cd /opt/oracle/ \
|
||||
&& wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
|
||||
&& unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
|
||||
&& sh -c "echo /opt/oracle/instantclient_19_10 > /etc/ld.so.conf.d/oracle-instantclient.conf" \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip
|
||||
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
set -ex \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install --upgrade setuptools wheel \
|
||||
&& \
|
||||
if [ "${TARGETARCH}" == "loong64" ]; then \
|
||||
pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl; \
|
||||
pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl; \
|
||||
pip install https://download.jumpserver.org/pypi/simple/PyNaCl/PyNaCl-1.5.0-cp39-cp39-linux_loongarch64.whl; \
|
||||
pip install https://download.jumpserver.org/pypi/simple/grpcio/grpcio-1.54.2-cp39-cp39-linux_loongarch64.whl; \
|
||||
fi \
|
||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install -r requirements/requirements.txt
|
||||
|
||||
|
||||
95
Dockerfile.loong64
Normal file
95
Dockerfile.loong64
Normal file
@@ -0,0 +1,95 @@
|
||||
FROM python:3.9-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.9-slim
|
||||
ARG TARGETARCH
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
locales \
|
||||
openssh-client \
|
||||
procps \
|
||||
sshpass \
|
||||
telnet \
|
||||
unzip \
|
||||
vim \
|
||||
git \
|
||||
wget"
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
set -ex \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install --upgrade setuptools wheel \
|
||||
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl \
|
||||
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl \
|
||||
&& pip install $(grep 'PyNaCl' requirements/requirements.txt) \
|
||||
&& GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \
|
||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& rm -rf /tmp/build
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
2
GITSHA
2
GITSHA
@@ -1 +1 @@
|
||||
fcfd7bb469b89c09ec8ed90ef0257266bb63a557
|
||||
19276e6bd4bf7bcc805351c89c90e5af8eafcca9
|
||||
|
||||
63
README.md
63
README.md
@@ -10,26 +10,10 @@
|
||||
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
JumpServer <a href="https://github.com/jumpserver/jumpserver/releases/tag/v3.0.0">v3.0</a> 正式发布。
|
||||
<br>
|
||||
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||
</p>
|
||||
--------------------------
|
||||
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||
|
||||
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||
|
||||
- **SSH**: Linux / Unix / 网络设备 等;
|
||||
- **Windows**: Web 方式连接 / 原生 RDP 连接;
|
||||
- **数据库**: MySQL / MariaDB / PostgreSQL / Oracle / SQLServer / ClickHouse 等;
|
||||
- **NoSQL**: Redis / MongoDB 等;
|
||||
- **GPT**: ChatGPT 等;
|
||||
- **云服务**: Kubernetes / VMware vSphere 等;
|
||||
- **Web 站点**: 各类系统的 Web 管理后台;
|
||||
- **应用**: 通过 Remote App 连接各类应用。
|
||||
|
||||
## 产品特色
|
||||
|
||||
- **开源**: 零门槛,线上快速获取和安装;
|
||||
@@ -38,10 +22,12 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
- **多云支持**: 一套系统,同时管理不同云上面的资产;
|
||||
- **多租户**: 一套系统,多个子公司或部门同时使用;
|
||||
- **云端存储**: 审计录像云端存储,永不丢失;
|
||||
- **多应用支持**: 全面支持各类资产,包括服务器、数据库、Windows RemoteApp、Kubernetes 等;
|
||||
- **安全可靠**: 被广泛使用、验证和信赖,连续 9 年的持续研发投入和产品更新升级。
|
||||
|
||||
## UI 展示
|
||||
|
||||

|
||||

|
||||
|
||||
## 在线体验
|
||||
|
||||
@@ -55,9 +41,9 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
|
||||
## 快速开始
|
||||
|
||||
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
|
||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||
- [手动安装](https://github.com/jumpserver/installer)
|
||||
- [产品文档](https://docs.jumpserver.org)
|
||||
- [在线学习](https://edu.fit2cloud.com/page/2635362)
|
||||
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
|
||||
|
||||
## 案例研究
|
||||
@@ -75,11 +61,14 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
- [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
|
||||
- [江苏农信:JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
|
||||
|
||||
## 社区交流
|
||||
## 社区
|
||||
|
||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。
|
||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)
|
||||
或加入到我们的社区当中进行进一步交流沟通。
|
||||
|
||||
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
|
||||
### 微信交流群
|
||||
|
||||
<img src="https://download.jumpserver.org/images/wecom-group.jpeg" alt="微信群二维码" width="200"/>
|
||||
|
||||
### 参与贡献
|
||||
|
||||
@@ -89,20 +78,15 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
||||
|
||||
## 组件项目
|
||||
|
||||
| 项目 | 状态 | 描述 |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目 |
|
||||
| [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 项目 |
|
||||
| [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 安装包 项目 |
|
||||
| 项目 | 状态 | 描述 |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
|
||||
## 安全说明
|
||||
|
||||
@@ -112,6 +96,11 @@ JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.ju
|
||||
- 邮箱:support@fit2cloud.com
|
||||
- 电话:400-052-0755
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Apache Guacamole](https://guacamole.apache.org/): Web 页面连接 RDP、SSH、VNC 等协议资产,JumpServer Lion 组件使用到该项目;
|
||||
- [OmniDB](https://omnidb.org/): Web 页面连接使用数据库,JumpServer Web 数据库组件使用到该项目。
|
||||
|
||||
## License & Copyright
|
||||
|
||||
Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.generics import ListAPIView, CreateAPIView
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.filters import AccountFilterSet
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset, Node
|
||||
from common.api import ExtraFilterFieldsMixin
|
||||
from common.permissions import UserConfirmation, ConfirmType, IsValidUser
|
||||
from assets.models import Asset
|
||||
from common.permissions import UserConfirmation, ConfirmType
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
__all__ = [
|
||||
'AccountViewSet', 'AccountSecretsViewSet',
|
||||
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
|
||||
'AccountHistoriesSecretAPI'
|
||||
]
|
||||
|
||||
|
||||
class AccountViewSet(OrgBulkModelViewSet):
|
||||
model = Account
|
||||
search_fields = ('username', 'name', 'asset__name', 'asset__address')
|
||||
search_fields = ('username', 'asset__address', 'name')
|
||||
filterset_class = AccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSerializer,
|
||||
@@ -30,9 +28,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
rbac_perms = {
|
||||
'partial_update': ['accounts.change_account'],
|
||||
'su_from_accounts': 'accounts.view_account',
|
||||
'clear_secret': 'accounts.change_account',
|
||||
}
|
||||
export_as_zip = True
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-accounts')
|
||||
def su_from_accounts(self, request, *args, **kwargs):
|
||||
@@ -46,43 +42,11 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
accounts = asset.accounts.all()
|
||||
else:
|
||||
accounts = Account.objects.none()
|
||||
accounts = []
|
||||
accounts = self.filter_queryset(accounts)
|
||||
serializer = serializers.AccountSerializer(accounts, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(
|
||||
methods=['get'], detail=False, url_path='username-suggestions',
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
def username_suggestions(self, request, *args, **kwargs):
|
||||
asset_ids = request.query_params.get('assets')
|
||||
node_keys = request.query_params.get('keys')
|
||||
username = request.query_params.get('username')
|
||||
|
||||
assets = Asset.objects.all()
|
||||
if asset_ids:
|
||||
assets = assets.filter(id__in=asset_ids.split(','))
|
||||
if node_keys:
|
||||
patten = Node.get_node_all_children_key_pattern(node_keys.split(','))
|
||||
assets = assets.filter(nodes__key__regex=patten)
|
||||
|
||||
accounts = Account.objects.filter(asset__in=assets)
|
||||
if username:
|
||||
accounts = accounts.filter(username__icontains=username)
|
||||
usernames = list(accounts.values_list('username', flat=True).distinct()[:10])
|
||||
usernames.sort()
|
||||
common = [i for i in usernames if i in usernames if i.lower() in ['root', 'admin', 'administrator']]
|
||||
others = [i for i in usernames if i not in common]
|
||||
usernames = common + others
|
||||
return Response(data=usernames)
|
||||
|
||||
@action(methods=['patch'], detail=False, url_path='clear-secret')
|
||||
def clear_secret(self, request, *args, **kwargs):
|
||||
account_ids = request.data.get('account_ids', [])
|
||||
self.model.objects.filter(id__in=account_ids).update(secret=None)
|
||||
return Response(status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
@@ -99,21 +63,7 @@ class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
}
|
||||
|
||||
|
||||
class AssetAccountBulkCreateApi(CreateAPIView):
|
||||
serializer_class = serializers.AssetAccountBulkSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'accounts.add_account',
|
||||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.create(serializer.validated_data)
|
||||
serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True)
|
||||
return Response(data=serializer.data, status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, ListAPIView):
|
||||
class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
|
||||
model = Account.history.model
|
||||
serializer_class = serializers.AccountHistorySerializer
|
||||
http_method_names = ['get', 'options']
|
||||
@@ -125,10 +75,6 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, List
|
||||
def get_object(self):
|
||||
return get_object_or_404(Account, pk=self.kwargs.get('pk'))
|
||||
|
||||
@staticmethod
|
||||
def filter_spm_queryset(resource_ids, queryset):
|
||||
return queryset.filter(history_id__in=resource_ids)
|
||||
|
||||
def get_queryset(self):
|
||||
account = self.get_object()
|
||||
histories = account.history.all()
|
||||
|
||||
@@ -24,16 +24,15 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
accounts = data.get('accounts', [])
|
||||
params = data.get('params')
|
||||
account_ids = [str(a.id) for a in accounts]
|
||||
|
||||
if data['action'] == 'push':
|
||||
task = push_accounts_to_assets_task.delay(account_ids, params)
|
||||
task = push_accounts_to_assets_task.delay(account_ids)
|
||||
else:
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
if not asset.auto_config['ansible_enabled'] or \
|
||||
not asset.auto_config['ping_enabled']:
|
||||
if not asset.auto_info['ansible_enabled'] or \
|
||||
not asset.auto_info['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = verify_accounts_connectivity_task.delay(account_ids)
|
||||
|
||||
|
||||
@@ -1,58 +1,19 @@
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.models import AccountTemplate
|
||||
from assets.const import Protocol
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from common.permissions import UserConfirmation, ConfirmType
|
||||
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
|
||||
class AccountTemplateFilterSet(BaseFilterSet):
|
||||
protocols = drf_filters.CharFilter(method='filter_protocols')
|
||||
|
||||
class Meta:
|
||||
model = AccountTemplate
|
||||
fields = ('username', 'name')
|
||||
|
||||
@staticmethod
|
||||
def filter_protocols(queryset, name, value):
|
||||
secret_types = set()
|
||||
protocols = value.split(',')
|
||||
protocol_secret_type_map = Protocol.settings()
|
||||
for p in protocols:
|
||||
if p not in protocol_secret_type_map:
|
||||
continue
|
||||
_st = protocol_secret_type_map[p].get('secret_types', [])
|
||||
secret_types.update(_st)
|
||||
if not secret_types:
|
||||
secret_types = ['password']
|
||||
queryset = queryset.filter(secret_type__in=secret_types)
|
||||
return queryset
|
||||
from accounts import serializers
|
||||
from accounts.models import AccountTemplate
|
||||
|
||||
|
||||
class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||
model = AccountTemplate
|
||||
filterset_class = AccountTemplateFilterSet
|
||||
filterset_fields = ("username", 'name')
|
||||
search_fields = ('username', 'name')
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountTemplateSerializer,
|
||||
'default': serializers.AccountTemplateSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
'su_from_account_templates': 'accounts.view_accounttemplate',
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-account-templates')
|
||||
def su_from_account_templates(self, request, *args, **kwargs):
|
||||
pk = request.query_params.get('template_id')
|
||||
templates = AccountTemplate.get_su_from_account_templates(pk)
|
||||
templates = self.filter_queryset(templates)
|
||||
serializer = self.get_serializer(templates, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
|
||||
class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet):
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.const import Source
|
||||
from accounts.filters import GatheredAccountFilterSet
|
||||
from accounts.models import GatherAccountsAutomation
|
||||
from accounts.models import GatheredAccount
|
||||
@@ -48,12 +50,22 @@ class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||
'default': serializers.GatheredAccountSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'sync_accounts': 'assets.add_gatheredaccount',
|
||||
'sync_account': 'assets.add_gatheredaccount',
|
||||
}
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='sync-accounts')
|
||||
def sync_accounts(self, request, *args, **kwargs):
|
||||
gathered_account_ids = request.data.get('gathered_account_ids')
|
||||
gathered_accounts = self.model.objects.filter(id__in=gathered_account_ids)
|
||||
self.model.sync_accounts(gathered_accounts)
|
||||
@action(methods=['post'], detail=True, url_path='sync')
|
||||
def sync_account(self, request, *args, **kwargs):
|
||||
gathered_account = super().get_object()
|
||||
asset = gathered_account.asset
|
||||
username = gathered_account.username
|
||||
accounts = asset.accounts.filter(username=username)
|
||||
|
||||
if accounts.exists():
|
||||
accounts.update(source=Source.COLLECTED)
|
||||
else:
|
||||
asset.accounts.model.objects.create(
|
||||
asset=asset, username=username,
|
||||
name=f'{username}-{_("Collected")}',
|
||||
source=Source.COLLECTED
|
||||
)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
register: ping_info
|
||||
|
||||
- name: Change asset password
|
||||
custom_command:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
commands: "{{ params.commands }}"
|
||||
first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
|
||||
ignore_errors: true
|
||||
when: ping_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
@@ -1,20 +0,0 @@
|
||||
id: change_secret_by_ssh
|
||||
name: "{{ 'SSH account change secret' | trans }}"
|
||||
category:
|
||||
- device
|
||||
- host
|
||||
type:
|
||||
- all
|
||||
method: change_secret
|
||||
params:
|
||||
- name: commands
|
||||
type: list
|
||||
label: '自定义命令'
|
||||
default: [ '' ]
|
||||
help_text: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||
|
||||
i18n:
|
||||
SSH account change secret:
|
||||
zh: 使用 SSH 命令行自定义改密
|
||||
ja: SSH コマンドライン方式でカスタムパスワード変更
|
||||
en: Custom password change by SSH command line
|
||||
@@ -38,8 +38,8 @@
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
mongodb_ping:
|
||||
@@ -53,3 +53,6 @@
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
id: change_secret_mongodb
|
||||
name: "{{ 'MongoDB account change secret' | trans }}"
|
||||
name: Change secret for MongoDB
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
MongoDB account change secret:
|
||||
zh: 使用 Ansible 模块 mongodb 执行 MongoDB 账号改密
|
||||
ja: Ansible mongodb モジュールを使用して MongoDB アカウントのパスワード変更
|
||||
en: Using Ansible module mongodb to change MongoDB account secret
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.mysql.mysql_info:
|
||||
@@ -38,3 +38,6 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
filter: version
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
@@ -1,13 +1,7 @@
|
||||
id: change_secret_mysql
|
||||
name: "{{ 'MySQL account change secret' | trans }}"
|
||||
name: Change secret for MySQL
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
MySQL account change secret:
|
||||
zh: 使用 Ansible 模块 mysql 执行 MySQL 账号改密
|
||||
ja: Ansible mysql モジュールを使用して MySQL アカウントのパスワード変更
|
||||
en: Using Ansible module mysql to change MySQL account secret
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
mode: "{{ jms_account.mode }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
oracle_ping:
|
||||
@@ -39,3 +39,6 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: change_secret_oracle
|
||||
name: "{{ 'Oracle account change secret' | trans }}"
|
||||
name: Change secret for Oracle
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
Oracle account change secret:
|
||||
zh: Oracle 账号改密
|
||||
ja: Oracle アカウントのパスワード変更
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.postgresql.postgresql_ping:
|
||||
@@ -39,3 +39,8 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: change_secret_postgresql
|
||||
name: "{{ 'PostgreSQL account change secret' | trans }}"
|
||||
name: Change secret for PostgreSQL
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
PostgreSQL account change secret:
|
||||
zh: PostgreSQL 账号改密
|
||||
ja: PostgreSQL アカウントのパスワード変更
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
register: change_info
|
||||
|
||||
- name: Add SQLServer user
|
||||
community.general.mssql_script:
|
||||
@@ -52,8 +52,8 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.general.mssql_script:
|
||||
@@ -64,3 +64,6 @@
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: |
|
||||
SELECT @@version
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: change_secret_sqlserver
|
||||
name: "{{ 'SQLServer account change secret' | trans }}"
|
||||
name: Change secret for SQLServer
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
SQLServer account change secret:
|
||||
zh: SQLServer 账号改密
|
||||
ja: SQLServer アカウントのパスワード変更
|
||||
|
||||
@@ -4,54 +4,33 @@
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Check user
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
expires: -1
|
||||
state: present
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
|
||||
- name: Add user groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
|
||||
- name: Change password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: create user If it already exists, no operation will be performed
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
- secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
@@ -63,7 +42,7 @@
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "password"
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
@@ -72,4 +51,4 @@
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
@@ -1,55 +1,6 @@
|
||||
id: change_secret_aix
|
||||
name: "{{ 'AIX account change secret' | trans }}"
|
||||
name: Change secret for aix
|
||||
category: host
|
||||
type:
|
||||
- AIX
|
||||
method: change_secret
|
||||
params:
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
AIX account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (DES)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
|
||||
en: 'Using Ansible module user to change account secret (DES)'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
Params home help text:
|
||||
zh: '默认家目录 /home/{账号用户名}'
|
||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||
en: 'Default home directory /home/{account username}'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
en: 'Home'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
|
||||
@@ -4,54 +4,33 @@
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Check user
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
expires: -1
|
||||
state: present
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
|
||||
- name: Add user groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
|
||||
- name: Change password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: create user If it already exists, no operation will be performed
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
- secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
@@ -63,7 +42,7 @@
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "password"
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
@@ -72,4 +51,4 @@
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "ssh_key"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
@@ -1,57 +1,7 @@
|
||||
id: change_secret_posix
|
||||
name: "{{ 'Posix account change secret' | trans }}"
|
||||
name: Change secret for posix
|
||||
category: host
|
||||
type:
|
||||
- unix
|
||||
- linux
|
||||
method: change_secret
|
||||
params:
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
help_text: ''
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Posix account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)'
|
||||
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
|
||||
en: 'Using Ansible module user to change account secret (SHA512)'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
Params home help text:
|
||||
zh: '默认家目录 /home/{账号用户名}'
|
||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||
en: 'Default home directory /home/{account username}'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
en: 'Home'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
|
||||
@@ -8,16 +8,19 @@
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
|
||||
- name: Get groups of a Windows user
|
||||
ansible.windows.win_user:
|
||||
name: "{{ jms_account.username }}"
|
||||
register: user_info
|
||||
|
||||
- name: Change password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups: "{{ user_info.groups[0].name }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
|
||||
@@ -1,26 +1,7 @@
|
||||
id: change_secret_local_windows
|
||||
name: "{{ 'Windows account change secret' | trans }}"
|
||||
name: Change secret local account for Windows
|
||||
version: 1
|
||||
method: change_secret
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account change secret:
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密'
|
||||
ja: 'Ansible win_user モジュールを使用して Windows アカウントのパスワード変更'
|
||||
en: 'Using Ansible module win_user to change Windows account secret'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from accounts.models import ChangeSecretRecord
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg
|
||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||
from assets.const import HostTypes
|
||||
from common.utils import get_logger
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
from common.utils.timezone import local_now_display
|
||||
from users.models import User
|
||||
@@ -28,23 +28,23 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.method_hosts_mapper = defaultdict(list)
|
||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||
self.secret_type = self.execution.snapshot['secret_type']
|
||||
self.secret_strategy = self.execution.snapshot.get(
|
||||
'secret_strategy', SecretStrategy.custom
|
||||
)
|
||||
self.ssh_key_change_strategy = self.execution.snapshot.get(
|
||||
'ssh_key_change_strategy', SSHKeyStrategy.add
|
||||
)
|
||||
self.account_ids = self.execution.snapshot['accounts']
|
||||
self.snapshot_account_usernames = self.execution.snapshot['accounts']
|
||||
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.change_secret
|
||||
|
||||
def get_ssh_params(self, account, secret, secret_type):
|
||||
def get_kwargs(self, account, secret):
|
||||
kwargs = {}
|
||||
if secret_type != SecretType.SSH_KEY:
|
||||
if self.secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
kwargs['strategy'] = self.ssh_key_change_strategy
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
@@ -54,34 +54,18 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||
return kwargs
|
||||
|
||||
def secret_generator(self, secret_type):
|
||||
@lazyproperty
|
||||
def secret_generator(self):
|
||||
return SecretGenerator(
|
||||
self.secret_strategy, secret_type,
|
||||
self.secret_strategy, self.secret_type,
|
||||
self.execution.snapshot.get('password_rules')
|
||||
)
|
||||
|
||||
def get_secret(self, secret_type):
|
||||
def get_secret(self):
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
return self.execution.snapshot['secret']
|
||||
else:
|
||||
return self.secret_generator(secret_type).get_secret()
|
||||
|
||||
def get_accounts(self, privilege_account):
|
||||
if not privilege_account:
|
||||
print(f'not privilege account')
|
||||
return []
|
||||
|
||||
asset = privilege_account.asset
|
||||
accounts = asset.accounts.all()
|
||||
accounts = accounts.filter(id__in=self.account_ids)
|
||||
if self.secret_type:
|
||||
accounts = accounts.filter(secret_type=self.secret_type)
|
||||
|
||||
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
|
||||
accounts = accounts.filter(privileged=False).exclude(
|
||||
username__in=['root', 'administrator', privilege_account.username]
|
||||
)
|
||||
return accounts
|
||||
return self.secret_generator.get_secret()
|
||||
|
||||
def host_callback(
|
||||
self, host, asset=None, account=None,
|
||||
@@ -94,10 +78,17 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
accounts = asset.accounts.all()
|
||||
if account:
|
||||
accounts = accounts.exclude(username=account.username)
|
||||
|
||||
if '*' not in self.snapshot_account_usernames:
|
||||
accounts = accounts.filter(username__in=self.snapshot_account_usernames)
|
||||
|
||||
accounts = accounts.filter(secret_type=self.secret_type)
|
||||
if not accounts:
|
||||
print('没有发现待改密账号: %s 用户ID: %s 类型: %s' % (
|
||||
asset.name, self.account_ids, self.secret_type
|
||||
print('没有发现待改密账号: %s 用户名: %s 类型: %s' % (
|
||||
asset.name, self.snapshot_account_usernames, self.secret_type
|
||||
))
|
||||
return []
|
||||
|
||||
@@ -106,17 +97,16 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
method_hosts = [h for h in method_hosts if h != host['name']]
|
||||
inventory_hosts = []
|
||||
records = []
|
||||
host['secret_type'] = self.secret_type
|
||||
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
print(f'Windows {asset} does not support ssh key push')
|
||||
print(f'Windows {asset} does not support ssh key push \n')
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
h['name'] += '(' + account.username + ')'
|
||||
new_secret = self.get_secret(secret_type)
|
||||
new_secret = self.get_secret()
|
||||
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
@@ -126,15 +116,15 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
self.name_recorder_mapper[h['name']] = recorder
|
||||
|
||||
private_key_path = None
|
||||
if secret_type == SecretType.SSH_KEY:
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['kwargs'] = self.get_kwargs(account, new_secret)
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret_type': account.secret_type,
|
||||
'secret': new_secret,
|
||||
'private_key_path': private_key_path
|
||||
}
|
||||
@@ -216,7 +206,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
serializer = serializer_cls(recorders, many=True)
|
||||
|
||||
header = [str(v.label) for v in serializer.child.fields.values()]
|
||||
rows = [[str(i) for i in row.values()] for row in serializer.data]
|
||||
rows = [list(row.values()) for row in serializer.data]
|
||||
if not rows:
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: gather_accounts_mongodb
|
||||
name: "{{ 'MongoDB account gather' | trans }}"
|
||||
name: Gather account from MongoDB
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
MongoDB account gather:
|
||||
zh: MongoDB 账号收集
|
||||
ja: MongoDB アカウントの収集
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
id: gather_accounts_mysql
|
||||
name: "{{ 'MySQL account gather' | trans }}"
|
||||
name: Gather account from MySQL
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
MySQL account gather:
|
||||
zh: MySQL 账号收集
|
||||
ja: MySQL アカウントの収集
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: gather_accounts_oracle
|
||||
name: "{{ 'Oracle account gather' | trans }}"
|
||||
name: Gather account from Oracle
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
Oracle account gather:
|
||||
zh: Oracle 账号收集
|
||||
ja: Oracle アカウントの収集
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: gather_accounts_postgresql
|
||||
name: "{{ 'PostgreSQL account gather' | trans }}"
|
||||
name: Gather account for PostgreSQL
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
PostgreSQL account gather:
|
||||
zh: PostgreSQL 账号收集
|
||||
ja: PostgreSQL アカウントの収集
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import re
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
__all__ = ['GatherAccountsFilter']
|
||||
@@ -15,8 +13,8 @@ class GatherAccountsFilter:
|
||||
def mysql_filter(info):
|
||||
result = {}
|
||||
for _, user_dict in info.items():
|
||||
for username, _ in user_dict.items():
|
||||
if len(username.split('.')) == 1:
|
||||
for username, data in user_dict.items():
|
||||
if data.get('account_locked') == 'N':
|
||||
result[username] = {}
|
||||
return result
|
||||
|
||||
@@ -29,25 +27,18 @@ class GatherAccountsFilter:
|
||||
|
||||
@staticmethod
|
||||
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}')
|
||||
result = {}
|
||||
for line in info:
|
||||
usernames = username_pattern.findall(line)
|
||||
username = ''.join(usernames)
|
||||
if username:
|
||||
result[username] = {}
|
||||
else:
|
||||
data = line.split('@')
|
||||
if len(data) == 1:
|
||||
result[line] = {}
|
||||
continue
|
||||
ip_addrs = ip_pattern.findall(line)
|
||||
ip_addr = ''.join(ip_addrs)
|
||||
if ip_addr:
|
||||
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')
|
||||
result[username].update({'date': date})
|
||||
|
||||
if len(data) != 3:
|
||||
continue
|
||||
username, address, dt = data
|
||||
date = timezone.datetime.strptime(f'{dt} +0800', '%b %d %H:%M:%S %Y %z')
|
||||
result[username] = {'address': address, 'date': date}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
@@ -69,6 +60,4 @@ class GatherAccountsFilter:
|
||||
if not run_method_name:
|
||||
return info
|
||||
|
||||
if hasattr(self, f'{run_method_name}_filter'):
|
||||
return getattr(self, f'{run_method_name}_filter')(info)
|
||||
return info
|
||||
return getattr(self, f'{run_method_name}_filter')(info)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
ansible.builtin.shell:
|
||||
cmd: >
|
||||
users=$(getent passwd | grep -v nologin | grep -v shutdown | awk -F":" '{ print $1 }');for i in $users;
|
||||
do k=$(last -w -F $i -1 | head -1 | grep -v ^$ | awk '{ print $0 }')
|
||||
do k=$(last -w -F $i -1 | head -1 | grep -v ^$ | awk '{ print $1"@"$3"@"$5,$6,$7,$8 }')
|
||||
if [ -n "$k" ]; then
|
||||
echo $k
|
||||
else
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
id: gather_accounts_posix
|
||||
name: "{{ 'Posix account gather' | trans }}"
|
||||
name: Gather posix account
|
||||
category: host
|
||||
type:
|
||||
- linux
|
||||
- unix
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
Posix account gather:
|
||||
zh: 使用命令 getent passwd 收集 Posix 资产账号
|
||||
ja: コマンド getent を使用してアセットアカウントを収集する
|
||||
en: Using command getent to gather accounts
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
id: gather_accounts_windows
|
||||
name: "{{ 'Windows account gather' | trans }}"
|
||||
name: Gather account windows
|
||||
version: 1
|
||||
method: gather_accounts
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
|
||||
i18n:
|
||||
Windows account gather:
|
||||
zh: 使用命令 net user 收集 Windows 账号
|
||||
ja: コマンド net user を使用して Windows アカウントを収集する
|
||||
en: Using command net user to gather accounts
|
||||
|
||||
@@ -12,7 +12,6 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_asset_mapper = {}
|
||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
@@ -23,41 +22,29 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
||||
self.host_asset_mapper[host['name']] = asset
|
||||
return host
|
||||
|
||||
def filter_success_result(self, tp, result):
|
||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||
def filter_success_result(self, host, result):
|
||||
result = GatherAccountsFilter(host).run(self.method_id_meta_mapper, result)
|
||||
return result
|
||||
@staticmethod
|
||||
def generate_data(asset, result):
|
||||
data = []
|
||||
for username, info in result.items():
|
||||
d = {'asset': asset, 'username': username, 'present': True}
|
||||
if info.get('date'):
|
||||
d['date_last_login'] = info['date']
|
||||
if info.get('address'):
|
||||
d['address_last_login'] = info['address'][:32]
|
||||
data.append(d)
|
||||
return data
|
||||
|
||||
def update_or_create_accounts(self, asset, result):
|
||||
data = self.generate_data(asset, result)
|
||||
@staticmethod
|
||||
def update_or_create_gathered_accounts(asset, result):
|
||||
with tmp_to_org(asset.org_id):
|
||||
gathered_accounts = []
|
||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||
for d in data:
|
||||
username = d['username']
|
||||
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
||||
for username, data in result.items():
|
||||
d = {'asset': asset, 'username': username, 'present': True}
|
||||
if data.get('date'):
|
||||
d['date_last_login'] = data['date']
|
||||
if data.get('address'):
|
||||
d['address_last_login'] = data['address'][:32]
|
||||
GatheredAccount.objects.update_or_create(
|
||||
defaults=d, asset=asset, username=username,
|
||||
)
|
||||
gathered_accounts.append(gathered_account)
|
||||
if not self.is_sync_account:
|
||||
return
|
||||
GatheredAccount.sync_accounts(gathered_accounts)
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
info = result.get('debug', {}).get('res', {}).get('info', {})
|
||||
asset = self.host_asset_mapper.get(host)
|
||||
if asset and info:
|
||||
result = self.filter_success_result(asset.type, info)
|
||||
self.update_or_create_accounts(asset, result)
|
||||
self.update_or_create_gathered_accounts(asset, result)
|
||||
else:
|
||||
logger.error("Not found info".format(host))
|
||||
|
||||
@@ -1,6 +1,30 @@
|
||||
import os
|
||||
import copy
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from assets.automations.methods import get_platform_automation_methods
|
||||
|
||||
|
||||
def copy_change_secret_to_push_account(methods):
|
||||
push_account = AutomationTypes.push_account
|
||||
change_secret = AutomationTypes.change_secret
|
||||
copy_methods = copy.deepcopy(methods)
|
||||
for method in copy_methods:
|
||||
if not method['id'].startswith(change_secret):
|
||||
continue
|
||||
copy_method = copy.deepcopy(method)
|
||||
copy_method['method'] = push_account.value
|
||||
copy_method['id'] = copy_method['id'].replace(
|
||||
change_secret, push_account
|
||||
)
|
||||
copy_method['name'] = copy_method['name'].replace(
|
||||
'Change secret', 'Push account'
|
||||
)
|
||||
methods.append(copy_method)
|
||||
return methods
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
platform_automation_methods = get_platform_automation_methods(BASE_DIR)
|
||||
automation_methods = get_platform_automation_methods(BASE_DIR)
|
||||
|
||||
platform_automation_methods = copy_change_secret_to_push_account(automation_methods)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
mongodb_ping:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: db_info
|
||||
|
||||
- name: Display MongoDB version
|
||||
debug:
|
||||
var: db_info.server_version
|
||||
when: db_info is succeeded
|
||||
|
||||
- name: Change MongoDB password
|
||||
mongodb_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
|
||||
- name: Verify password
|
||||
mongodb_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
@@ -1,12 +0,0 @@
|
||||
id: push_account_mongodb
|
||||
name: "{{ 'MongoDB account push' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: push_account
|
||||
|
||||
i18n:
|
||||
MongoDB account push:
|
||||
zh: 使用 Ansible 模块 mongodb 执行 MongoDB 账号推送
|
||||
ja: Ansible mongodb モジュールを使用してアカウントをプッシュする
|
||||
en: Using Ansible module mongodb to push account
|
||||
@@ -1,40 +0,0 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
community.mysql.mysql_info:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
- name: MySQL version
|
||||
debug:
|
||||
var: db_info.version.full
|
||||
|
||||
- name: Change MySQL password
|
||||
community.mysql.mysql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
|
||||
- name: Verify password
|
||||
community.mysql.mysql_info:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
filter: version
|
||||
@@ -1,13 +0,0 @@
|
||||
id: push_account_mysql
|
||||
name: "{{ 'MySQL account push' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: push_account
|
||||
|
||||
i18n:
|
||||
MySQL account push:
|
||||
zh: 使用 Ansible 模块 mysql 执行 MySQL 账号推送
|
||||
ja: Ansible mysql モジュールを使用してアカウントをプッシュする
|
||||
en: Using Ansible module mysql to push account
|
||||
@@ -1,41 +0,0 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
oracle_ping:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ jms_account.mode }}"
|
||||
register: db_info
|
||||
|
||||
- name: Display Oracle version
|
||||
debug:
|
||||
var: db_info.server_version
|
||||
when: db_info is succeeded
|
||||
|
||||
- name: Change Oracle password
|
||||
oracle_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ jms_account.mode }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
|
||||
- name: Verify password
|
||||
oracle_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
@@ -1,12 +0,0 @@
|
||||
id: push_account_oracle
|
||||
name: "{{ 'Oracle account push' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: push_account
|
||||
|
||||
i18n:
|
||||
Oracle account push:
|
||||
zh: 使用 Python 模块 oracledb 执行 Oracle 账号推送
|
||||
ja: Python oracledb モジュールを使用してアカウントをプッシュする
|
||||
en: Using Python module oracledb to push account
|
||||
@@ -1,44 +0,0 @@
|
||||
- hosts: postgre
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
community.postgresql.postgresql_ping:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
- name: Display PostgreSQL version
|
||||
debug:
|
||||
var: result.server_version.full
|
||||
when: result is succeeded
|
||||
|
||||
- name: Change PostgreSQL password
|
||||
community.postgresql.postgresql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
role_attr_flags: LOGIN
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
|
||||
- name: Verify password
|
||||
community.postgresql.postgresql_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
@@ -1,12 +0,0 @@
|
||||
id: push_account_postgresql
|
||||
name: "{{ 'PostgreSQL account push' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: push_account
|
||||
|
||||
i18n:
|
||||
PostgreSQL account push:
|
||||
zh: 使用 Ansible 模块 postgresql 执行 PostgreSQL 账号推送
|
||||
ja: Ansible postgresql モジュールを使用してアカウントをプッシュする
|
||||
en: Using Ansible module postgresql to push account
|
||||
@@ -1,68 +0,0 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: |
|
||||
SELECT @@version
|
||||
register: db_info
|
||||
|
||||
- name: SQLServer version
|
||||
set_fact:
|
||||
info:
|
||||
version: "{{ db_info.query_results[0][0][0][0].splitlines()[0] }}"
|
||||
- debug:
|
||||
var: info
|
||||
|
||||
- name: Check whether SQLServer User exist
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
|
||||
when: db_info is succeeded
|
||||
register: user_exist
|
||||
|
||||
- name: Change SQLServer password
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
register: change_info
|
||||
|
||||
- name: Add SQLServer user
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
ignore_errors: true
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
script: |
|
||||
SELECT @@version
|
||||
@@ -1,12 +0,0 @@
|
||||
id: push_account_sqlserver
|
||||
name: "{{ 'SQLServer account push' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: push_account
|
||||
|
||||
i18n:
|
||||
SQLServer account push:
|
||||
zh: 使用 Ansible 模块 mssql 执行 SQLServer 账号推送
|
||||
ja: Ansible mssql モジュールを使用してアカウントをプッシュする
|
||||
en: Using Ansible module mssql to push account
|
||||
@@ -1,80 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Push user
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
expires: -1
|
||||
state: present
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
|
||||
- name: Add user groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
|
||||
- name: Push user password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Push SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Set 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:
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "ssh_key"
|
||||
@@ -1,36 +0,0 @@
|
||||
id: push_account_aix
|
||||
name: "{{ 'Aix account push' | trans }}"
|
||||
category: host
|
||||
type:
|
||||
- AIX
|
||||
method: push_account
|
||||
params:
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: '家目录'
|
||||
default: ''
|
||||
help_text: '默认家目录 /home/系统用户名: /home/username'
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
|
||||
i18n:
|
||||
Aix account push:
|
||||
zh: 使用 Ansible 模块 user 执行 Aix 账号推送 (DES)
|
||||
ja: Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)
|
||||
en: Using Ansible module user to push account (DES)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: Push user
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
shell: "{{ params.shell }}"
|
||||
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||
groups: "{{ params.groups }}"
|
||||
expires: -1
|
||||
state: present
|
||||
|
||||
- name: "Add {{ account.username }} group"
|
||||
ansible.builtin.group:
|
||||
name: "{{ account.username }}"
|
||||
state: present
|
||||
|
||||
- name: Add user groups
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
groups: "{{ params.groups }}"
|
||||
when: params.groups
|
||||
|
||||
- name: Push user password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ ssh_params.dest }}"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
|
||||
- name: Push SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
when: account.secret_type == "ssh_key"
|
||||
|
||||
- name: Set 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:
|
||||
- params.sudo
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: account.secret_type == "ssh_key"
|
||||
@@ -1,37 +0,0 @@
|
||||
id: push_account_posix
|
||||
name: "{{ 'Posix account push' | trans }}"
|
||||
category: host
|
||||
type:
|
||||
- unix
|
||||
- linux
|
||||
method: push_account
|
||||
params:
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
help_text: ''
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: '家目录'
|
||||
default: ''
|
||||
help_text: '默认家目录 /home/系统用户名: /home/username'
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
|
||||
i18n:
|
||||
Posix account push:
|
||||
zh: 使用 Ansible 模块 user 执行账号推送 (sha512)
|
||||
ja: Ansible user モジュールを使用してアカウントをプッシュする (sha512)
|
||||
en: Using Ansible module user to push account (sha512)
|
||||
@@ -1,31 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
# - name: Print variables
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
- name: Push user password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.windows.win_ping:
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password"
|
||||
@@ -1,19 +0,0 @@
|
||||
id: push_account_local_windows
|
||||
name: "{{ 'Windows account push' | trans }}"
|
||||
version: 1
|
||||
method: push_account
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
|
||||
i18n:
|
||||
Windows account push:
|
||||
zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送
|
||||
ja: Ansible win_user モジュールを使用して Windows アカウントをプッシュする
|
||||
en: Using Ansible module win_user to push account
|
||||
@@ -1,6 +1,9 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType, Connectivity
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from accounts.const import AutomationTypes, SecretType
|
||||
from accounts.models import Account
|
||||
from assets.const import HostTypes
|
||||
from common.utils import get_logger
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
@@ -16,6 +19,36 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
def method_type(cls):
|
||||
return AutomationTypes.push_account
|
||||
|
||||
def create_nonlocal_accounts(self, accounts, snapshot_account_usernames, asset):
|
||||
secret_type = self.secret_type
|
||||
usernames = accounts.filter(secret_type=secret_type).values_list(
|
||||
'username', flat=True
|
||||
)
|
||||
create_usernames = set(snapshot_account_usernames) - set(usernames)
|
||||
create_account_objs = [
|
||||
Account(
|
||||
name=f'{username}-{secret_type}', username=username,
|
||||
secret_type=secret_type, asset=asset,
|
||||
)
|
||||
for username in create_usernames
|
||||
]
|
||||
Account.objects.bulk_create(create_account_objs)
|
||||
|
||||
def get_accounts(self, privilege_account, accounts: QuerySet):
|
||||
if not privilege_account:
|
||||
print(f'not privilege account')
|
||||
return []
|
||||
snapshot_account_usernames = self.execution.snapshot['accounts']
|
||||
if '*' in snapshot_account_usernames:
|
||||
return accounts.exclude(username=privilege_account.username)
|
||||
|
||||
asset = privilege_account.asset
|
||||
self.create_nonlocal_accounts(accounts, snapshot_account_usernames, asset)
|
||||
accounts = asset.accounts.exclude(username=privilege_account.username).filter(
|
||||
username__in=snapshot_account_usernames, secret_type=self.secret_type
|
||||
)
|
||||
return accounts
|
||||
|
||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||
host = super(ChangeSecretManager, self).host_callback(
|
||||
host, asset=asset, account=account, automation=automation,
|
||||
@@ -24,37 +57,34 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
accounts = asset.accounts.all()
|
||||
accounts = self.get_accounts(account, accounts)
|
||||
inventory_hosts = []
|
||||
host['secret_type'] = self.secret_type
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
msg = f'Windows {asset} does not support ssh key push'
|
||||
msg = f'Windows {asset} does not support ssh key push \n'
|
||||
print(msg)
|
||||
return inventory_hosts
|
||||
|
||||
host['ssh_params'] = {}
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
secret_type = account.secret_type
|
||||
h['name'] += '(' + account.username + ')'
|
||||
if self.secret_type is None:
|
||||
new_secret = account.secret
|
||||
else:
|
||||
new_secret = self.get_secret(secret_type)
|
||||
new_secret = self.get_secret()
|
||||
|
||||
self.name_recorder_mapper[h['name']] = {
|
||||
'account': account, 'new_secret': new_secret,
|
||||
}
|
||||
|
||||
private_key_path = None
|
||||
if secret_type == SecretType.SSH_KEY:
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
||||
h['kwargs'] = self.get_kwargs(account, new_secret)
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret_type': account.secret_type,
|
||||
'secret': new_secret,
|
||||
'private_key_path': private_key_path
|
||||
}
|
||||
@@ -74,7 +104,6 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
return
|
||||
account.secret = new_secret
|
||||
account.save(update_fields=['secret'])
|
||||
account.set_connectivity(Connectivity.OK)
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
pass
|
||||
@@ -83,9 +112,9 @@ class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||
logger.error("Pust account error: ", e)
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
if not self.check_secret():
|
||||
return
|
||||
super(ChangeSecretManager, self).run(*args, **kwargs)
|
||||
super().run(*args, **kwargs)
|
||||
|
||||
# @classmethod
|
||||
# def trigger_by_asset_create(cls, asset):
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_shell_type: sh
|
||||
ansible_connection: local
|
||||
|
||||
tasks:
|
||||
- name: Verify account (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
@@ -1,13 +0,0 @@
|
||||
id: verify_account_by_rdp
|
||||
name: "{{ 'Windows rdp account verify' | trans }}"
|
||||
category:
|
||||
- host
|
||||
type:
|
||||
- windows
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
Windows rdp account verify:
|
||||
zh: 使用 Python 模块 pyfreerdp 验证账号
|
||||
ja: Python モジュール pyfreerdp を使用してアカウントを検証する
|
||||
en: Using Python module pyfreerdp to verify account
|
||||
@@ -1,14 +0,0 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
|
||||
tasks:
|
||||
- name: Verify account (paramiko)
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
@@ -1,14 +0,0 @@
|
||||
id: verify_account_by_ssh
|
||||
name: "{{ 'SSH account verify' | trans }}"
|
||||
category:
|
||||
- device
|
||||
- host
|
||||
type:
|
||||
- all
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
SSH account verify:
|
||||
zh: 使用 Python 模块 paramiko 验证账号
|
||||
ja: Python モジュール paramiko を使用してアカウントを検証する
|
||||
en: Using Python module paramiko to verify account
|
||||
@@ -1,12 +1,6 @@
|
||||
id: verify_account_mongodb
|
||||
name: "{{ 'MongoDB account verify' | trans }}"
|
||||
name: Verify account from MongoDB
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
MongoDB account verify:
|
||||
zh: 使用 Ansible 模块 mongodb 验证账号
|
||||
ja: Ansible mongodb モジュールを使用してアカウントを検証する
|
||||
en: Using Ansible module mongodb to verify account
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
id: verify_account_mysql
|
||||
name: "{{ 'MySQL account verify' | trans }}"
|
||||
name: Verify account from MySQL
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
MySQL account verify:
|
||||
zh: 使用 Ansible 模块 mysql 验证账号
|
||||
ja: Ansible mysql モジュールを使用してアカウントを検証する
|
||||
en: Using Ansible module mysql to verify account
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
id: verify_account_oracle
|
||||
name: "{{ 'Oracle account verify' | trans }}"
|
||||
name: Verify account from Oracle
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
Oracle account verify:
|
||||
zh: 使用 Python 模块 oracledb 验证账号
|
||||
ja: Python モジュール oracledb を使用してアカウントを検証する
|
||||
en: Using Python module oracledb to verify account
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
id: verify_account_postgresql
|
||||
name: "{{ 'PostgreSQL account verify' | trans }}"
|
||||
name: Verify account for PostgreSQL
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
PostgreSQL account verify:
|
||||
zh: 使用 Ansible 模块 postgresql 验证账号
|
||||
ja: Ansible postgresql モジュールを使用してアカウントを検証する
|
||||
en: Using Ansible module postgresql to verify account
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
id: verify_account_sqlserver
|
||||
name: "{{ 'SQLServer account verify' | trans }}"
|
||||
name: Verify account from SQLServer
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
SQLServer account verify:
|
||||
zh: 使用 Ansible 模块 mssql 验证账号
|
||||
ja: Ansible mssql モジュールを使用してアカウントを検証する
|
||||
en: Using Ansible module mssql to verify account
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
id: verify_account_posix
|
||||
name: "{{ 'Posix account verify' | trans }}"
|
||||
name: Verify posix account
|
||||
category: host
|
||||
type:
|
||||
- linux
|
||||
- unix
|
||||
method: verify_account
|
||||
|
||||
i18n:
|
||||
Posix account verify:
|
||||
zh: 使用 Ansible 模块 ping 验证账号
|
||||
ja: Ansible ping モジュールを使用してアカウントを検証する
|
||||
en: Using Ansible module ping to verify account
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
- hosts: windows
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify account
|
||||
ansible.windows.win_ping:
|
||||
vars:
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
id: verify_account_windows
|
||||
name: "{{ 'Windows account verify' | trans }}"
|
||||
name: Verify account windows
|
||||
version: 1
|
||||
method: verify_account
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
|
||||
i18n:
|
||||
Windows account verify:
|
||||
zh: 使用 Ansible 模块 win_ping 验证账号
|
||||
ja: Ansible win_ping モジュールを使用してアカウントを検証する
|
||||
en: Using Ansible module win_ping to verify account
|
||||
|
||||
@@ -25,15 +25,6 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n')
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.verify_account
|
||||
|
||||
def get_accounts(self, privilege_account, accounts: QuerySet):
|
||||
account_ids = self.execution.snapshot['accounts']
|
||||
accounts = accounts.filter(id__in=account_ids)
|
||||
return accounts
|
||||
|
||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||
host = super().host_callback(
|
||||
host, asset=asset, account=account,
|
||||
@@ -71,6 +62,16 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
inventory_hosts.append(h)
|
||||
return inventory_hosts
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.verify_account
|
||||
|
||||
def get_accounts(self, privilege_account, accounts: QuerySet):
|
||||
snapshot_account_usernames = self.execution.snapshot['accounts']
|
||||
if '*' not in snapshot_account_usernames:
|
||||
accounts = accounts.filter(username__in=snapshot_account_usernames)
|
||||
return accounts
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
account = self.host_account_mapper.get(host)
|
||||
account.set_connectivity(Connectivity.OK)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from common.utils import get_logger
|
||||
from accounts.const import AutomationTypes
|
||||
from assets.automations.ping_gateway.manager import PingGatewayManager
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -16,6 +16,6 @@ class VerifyGatewayAccountManager(PingGatewayManager):
|
||||
logger.info(">>> 开始执行测试网关账号可连接性任务")
|
||||
|
||||
def get_accounts(self, gateway):
|
||||
account_ids = self.execution.snapshot['accounts']
|
||||
accounts = gateway.accounts.filter(id__in=account_ids)
|
||||
usernames = self.execution.snapshot['accounts']
|
||||
accounts = gateway.accounts.filter(username__in=usernames)
|
||||
return accounts
|
||||
|
||||
@@ -7,23 +7,14 @@ class SecretType(TextChoices):
|
||||
SSH_KEY = 'ssh_key', _('SSH key')
|
||||
ACCESS_KEY = 'access_key', _('Access key')
|
||||
TOKEN = 'token', _('Token')
|
||||
API_KEY = 'api_key', _("API key")
|
||||
|
||||
|
||||
class AliasAccount(TextChoices):
|
||||
ALL = '@ALL', _('All')
|
||||
INPUT = '@INPUT', _('Manual input')
|
||||
USER = '@USER', _('Dynamic user')
|
||||
ANON = '@ANON', _('Anonymous account')
|
||||
|
||||
|
||||
class Source(TextChoices):
|
||||
LOCAL = 'local', _('Local')
|
||||
COLLECTED = 'collected', _('Collected')
|
||||
TEMPLATE = 'template', _('Template')
|
||||
|
||||
|
||||
class AccountInvalidPolicy(TextChoices):
|
||||
SKIP = 'skip', _('Skip')
|
||||
UPDATE = 'update', _('Update')
|
||||
ERROR = 'error', _('Failed')
|
||||
|
||||
@@ -48,7 +48,7 @@ class SecretStrategy(models.TextChoices):
|
||||
class SSHKeyStrategy(models.TextChoices):
|
||||
add = 'add', _('Append SSH KEY')
|
||||
set = 'set', _('Empty and append SSH KEY')
|
||||
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
|
||||
set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ')
|
||||
|
||||
|
||||
class TriggerChoice(models.TextChoices, TreeChoices):
|
||||
|
||||
@@ -5,6 +5,7 @@ from django_filters import rest_framework as drf_filters
|
||||
|
||||
from assets.models import Node
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
||||
from .models import Account, GatheredAccount
|
||||
|
||||
|
||||
@@ -45,7 +46,7 @@ class AccountFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = ['id', 'asset_id', 'source_id', 'secret_type']
|
||||
fields = ['id', 'asset_id']
|
||||
|
||||
|
||||
class GatheredAccountFilterSet(BaseFilterSet):
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
# Generated by Django 3.2.14 on 2022-12-28 07:29
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import common.db.encoder
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -31,16 +29,13 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('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')),
|
||||
('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')),
|
||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
@@ -66,8 +61,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
|
||||
('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')),
|
||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('version', models.IntegerField(default=0, verbose_name='Version')),
|
||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||
@@ -102,8 +96,7 @@ class Migration(migrations.Migration):
|
||||
('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')),
|
||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
|
||||
('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# Generated by Django 3.2.16 on 2022-12-30 08:08
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import common.db.encoder
|
||||
import common.db.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -55,8 +53,7 @@ class Migration(migrations.Migration):
|
||||
primary_key=True, serialize=False, to='assets.baseautomation')),
|
||||
('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')),
|
||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
|
||||
('random_one', 'All assets use the same random password'),
|
||||
('random_all',
|
||||
@@ -159,8 +156,7 @@ class Migration(migrations.Migration):
|
||||
primary_key=True, serialize=False, to='assets.baseautomation')),
|
||||
('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')),
|
||||
('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')),
|
||||
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
|
||||
('random_one', 'All assets use the same random password'),
|
||||
('random_all',
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# Generated by Django 3.2.16 on 2023-03-07 07:36
|
||||
|
||||
from django.db import migrations
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
def get_nodes_all_assets(apps, *nodes):
|
||||
node_model = apps.get_model('assets', 'Node')
|
||||
asset_model = apps.get_model('assets', 'Asset')
|
||||
node_ids = set()
|
||||
descendant_node_query = Q()
|
||||
for n in nodes:
|
||||
node_ids.add(n.id)
|
||||
descendant_node_query |= Q(key__istartswith=f'{n.key}:')
|
||||
if descendant_node_query:
|
||||
_ids = node_model.objects.order_by().filter(descendant_node_query).values_list('id', flat=True)
|
||||
node_ids.update(_ids)
|
||||
return asset_model.objects.order_by().filter(nodes__id__in=node_ids).distinct()
|
||||
|
||||
|
||||
def get_all_assets(apps, snapshot):
|
||||
node_model = apps.get_model('assets', 'Node')
|
||||
asset_model = apps.get_model('assets', 'Asset')
|
||||
asset_ids = snapshot.get('assets', [])
|
||||
node_ids = snapshot.get('nodes', [])
|
||||
|
||||
nodes = node_model.objects.filter(id__in=node_ids)
|
||||
node_asset_ids = get_nodes_all_assets(apps, *nodes).values_list('id', flat=True)
|
||||
asset_ids = set(list(asset_ids) + list(node_asset_ids))
|
||||
return asset_model.objects.filter(id__in=asset_ids)
|
||||
|
||||
|
||||
def migrate_account_usernames_to_ids(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
execution_model = apps.get_model('accounts', 'AutomationExecution')
|
||||
account_model = apps.get_model('accounts', 'Account')
|
||||
executions = execution_model.objects.using(db_alias).all()
|
||||
executions_update = []
|
||||
for execution in executions:
|
||||
snapshot = execution.snapshot
|
||||
accounts = account_model.objects.none()
|
||||
account_usernames = snapshot.get('accounts', [])
|
||||
for asset in get_all_assets(apps, snapshot):
|
||||
accounts = accounts | asset.accounts.all()
|
||||
secret_type = snapshot.get('secret_type')
|
||||
if secret_type:
|
||||
ids = accounts.filter(
|
||||
username__in=account_usernames,
|
||||
secret_type=secret_type
|
||||
).values_list('id', flat=True)
|
||||
else:
|
||||
ids = accounts.filter(
|
||||
username__in=account_usernames
|
||||
).values_list('id', flat=True)
|
||||
snapshot['accounts'] = [str(_id) for _id in ids]
|
||||
execution.snapshot = snapshot
|
||||
executions_update.append(execution)
|
||||
|
||||
execution_model.objects.bulk_update(executions_update, ['snapshot'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('accounts', '0008_alter_gatheredaccount_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_account_usernames_to_ids),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 3.2.16 on 2023-03-23 08:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('accounts', '0009_account_usernames_to_ids'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='gatheraccountsautomation',
|
||||
name='is_sync_account',
|
||||
field=models.BooleanField(blank=True, default=False, verbose_name='Is sync account'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='source_id',
|
||||
field=models.CharField(max_length=128, null=True, blank=True, verbose_name='Source ID'),
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 3.2.17 on 2023-05-06 06:43
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0010_gatheraccountsautomation_is_sync_account'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='accounttemplate',
|
||||
name='su_from',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='accounts.accounttemplate', verbose_name='Su from'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='changesecretautomation',
|
||||
name='ssh_key_change_strategy',
|
||||
field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pushaccountautomation',
|
||||
name='ssh_key_change_strategy',
|
||||
field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (Replace only keys pushed by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'),
|
||||
),
|
||||
]
|
||||
@@ -1,3 +1,3 @@
|
||||
from .base import *
|
||||
from .account import *
|
||||
from .automations import *
|
||||
from .base import *
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from django.db import models
|
||||
from django.db.models import Count, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from simple_history.models import HistoricalRecords
|
||||
|
||||
@@ -55,7 +53,6 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account')
|
||||
@@ -88,44 +85,26 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
def has_secret(self):
|
||||
return bool(self.secret)
|
||||
|
||||
@classmethod
|
||||
def get_special_account(cls, name):
|
||||
if name == AliasAccount.INPUT.value:
|
||||
return cls.get_manual_account()
|
||||
elif name == AliasAccount.ANON.value:
|
||||
return cls.get_anonymous_account()
|
||||
else:
|
||||
return cls(name=name, username=name, secret=None)
|
||||
|
||||
@classmethod
|
||||
def get_manual_account(cls):
|
||||
""" @INPUT 手动登录的账号(any) """
|
||||
return cls(name=AliasAccount.INPUT.label, username=AliasAccount.INPUT.value, secret=None)
|
||||
|
||||
@classmethod
|
||||
def get_anonymous_account(cls):
|
||||
return cls(name=AliasAccount.ANON.label, username=AliasAccount.ANON.value, secret=None)
|
||||
@lazyproperty
|
||||
def versions(self):
|
||||
return self.history.count()
|
||||
|
||||
@classmethod
|
||||
def get_user_account(cls):
|
||||
""" @USER 动态用户的账号(self) """
|
||||
return cls(name=AliasAccount.USER.label, username=AliasAccount.USER.value, secret=None)
|
||||
|
||||
@lazyproperty
|
||||
def versions(self):
|
||||
return self.history.count()
|
||||
|
||||
def get_su_from_accounts(self):
|
||||
""" 排除自己和以自己为 su-from 的账号 """
|
||||
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
||||
|
||||
|
||||
class AccountTemplate(BaseAccount):
|
||||
su_from = models.ForeignKey(
|
||||
'self', related_name='su_to', null=True,
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account template')
|
||||
unique_together = (
|
||||
@@ -136,65 +115,5 @@ class AccountTemplate(BaseAccount):
|
||||
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_su_from_account_templates(cls, pk=None):
|
||||
if pk is None:
|
||||
return cls.objects.all()
|
||||
return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk))
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}({self.username})'
|
||||
|
||||
def get_su_from_account(self, asset):
|
||||
su_from = self.su_from
|
||||
if su_from and asset.platform.su_enabled:
|
||||
account = asset.accounts.filter(
|
||||
username=su_from.username,
|
||||
secret_type=su_from.secret_type
|
||||
).first()
|
||||
return account
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
@staticmethod
|
||||
def bulk_update_accounts(accounts, data):
|
||||
history_model = Account.history.model
|
||||
account_ids = accounts.values_list('id', flat=True)
|
||||
history_accounts = history_model.objects.filter(id__in=account_ids)
|
||||
account_id_count_map = {
|
||||
str(i['id']): i['count']
|
||||
for i in history_accounts.values('id').order_by('id')
|
||||
.annotate(count=Count(1)).values('id', 'count')
|
||||
}
|
||||
|
||||
for account in accounts:
|
||||
account_id = str(account.id)
|
||||
account.version = account_id_count_map.get(account_id) + 1
|
||||
for k, v in data.items():
|
||||
setattr(account, k, v)
|
||||
Account.objects.bulk_update(accounts, ['version', 'secret'])
|
||||
|
||||
@staticmethod
|
||||
def bulk_create_history_accounts(accounts, user_id):
|
||||
history_model = Account.history.model
|
||||
history_account_objs = []
|
||||
for account in accounts:
|
||||
history_account_objs.append(
|
||||
history_model(
|
||||
id=account.id,
|
||||
version=account.version,
|
||||
secret=account.secret,
|
||||
secret_type=account.secret_type,
|
||||
history_user_id=user_id,
|
||||
history_date=timezone.now()
|
||||
)
|
||||
)
|
||||
history_model.objects.bulk_create(history_account_objs)
|
||||
|
||||
def bulk_sync_account_secret(self, accounts, user_id):
|
||||
""" 批量同步账号密码 """
|
||||
if not accounts:
|
||||
return
|
||||
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
||||
self.bulk_create_history_accounts(accounts, user_id)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.db import fields
|
||||
from common.db.models import JMSBaseModel
|
||||
from accounts.const import (
|
||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||
)
|
||||
from accounts.models import Account
|
||||
from common.db import fields
|
||||
from common.db.models import JMSBaseModel
|
||||
from .base import AccountBaseAutomation
|
||||
|
||||
__all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin']
|
||||
@@ -28,34 +27,18 @@ class ChangeSecretMixin(models.Model):
|
||||
default=SSHKeyStrategy.add, verbose_name=_('SSH key change strategy')
|
||||
)
|
||||
|
||||
get_all_assets: callable # get all assets
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def create_nonlocal_accounts(self, usernames, asset):
|
||||
pass
|
||||
|
||||
def get_account_ids(self):
|
||||
usernames = self.accounts
|
||||
accounts = Account.objects.none()
|
||||
for asset in self.get_all_assets():
|
||||
self.create_nonlocal_accounts(usernames, asset)
|
||||
accounts = accounts | asset.accounts.all()
|
||||
account_ids = accounts.filter(
|
||||
username__in=usernames, secret_type=self.secret_type
|
||||
).values_list('id', flat=True)
|
||||
return [str(_id) for _id in account_ids]
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'secret': self.secret,
|
||||
'secret_type': self.secret_type,
|
||||
'accounts': self.get_account_ids(),
|
||||
'password_rules': self.password_rules,
|
||||
'secret_strategy': self.secret_strategy,
|
||||
'password_rules': self.password_rules,
|
||||
'ssh_key_change_strategy': self.ssh_key_change_strategy,
|
||||
|
||||
})
|
||||
return attr_json
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes, Source
|
||||
from accounts.models import Account
|
||||
from accounts.const import AutomationTypes
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from .base import AccountBaseAutomation
|
||||
|
||||
@@ -21,25 +19,6 @@ class GatheredAccount(JMSOrgBaseModel):
|
||||
def address(self):
|
||||
return self.asset.address
|
||||
|
||||
@staticmethod
|
||||
def sync_accounts(gathered_accounts):
|
||||
account_objs = []
|
||||
for gathered_account in gathered_accounts:
|
||||
asset_id = gathered_account.asset_id
|
||||
username = gathered_account.username
|
||||
accounts = Account.objects.filter(
|
||||
Q(asset_id=asset_id, username=username) |
|
||||
Q(asset_id=asset_id, name=username)
|
||||
)
|
||||
if accounts.exists():
|
||||
continue
|
||||
account = Account(
|
||||
asset_id=asset_id, username=username,
|
||||
name=username, source=Source.COLLECTED
|
||||
)
|
||||
account_objs.append(account)
|
||||
Account.objects.bulk_create(account_objs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Gather account automation')
|
||||
unique_together = [
|
||||
@@ -52,17 +31,6 @@ class GatheredAccount(JMSOrgBaseModel):
|
||||
|
||||
|
||||
class GatherAccountsAutomation(AccountBaseAutomation):
|
||||
is_sync_account = models.BooleanField(
|
||||
default=False, blank=True, verbose_name=_("Is sync account")
|
||||
)
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'is_sync_account': self.is_sync_account,
|
||||
})
|
||||
return attr_json
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.type = AutomationTypes.gather_accounts
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@@ -2,8 +2,6 @@ from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import Account
|
||||
from jumpserver.utils import has_valid_xpack_license
|
||||
from .base import AccountBaseAutomation
|
||||
from .change_secret import ChangeSecretMixin
|
||||
|
||||
@@ -15,21 +13,6 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
username = models.CharField(max_length=128, verbose_name=_('Username'))
|
||||
action = models.CharField(max_length=16, verbose_name=_('Action'))
|
||||
|
||||
def create_nonlocal_accounts(self, usernames, asset):
|
||||
secret_type = self.secret_type
|
||||
account_usernames = asset.accounts.filter(secret_type=self.secret_type).values_list(
|
||||
'username', flat=True
|
||||
)
|
||||
create_usernames = set(usernames) - set(account_usernames)
|
||||
create_account_objs = [
|
||||
Account(
|
||||
name=f'{username}-{secret_type}', username=username,
|
||||
secret_type=secret_type, asset=asset,
|
||||
)
|
||||
for username in create_usernames
|
||||
]
|
||||
Account.objects.bulk_create(create_account_objs)
|
||||
|
||||
def set_period_schedule(self):
|
||||
pass
|
||||
|
||||
@@ -44,15 +27,12 @@ class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation):
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.type = AutomationTypes.push_account
|
||||
if not has_valid_xpack_license():
|
||||
self.is_periodic = False
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def to_attr_json(self):
|
||||
attr_json = super().to_attr_json()
|
||||
attr_json.update({
|
||||
'username': self.username,
|
||||
'params': self.params,
|
||||
'username': self.username
|
||||
})
|
||||
return attr_json
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from accounts.const import SecretType
|
||||
from common.db import fields
|
||||
from common.utils import (
|
||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
||||
random_string, lazyproperty, parse_ssh_public_key_str
|
||||
)
|
||||
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
||||
|
||||
@@ -118,13 +118,7 @@ class BaseAccount(JMSOrgBaseModel):
|
||||
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
|
||||
key_path = os.path.join(tmp_dir, key_name)
|
||||
if not os.path.exists(key_path):
|
||||
# https://github.com/ansible/ansible-runner/issues/544
|
||||
# ssh requires OpenSSH format keys to have a full ending newline.
|
||||
# It does not require this for old-style PEM keys.
|
||||
with open(key_path, 'w') as f:
|
||||
f.write(self.secret)
|
||||
if is_openssh_format_key(self.secret.encode('utf-8')):
|
||||
f.write("\n")
|
||||
self.private_key_obj.write_private_key_file(key_path)
|
||||
os.chmod(key_path, 0o400)
|
||||
return key_path
|
||||
|
||||
|
||||
@@ -1,180 +1,75 @@
|
||||
import uuid
|
||||
from copy import deepcopy
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from accounts.const import SecretType, Source, AccountInvalidPolicy
|
||||
from accounts.const import SecretType, Source
|
||||
from accounts.models import Account, AccountTemplate
|
||||
from accounts.tasks import push_accounts_to_assets_task
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Asset
|
||||
from common.serializers import SecretReadableMixin
|
||||
from common.serializers import SecretReadableMixin, BulkModelSerializer
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from common.utils import get_logger
|
||||
from .base import BaseAccountSerializer, AuthValidateMixin
|
||||
|
||||
logger = get_logger(__name__)
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
|
||||
class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=AccountTemplate.objects, required=False,
|
||||
label=_("Template"), write_only=True, allow_null=True
|
||||
)
|
||||
push_now = serializers.BooleanField(
|
||||
default=False, label=_("Push now"), write_only=True
|
||||
)
|
||||
params = serializers.JSONField(
|
||||
decoder=None, encoder=None, required=False, style={'base_template': 'textarea.html'}
|
||||
)
|
||||
on_invalid = LabeledChoiceField(
|
||||
choices=AccountInvalidPolicy.choices, default=AccountInvalidPolicy.ERROR,
|
||||
write_only=True, allow_null=True, label=_('Exist policy'),
|
||||
)
|
||||
_template = None
|
||||
clean_auth_fields: callable
|
||||
class AccountSerializerCreateValidateMixin:
|
||||
from_id: str
|
||||
template: bool
|
||||
push_now: bool
|
||||
replace_attrs: callable
|
||||
|
||||
class Meta:
|
||||
fields = ['template', 'push_now', 'params', 'on_invalid']
|
||||
def to_internal_value(self, data):
|
||||
from_id = data.pop('id', None)
|
||||
ret = super().to_internal_value(data)
|
||||
self.from_id = from_id
|
||||
return ret
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_initial_value()
|
||||
def set_secret(self, attrs):
|
||||
_id = self.from_id
|
||||
template = attrs.pop('template', None)
|
||||
|
||||
def set_initial_value(self):
|
||||
if not getattr(self, 'initial_data', None):
|
||||
return
|
||||
if isinstance(self.initial_data, dict):
|
||||
initial_data = [self.initial_data]
|
||||
else:
|
||||
initial_data = self.initial_data
|
||||
if _id and template:
|
||||
account_template = AccountTemplate.objects.get(id=_id)
|
||||
attrs['secret'] = account_template.secret
|
||||
elif _id and not template:
|
||||
account = Account.objects.get(id=_id)
|
||||
attrs['secret'] = account.secret
|
||||
return attrs
|
||||
|
||||
for data in initial_data:
|
||||
if not data.get('asset') and not self.instance:
|
||||
raise serializers.ValidationError({'asset': UniqueTogetherValidator.missing_message})
|
||||
asset = data.get('asset') or self.instance.asset
|
||||
self.from_template_if_need(data)
|
||||
self.set_uniq_name_if_need(data, asset)
|
||||
|
||||
def set_uniq_name_if_need(self, initial_data, asset):
|
||||
name = initial_data.get('name')
|
||||
if name is not None:
|
||||
return
|
||||
if not name:
|
||||
name = initial_data.get('username')
|
||||
if self.instance and self.instance.name == name:
|
||||
return
|
||||
if Account.objects.filter(name=name, asset=asset).exists():
|
||||
name = name + '_' + uuid.uuid4().hex[:4]
|
||||
initial_data['name'] = name
|
||||
|
||||
def from_template_if_need(self, initial_data):
|
||||
if isinstance(initial_data, str):
|
||||
return
|
||||
|
||||
template_id = initial_data.pop('template', None)
|
||||
if not template_id:
|
||||
return
|
||||
|
||||
if isinstance(template_id, (str, uuid.UUID)):
|
||||
template = AccountTemplate.objects.filter(id=template_id).first()
|
||||
else:
|
||||
template = template_id
|
||||
if not template:
|
||||
raise serializers.ValidationError({'template': 'Template not found'})
|
||||
|
||||
self._template = template
|
||||
# Set initial data from template
|
||||
ignore_fields = ['id', 'date_created', 'date_updated', 'su_from', 'org_id']
|
||||
field_names = [
|
||||
field.name for field in template._meta.fields
|
||||
if field.name not in ignore_fields
|
||||
]
|
||||
attrs = {}
|
||||
for name in field_names:
|
||||
value = getattr(template, name, None)
|
||||
if value is None:
|
||||
continue
|
||||
attrs[name] = value
|
||||
initial_data.update(attrs)
|
||||
initial_data.update({
|
||||
'source': Source.TEMPLATE,
|
||||
'source_id': str(template.id)
|
||||
})
|
||||
asset_id = initial_data.get('asset')
|
||||
if isinstance(asset_id, list) or not asset_id:
|
||||
return
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
initial_data['su_from'] = template.get_su_from_account(asset)
|
||||
def validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
return self.set_secret(attrs)
|
||||
|
||||
@staticmethod
|
||||
def push_account_if_need(instance, push_now, params, stat):
|
||||
if not push_now or stat not in ['created', 'updated']:
|
||||
def push_account(instance, push_now):
|
||||
if not push_now:
|
||||
return
|
||||
push_accounts_to_assets_task.delay([str(instance.id)], params)
|
||||
|
||||
def get_validators(self):
|
||||
_validators = super().get_validators()
|
||||
if getattr(self, 'initial_data', None) is None:
|
||||
return _validators
|
||||
|
||||
on_invalid = self.initial_data.get('on_invalid')
|
||||
if on_invalid == AccountInvalidPolicy.ERROR and not self.parent:
|
||||
return _validators
|
||||
_validators = [v for v in _validators if not isinstance(v, UniqueTogetherValidator)]
|
||||
return _validators
|
||||
|
||||
@staticmethod
|
||||
def do_create(vd):
|
||||
on_invalid = vd.pop('on_invalid', None)
|
||||
|
||||
q = Q()
|
||||
if vd.get('name'):
|
||||
q |= Q(name=vd['name'])
|
||||
if vd.get('username'):
|
||||
q |= Q(username=vd['username'], secret_type=vd.get('secret_type'))
|
||||
|
||||
instance = Account.objects.filter(asset=vd['asset']).filter(q).first()
|
||||
# 不存在这个资产,不用关系策略
|
||||
if not instance:
|
||||
instance = Account.objects.create(**vd)
|
||||
return instance, 'created'
|
||||
|
||||
if on_invalid == AccountInvalidPolicy.SKIP:
|
||||
return instance, 'skipped'
|
||||
elif on_invalid == AccountInvalidPolicy.UPDATE:
|
||||
for k, v in vd.items():
|
||||
setattr(instance, k, v)
|
||||
instance.save()
|
||||
return instance, 'updated'
|
||||
else:
|
||||
raise serializers.ValidationError('Account already exists')
|
||||
push_accounts_to_assets_task.delay([str(instance.id)])
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
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)
|
||||
instance = super().create(validated_data)
|
||||
self.push_account(instance, push_now)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
# account cannot be modified
|
||||
validated_data.pop('username', None)
|
||||
validated_data.pop('on_invalid', None)
|
||||
push_now = validated_data.pop('push_now', None)
|
||||
params = validated_data.pop('params', None)
|
||||
validated_data['source_id'] = None
|
||||
instance = super().update(instance, validated_data)
|
||||
self.push_account_if_need(instance, push_now, params, 'updated')
|
||||
self.push_account(instance, push_now)
|
||||
return instance
|
||||
|
||||
|
||||
class AccountSerializerCreateMixin(AccountSerializerCreateValidateMixin, BulkModelSerializer):
|
||||
template = serializers.BooleanField(
|
||||
default=False, label=_("Template"), write_only=True
|
||||
)
|
||||
push_now = serializers.BooleanField(
|
||||
default=False, label=_("Push now"), write_only=True
|
||||
)
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
|
||||
|
||||
class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
platform = ObjectRelatedField(read_only=True)
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
@@ -182,11 +77,11 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_config']
|
||||
fields = ['id', 'name', 'address', 'type', 'category', 'platform', 'auto_info']
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if isinstance(data, dict):
|
||||
i = data.get('id') or data.get('pk')
|
||||
i = data.get('id')
|
||||
else:
|
||||
i = data
|
||||
|
||||
@@ -196,13 +91,9 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
||||
raise serializers.ValidationError(_('Asset not found'))
|
||||
|
||||
|
||||
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||
class AccountSerializer(AccountSerializerCreateMixin, BaseAccountSerializer):
|
||||
asset = AccountAssetSerializer(label=_('Asset'))
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
source = LabeledChoiceField(
|
||||
choices=Source.choices, label=_("Source"), required=False,
|
||||
allow_null=True, default=Source.LOCAL
|
||||
)
|
||||
source = LabeledChoiceField(choices=Source.choices, label=_("Source"), read_only=True)
|
||||
su_from = ObjectRelatedField(
|
||||
required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
|
||||
label=_('Su from'), attrs=('id', 'name', 'username')
|
||||
@@ -211,203 +102,27 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = Account
|
||||
fields = BaseAccountSerializer.Meta.fields + [
|
||||
'su_from', 'asset', 'version',
|
||||
'source', 'source_id', 'connectivity',
|
||||
] + AccountCreateUpdateSerializerMixin.Meta.fields
|
||||
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + [
|
||||
'connectivity'
|
||||
'su_from', 'asset', 'template', 'version',
|
||||
'push_now', 'source', 'connectivity',
|
||||
]
|
||||
extra_kwargs = {
|
||||
**BaseAccountSerializer.Meta.extra_kwargs,
|
||||
'name': {'required': False},
|
||||
'source_id': {'required': False, 'allow_null': True},
|
||||
'name': {'required': False, 'allow_null': True},
|
||||
}
|
||||
|
||||
def validate_name(self, value):
|
||||
if not value:
|
||||
value = self.initial_data.get('username')
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related(
|
||||
'asset', 'asset__platform',
|
||||
'asset__platform__automation'
|
||||
)
|
||||
queryset = queryset \
|
||||
.prefetch_related('asset', 'asset__platform', 'asset__platform__automation')
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
||||
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||
state = serializers.CharField(read_only=True, label=_('State'))
|
||||
error = serializers.CharField(read_only=True, label=_('Error'))
|
||||
changed = serializers.BooleanField(read_only=True, label=_('Changed'))
|
||||
|
||||
|
||||
class AssetAccountBulkSerializer(
|
||||
AccountCreateUpdateSerializerMixin, AuthValidateMixin, serializers.ModelSerializer
|
||||
):
|
||||
su_from_username = serializers.CharField(
|
||||
max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from"),
|
||||
allow_blank=True,
|
||||
)
|
||||
assets = serializers.PrimaryKeyRelatedField(queryset=Asset.objects, many=True, label=_('Assets'))
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
'name', 'username', 'secret', 'secret_type', 'passphrase',
|
||||
'privileged', 'is_active', 'comment', 'template',
|
||||
'on_invalid', 'push_now', 'assets', 'su_from_username',
|
||||
'source', 'source_id',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': False},
|
||||
'secret_type': {'required': False},
|
||||
'source': {'required': False, 'allow_null': True},
|
||||
'source_id': {'required': False, 'allow_null': True},
|
||||
}
|
||||
|
||||
def set_initial_value(self):
|
||||
if not getattr(self, 'initial_data', None):
|
||||
return
|
||||
initial_data = self.initial_data
|
||||
self.from_template_if_need(initial_data)
|
||||
|
||||
@staticmethod
|
||||
def get_filter_lookup(vd):
|
||||
return {
|
||||
'username': vd['username'],
|
||||
'secret_type': vd['secret_type'],
|
||||
'asset': vd['asset'],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_uniq_name(vd):
|
||||
return vd['name'] + '-' + uuid.uuid4().hex[:4]
|
||||
|
||||
@staticmethod
|
||||
def _handle_update_create(vd, lookup):
|
||||
ori = Account.objects.filter(**lookup).first()
|
||||
if ori and ori.secret == vd.get('secret'):
|
||||
return ori, False, 'skipped'
|
||||
|
||||
instance, value = Account.objects.update_or_create(defaults=vd, **lookup)
|
||||
state = 'created' if value else 'updated'
|
||||
return instance, True, state
|
||||
|
||||
@staticmethod
|
||||
def _handle_skip_create(vd, lookup):
|
||||
instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
|
||||
state = 'created' if value else 'skipped'
|
||||
return instance, value, state
|
||||
|
||||
@staticmethod
|
||||
def _handle_err_create(vd, lookup):
|
||||
instance, value = Account.objects.get_or_create(defaults=vd, **lookup)
|
||||
if not value:
|
||||
raise serializers.ValidationError(_('Account already exists'))
|
||||
return instance, True, 'created'
|
||||
|
||||
def generate_su_from_data(self, validated_data):
|
||||
template = self._template
|
||||
asset = validated_data['asset']
|
||||
su_from = validated_data.get('su_from')
|
||||
su_from_username = validated_data.pop('su_from_username', None)
|
||||
if template:
|
||||
su_from = template.get_su_from_account(asset)
|
||||
elif su_from_username:
|
||||
su_from = asset.accounts.filter(username=su_from_username).first()
|
||||
validated_data['su_from'] = su_from
|
||||
|
||||
def perform_create(self, vd, handler):
|
||||
lookup = self.get_filter_lookup(vd)
|
||||
vd = deepcopy(vd)
|
||||
self.generate_su_from_data(vd)
|
||||
try:
|
||||
instance, changed, state = handler(vd, lookup)
|
||||
except IntegrityError:
|
||||
vd['name'] = self.get_uniq_name(vd)
|
||||
instance, changed, state = handler(vd, lookup)
|
||||
return instance, changed, state
|
||||
|
||||
def get_create_handler(self, on_invalid):
|
||||
if on_invalid == 'update':
|
||||
handler = self._handle_update_create
|
||||
elif on_invalid == 'skip':
|
||||
handler = self._handle_skip_create
|
||||
else:
|
||||
handler = self._handle_err_create
|
||||
return handler
|
||||
|
||||
def perform_bulk_create(self, vd):
|
||||
assets = vd.pop('assets')
|
||||
on_invalid = vd.pop('on_invalid', 'skip')
|
||||
secret_type = vd.get('secret_type', 'password')
|
||||
|
||||
if not vd.get('name'):
|
||||
vd['name'] = vd.get('username')
|
||||
|
||||
create_handler = self.get_create_handler(on_invalid)
|
||||
asset_ids = [asset.id for asset in assets]
|
||||
secret_type_supports = Asset.get_secret_type_assets(asset_ids, secret_type)
|
||||
|
||||
_results = {}
|
||||
for asset in assets:
|
||||
if asset not in secret_type_supports:
|
||||
_results[asset] = {
|
||||
'error': _('Asset does not support this secret type: %s') % secret_type,
|
||||
'state': 'error',
|
||||
}
|
||||
continue
|
||||
|
||||
vd = vd.copy()
|
||||
vd['asset'] = asset
|
||||
try:
|
||||
self.clean_auth_fields(vd)
|
||||
instance, changed, state = self.perform_create(vd, create_handler)
|
||||
_results[asset] = {
|
||||
'changed': changed, 'instance': instance.id, 'state': state
|
||||
}
|
||||
except serializers.ValidationError as e:
|
||||
_results[asset] = {'error': e.detail[0], 'state': 'error'}
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
_results[asset] = {'error': str(e), 'state': 'error'}
|
||||
|
||||
results = [{'asset': asset, **result} for asset, result in _results.items()]
|
||||
state_score = {'created': 3, 'updated': 2, 'skipped': 1, 'error': 0}
|
||||
results = sorted(results, key=lambda x: state_score.get(x['state'], 4))
|
||||
|
||||
if on_invalid != 'error':
|
||||
return results
|
||||
|
||||
errors = []
|
||||
errors.extend([result for result in results if result['state'] == 'error'])
|
||||
for result in results:
|
||||
if result['state'] != 'skipped':
|
||||
continue
|
||||
errors.append({
|
||||
'error': _('Account has exist'),
|
||||
'state': 'error',
|
||||
'asset': str(result['asset'])
|
||||
})
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def push_accounts_if_need(results, push_now):
|
||||
if not push_now:
|
||||
return
|
||||
accounts = [str(v['instance']) for v in results if v.get('instance')]
|
||||
push_accounts_to_assets_task.delay(accounts)
|
||||
|
||||
def create(self, validated_data):
|
||||
push_now = validated_data.pop('push_now', False)
|
||||
results = self.perform_bulk_create(validated_data)
|
||||
self.push_accounts_if_need(results, push_now)
|
||||
for res in results:
|
||||
res['asset'] = str(res['asset'])
|
||||
return results
|
||||
|
||||
|
||||
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
class Meta(AccountSerializer.Meta):
|
||||
extra_kwargs = {
|
||||
@@ -417,19 +132,11 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
|
||||
|
||||
class AccountHistorySerializer(serializers.ModelSerializer):
|
||||
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
|
||||
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Account.history.model
|
||||
fields = [
|
||||
'id', 'secret', 'secret_type', 'version', 'history_date',
|
||||
'history_user'
|
||||
]
|
||||
fields = ['id', 'secret', 'secret_type', 'version', 'history_date', 'history_user']
|
||||
read_only_fields = fields
|
||||
extra_kwargs = {
|
||||
'history_user': {'label': _('User')},
|
||||
'history_date': {'label': _('Date')},
|
||||
}
|
||||
|
||||
|
||||
class AccountTaskSerializer(serializers.Serializer):
|
||||
@@ -443,7 +150,3 @@ class AccountTaskSerializer(serializers.Serializer):
|
||||
queryset=Account.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
params = serializers.JSONField(
|
||||
decoder=None, encoder=None, required=False,
|
||||
style={'base_template': 'textarea.html'}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
||||
|
||||
@@ -13,10 +13,10 @@ __all__ = ['AuthValidateMixin', 'BaseAccountSerializer']
|
||||
|
||||
class AuthValidateMixin(serializers.Serializer):
|
||||
secret_type = LabeledChoiceField(
|
||||
choices=SecretType.choices, label=_('Secret type'), default='password'
|
||||
choices=SecretType.choices, required=True, label=_('Secret type')
|
||||
)
|
||||
secret = EncryptedField(
|
||||
label=_('Secret'), required=False, max_length=40960, allow_blank=True,
|
||||
label=_('Secret/Password'), required=False, max_length=40960, allow_blank=True,
|
||||
allow_null=True, write_only=True,
|
||||
)
|
||||
passphrase = serializers.CharField(
|
||||
@@ -33,8 +33,7 @@ class AuthValidateMixin(serializers.Serializer):
|
||||
return secret
|
||||
elif secret_type == SecretType.SSH_KEY:
|
||||
passphrase = passphrase if passphrase else None
|
||||
secret = validate_ssh_key(secret, passphrase)
|
||||
return secret
|
||||
return validate_ssh_key(secret, passphrase)
|
||||
else:
|
||||
return secret
|
||||
|
||||
@@ -42,9 +41,8 @@ class AuthValidateMixin(serializers.Serializer):
|
||||
secret_type = validated_data.get('secret_type')
|
||||
passphrase = validated_data.get('passphrase')
|
||||
secret = validated_data.pop('secret', None)
|
||||
validated_data['secret'] = self.handle_secret(
|
||||
secret, secret_type, passphrase
|
||||
)
|
||||
self.handle_secret(secret, secret_type, passphrase)
|
||||
validated_data['secret'] = secret
|
||||
for field in ('secret',):
|
||||
value = validated_data.get(field)
|
||||
if not value:
|
||||
@@ -77,9 +75,6 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
extra_kwargs = {
|
||||
'name': {'required': True},
|
||||
'spec_info': {'label': _('Spec info')},
|
||||
'username': {'help_text': _(
|
||||
"Tip: If no username is required for authentication, fill in `null`, "
|
||||
"If AD account, like `username@domain`"
|
||||
)},
|
||||
}
|
||||
|
||||
@@ -1,52 +1,23 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.models import AccountTemplate, Account
|
||||
from accounts.models import AccountTemplate
|
||||
from common.serializers import SecretReadableMixin
|
||||
from common.serializers.fields import ObjectRelatedField
|
||||
from .base import BaseAccountSerializer
|
||||
|
||||
|
||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
is_sync_account = serializers.BooleanField(default=False, write_only=True)
|
||||
_is_sync_account = False
|
||||
|
||||
su_from = ObjectRelatedField(
|
||||
required=False, queryset=AccountTemplate.objects, allow_null=True,
|
||||
allow_empty=True, label=_('Su from'), attrs=('id', 'name', 'username')
|
||||
)
|
||||
|
||||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = AccountTemplate
|
||||
fields = BaseAccountSerializer.Meta.fields + ['is_sync_account', 'su_from']
|
||||
|
||||
def sync_accounts_secret(self, instance, diff):
|
||||
if not self._is_sync_account or 'secret' not in diff:
|
||||
return
|
||||
query_data = {
|
||||
'source_id': instance.id,
|
||||
'username': instance.username,
|
||||
'secret_type': instance.secret_type
|
||||
}
|
||||
accounts = Account.objects.filter(**query_data)
|
||||
instance.bulk_sync_account_secret(accounts, self.context['request'].user.id)
|
||||
|
||||
def validate(self, attrs):
|
||||
self._is_sync_account = attrs.pop('is_sync_account', None)
|
||||
attrs = super().validate(attrs)
|
||||
return attrs
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
diff = {
|
||||
k: v for k, v in validated_data.items()
|
||||
if getattr(instance, k, None) != v
|
||||
}
|
||||
instance = super().update(instance, validated_data)
|
||||
if {'username', 'secret_type'} & set(diff.keys()):
|
||||
Account.objects.filter(source_id=instance.id).update(source_id=None)
|
||||
else:
|
||||
self.sync_accounts_secret(instance, diff)
|
||||
return instance
|
||||
# @classmethod
|
||||
# def validate_required(cls, attrs):
|
||||
# # TODO 选择模版后检查一些必填项
|
||||
# required_field_dict = {}
|
||||
# error = _('This field is required.')
|
||||
# for k, v in cls().fields.items():
|
||||
# if v.required and k not in attrs:
|
||||
# required_field_dict[k] = error
|
||||
# if not required_field_dict:
|
||||
# return
|
||||
# raise serializers.ValidationError(required_field_dict)
|
||||
|
||||
|
||||
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from accounts.models import AutomationExecution
|
||||
@@ -63,17 +63,15 @@ class AutomationExecutionSerializer(serializers.ModelSerializer):
|
||||
|
||||
@staticmethod
|
||||
def get_snapshot(obj):
|
||||
tp = obj.snapshot.get('type', '')
|
||||
type_display = tp if not hasattr(AutomationTypes, tp) \
|
||||
else getattr(AutomationTypes, tp).label
|
||||
tp = obj.snapshot['type']
|
||||
snapshot = {
|
||||
'type': tp,
|
||||
'name': obj.snapshot.get('name'),
|
||||
'comment': obj.snapshot.get('comment'),
|
||||
'accounts': obj.snapshot.get('accounts'),
|
||||
'node_amount': len(obj.snapshot.get('nodes', [])),
|
||||
'asset_amount': len(obj.snapshot.get('assets', [])),
|
||||
'type_display': type_display,
|
||||
'name': obj.snapshot['name'],
|
||||
'comment': obj.snapshot['comment'],
|
||||
'accounts': obj.snapshot['accounts'],
|
||||
'node_amount': len(obj.snapshot['nodes']),
|
||||
'asset_amount': len(obj.snapshot['assets']),
|
||||
'type_display': getattr(AutomationTypes, tp).label,
|
||||
}
|
||||
return snapshot
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields + read_only_fields + [
|
||||
'secret_type', 'secret_strategy', 'secret', 'password_rules',
|
||||
'ssh_key_change_strategy', 'passphrase', 'recipients', 'params'
|
||||
'ssh_key_change_strategy', 'passphrase', 'recipients',
|
||||
]
|
||||
extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{
|
||||
'accounts': {'required': True},
|
||||
@@ -58,7 +58,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
||||
"Currently only mail sending is supported"
|
||||
)},
|
||||
}}
|
||||
|
||||
@property
|
||||
def model_type(self):
|
||||
return AutomationTypes.change_secret
|
||||
|
||||
@@ -17,8 +17,7 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
|
||||
class Meta:
|
||||
model = GatherAccountsAutomation
|
||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields \
|
||||
+ ['is_sync_account'] + read_only_fields
|
||||
fields = BaseAutomationSerializer.Meta.fields + read_only_fields
|
||||
|
||||
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ from .change_secret import (
|
||||
|
||||
|
||||
class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer):
|
||||
|
||||
class Meta(ChangeSecretAutomationSerializer.Meta):
|
||||
model = PushAccountAutomation
|
||||
fields = [
|
||||
|
||||
@@ -8,8 +8,8 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Account)
|
||||
def on_account_pre_save(sender, instance, **kwargs):
|
||||
if instance.version == 0:
|
||||
def on_account_pre_save(sender, instance, created=False, **kwargs):
|
||||
if created:
|
||||
instance.version = 1
|
||||
else:
|
||||
instance.version = instance.history.count()
|
||||
|
||||
@@ -8,7 +8,7 @@ from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def task_activity_callback(self, pid, trigger, tp, *args, **kwargs):
|
||||
def task_activity_callback(self, pid, trigger, tp):
|
||||
model = AutomationTypes.get_type_model(tp)
|
||||
with tmp_to_root_org():
|
||||
instance = get_object_or_none(model, pk=pid)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user