Compare commits

..

1 Commits

Author SHA1 Message Date
fit2bot
3689bc62ab feat: Update v3.10.3 2024-01-24 17:58:29 +08:00
1235 changed files with 38494 additions and 65923 deletions

View File

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

11
.github/ISSUE_TEMPLATE/----.md vendored Normal file
View File

@@ -0,0 +1,11 @@
---
name: 需求建议
about: 提出针对本项目的想法和建议
title: "[Feature] "
labels: 类型:需求
assignees:
- ibuler
- baijiangjie
---
**请描述您的需求或者改进建议.**

View File

@@ -1,72 +0,0 @@
name: '🐛 Bug Report'
description: 'Report an Bug'
title: '[Bug] '
labels: ['🐛 Bug']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: 'Product Version'
description: The versions prior to v2.28 (inclusive) are no longer supported.
validations:
required: true
- type: checkboxes
attributes:
label: 'Product Edition'
options:
- label: 'Community Edition'
- label: 'Enterprise Edition'
- label: 'Enterprise Trial Edition'
validations:
required: true
- type: checkboxes
attributes:
label: 'Installation Method'
options:
- label: 'Online Installation (One-click command installation)'
- label: 'Offline Package Installation'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: 'Source Code'
- type: textarea
attributes:
label: 'Environment Information'
description: Please provide a clear and concise description outlining your environment information.
validations:
required: true
- type: textarea
attributes:
label: '🐛 Bug Description'
description:
Please provide a clear and concise description of the defect. If the issue is complex, please provide detailed explanations. <br/>
Unclear descriptions will not be processed. Please ensure you provide enough detail and information to support replicating and fixing the defect.
validations:
required: true
- type: textarea
attributes:
label: 'Recurrence Steps'
description: Please provide a clear and concise description outlining how to reproduce the issue.
validations:
required: true
- type: textarea
attributes:
label: 'Expected Behavior'
description: Please provide a clear and concise description of what you expect to happen.
- type: textarea
attributes:
label: 'Additional Information'
description: Please add any additional background information about the issue here.
- type: textarea
attributes:
label: 'Attempted Solutions'
description: If you have already attempted to solve the issue, please list the solutions you have tried here.

View File

@@ -1,72 +0,0 @@
name: '🐛 反馈缺陷'
description: '反馈一个缺陷'
title: '[Bug] '
labels: ['🐛 Bug']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: '产品版本'
description: 不再支持 v2.28(含)之前的版本。
validations:
required: true
- type: checkboxes
attributes:
label: '版本类型'
options:
- label: '社区版'
- label: '企业版'
- label: '企业试用版'
validations:
required: true
- type: checkboxes
attributes:
label: '安装方式'
options:
- label: '在线安装 (一键命令安装)'
- label: '离线包安装'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: '源码安装'
- type: textarea
attributes:
label: '环境信息'
description: 请提供一个清晰且简洁的描述,说明你的环境信息。
validations:
required: true
- type: textarea
attributes:
label: '🐛 缺陷描述'
description: |
请提供一个清晰且简洁的缺陷描述,如果问题比较复杂,也请详细说明。<br/>
针对不清晰的描述信息将不予处理。请确保提供足够的细节和信息,以支持对缺陷进行复现和修复。
validations:
required: true
- type: textarea
attributes:
label: '复现步骤'
description: 请提供一个清晰且简洁的描述,说明如何复现问题。
validations:
required: true
- type: textarea
attributes:
label: '期望结果'
description: 请提供一个清晰且简洁的描述,说明你期望发生什么。
- type: textarea
attributes:
label: '补充信息'
description: 在这里添加关于问题的任何其他背景信息。
- type: textarea
attributes:
label: '尝试过的解决方案'
description: 如果你已经尝试解决问题,请在此列出你尝试过的解决方案。

View File

@@ -1,56 +0,0 @@
name: '⭐️ Feature Request'
description: 'Suggest an idea'
title: '[Feature] '
labels: ['⭐️ Feature Request']
assignees:
- baijiangjie
- ibuler
body:
- type: input
attributes:
label: 'Product Version'
description: The versions prior to v2.28 (inclusive) are no longer supported.
validations:
required: true
- type: checkboxes
attributes:
label: 'Product Edition'
options:
- label: 'Community Edition'
- label: 'Enterprise Edition'
- label: 'Enterprise Trial Edition'
validations:
required: true
- type: checkboxes
attributes:
label: 'Installation Method'
options:
- label: 'Online Installation (One-click command installation)'
- label: 'Offline Package Installation'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: 'Source Code'
- type: textarea
attributes:
label: '⭐️ Feature Description'
description: |
Please add a clear and concise description of the problem you aim to solve with this feature request.<br/>
Unclear descriptions will not be processed.
validations:
required: true
- type: textarea
attributes:
label: 'Proposed Solution'
description: Please provide a clear and concise description of the solution you desire.
validations:
required: true
- type: textarea
attributes:
label: 'Additional Information'
description: Please add any additional background information about the issue here.

View File

@@ -1,56 +0,0 @@
name: '⭐️ 功能需求'
description: '提出需求或建议'
title: '[Feature] '
labels: ['⭐️ Feature Request']
assignees:
- baijiangjie
- ibuler
body:
- type: input
attributes:
label: '产品版本'
description: 不再支持 v2.28(含)之前的版本。
validations:
required: true
- type: checkboxes
attributes:
label: '版本类型'
options:
- label: '社区版'
- label: '企业版'
- label: '企业试用版'
validations:
required: true
- type: checkboxes
attributes:
label: '安装方式'
options:
- label: '在线安装 (一键命令安装)'
- label: '离线包安装'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: '源码安装'
- type: textarea
attributes:
label: '⭐️ 需求描述'
description: |
请添加一个清晰且简洁的问题描述,阐述你希望通过这个功能需求解决的问题。<br/>
针对不清晰的描述信息将不予处理。
validations:
required: true
- type: textarea
attributes:
label: '解决方案'
description: 请清晰且简洁地描述你想要的解决方案。
validations:
required: true
- type: textarea
attributes:
label: '补充信息'
description: 在这里添加关于问题的任何其他背景信息。

View File

@@ -1,60 +0,0 @@
name: '🤔 Question'
description: 'Pose a question'
title: '[Question] '
labels: ['🤔 Question']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: 'Product Version'
description: The versions prior to v2.28 (inclusive) are no longer supported.
validations:
required: true
- type: checkboxes
attributes:
label: 'Product Edition'
options:
- label: 'Community Edition'
- label: 'Enterprise Edition'
- label: 'Enterprise Trial Edition'
validations:
required: true
- type: checkboxes
attributes:
label: 'Installation Method'
options:
- label: 'Online Installation (One-click command installation)'
- label: 'Offline Package Installation'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: 'Source Code'
- type: textarea
attributes:
label: 'Environment Information'
description: Please provide a clear and concise description outlining your environment information.
validations:
required: true
- type: textarea
attributes:
label: '🤔 Question Description'
description: |
Please provide a clear and concise description of the defect. If the issue is complex, please provide detailed explanations. <br/>
Unclear descriptions will not be processed.
validations:
required: true
- type: textarea
attributes:
label: 'Expected Behavior'
description: Please provide a clear and concise description of what you expect to happen.
- type: textarea
attributes:
label: 'Additional Information'
description: Please add any additional background information about the issue here.

View File

@@ -1,61 +0,0 @@
name: '🤔 问题咨询'
description: '提出一个问题'
title: '[Question] '
labels: ['🤔 Question']
assignees:
- baijiangjie
body:
- type: input
attributes:
label: '产品版本'
description: 不再支持 v2.28(含)之前的版本。
validations:
required: true
- type: checkboxes
attributes:
label: '版本类型'
options:
- label: '社区版'
- label: '企业版'
- label: '企业试用版'
validations:
required: true
- type: checkboxes
attributes:
label: '安装方式'
options:
- label: '在线安装 (一键命令安装)'
- label: '离线包安装'
- label: 'All-in-One'
- label: '1Panel'
- label: 'Kubernetes'
- label: '源码安装'
- type: textarea
attributes:
label: '环境信息'
description: 请在此详细描述你的环境信息,如操作系统、浏览器和部署架构等。
validations:
required: true
- type: textarea
attributes:
label: '🤔 问题描述'
description: |
请提供一个清晰且简洁的问题描述,如果问题比较复杂,也请详细说明。<br/>
针对不清晰的描述信息将不予处理。
validations:
required: true
- type: textarea
attributes:
label: '期望结果'
description: 请提供一个清晰且简洁的描述,说明你期望发生什么。
- type: textarea
attributes:
label: '补充信息'
description: 在这里添加关于问题的任何其他背景信息。

22
.github/ISSUE_TEMPLATE/bug---.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进
title: "[Bug] "
labels: 类型:Bug
assignees:
- baijiangjie
---
**JumpServer 版本( v2.28 之前的版本不再支持 )**
**浏览器版本**
**Bug 描述**
**Bug 重现步骤(有截图更好)**
1.
2.
3.

10
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[Question] "
labels: 类型:提问
assignees:
- baijiangjie
---
**请描述您的问题.**

View File

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

View File

@@ -1,31 +0,0 @@
name: Check I18n files CompileMessages
on:
pull_request:
branches:
- 'dev'
paths:
- 'apps/i18n/core/**/*.po'
types:
- opened
- synchronize
- reopened
jobs:
compile-messages-check:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and check compilemessages
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
push: false
file: Dockerfile
target: stage-build
tags: jumpserver/core:stage-build

View File

@@ -1,24 +0,0 @@
name: Publish Release to Discord
on:
release:
types: [published]
jobs:
send_discord_notification:
runs-on: ubuntu-latest
if: startsWith(github.event.release.tag_name, 'v4.')
steps:
- name: Send release notification to Discord
env:
WEBHOOK_URL: ${{ secrets.DISCORD_CHANGELOG_WEBHOOK }}
run: |
# 获取标签名称和 release body
TAG_NAME="${{ github.event.release.tag_name }}"
RELEASE_BODY="${{ github.event.release.body }}"
# 使用 jq 构建 JSON 数据,以确保安全传递
JSON_PAYLOAD=$(jq -n --arg tag "# JumpServer $TAG_NAME Released! 🚀" --arg body "$RELEASE_BODY" '{content: "\($tag)\n\($body)"}')
# 使用 curl 发送 JSON 数据
curl -X POST -H "Content-Type: application/json" -d "$JSON_PAYLOAD" "$WEBHOOK_URL"

View File

@@ -12,9 +12,7 @@ jobs:
uses: actions-cool/issues-helper@v2 uses: actions-cool/issues-helper@v2
with: with:
actions: 'close-issues' actions: 'close-issues'
labels: '⏳ Pending feedback' labels: '状态:待反馈'
inactive-day: 30 inactive-day: 30
body: | body: |
You haven't provided feedback for over 30 days.
We will close this issue. If you have any further needs, you can reopen it or submit a new issue.
您超过 30 天未反馈信息,我们将关闭该 issue如有需求您可以重新打开或者提交新的 issue。 您超过 30 天未反馈信息,我们将关闭该 issue如有需求您可以重新打开或者提交新的 issue。

View File

@@ -13,4 +13,4 @@ jobs:
if: ${{ !github.event.issue.pull_request }} if: ${{ !github.event.issue.pull_request }}
with: with:
actions: 'remove-labels' actions: 'remove-labels'
labels: '🔔 Pending processing,⏳ Pending feedback' labels: '状态:待处理,状态:待反馈'

View File

@@ -13,13 +13,13 @@ jobs:
uses: actions-cool/issues-helper@v2 uses: actions-cool/issues-helper@v2
with: with:
actions: 'add-labels' actions: 'add-labels'
labels: '🔔 Pending processing' labels: '状态:待处理'
- name: Remove require reply label - name: Remove require reply label
uses: actions-cool/issues-helper@v2 uses: actions-cool/issues-helper@v2
with: with:
actions: 'remove-labels' actions: 'remove-labels'
labels: '⏳ Pending feedback' labels: '状态:待反馈'
add-label-if-is-member: add-label-if-is-member:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -55,11 +55,11 @@ jobs:
uses: actions-cool/issues-helper@v2 uses: actions-cool/issues-helper@v2
with: with:
actions: 'add-labels' actions: 'add-labels'
labels: '⏳ Pending feedback' labels: '状态:待反馈'
- name: Remove require handle label - name: Remove require handle label
if: contains(steps.member_names.outputs.data, github.event.comment.user.login) if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
uses: actions-cool/issues-helper@v2 uses: actions-cool/issues-helper@v2
with: with:
actions: 'remove-labels' actions: 'remove-labels'
labels: '🔔 Pending processing' labels: '状态:待处理'

View File

@@ -13,4 +13,4 @@ jobs:
if: ${{ !github.event.issue.pull_request }} if: ${{ !github.event.issue.pull_request }}
with: with:
actions: 'add-labels' actions: 'add-labels'
labels: '🔔 Pending processing' labels: '状态:待处理'

36
.github/workflows/jms-build-test.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: "Run Build Test"
on:
push:
branches:
- pr@*
- repr@*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/build-push-action@v3
with:
context: .
push: false
tags: jumpserver/core-ce:test
file: Dockerfile-ce
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
- uses: LouisBrunner/checks-action@v1.5.0
if: always()
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Check Build
conclusion: ${{ job.status }}

View File

@@ -1,63 +0,0 @@
name: "Run Build Test"
on:
push:
paths:
- 'Dockerfile'
- 'Dockerfile*'
- 'Dockerfile-*'
- 'pyproject.toml'
- 'poetry.lock'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
component: [core]
version: [v4]
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare Build
run: |
sed -i 's@^FROM registry.fit2cloud.com/jumpserver@FROM ghcr.io/jumpserver@g' Dockerfile-ee
- name: Build CE Image
uses: docker/build-push-action@v5
with:
context: .
push: true
file: Dockerfile
tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}-ce
platforms: linux/amd64
build-args: |
VERSION=${{ matrix.version }}
APT_MIRROR=http://deb.debian.org
PIP_MIRROR=https://pypi.org/simple
outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build EE Image
uses: docker/build-push-action@v5
with:
context: .
push: false
file: Dockerfile-ee
tags: ghcr.io/jumpserver/${{ matrix.component }}:${{ matrix.version }}
platforms: linux/amd64
build-args: |
VERSION=${{ matrix.version }}
APT_MIRROR=http://deb.debian.org
PIP_MIRROR=https://pypi.org/simple
outputs: type=image,oci-mediatypes=true,compression=zstd,compression-level=3,force-compression=true
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -10,4 +10,3 @@ jobs:
- uses: jumpserver/action-generic-handler@master - uses: jumpserver/action-generic-handler@master
env: env:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }} GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}

View File

@@ -1,28 +0,0 @@
name: LLM Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
- uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
LANGUAGE: English
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen2-1.5b-instruct
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
top_p: 1
temperature: 1
# max_tokens: 10000
MAX_PATCH_LENGTH: 10000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"

2
.gitignore vendored
View File

@@ -43,5 +43,3 @@ releashe
data/* data/*
test.py test.py
.history/ .history/
.test/
*.mo

View File

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

View File

@@ -1,10 +1,5 @@
# Contributing # Contributing
As a contributor, you should agree that:
- The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary.
- Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations.
## Create pull request ## Create pull request
PR are always welcome, even if they only contain small fixes like typos or a few lines of code. If there will be a significant effort, please document it as an issue and get a discussion going before starting to work on it. PR are always welcome, even if they only contain small fixes like typos or a few lines of code. If there will be a significant effort, please document it as an issue and get a discussion going before starting to work on it.

View File

@@ -1,68 +0,0 @@
FROM jumpserver/core-base:20241105_025649 AS stage-build
ARG VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN echo > /opt/jumpserver/config.yml \
&& \
if [ -n "${VERSION}" ]; then \
sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" apps/jumpserver/const.py; \
fi
RUN set -ex \
&& export SECRET_KEY=$(head -c100 < /dev/urandom | base64 | tr -dc A-Za-z0-9 | head -c 48) \
&& . /opt/py3/bin/activate \
&& cd apps \
&& python manage.py compilemessages
FROM python:3.11-slim-bullseye
ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH
ARG DEPENDENCIES=" \
libldap2-dev \
libx11-dev"
ARG TOOLS=" \
cron \
ca-certificates \
default-libmysqlclient-dev \
openssh-client \
sshpass \
bubblewrap"
ARG APT_MIRROR=http://deb.debian.org
RUN set -ex \
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update > /dev/null \
&& 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 "no" | dpkg-reconfigure dash \
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/* \
&& echo "0 3 * * * root find /tmp -type f -mtime +1 -size +1M -exec rm -f {} \; && date > /tmp/clean.log" > /etc/cron.d/cleanup_tmp \
&& chmod 0644 /etc/cron.d/cleanup_tmp
COPY --from=stage-build /opt /opt
COPY --from=stage-build /usr/local/bin /usr/local/bin
COPY --from=stage-build /opt/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
WORKDIR /opt/jumpserver
VOLUME /opt/jumpserver/data
ENTRYPOINT ["./entrypoint.sh"]
EXPOSE 8080
STOPSIGNAL SIGQUIT
CMD ["start", "all"]

View File

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

125
Dockerfile-ce Normal file
View File

@@ -0,0 +1,125 @@
FROM python:3.11-slim-bullseye as stage-1
ARG TARGETARCH
ARG VERSION
ENV VERSION=$VERSION
WORKDIR /opt/jumpserver
ADD . .
RUN echo > /opt/jumpserver/config.yml \
&& cd utils && bash -ixeu build.sh
FROM python:3.11-slim-bullseye as stage-2
ARG TARGETARCH
ARG BUILD_DEPENDENCIES=" \
g++ \
make \
pkg-config"
ARG DEPENDENCIES=" \
freetds-dev \
libpq-dev \
libffi-dev \
libjpeg-dev \
libkrb5-dev \
libldap2-dev \
libsasl2-dev \
libssl-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
freerdp2-dev \
libaio-dev"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
git \
git-lfs \
unzip \
xz-utils \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& 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} \
&& echo "no" | dpkg-reconfigure dash
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
set -ex \
&& python3 -m venv /opt/py3 \
&& pip install poetry -i ${PIP_MIRROR} \
&& poetry config virtualenvs.create false \
&& . /opt/py3/bin/activate \
&& poetry install
FROM python:3.11-slim-bullseye
ARG TARGETARCH
ENV LANG=zh_CN.UTF-8 \
PATH=/opt/py3/bin:$PATH
ARG DEPENDENCIES=" \
libjpeg-dev \
libx11-dev \
freerdp2-dev \
libxmlsec1-openssl"
ARG TOOLS=" \
ca-certificates \
curl \
default-libmysqlclient-dev \
default-mysql-client \
iputils-ping \
locales \
nmap \
openssh-client \
patch \
sshpass \
telnet \
vim \
wget"
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
&& rm -f /etc/apt/apt.conf.d/docker-clean \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& apt-get update \
&& 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 "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
COPY --from=stage-2 /opt/py3 /opt/py3
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
WORKDIR /opt/jumpserver
ARG VERSION
ENV VERSION=$VERSION
VOLUME /opt/jumpserver/data
EXPOSE 8080
ENTRYPOINT ["./entrypoint.sh"]

View File

@@ -1,34 +1,5 @@
ARG VERSION=dev ARG VERSION
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
FROM jumpserver/core:${VERSION}-ce
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
ARG TOOLS=" \
g++ \
curl \
iputils-ping \
netcat-openbsd \
nmap \
telnet \
vim \
wget"
RUN set -ex \
&& apt-get update \
&& apt-get -y install --no-install-recommends ${TOOLS} \
&& apt-get clean all \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
COPY poetry.lock pyproject.toml ./
RUN set -ex \
&& . /opt/py3/bin/activate \
&& pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& poetry install --only xpack \
&& poetry cache clear pypi --all
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack

1
GITSHA Normal file
View File

@@ -0,0 +1 @@
1f1c1a9157477f07ebfe97ca25b31084da3a67ea

182
README.md
View File

@@ -1,115 +1,125 @@
<div align="center"> <p align="center">
<a name="readme-top"></a> <a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
<a href="https://jumpserver.org/index-en.html"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a> </p>
<h3 align="center">广受欢迎的开源堡垒机</h3>
## An open-source PAM tool (Bastion Host)
[![][license-shield]][license-link] <p align="center">
[![][discord-shield]][discord-link] <a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
[![][docker-shield]][docker-link] <a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Docker pulls"></a>
[![][github-release-shield]][github-release-link] <a href="https://github.com/jumpserver/jumpserver/releases/latest"><img src="https://img.shields.io/github/v/release/jumpserver/jumpserver" alt="Latest release"></a>
[![][github-stars-shield]][github-stars-link] <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>
**English** · [简体中文](./README.zh-CN.md)
</div>
<br/>
## What is JumpServer? <p align="center">
9 年时间,倾情投入,用心做好一款开源堡垒机。
</p>
JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser. ------------------------------
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
![JumpServer Overview](https://github.com/jumpserver/jumpserver/assets/32935519/35a371cb-8590-40ed-88ec-f351f8cf9045) JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
## Quickstart - **SSH**: Linux / Unix / 网络设备 等;
- **Windows**: Web 方式连接 / 原生 RDP 连接;
- **数据库**: MySQL / MariaDB / PostgreSQL / Oracle / SQLServer / ClickHouse 等;
- **NoSQL**: Redis / MongoDB 等;
- **GPT**: ChatGPT 等;
- **云服务**: Kubernetes / VMware vSphere 等;
- **Web 站点**: 各类系统的 Web 管理后台;
- **应用**: 通过 Remote App 连接各类应用。
Prepare a clean Linux Server ( 64 bit, >= 4c8g ) ## 产品特色
```sh - **开源**: 零门槛,线上快速获取和安装;
curl -sSL https://github.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash - **无插件**: 仅需浏览器,极致的 Web Terminal 使用体验;
``` - **分布式**: 支持分布式部署和横向扩展,轻松支持大规模并发访问;
- **多云支持**: 一套系统,同时管理不同云上面的资产;
- **多租户**: 一套系统,多个子公司或部门同时使用;
- **云端存储**: 审计录像云端存储,永不丢失;
Access JumpServer in your browser at `http://your-jumpserver-ip/` ## UI 展示
- Username: `admin`
- Password: `ChangeMe`
[![JumpServer Quickstart](https://github.com/user-attachments/assets/0f32f52b-9935-485e-8534-336c63389612)](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart") ![UI展示](https://docs.jumpserver.org/zh/v3/img/dashboard.png)
## Screenshots ## 在线体验
<table style="border-collapse: collapse; border: 1px solid black;"> - 环境地址:<https://demo.jumpserver.org/>
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/99fabe5b-0475-4a53-9116-4c370a1426c4" alt="JumpServer Console" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/a424d731-1c70-4108-a7d8-5bbf387dda9a" alt="JumpServer Audits" /></td>
</tr>
<tr> | :warning: 注意 |
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/393d2c27-a2d0-4dea-882d-00ed509e00c9" alt="JumpServer Workbench" /></td> |:-----------------------------|
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/3a2611cd-8902-49b8-b82b-2a6dac851f3e" alt="JumpServer Settings" /></td> | 该环境仅作体验目的使用,我们会定时清理、重置数据! |
</tr> | 请勿修改体验环境用户的密码! |
| 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! |
<tr> ## 快速开始
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/1e236093-31f7-4563-8eb1-e36d865f1568" alt="JumpServer SSH" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/69373a82-f7ab-41e8-b763-bbad2ba52167" alt="JumpServer RDP" /></td>
</tr>
<tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/5bed98c6-cbe8-4073-9597-d53c69dc3957" alt="JumpServer K8s" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/b80ad654-548f-42bc-ba3d-c1cfdf1b46d6" alt="JumpServer DB" /></td>
</tr>
</table>
## Components - [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
- [产品文档](https://docs.jumpserver.org)
- [在线学习](https://edu.fit2cloud.com/page/2635362)
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
JumpServer consists of multiple key components, which collectively form the functional framework of JumpServer, providing users with comprehensive capabilities for operations management and security control. ## 案例研究
| Project | Status | Description | - [腾讯音乐娱乐集团基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| - [腾讯海外游戏基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
| [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 | - [万华化学通过JumpServer管理全球化分布式IT资产并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
| [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 | - [雪花啤酒JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector | - [顺丰科技JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector | - [沐瞳游戏通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB | - [携程JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector | - [大智慧JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) | - [小红书JumpServer 堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) | - [中手游JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector | - [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
## Contributing ## 社区交流
Welcome to submit PR to contribute. Please refer to [CONTRIBUTING.md][contributing-link] for guidelines. 如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。
## Security 您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
JumpServer is a mission critical product. Please refer to the Basic Security Recommendations for installation and deployment. If you encounter any security-related issues, please contact us directly: ### 参与贡献
- Email: support@fit2cloud.com 欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
## License ## 组件项目
| 项目 | 状态 | 描述 |
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目 |
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer RDP 代理 Connector 项目 |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-私有发布-red" /> | JumpServer 远程应用 Connector 项目 (Linux) |
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core API 通信的组件项目 |
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
## 安全说明
JumpServer是一款安全产品请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/)
进行安装部署。如果您发现安全相关问题,请直接联系我们:
- 邮箱support@fit2cloud.com
- 电话400-052-0755
## License & Copyright
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved. Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-3.0.html https://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
<!-- JumpServer official link --> language governing permissions and limitations under the License.
[docs-link]: https://jumpserver.com/docs
[discord-link]: https://discord.com/invite/W6vYXmAQG2
[contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md
<!-- JumpServer Other link-->
[license-link]: https://www.gnu.org/licenses/gpl-3.0.html
[docker-link]: https://hub.docker.com/u/jumpserver
[github-release-link]: https://github.com/jumpserver/jumpserver/releases/latest
[github-stars-link]: https://github.com/jumpserver/jumpserver
[github-issues-link]: https://github.com/jumpserver/jumpserver/issues
<!-- Shield link-->
[github-release-shield]: https://img.shields.io/github/v/release/jumpserver/jumpserver
[github-stars-shield]: https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square
[docker-shield]: https://img.shields.io/docker/pulls/jumpserver/jms_all.svg
[license-shield]: https://img.shields.io/github/license/jumpserver/jumpserver
[discord-shield]: https://img.shields.io/discord/1194233267294052363?style=flat&logo=discord&logoColor=%23f5f5f5&labelColor=%235462eb&color=%235462eb
<!-- Image link -->

View File

@@ -1,121 +0,0 @@
<p align="center">
<a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
</p>
<h3 align="center">广受欢迎的开源堡垒机</h3>
<p align="center">
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Docker pulls"></a>
<a href="https://github.com/jumpserver/jumpserver/releases/latest"><img src="https://img.shields.io/github/v/release/jumpserver/jumpserver" alt="Latest release"></a>
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
</p>
<p align="center">
10 年时间,倾情投入,用心做好一款开源堡垒机。
</p>
------------------------------
## JumpServer 是什么?
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 连接各类应用。
## 产品特色
- **开源**: 零门槛,线上快速获取和安装;
- **无插件**: 仅需浏览器,极致的 Web Terminal 使用体验;
- **分布式**: 支持分布式部署和横向扩展,轻松支持大规模并发访问;
- **多云支持**: 一套系统,同时管理不同云上面的资产;
- **多租户**: 一套系统,多个子公司或部门同时使用;
- **云端存储**: 审计录像云端存储,永不丢失;
## 快速开始
- [快速入门](https://docs.jumpserver.org/zh/v3/quick_start/)
- [产品文档](https://docs.jumpserver.org)
- [在线学习](https://edu.fit2cloud.com/page/2635362)
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
## UI 展示
![UI展示](https://docs.jumpserver.org/zh/v3/img/dashboard.png)
## 在线体验
- 环境地址:<https://demo.jumpserver.org/>
| :warning: 注意 |
|:-----------------------------|
| 该环境仅作体验目的使用,我们会定时清理、重置数据! |
| 请勿修改体验环境用户的密码! |
| 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! |
## 案例研究
- [腾讯音乐娱乐集团基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
- [腾讯海外游戏基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
- [万华化学通过JumpServer管理全球化分布式IT资产并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
- [雪花啤酒JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
- [顺丰科技JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [沐瞳游戏通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213)
- [携程JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [大智慧JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [小红书JumpServer 堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [中手游JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)
## 社区交流
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)。
您也可以到我们的 [社区论坛](https://bbs.fit2cloud.com/c/js/5) 当中进行交流沟通。
## 参与贡献
欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
## 组件项目
| Project | Status | Description |
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI |
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal |
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector |
| [Lion](https://github.com/jumpserver/lion) | <a href="https://github.com/jumpserver/lion/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion.svg" /></a> | JumpServer Graphical Protocol Connector |
| [Chen](https://github.com/jumpserver/chen) | <a href="https://github.com/jumpserver/chen/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen.svg" /> | JumpServer Web DB |
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
## 安全说明
JumpServer是一款安全产品请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/)
进行安装部署。如果您发现安全相关问题,请直接联系我们:
- 邮箱support@fit2cloud.com
- 电话400-052-0755
## License & Copyright
Copyright (c) 2014-2024 飞致云 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-3.0.html
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "
AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific
language governing permissions and limitations under the License.

94
README_EN.md Normal file
View File

@@ -0,0 +1,94 @@
<p align="center"><a href="https://jumpserver.org"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a></p>
<h3 align="center">Open Source Bastion Host</h3>
<p align="center">
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://img.shields.io/github/license/jumpserver/jumpserver" alt="License: GPLv3"></a>
<a href="https://shields.io/github/downloads/jumpserver/jumpserver/total"><img src="https://shields.io/github/downloads/jumpserver/jumpserver/total" alt=" release"></a>
<a href="https://hub.docker.com/u/jumpserver"><img src="https://img.shields.io/docker/pulls/jumpserver/jms_all.svg" alt="Codacy"></a>
<a href="https://github.com/jumpserver/jumpserver"><img src="https://img.shields.io/github/stars/jumpserver/jumpserver?color=%231890FF&style=flat-square" alt="Stars"></a>
</p>
JumpServer is the world's first open-source Bastion Host and is licensed under the GPLv3. It is a 4A-compliant professional operation and maintenance security audit system.
JumpServer uses Python / Django for development, follows Web 2.0 specifications, and is equipped with an industry-leading Web Terminal solution that provides a beautiful user interface and great user experience
JumpServer adopts a distributed architecture to support multi-branch deployment across multiple cross-regional areas. The central node provides APIs, and login nodes are deployed in each branch. It can be scaled horizontally without concurrency restrictions.
Change the world by taking every little step
----
### Advantages
- Open Source: huge transparency and free to access with quick installation process.
- Distributed: support large-scale concurrent access with ease.
- No Plugin required: all you need is a browser, the ultimate Web Terminal experience.
- Multi-Cloud supported: a unified system to manage assets on different clouds at the same time
- Cloud storage: audit records are stored in the cloud. Data lost no more!
- Multi-Tenant system: multiple subsidiary companies or departments access the same system simultaneously.
- Many applications supported: link to databases, windows remote applications, and Kubernetes cluster, etc.
### JumpServer Component Projects
- [Lina](https://github.com/jumpserver/lina) JumpServer Web UI
- [Luna](https://github.com/jumpserver/luna) JumpServer Web Terminal
- [KoKo](https://github.com/jumpserver/koko) JumpServer Character protocaol Connector, replace original Python Version [Coco](https://github.com/jumpserver/coco)
- [Lion](https://github.com/jumpserver/lion-release) JumpServer Graphics protocol Connectorrely on [Apache Guacamole](https://guacamole.apache.org/)
### Contribution
If you have any good ideas or helping us to fix bugs, please submit a Pull Request and accept our thanks :)
Thanks to the following contributors for making JumpServer better everyday!
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/jumpserver" />
</a>
<a href="https://github.com/jumpserver/koko/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/koko" />
</a>
<a href="https://github.com/jumpserver/lina/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/lina" />
</a>
<a href="https://github.com/jumpserver/luna/graphs/contributors">
<img src="https://contrib.rocks/image?repo=jumpserver/luna" />
</a>
### Thanks to
- [Apache Guacamole](https://guacamole.apache.org/) Web page connection RDP, SSH, VNC protocol equipment. JumpServer graphical connection dependent.
- [OmniDB](https://omnidb.org/) Web page connection to databases. JumpServer Web database dependent.
### JumpServer Enterprise Version
- [Apply for it](https://jinshuju.net/f/kyOYpi)
### Case Study
- [JumpServer 堡垒机护航顺丰科技超大规模资产安全运维](https://blog.fit2cloud.com/?p=1147)
- [JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882)
- [携程 JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851)
- [小红书的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516)
- [JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732)
- [中通快递JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708)
- [东方明珠JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687)
- [江苏农信JumpServer堡垒机助力行业云安全运维](https://blog.fit2cloud.com/?p=666)。
### For safety instructions
JumpServer is a security product. Please refer to [Basic Security Recommendations](https://docs.jumpserver.org/zh/master/install/install_security/) for deployment and installation.
If you find a security problem, please contact us directly
- ibuler@fit2cloud.com
- support@fit2cloud.com
- 400-052-0755
### License & Copyright
Copyright (c) 2014-2022 FIT2CLOUD Tech, Inc., All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-3.0.htmll
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@@ -1,12 +1,11 @@
from django.db.models import Q
from rest_framework.generics import CreateAPIView from rest_framework.generics import CreateAPIView
from accounts import serializers from accounts import serializers
from accounts.models import Account
from accounts.permissions import AccountTaskActionPermission from accounts.permissions import AccountTaskActionPermission
from accounts.tasks import ( from accounts.tasks import (
remove_accounts_task, verify_accounts_connectivity_task, push_accounts_to_assets_task remove_accounts_task, verify_accounts_connectivity_task, push_accounts_to_assets_task
) )
from assets.exceptions import NotSupportedTemporarilyError
from authentication.permissions import UserConfirmation, ConfirmType from authentication.permissions import UserConfirmation, ConfirmType
__all__ = [ __all__ = [
@@ -27,35 +26,25 @@ class AccountsTaskCreateAPI(CreateAPIView):
] ]
return super().get_permissions() return super().get_permissions()
@staticmethod
def get_account_ids(data, action):
account_type = 'gather_accounts' if action == 'remove' else 'accounts'
accounts = data.get(account_type, [])
account_ids = [str(a.id) for a in accounts]
if action == 'remove':
return account_ids
assets = data.get('assets', [])
asset_ids = [str(a.id) for a in assets]
ids = Account.objects.filter(
Q(id__in=account_ids) | Q(asset_id__in=asset_ids)
).distinct().values_list('id', flat=True)
return [str(_id) for _id in ids]
def perform_create(self, serializer): def perform_create(self, serializer):
data = serializer.validated_data data = serializer.validated_data
action = data['action'] accounts = data.get('accounts', [])
ids = self.get_account_ids(data, action) params = data.get('params')
account_ids = [str(a.id) for a in accounts]
if action == 'push': if data['action'] == 'push':
task = push_accounts_to_assets_task.delay(ids, data.get('params')) task = push_accounts_to_assets_task.delay(account_ids, params)
elif action == 'remove': elif data['action'] == 'remove':
task = remove_accounts_task.delay(ids) gather_accounts = data.get('gather_accounts', [])
elif action == 'verify': gather_account_ids = [str(a.id) for a in gather_accounts]
task = verify_accounts_connectivity_task.delay(ids) task = remove_accounts_task.delay(gather_account_ids)
else: else:
raise ValueError(f"Invalid action: {action}") account = accounts[0]
asset = account.asset
if not asset.auto_config['ansible_enabled'] or \
not asset.auto_config['ping_enabled']:
raise NotSupportedTemporarilyError()
task = verify_accounts_connectivity_task.delay(account_ids)
data = getattr(serializer, '_data', {}) data = getattr(serializer, '_data', {})
data["task"] = task.id data["task"] = task.id

View File

@@ -18,8 +18,9 @@ __all__ = [
class AccountBackupPlanViewSet(OrgBulkModelViewSet): class AccountBackupPlanViewSet(OrgBulkModelViewSet):
model = AccountBackupAutomation model = AccountBackupAutomation
filterset_fields = ('name',) filter_fields = ('name',)
search_fields = filterset_fields search_fields = filter_fields
ordering = ('name',)
serializer_class = serializers.AccountBackupSerializer serializer_class = serializers.AccountBackupSerializer

View File

@@ -20,8 +20,8 @@ __all__ = [
class AutomationAssetsListApi(generics.ListAPIView): class AutomationAssetsListApi(generics.ListAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.AutomationAssetsSerializer serializer_class = serializers.AutomationAssetsSerializer
filterset_fields = ("name", "address") filter_fields = ("name", "address")
search_fields = filterset_fields search_fields = filter_fields
def get_object(self): def get_object(self):
pk = self.kwargs.get('pk') pk = self.kwargs.get('pk')

View File

@@ -6,12 +6,9 @@ from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from accounts.filters import ChangeSecretRecordFilterSet
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
from accounts.tasks import execute_automation_record_task from accounts.tasks import execute_automation_record_task
from authentication.permissions import UserConfirmation, ConfirmType
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from rbac.permissions import RBACPermission
from .base import ( from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet AutomationNodeAddRemoveApi, AutomationExecutionViewSet
@@ -27,54 +24,35 @@ __all__ = [
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet): class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
model = ChangeSecretAutomation model = ChangeSecretAutomation
filterset_fields = ('name', 'secret_type', 'secret_strategy') filter_fields = ('name', 'secret_type', 'secret_strategy')
search_fields = filterset_fields search_fields = filter_fields
serializer_class = serializers.ChangeSecretAutomationSerializer serializer_class = serializers.ChangeSecretAutomationSerializer
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
filterset_class = ChangeSecretRecordFilterSet serializer_class = serializers.ChangeSecretRecordSerializer
filterset_fields = ('asset_id', 'execution_id')
search_fields = ('asset__address',) search_fields = ('asset__address',)
tp = AutomationTypes.change_secret tp = AutomationTypes.change_secret
serializer_classes = {
'default': serializers.ChangeSecretRecordSerializer,
'secret': serializers.ChangeSecretRecordViewSecretSerializer,
}
rbac_perms = { rbac_perms = {
'execute': 'accounts.add_changesecretexecution', 'execute': 'accounts.add_changesecretexecution',
'secret': 'accounts.view_changesecretrecord',
} }
def get_permissions(self):
if self.action == 'secret':
self.permission_classes = [
RBACPermission,
UserConfirmation.require(ConfirmType.MFA)
]
return super().get_permissions()
def get_queryset(self): def get_queryset(self):
return ChangeSecretRecord.objects.all() return ChangeSecretRecord.objects.all()
@action(methods=['post'], detail=False, url_path='execute') @action(methods=['post'], detail=False, url_path='execute')
def execute(self, request, *args, **kwargs): def execute(self, request, *args, **kwargs):
record_ids = request.data.get('record_ids') record_id = request.data.get('record_id')
records = self.get_queryset().filter(id__in=record_ids) record = self.get_queryset().filter(pk=record_id)
execution_count = records.values_list('execution_id', flat=True).distinct().count() if not record:
if execution_count != 1:
return Response( return Response(
{'detail': 'Only one execution is allowed to execute'}, {'detail': 'record not found'},
status=status.HTTP_400_BAD_REQUEST status=status.HTTP_404_NOT_FOUND
) )
task = execute_automation_record_task.delay(record_ids, self.tp) task = execute_automation_record_task.delay(record_id, self.tp)
return Response({'task': task.id}, status=status.HTTP_200_OK) return Response({'task': task.id}, status=status.HTTP_200_OK)
@action(methods=['get'], detail=True, url_path='secret')
def secret(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
class ChangSecretExecutionViewSet(AutomationExecutionViewSet): class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = ( rbac_perms = (

View File

@@ -20,8 +20,8 @@ __all__ = [
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet): class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
model = GatherAccountsAutomation model = GatherAccountsAutomation
filterset_fields = ('name',) filter_fields = ('name',)
search_fields = filterset_fields search_fields = filter_fields
serializer_class = serializers.GatherAccountAutomationSerializer serializer_class = serializers.GatherAccountAutomationSerializer

View File

@@ -20,8 +20,8 @@ __all__ = [
class PushAccountAutomationViewSet(OrgBulkModelViewSet): class PushAccountAutomationViewSet(OrgBulkModelViewSet):
model = PushAccountAutomation model = PushAccountAutomation
filterset_fields = ('name', 'secret_type', 'secret_strategy') filter_fields = ('name', 'secret_type', 'secret_strategy')
search_fields = filterset_fields search_fields = filter_fields
serializer_class = serializers.PushAccountAutomationSerializer serializer_class = serializers.PushAccountAutomationSerializer

View File

@@ -4,7 +4,6 @@ from django.apps import AppConfig
class AccountsConfig(AppConfig): class AccountsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'accounts' name = 'accounts'
verbose_name = 'App Accounts'
def ready(self): def ready(self):
from . import signal_handlers # noqa from . import signal_handlers # noqa

View File

@@ -3,11 +3,10 @@ import time
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from xlsxwriter import Workbook from xlsxwriter import Workbook
from accounts.const import AccountBackupType from accounts.const.automation import AccountBackupType
from accounts.models.automations.backup_account import AccountBackupAutomation from accounts.models.automations.backup_account import AccountBackupAutomation
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
from accounts.serializers import AccountSecretSerializer from accounts.serializers import AccountSecretSerializer
@@ -18,7 +17,7 @@ from terminal.models.component.storage import ReplayStorage
from users.models import User from users.models import User
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
split_help_text = _('The account key will be split into two parts and sent')
class RecipientsNotFound(Exception): class RecipientsNotFound(Exception):
pass pass
@@ -116,8 +115,8 @@ class AssetAccountHandler(BaseAccountHandler):
data = AccountSecretSerializer(_accounts, many=True).data data = AccountSecretSerializer(_accounts, many=True).data
cls.handler_secret(data, section) cls.handler_secret(data, section)
data_map.update(cls.add_rows(data, header_fields, sheet_name)) data_map.update(cls.add_rows(data, header_fields, sheet_name))
number_of_backup_accounts = _('Number of backup accounts')
print('\n\033[33m- {}: {}\033[0m'.format(number_of_backup_accounts, accounts.count())) print('\n\033[33m- 共备份 {} 条账号\033[0m'.format(accounts.count()))
return data_map return data_map
@@ -128,10 +127,9 @@ class AccountBackupHandler:
self.is_frozen = False # 任务状态冻结标志 self.is_frozen = False # 任务状态冻结标志
def create_excel(self, section='complete'): def create_excel(self, section='complete'):
hint = _('Generating asset or application related backup information files')
print( print(
'\n' '\n'
f'\033[32m>>> {hint}\033[0m' '\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
'' ''
) )
# Print task start date # Print task start date
@@ -153,9 +151,7 @@ class AccountBackupHandler:
wb.close() wb.close()
files.append(filename) files.append(filename)
timedelta = round((time.time() - time_start), 2) timedelta = round((time.time() - time_start), 2)
time_cost = _('Time cost') print('创建备份文件完成: 用时 {}s'.format(timedelta))
file_created = _('Backup file creation completed')
print('{}: {} {}s'.format(file_created, time_cost, timedelta))
return files return files
def send_backup_mail(self, files, recipients): def send_backup_mail(self, files, recipients):
@@ -164,7 +160,7 @@ class AccountBackupHandler:
recipients = User.objects.filter(id__in=list(recipients)) recipients = User.objects.filter(id__in=list(recipients))
print( print(
'\n' '\n'
f'\033[32m>>> {_("Start sending backup emails")}\033[0m' '\033[32m>>> 开始发送备份邮件\033[0m'
'' ''
) )
plan_name = self.plan_name plan_name = self.plan_name
@@ -172,12 +168,12 @@ class AccountBackupHandler:
if not user.secret_key: if not user.secret_key:
attachment_list = [] attachment_list = []
else: else:
password = user.secret_key.encode('utf8')
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip') attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
encrypt_and_compress_zip_file(attachment, user.secret_key, files) encrypt_and_compress_zip_file(attachment, password, files)
attachment_list = [attachment, ] attachment_list = [attachment, ]
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list) AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
email_sent_to = _('Email sent to') print('邮件已发送至{}({})'.format(user, user.email))
print('{} {}({})'.format(email_sent_to, user, user.email))
for file in files: for file in files:
os.remove(file) os.remove(file)
@@ -187,22 +183,21 @@ class AccountBackupHandler:
recipients = ReplayStorage.objects.filter(id__in=list(recipients)) recipients = ReplayStorage.objects.filter(id__in=list(recipients))
print( print(
'\n' '\n'
'\033[32m>>> 📃 ---> sftp \033[0m' '\033[32m>>> 开始发送备份文件到sftp服务器\033[0m'
'' ''
) )
plan_name = self.plan_name plan_name = self.plan_name
encrypt_file = _('Encrypting files using encryption password')
for rec in recipients: for rec in recipients:
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip') attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
if password: if password:
print(f'\033[32m>>> {encrypt_file}\033[0m') print('\033[32m>>> 使用加密密码对文件进行加密中\033[0m')
password = password.encode('utf8')
encrypt_and_compress_zip_file(attachment, password, files) encrypt_and_compress_zip_file(attachment, password, files)
else: else:
zip_files(attachment, files) zip_files(attachment, files)
attachment_list = attachment attachment_list = attachment
AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list) AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list)
file_sent_to = _('The backup file will be sent to') print('备份文件将发送至{}({})'.format(rec.name, rec.id))
print('{}: {}({})'.format(file_sent_to, rec.name, rec.id))
for file in files: for file in files:
os.remove(file) os.remove(file)
@@ -210,15 +205,14 @@ class AccountBackupHandler:
self.execution.reason = reason[:1024] self.execution.reason = reason[:1024]
self.execution.is_success = is_success self.execution.is_success = is_success
self.execution.save() self.execution.save()
finish = _('Finish') print('\n已完成对任务状态的更新\n')
print(f'\n{finish}\n')
@staticmethod @staticmethod
def step_finished(is_success): def step_finished(is_success):
if is_success: if is_success:
print(_('Success')) print('任务执行成功')
else: else:
print(_('Failed')) print('任务执行失败')
def _run(self): def _run(self):
is_success = False is_success = False
@@ -231,6 +225,8 @@ class AccountBackupHandler:
self.backup_by_obj_storage() self.backup_by_obj_storage()
except Exception as e: except Exception as e:
self.is_frozen = True self.is_frozen = True
print('任务执行被异常中断')
print('下面打印发生异常的 Traceback 信息 : ')
print(e) print(e)
error = str(e) error = str(e)
else: else:
@@ -245,16 +241,15 @@ class AccountBackupHandler:
zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', []) obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', []) obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
no_assigned_sftp_server = _('The backup task has no assigned sftp server')
if not obj_recipients_part_one and not obj_recipients_part_two: if not obj_recipients_part_one and not obj_recipients_part_two:
print( print(
'\n' '\n'
f'\033[31m>>> {no_assigned_sftp_server}\033[0m' '\033[31m>>> 该备份任务未分配sftp服务器\033[0m'
'' ''
) )
raise RecipientsNotFound('Not Found Recipients') raise RecipientsNotFound('Not Found Recipients')
if obj_recipients_part_one and obj_recipients_part_two: if obj_recipients_part_one and obj_recipients_part_two:
print(f'\033[32m>>> {split_help_text}\033[0m') print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
files = self.create_excel(section='front') files = self.create_excel(section='front')
self.send_backup_obj_storage(files, obj_recipients_part_one, zip_encrypt_password) self.send_backup_obj_storage(files, obj_recipients_part_one, zip_encrypt_password)
@@ -266,19 +261,17 @@ class AccountBackupHandler:
self.send_backup_obj_storage(files, recipients, zip_encrypt_password) self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
def backup_by_email(self): def backup_by_email(self):
warn_text = _('The backup task has no assigned recipient')
recipients_part_one = self.execution.snapshot.get('recipients_part_one', []) recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
recipients_part_two = self.execution.snapshot.get('recipients_part_two', []) recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
if not recipients_part_one and not recipients_part_two: if not recipients_part_one and not recipients_part_two:
print( print(
'\n' '\n'
f'\033[31m>>> {warn_text}\033[0m' '\033[31m>>> 该备份任务未分配收件人\033[0m'
'' ''
) )
raise RecipientsNotFound('Not Found Recipients') raise RecipientsNotFound('Not Found Recipients')
if recipients_part_one and recipients_part_two: if recipients_part_one and recipients_part_two:
print(f'\033[32m>>> {split_help_text}\033[0m') print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
files = self.create_excel(section='front') files = self.create_excel(section='front')
self.send_backup_mail(files, recipients_part_one) self.send_backup_mail(files, recipients_part_one)
@@ -290,18 +283,15 @@ class AccountBackupHandler:
self.send_backup_mail(files, recipients) self.send_backup_mail(files, recipients)
def run(self): def run(self):
plan_start = _('Plan start') print('任务开始: {}'.format(local_now_display()))
plan_end = _('Plan end')
time_cost = _('Time cost')
error = _('An exception occurred during task execution')
print('{}: {}'.format(plan_start, local_now_display()))
time_start = time.time() time_start = time.time()
try: try:
self._run() self._run()
except Exception as e: except Exception as e:
print(error) print('任务运行出现异常')
print('下面显示异常 Traceback 信息: ')
print(e) print(e)
finally: finally:
print('\n{}: {}'.format(plan_end, local_now_display())) print('\n任务结束: {}'.format(local_now_display()))
timedelta = round((time.time() - time_start), 2) timedelta = round((time.time() - time_start), 2)
print('{}: {}s'.format(time_cost, timedelta)) print('用时: {}s'.format(timedelta))

View File

@@ -3,7 +3,6 @@
import time import time
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from common.utils.timezone import local_now_display from common.utils.timezone import local_now_display
from .handlers import AccountBackupHandler from .handlers import AccountBackupHandler
@@ -20,8 +19,7 @@ class AccountBackupManager:
def do_run(self): def do_run(self):
execution = self.execution execution = self.execution
account_backup_execution_being_executed = _('The account backup plan is being executed') print('\n\033[33m# 账号备份计划正在执行\033[0m')
print(f'\n\033[33m# {account_backup_execution_being_executed}\033[0m')
handler = AccountBackupHandler(execution) handler = AccountBackupHandler(execution)
handler.run() handler.run()
@@ -34,11 +32,9 @@ class AccountBackupManager:
self.date_end = timezone.now() self.date_end = timezone.now()
print('\n\n' + '-' * 80) print('\n\n' + '-' * 80)
plan_execution_end = _('Plan execution end') print('计划执行结束 {}\n'.format(local_now_display()))
print('{} {}\n'.format(plan_execution_end, local_now_display()))
self.timedelta = self.time_end - self.time_start self.timedelta = self.time_end - self.time_start
time_cost = _('Time cost') print('用时: {}s'.format(self.timedelta))
print('{}: {}s'.format(time_cost, self.timedelta))
self.execution.timedelta = self.timedelta self.execution.timedelta = self.timedelta
self.execution.save() self.execution.save()

View File

@@ -13,13 +13,11 @@
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}" login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ jms_custom_become | default(False) }}" become: "{{ custom_become | default(False) }}"
become_method: "{{ jms_custom_become_method | default('su') }}" become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ jms_custom_become_user | default('') }}" become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ jms_custom_become_password | default('') }}" become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}" become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
register: ping_info register: ping_info
delegate_to: localhost delegate_to: localhost
@@ -31,11 +29,11 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_secret_type: "{{ jms_account.secret_type }}" login_secret_type: "{{ jms_account.secret_type }}"
login_private_key_path: "{{ jms_account.private_key_path }}" login_private_key_path: "{{ jms_account.private_key_path }}"
become: "{{ jms_custom_become | default(False) }}" become: "{{ custom_become | default(False) }}"
become_method: "{{ jms_custom_become_method | default('su') }}" become_method: "{{ custom_become_method | default('su') }}"
become_user: "{{ jms_custom_become_user | default('') }}" become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ jms_custom_become_password | default('') }}" become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}" become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
commands: "{{ params.commands }}" commands: "{{ params.commands }}"
@@ -56,6 +54,4 @@
become_user: "{{ account.become.ansible_user | default('') }}" become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
delegate_to: localhost delegate_to: localhost

View File

@@ -7,7 +7,6 @@ type:
- all - all
method: change_secret method: change_secret
protocol: ssh protocol: ssh
priority: 50
params: params:
- name: commands - name: commands
type: list type: list

View File

@@ -4,9 +4,6 @@
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
db_name: "{{ jms_asset.spec_info.db_name }}" db_name: "{{ jms_asset.spec_info.db_name }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Test MySQL connection - name: Test MySQL connection
@@ -16,9 +13,9 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version
register: db_info register: db_info
@@ -33,9 +30,9 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
@@ -50,7 +47,7 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version

View File

@@ -39,4 +39,3 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.spec_info.db_name }}" login_database: "{{ jms_asset.spec_info.db_name }}"
mode: "{{ account.mode }}"

View File

@@ -2,10 +2,6 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Test PostgreSQL connection - name: Test PostgreSQL connection
@@ -15,10 +11,6 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}" login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
register: result register: result
failed_when: not result.is_available failed_when: not result.is_available
@@ -36,10 +28,6 @@
db: "{{ jms_asset.spec_info.db_name }}" db: "{{ jms_asset.spec_info.db_name }}"
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
role_attr_flags: LOGIN role_attr_flags: LOGIN
ignore_errors: true ignore_errors: true
when: result is succeeded when: result is succeeded
@@ -51,7 +39,3 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}" db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"

View File

@@ -14,25 +14,26 @@
- name: "Add {{ account.username }} user" - name: "Add {{ account.username }} user"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" shell: "{{ params.shell }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" home: "{{ params.home | default('/home/' + account.username, true) }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" groups: "{{ params.groups }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1 expires: -1
state: present state: present
when: user_info.failed when: user_info.failed
- name: "Set {{ account.username }} sudo setting" - name: "Add {{ account.username }} group"
ansible.builtin.lineinfile: ansible.builtin.group:
dest: /etc/sudoers name: "{{ account.username }}"
state: present state: present
regexp: "^{{ account.username }} ALL=" when: user_info.failed
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s - name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when: when:
- user_info.failed or params.modify_sudo - user_info.failed
- params.sudo - params.groups
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
@@ -42,40 +43,14 @@
ignore_errors: true ignore_errors: true
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: remove jumpserver ssh key
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys" dest: "{{ ssh_params.dest }}"
regexp: "{{ ssh_params.regexp }}" regexp: "{{ ssh_params.regexp }}"
state: absent state: absent
when: when:
- account.secret_type == "ssh_key" - account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms" - ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key" - name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key: ansible.builtin.authorized_key:
@@ -84,6 +59,17 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection
@@ -93,13 +79,12 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
become: "{{ account.become.ansible_become | default(False) }}" become: "{{ account.become.ansible_become | default(False) }}"
become_method: su become_method: su
become_user: "{{ account.become.ansible_user | default('') }}" become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" when: account.secret_type == "password"
delegate_to: localhost delegate_to: localhost
@@ -109,7 +94,6 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
delegate_to: localhost delegate_to: localhost

View File

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

View File

@@ -14,25 +14,26 @@
- name: "Add {{ account.username }} user" - name: "Add {{ account.username }} user"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" shell: "{{ params.shell }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" home: "{{ params.home | default('/home/' + account.username, true) }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" groups: "{{ params.groups }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1 expires: -1
state: present state: present
when: user_info.failed when: user_info.failed
- name: "Set {{ account.username }} sudo setting" - name: "Add {{ account.username }} group"
ansible.builtin.lineinfile: ansible.builtin.group:
dest: /etc/sudoers name: "{{ account.username }}"
state: present state: present
regexp: "^{{ account.username }} ALL=" when: user_info.failed
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s - name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when: when:
- user_info.failed or params.modify_sudo - user_info.failed
- params.sudo - params.groups
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
@@ -42,40 +43,14 @@
ignore_errors: true ignore_errors: true
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: remove jumpserver ssh key
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys" dest: "{{ ssh_params.dest }}"
regexp: "{{ ssh_params.regexp }}" regexp: "{{ ssh_params.regexp }}"
state: absent state: absent
when: when:
- account.secret_type == "ssh_key" - account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms" - ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key" - name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key: ansible.builtin.authorized_key:
@@ -84,6 +59,17 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection
@@ -93,13 +79,12 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
become: "{{ account.become.ansible_become | default(False) }}" become: "{{ account.become.ansible_become | default(False) }}"
become_method: su become_method: su
become_user: "{{ account.become.ansible_user | default('') }}" become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" when: account.secret_type == "password"
delegate_to: localhost delegate_to: localhost
@@ -109,7 +94,6 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
delegate_to: localhost delegate_to: localhost

View File

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

View File

@@ -25,11 +25,11 @@
- name: Verify password (pyfreerdp) - name: Verify password (pyfreerdp)
rdp_ping: rdp_ping:
login_host: "{{ jms_asset.origin_address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}" login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}" login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default({}) }}" login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password" when: account.secret_type == "password"
delegate_to: localhost delegate_to: localhost

View File

@@ -5,7 +5,6 @@ method: change_secret
category: host category: host
type: type:
- windows - windows
priority: 49
params: params:
- name: groups - name: groups
type: str type: str

View File

@@ -4,12 +4,11 @@ from copy import deepcopy
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from xlsxwriter import Workbook from xlsxwriter import Workbook
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy
from accounts.models import ChangeSecretRecord, BaseAccountQuerySet from accounts.models import ChangeSecretRecord
from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg from accounts.notifications import ChangeSecretExecutionTaskMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer from accounts.serializers import ChangeSecretRecordBackUpSerializer
from assets.const import HostTypes from assets.const import HostTypes
from common.utils import get_logger from common.utils import get_logger
@@ -27,7 +26,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.record_map = self.execution.snapshot.get('record_map', {}) self.record_id = self.execution.snapshot.get('record_id')
self.secret_type = self.execution.snapshot.get('secret_type') self.secret_type = self.execution.snapshot.get('secret_type')
self.secret_strategy = self.execution.snapshot.get( self.secret_strategy = self.execution.snapshot.get(
'secret_strategy', SecretStrategy.custom 'secret_strategy', SecretStrategy.custom
@@ -50,6 +49,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
if kwargs['strategy'] == SSHKeyStrategy.set_jms: if kwargs['strategy'] == SSHKeyStrategy.set_jms:
username = account.username
path = f'/{username}' if username == "root" else f'/home/{username}'
kwargs['dest'] = f'{path}/.ssh/authorized_keys'
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip()) kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
return kwargs return kwargs
@@ -65,10 +67,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
else: else:
return self.secret_generator(secret_type).get_secret() return self.secret_generator(secret_type).get_secret()
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None: def get_accounts(self, privilege_account):
if not privilege_account: if not privilege_account:
print('Not privilege account') print(f'not privilege account')
return return []
asset = privilege_account.asset asset = privilege_account.asset
accounts = asset.accounts.all() accounts = asset.accounts.all()
@@ -94,9 +96,10 @@ class ChangeSecretManager(AccountBasePlaybookManager):
return host return host
accounts = self.get_accounts(account) accounts = self.get_accounts(account)
error_msg = _("No pending accounts found")
if not accounts: if not accounts:
print(f'{asset}: {error_msg}') print('没有发现待处理的账号: %s 用户ID: %s 类型: %s' % (
asset.name, self.account_ids, self.secret_type
))
return [] return []
records = [] records = []
@@ -105,9 +108,6 @@ class ChangeSecretManager(AccountBasePlaybookManager):
print(f'Windows {asset} does not support ssh key push') print(f'Windows {asset} does not support ssh key push')
return inventory_hosts return inventory_hosts
if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
host['ssh_params'] = {} host['ssh_params'] = {}
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
@@ -118,25 +118,14 @@ class ChangeSecretManager(AccountBasePlaybookManager):
else: else:
new_secret = self.get_secret(secret_type) new_secret = self.get_secret(secret_type)
if new_secret is None: if self.record_id is None:
print(f'new_secret is None, account: {account}')
continue
asset_account_id = f'{asset.id}-{account.id}'
if asset_account_id not in self.record_map:
recorder = ChangeSecretRecord( recorder = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution, asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret, old_secret=account.secret, new_secret=new_secret,
comment=f'{account.username}@{asset.address}'
) )
records.append(recorder) records.append(recorder)
else: else:
record_id = self.record_map[asset_account_id] recorder = ChangeSecretRecord.objects.get(id=self.record_id)
try:
recorder = ChangeSecretRecord.objects.get(id=record_id)
except ChangeSecretRecord.DoesNotExist:
print(f"Record {record_id} not found")
continue
self.name_recorder_mapper[h['name']] = recorder self.name_recorder_mapper[h['name']] = recorder
@@ -164,43 +153,25 @@ class ChangeSecretManager(AccountBasePlaybookManager):
recorder = self.name_recorder_mapper.get(host) recorder = self.name_recorder_mapper.get(host)
if not recorder: if not recorder:
return return
recorder.status = ChangeSecretRecordStatusChoice.success.value recorder.status = 'success'
recorder.date_finished = timezone.now() recorder.date_finished = timezone.now()
recorder.save()
account = recorder.account account = recorder.account
if not account: if not account:
print("Account not found, deleted ?") print("Account not found, deleted ?")
return return
account.secret = recorder.new_secret account.secret = recorder.new_secret
account.date_updated = timezone.now() account.date_updated = timezone.now()
account.save(update_fields=['secret', 'date_updated'])
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try:
recorder.save()
account.save(update_fields=['secret', 'version', 'date_updated'])
break
except Exception as e:
retry_count += 1
if retry_count == max_retries:
self.on_host_error(host, str(e), result)
else:
print(f'retry {retry_count} times for {host} recorder save error: {e}')
time.sleep(1)
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
recorder = self.name_recorder_mapper.get(host) recorder = self.name_recorder_mapper.get(host)
if not recorder: if not recorder:
return return
recorder.status = ChangeSecretRecordStatusChoice.failed.value recorder.status = 'failed'
recorder.date_finished = timezone.now() recorder.date_finished = timezone.now()
recorder.error = error recorder.error = error
try: recorder.save()
recorder.save()
except Exception as e:
print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
def on_runner_failed(self, runner, e): def on_runner_failed(self, runner, e):
logger.error("Account error: ", e) logger.error("Account error: ", e)
@@ -212,59 +183,23 @@ class ChangeSecretManager(AccountBasePlaybookManager):
return False return False
return True return True
@staticmethod
def get_summary(recorders):
total, succeed, failed = 0, 0, 0
for recorder in recorders:
if recorder.status == ChangeSecretRecordStatusChoice.success.value:
succeed += 1
else:
failed += 1
total += 1
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
return summary
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
if self.secret_type and not self.check_secret(): if self.secret_type and not self.check_secret():
self.execution.status = 'success'
self.execution.date_finished = timezone.now()
self.execution.save()
return return
super().run(*args, **kwargs) super().run(*args, **kwargs)
recorders = list(self.name_recorder_mapper.values()) if self.record_id:
summary = self.get_summary(recorders)
print(summary, end='')
if self.record_map:
return return
recorders = self.name_recorder_mapper.values()
recorders = list(recorders)
self.send_recorder_mail(recorders)
failed_recorders = [ def send_recorder_mail(self, recorders):
r for r in recorders
if r.status == ChangeSecretRecordStatusChoice.failed.value
]
recipients = self.execution.recipients recipients = self.execution.recipients
if not recorders or not recipients:
return
recipients = User.objects.filter(id__in=list(recipients.keys())) recipients = User.objects.filter(id__in=list(recipients.keys()))
if not recipients:
return
if failed_recorders:
name = self.execution.snapshot.get('name')
execution_id = str(self.execution.id)
_ids = [r.id for r in failed_recorders]
asset_account_errors = ChangeSecretRecord.objects.filter(
id__in=_ids).values_list('asset__name', 'account__username', 'error')
for user in recipients:
ChangeSecretFailedMsg(name, execution_id, user, asset_account_errors).publish()
if not recorders:
return
self.send_recorder_mail(recipients, recorders, summary)
def send_recorder_mail(self, recipients, recorders, summary):
name = self.execution.snapshot['name'] name = self.execution.snapshot['name']
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx') filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
@@ -274,10 +209,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
for user in recipients: for user in recipients:
attachments = [] attachments = []
if user.secret_key: if user.secret_key:
password = user.secret_key.encode('utf8')
attachment = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.zip') attachment = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.zip')
encrypt_and_compress_zip_file(attachment, user.secret_key, [filename]) encrypt_and_compress_zip_file(attachment, password, [filename])
attachments = [attachment] attachments = [attachment]
ChangeSecretExecutionTaskMsg(name, user, summary).publish(attachments) ChangeSecretExecutionTaskMsg(name, user).publish(attachments)
os.remove(filename) os.remove(filename)
@staticmethod @staticmethod

View File

@@ -3,9 +3,6 @@
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Get info - name: Get info
@@ -15,9 +12,9 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: users filter: users
register: db_info register: db_info

View File

@@ -2,10 +2,6 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Get info - name: Get info
@@ -15,10 +11,6 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}" login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
filter: "roles" filter: "roles"
register: db_info register: db_info

View File

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

View File

@@ -1,10 +1,9 @@
- hosts: demo - hosts: demo
gather_facts: no gather_facts: no
tasks: tasks:
- name: Gather windows account - name: Gather posix account
ansible.builtin.win_shell: net user ansible.builtin.win_shell: net user
register: result register: result
ignore_errors: true
- name: Define info by set_fact - name: Define info by set_fact
ansible.builtin.set_fact: ansible.builtin.set_fact:

View File

@@ -51,22 +51,14 @@ class GatherAccountsManager(AccountBasePlaybookManager):
data = self.generate_data(asset, result) data = self.generate_data(asset, result)
self.asset_account_info[asset] = data self.asset_account_info[asset] = data
@staticmethod
def get_nested_info(data, *keys):
for key in keys:
data = data.get(key, {})
if not data:
break
return data
def on_host_success(self, host, result): def on_host_success(self, host, result):
info = self.get_nested_info(result, 'debug', 'res', 'info') info = result.get('debug', {}).get('res', {}).get('info', {})
asset = self.host_asset_mapper.get(host) asset = self.host_asset_mapper.get(host)
if asset and info: if asset and info:
result = self.filter_success_result(asset.type, info) result = self.filter_success_result(asset.type, info)
self.collect_asset_account_info(asset, result) self.collect_asset_account_info(asset, result)
else: else:
print(f'\033[31m Not found {host} info \033[0m\n') logger.error(f'Not found {host} info')
def update_or_create_accounts(self): def update_or_create_accounts(self):
for asset, data in self.asset_account_info.items(): for asset, data in self.asset_account_info.items():
@@ -95,14 +87,12 @@ class GatherAccountsManager(AccountBasePlaybookManager):
return None, None return None, None
users = User.objects.filter(id__in=recipients) users = User.objects.filter(id__in=recipients)
if not users.exists(): if not users:
return users, None return users, None
asset_ids = self.asset_username_mapper.keys() asset_ids = self.asset_username_mapper.keys()
assets = Asset.objects.filter(id__in=asset_ids)
assets = Asset.objects.filter(id__in=asset_ids).prefetch_related('accounts')
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True) gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
asset_id_map = {str(asset.id): asset for asset in assets} asset_id_map = {str(asset.id): asset for asset in assets}
asset_id_username = list(assets.values_list('id', 'accounts__username')) asset_id_username = list(assets.values_list('id', 'accounts__username'))
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username'))) asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
@@ -111,24 +101,26 @@ class GatherAccountsManager(AccountBasePlaybookManager):
for asset_id, username in asset_id_username: for asset_id, username in asset_id_username:
system_asset_username_mapper[str(asset_id)].add(username) system_asset_username_mapper[str(asset_id)].add(username)
change_info = defaultdict(dict) change_info = {}
for asset_id, usernames in self.asset_username_mapper.items(): for asset_id, usernames in self.asset_username_mapper.items():
system_usernames = system_asset_username_mapper.get(asset_id) system_usernames = system_asset_username_mapper.get(asset_id)
if not system_usernames: if not system_usernames:
continue continue
add_usernames = usernames - system_usernames add_usernames = usernames - system_usernames
remove_usernames = system_usernames - usernames remove_usernames = system_usernames - usernames
k = f'{asset_id_map[asset_id]}[{asset_id}]'
if not add_usernames and not remove_usernames: if not add_usernames and not remove_usernames:
continue continue
change_info[str(asset_id_map[asset_id])] = { change_info[k] = {
'add_usernames': add_usernames, 'add_usernames': ', '.join(add_usernames),
'remove_usernames': remove_usernames 'remove_usernames': ', '.join(remove_usernames),
} }
return users, dict(change_info) return users, change_info
@staticmethod @staticmethod
def send_email_if_need(users, change_info): def send_email_if_need(users, change_info):

View File

@@ -4,9 +4,6 @@
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
db_name: "{{ jms_asset.spec_info.db_name }}" db_name: "{{ jms_asset.spec_info.db_name }}"
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Test MySQL connection - name: Test MySQL connection
@@ -16,9 +13,9 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version
register: db_info register: db_info
@@ -33,9 +30,9 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
@@ -50,7 +47,7 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version

View File

@@ -39,4 +39,3 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_database: "{{ jms_asset.spec_info.db_name }}" login_database: "{{ jms_asset.spec_info.db_name }}"
mode: "{{ account.mode }}"

View File

@@ -2,10 +2,6 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Test PostgreSQL connection - name: Test PostgreSQL connection
@@ -15,10 +11,6 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_db: "{{ jms_asset.spec_info.db_name }}" login_db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
register: result register: result
failed_when: not result.is_available failed_when: not result.is_available
@@ -36,10 +28,6 @@
db: "{{ jms_asset.spec_info.db_name }}" db: "{{ jms_asset.spec_info.db_name }}"
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
role_attr_flags: LOGIN role_attr_flags: LOGIN
ignore_errors: true ignore_errors: true
when: result is succeeded when: result is succeeded
@@ -52,10 +40,6 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}" db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
when: when:
- result is succeeded - result is succeeded
- change_info is succeeded - change_info is succeeded

View File

@@ -14,25 +14,26 @@
- name: "Add {{ account.username }} user" - name: "Add {{ account.username }} user"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" shell: "{{ params.shell }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" home: "{{ params.home | default('/home/' + account.username, true) }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" groups: "{{ params.groups }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1 expires: -1
state: present state: present
when: user_info.failed when: user_info.failed
- name: "Set {{ account.username }} sudo setting" - name: "Add {{ account.username }} group"
ansible.builtin.lineinfile: ansible.builtin.group:
dest: /etc/sudoers name: "{{ account.username }}"
state: present state: present
regexp: "^{{ account.username }} ALL=" when: user_info.failed
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s - name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when: when:
- user_info.failed or params.modify_sudo - user_info.failed
- params.sudo - params.groups
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
@@ -42,40 +43,14 @@
ignore_errors: true ignore_errors: true
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: remove jumpserver ssh key
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys" dest: "{{ ssh_params.dest }}"
regexp: "{{ ssh_params.regexp }}" regexp: "{{ ssh_params.regexp }}"
state: absent state: absent
when: when:
- account.secret_type == "ssh_key" - account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms" - ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key" - name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key: ansible.builtin.authorized_key:
@@ -84,6 +59,17 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection
@@ -93,13 +79,12 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
become: "{{ account.become.ansible_become | default(False) }}" become: "{{ account.become.ansible_become | default(False) }}"
become_method: su become_method: su
become_user: "{{ account.become.ansible_user | default('') }}" become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" when: account.secret_type == "password"
delegate_to: localhost delegate_to: localhost
@@ -109,8 +94,7 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
delegate_to: localhost delegate_to: localhost

View File

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

View File

@@ -14,25 +14,26 @@
- name: "Add {{ account.username }} user" - name: "Add {{ account.username }} user"
ansible.builtin.user: ansible.builtin.user:
name: "{{ account.username }}" name: "{{ account.username }}"
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" shell: "{{ params.shell }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" home: "{{ params.home | default('/home/' + account.username, true) }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" groups: "{{ params.groups }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: yes
expires: -1 expires: -1
state: present state: present
when: user_info.failed when: user_info.failed
- name: "Set {{ account.username }} sudo setting" - name: "Add {{ account.username }} group"
ansible.builtin.lineinfile: ansible.builtin.group:
dest: /etc/sudoers name: "{{ account.username }}"
state: present state: present
regexp: "^{{ account.username }} ALL=" when: user_info.failed
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s - name: "Add {{ account.username }} user to group"
ansible.builtin.user:
name: "{{ account.username }}"
groups: "{{ params.groups }}"
when: when:
- user_info.failed or params.modify_sudo - user_info.failed
- params.sudo - params.groups
- name: "Change {{ account.username }} password" - name: "Change {{ account.username }} password"
ansible.builtin.user: ansible.builtin.user:
@@ -42,40 +43,14 @@
ignore_errors: true ignore_errors: true
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: remove jumpserver ssh key
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
register: home_dir
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Check if home directory exists for {{ account.username }}"
ansible.builtin.stat:
path: "{{ home_dir.stdout.strip() }}"
register: home_dir_stat
when: account.secret_type == "ssh_key"
ignore_errors: yes
- name: "Ensure {{ account.username }} home directory exists"
ansible.builtin.file:
path: "{{ home_dir.stdout.strip() }}"
state: directory
owner: "{{ account.username }}"
group: "{{ account.username }}"
mode: '0750'
when:
- account.secret_type == "ssh_key"
- home_dir_stat.stat.exists == false
ignore_errors: yes
- name: Remove jumpserver ssh key
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys" dest: "{{ ssh_params.dest }}"
regexp: "{{ ssh_params.regexp }}" regexp: "{{ ssh_params.regexp }}"
state: absent state: absent
when: when:
- account.secret_type == "ssh_key" - account.secret_type == "ssh_key"
- ssh_params.strategy == "set_jms" - ssh_params.strategy == "set_jms"
ignore_errors: yes
- name: "Change {{ account.username }} SSH key" - name: "Change {{ account.username }} SSH key"
ansible.builtin.authorized_key: ansible.builtin.authorized_key:
@@ -84,6 +59,17 @@
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: "Set {{ account.username }} sudo setting"
ansible.builtin.lineinfile:
dest: /etc/sudoers
state: present
regexp: "^{{ account.username }} ALL="
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
validate: visudo -cf %s
when:
- user_info.failed
- params.sudo
- name: Refresh connection - name: Refresh connection
ansible.builtin.meta: reset_connection ansible.builtin.meta: reset_connection
@@ -93,13 +79,12 @@
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
become: "{{ account.become.ansible_become | default(False) }}" become: "{{ account.become.ansible_become | default(False) }}"
become_method: su become_method: su
become_user: "{{ account.become.ansible_user | default('') }}" become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password" when: account.secret_type == "password"
delegate_to: localhost delegate_to: localhost
@@ -109,8 +94,7 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
delegate_to: localhost delegate_to: localhost

View File

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

View File

@@ -25,11 +25,11 @@
- name: Verify password (pyfreerdp) - name: Verify password (pyfreerdp)
rdp_ping: rdp_ping:
login_host: "{{ jms_asset.origin_address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}" login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}" login_secret_type: "{{ account.secret_type }}"
gateway_args: "{{ jms_gateway | default({}) }}" login_private_key_path: "{{ account.private_key_path }}"
when: account.secret_type == "password" when: account.secret_type == "password"
delegate_to: localhost delegate_to: localhost

View File

@@ -5,7 +5,6 @@ method: push_account
category: host category: host
type: type:
- windows - windows
priority: 49
params: params:
- name: groups - name: groups
type: str type: str

View File

@@ -2,10 +2,6 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: "Remove account" - name: "Remove account"
@@ -15,8 +11,8 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
name: "{{ account.username }}" name: "{{ account.username }}"
state: absent state: absent

View File

@@ -2,10 +2,6 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: "Remove account" - name: "Remove account"
@@ -16,8 +12,4 @@
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}" db: "{{ jms_asset.spec_info.db_name }}"
name: "{{ account.username }}" name: "{{ account.username }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
state: absent state: absent

View File

@@ -12,13 +12,11 @@
path: "{{ user_home_dir.stdout }}" path: "{{ user_home_dir.stdout }}"
register: home_dir register: home_dir
when: user_home_dir.stdout != "" when: user_home_dir.stdout != ""
ignore_errors: yes
- name: "Rename user home directory if it exists" - name: "Rename user home directory if it exists"
ansible.builtin.command: ansible.builtin.command:
cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak" cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak"
when: home_dir.stat | default(false) and user_home_dir.stdout != "" when: home_dir.stat | default(false) and user_home_dir.stdout != ""
ignore_errors: yes
- name: "Remove account" - name: "Remove account"
ansible.builtin.user: ansible.builtin.user:

View File

@@ -4,4 +4,6 @@
- name: "Remove account" - name: "Remove account"
ansible.windows.win_user: ansible.windows.win_user:
name: "{{ account.username }}" name: "{{ account.username }}"
state: absent state: absent
purge: yes
force: yes

View File

@@ -60,11 +60,8 @@ class RemoveAccountManager(AccountBasePlaybookManager):
if not tuple_asset_gather_account: if not tuple_asset_gather_account:
return return
asset, gather_account = tuple_asset_gather_account asset, gather_account = tuple_asset_gather_account
try: Account.objects.filter(
Account.objects.filter( asset_id=asset.id,
asset_id=asset.id, username=gather_account.username
username=gather_account.username ).delete()
).delete() gather_account.delete()
gather_account.delete()
except Exception as e:
print(f'\033[31m Delete account {gather_account.username} failed: {e} \033[0m\n')

View File

@@ -3,7 +3,6 @@
vars: vars:
ansible_shell_type: sh ansible_shell_type: sh
ansible_connection: local ansible_connection: local
ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Verify account (pyfreerdp) - name: Verify account (pyfreerdp)
@@ -13,3 +12,4 @@
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}" login_secret_type: "{{ account.secret_type }}"
login_private_key_path: "{{ account.private_key_path }}"

View File

@@ -6,7 +6,6 @@ type:
- windows - windows
method: verify_account method: verify_account
protocol: rdp protocol: rdp
priority: 1
i18n: i18n:
Windows rdp account verify: Windows rdp account verify:

View File

@@ -19,5 +19,3 @@
become_user: "{{ account.become.ansible_user | default('') }}" become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"

View File

@@ -7,7 +7,6 @@ type:
- all - all
method: verify_account method: verify_account
protocol: ssh protocol: ssh
priority: 50
i18n: i18n:
SSH account verify: SSH account verify:

View File

@@ -3,9 +3,6 @@
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Verify account - name: Verify account
@@ -15,7 +12,7 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
check_hostname: "{{ check_ssl if check_ssl else omit }}" check_hostname: "{{ check_ssl if check_ssl else omit }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}" ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}" client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}" client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
filter: version filter: version

View File

@@ -2,10 +2,6 @@
gather_facts: no gather_facts: no
vars: vars:
ansible_python_interpreter: /opt/py3/bin/python ansible_python_interpreter: /opt/py3/bin/python
check_ssl: "{{ jms_asset.spec_info.use_ssl }}"
ca_cert: "{{ jms_asset.secret_info.ca_cert | default('') }}"
ssl_cert: "{{ jms_asset.secret_info.client_cert | default('') }}"
ssl_key: "{{ jms_asset.secret_info.client_key | default('') }}"
tasks: tasks:
- name: Verify account - name: Verify account
@@ -15,9 +11,5 @@
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
db: "{{ jms_asset.spec_info.db_name }}" db: "{{ jms_asset.spec_info.db_name }}"
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
ssl_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
ssl_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
register: result register: result
failed_when: not result.is_available failed_when: not result.is_available

View File

@@ -51,9 +51,6 @@ class VerifyAccountManager(AccountBasePlaybookManager):
h['name'] += '(' + account.username + ')' h['name'] += '(' + account.username + ')'
self.host_account_mapper[h['name']] = account self.host_account_mapper[h['name']] = account
secret = account.secret secret = account.secret
if secret is None:
print(f'account {account.name} secret is None')
continue
private_key_path = None private_key_path = None
if account.secret_type == SecretType.SSH_KEY: if account.secret_type == SecretType.SSH_KEY:
@@ -65,7 +62,7 @@ class VerifyAccountManager(AccountBasePlaybookManager):
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,
'secret_type': account.secret_type, 'secret_type': account.secret_type,
'secret': account.escape_jinja2_syntax(secret), 'secret': account.escape_jinja2_syntax(secret),
'private_key_path': private_key_path, 'private_key_path': private_key_path,
'become': account.get_ansible_become_auth(), 'become': account.get_ansible_become_auth(),
} }
@@ -76,14 +73,8 @@ class VerifyAccountManager(AccountBasePlaybookManager):
def on_host_success(self, host, result): def on_host_success(self, host, result):
account = self.host_account_mapper.get(host) account = self.host_account_mapper.get(host)
try: account.set_connectivity(Connectivity.OK)
account.set_connectivity(Connectivity.OK)
except Exception as e:
print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
account = self.host_account_mapper.get(host) account = self.host_account_mapper.get(host)
try: account.set_connectivity(Connectivity.ERR)
account.set_connectivity(Connectivity.ERR)
except Exception as e:
print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')

View File

@@ -1,5 +1,3 @@
from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
from assets.automations.ping_gateway.manager import PingGatewayManager from assets.automations.ping_gateway.manager import PingGatewayManager
from common.utils import get_logger from common.utils import get_logger
@@ -15,7 +13,7 @@ class VerifyGatewayAccountManager(PingGatewayManager):
@staticmethod @staticmethod
def before_runner_start(): def before_runner_start():
logger.info(_(">>> Start executing the task to test gateway account connectivity")) logger.info(">>> 开始执行测试网关账号可连接性任务")
def get_accounts(self, gateway): def get_accounts(self, gateway):
account_ids = self.execution.snapshot['accounts'] account_ids = self.execution.snapshot['accounts']

View File

@@ -1,21 +1,19 @@
from importlib import import_module from importlib import import_module
from django.utils.functional import LazyObject, empty from django.utils.functional import LazyObject
from common.utils import get_logger from common.utils import get_logger
from ..const import VaultTypeChoices from ..const import VaultTypeChoices
__all__ = ['vault_client', 'get_vault_client', 'refresh_vault_client'] __all__ = ['vault_client', 'get_vault_client']
logger = get_logger(__file__) logger = get_logger(__file__)
def get_vault_client(raise_exception=False, **kwargs): def get_vault_client(raise_exception=False, **kwargs):
tp = kwargs.get('VAULT_BACKEND') if kwargs.get('VAULT_ENABLED') else VaultTypeChoices.local enabled = kwargs.get('VAULT_ENABLED')
tp = 'hcp' if enabled else 'local'
# TODO: Temporary processing, subsequent deletion
tp = VaultTypeChoices.local if tp == VaultTypeChoices.azure else tp
try: try:
module_path = f'apps.accounts.backends.{tp}.main' module_path = f'apps.accounts.backends.{tp}.main'
client = import_module(module_path).Vault(**kwargs) client = import_module(module_path).Vault(**kwargs)
@@ -41,7 +39,3 @@ class VaultClient(LazyObject):
""" 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """ """ 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """
vault_client = VaultClient() vault_client = VaultClient()
def refresh_vault_client():
vault_client._wrapped = empty

View File

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

View File

@@ -1,70 +0,0 @@
import sys
from abc import ABC
from common.db.utils import Encryptor
from common.utils import lazyproperty
current_module = sys.modules[__name__]
__all__ = ['build_entry']
class BaseEntry(ABC):
def __init__(self, instance):
self.instance = instance
@lazyproperty
def full_path(self):
return self.path_spec
@property
def path_spec(self):
raise NotImplementedError
def to_internal_data(self):
secret = getattr(self.instance, '_secret', None)
if secret is not None:
secret = Encryptor(secret).encrypt()
return secret
@staticmethod
def to_external_data(secret):
if secret is not None:
secret = Encryptor(secret).decrypt()
return secret
class AccountEntry(BaseEntry):
@property
def path_spec(self):
# 长度 0-127
account_id = str(self.instance.id)[:18]
path = f'assets-{self.instance.asset_id}-accounts-{account_id}'
return path
class AccountTemplateEntry(BaseEntry):
@property
def path_spec(self):
path = f'account-templates-{self.instance.id}'
return path
class HistoricalAccountEntry(BaseEntry):
@property
def path_spec(self):
path = f'accounts-{self.instance.instance.id}-histories-{self.instance.history_id}'
return path
def build_entry(instance) -> BaseEntry:
class_name = instance.__class__.__name__
entry_class_name = f'{class_name}Entry'
entry_class = getattr(current_module, entry_class_name, None)
if not entry_class:
raise Exception(f'Entry class {entry_class_name} is not found')
return entry_class(instance)

View File

@@ -1,57 +0,0 @@
from common.db.utils import get_logger
from .entries import build_entry
from .service import AZUREVaultClient
from ..base import BaseVault
from ...const import VaultTypeChoices
logger = get_logger(__name__)
__all__ = ['Vault']
class Vault(BaseVault):
type = VaultTypeChoices.azure
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client = AZUREVaultClient(
vault_url=kwargs.get('VAULT_AZURE_HOST'),
tenant_id=kwargs.get('VAULT_AZURE_TENANT_ID'),
client_id=kwargs.get('VAULT_AZURE_CLIENT_ID'),
client_secret=kwargs.get('VAULT_AZURE_CLIENT_SECRET')
)
def is_active(self):
return self.client.is_active()
def _get(self, instance):
entry = build_entry(instance)
secret = self.client.get(name=entry.full_path)
secret = entry.to_external_data(secret)
return secret
def _create(self, instance):
entry = build_entry(instance)
secret = entry.to_internal_data()
self.client.create(name=entry.full_path, secret=secret)
def _update(self, instance):
entry = build_entry(instance)
secret = entry.to_internal_data()
self.client.update(name=entry.full_path, secret=secret)
def _delete(self, instance):
entry = build_entry(instance)
self.client.delete(name=entry.full_path)
def _clean_db_secret(self, instance):
instance.is_sync_metadata = False
instance.mark_secret_save_to_vault()
def _save_metadata(self, instance, metadata):
try:
entry = build_entry(instance)
self.client.update_metadata(name=entry.full_path, metadata=metadata)
except Exception as e:
logger.error(f'save metadata error: {e}')

View File

@@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
#
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
from common.utils import get_logger
logger = get_logger(__name__)
__all__ = ['AZUREVaultClient']
class AZUREVaultClient(object):
def __init__(self, vault_url, tenant_id, client_id, client_secret):
authentication_endpoint = 'https://login.microsoftonline.com/' \
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
credentials = ClientSecretCredential(
client_id=client_id, client_secret=client_secret, tenant_id=tenant_id, authority=authentication_endpoint
)
self.client = SecretClient(vault_url=vault_url, credential=credentials)
def is_active(self):
try:
self.client.set_secret('jumpserver', '666')
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(str(e))
return False, f'Vault is not reachable: {e}'
else:
return True, ''
def get(self, name, version=None):
try:
secret = self.client.get_secret(name, version)
return secret.value
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(f'get: {name} {str(e)}')
return ''
def create(self, name, secret):
if not secret:
secret = ''
self.client.set_secret(name, secret)
def update(self, name, secret):
if not secret:
secret = ''
self.client.set_secret(name, secret)
def delete(self, name):
self.client.begin_delete_secret(name)
def update_metadata(self, name, metadata: dict):
try:
self.client.update_secret_properties(name, tags=metadata)
except (ResourceNotFoundError, ClientAuthenticationError) as e:
logger.error(f'update_metadata: {name} {str(e)}')

View File

@@ -10,11 +10,6 @@ class BaseVault(ABC):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.enabled = kwargs.get('VAULT_ENABLED') self.enabled = kwargs.get('VAULT_ENABLED')
@property
@abstractmethod
def type(self):
raise NotImplementedError
def get(self, instance): def get(self, instance):
""" 返回 secret 值 """ """ 返回 secret 值 """
return self._get(instance) return self._get(instance)
@@ -25,6 +20,9 @@ class BaseVault(ABC):
self._clean_db_secret(instance) self._clean_db_secret(instance)
self.save_metadata(instance) self.save_metadata(instance)
if instance.is_sync_metadata:
self.save_metadata(instance)
def update(self, instance): def update(self, instance):
if not instance.secret_has_save_to_vault: if not instance.secret_has_save_to_vault:
self._update(instance) self._update(instance)

View File

@@ -3,16 +3,12 @@ from .entries import build_entry
from .service import VaultKVClient from .service import VaultKVClient
from ..base import BaseVault from ..base import BaseVault
from ...const import VaultTypeChoices __all__ = ['Vault']
logger = get_logger(__name__) logger = get_logger(__name__)
__all__ = ['Vault']
class Vault(BaseVault): class Vault(BaseVault):
type = VaultTypeChoices.hcp
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.client = VaultKVClient( self.client = VaultKVClient(

View File

@@ -1,6 +1,5 @@
from common.utils import get_logger from common.utils import get_logger
from ..base import BaseVault from ..base import BaseVault
from ...const import VaultTypeChoices
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -8,7 +7,6 @@ __all__ = ['Vault']
class Vault(BaseVault): class Vault(BaseVault):
type = VaultTypeChoices.local
def is_active(self): def is_active(self):
return True, '' return True, ''

View File

@@ -15,7 +15,6 @@ class AliasAccount(TextChoices):
INPUT = '@INPUT', _('Manual input') INPUT = '@INPUT', _('Manual input')
USER = '@USER', _('Dynamic user') USER = '@USER', _('Dynamic user')
ANON = '@ANON', _('Anonymous account') ANON = '@ANON', _('Anonymous account')
SPEC = '@SPEC', _('Specified account')
@classmethod @classmethod
def virtual_choices(cls): def virtual_choices(cls):

View File

@@ -16,7 +16,7 @@ DEFAULT_PASSWORD_RULES = {
__all__ = [ __all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity', 'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice', 'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice', 'PushAccountActionChoice', 'AccountBackupType'
] ]
@@ -49,9 +49,9 @@ class SecretStrategy(models.TextChoices):
class SSHKeyStrategy(models.TextChoices): class SSHKeyStrategy(models.TextChoices):
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
set = 'set', _('Empty and append SSH KEY')
add = 'add', _('Append SSH KEY') add = 'add', _('Append SSH KEY')
set = 'set', _('Empty and append SSH KEY')
set_jms = 'set_jms', _('Replace (Replace only keys pushed by JumpServer) ')
class TriggerChoice(models.TextChoices, TreeChoices): class TriggerChoice(models.TextChoices, TreeChoices):
@@ -103,9 +103,3 @@ class AccountBackupType(models.TextChoices):
email = 'email', _('Email') email = 'email', _('Email')
# 目前只支持sftp方式 # 目前只支持sftp方式
object_storage = 'object_storage', _('SFTP') object_storage = 'object_storage', _('SFTP')
class ChangeSecretRecordStatusChoice(models.TextChoices):
failed = 'failed', _('Failed')
success = 'success', _('Success')
pending = 'pending', _('Pending')

View File

@@ -7,4 +7,3 @@ __all__ = ['VaultTypeChoices']
class VaultTypeChoices(models.TextChoices): class VaultTypeChoices(models.TextChoices):
local = 'local', _('Database') local = 'local', _('Database')
hcp = 'hcp', _('HCP Vault') hcp = 'hcp', _('HCP Vault')
azure = 'azure', _('Azure Key Vault')

View File

@@ -5,7 +5,7 @@ from django_filters import rest_framework as drf_filters
from assets.models import Node from assets.models import Node
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from .models import Account, GatheredAccount, ChangeSecretRecord from .models import Account, GatheredAccount
class AccountFilterSet(BaseFilterSet): class AccountFilterSet(BaseFilterSet):
@@ -52,7 +52,6 @@ class AccountFilterSet(BaseFilterSet):
class GatheredAccountFilterSet(BaseFilterSet): class GatheredAccountFilterSet(BaseFilterSet):
node_id = drf_filters.CharFilter(method='filter_nodes') node_id = drf_filters.CharFilter(method='filter_nodes')
asset_id = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact') asset_id = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact')
asset_name = drf_filters.CharFilter(field_name='asset__name', lookup_expr='icontains')
@staticmethod @staticmethod
def filter_nodes(queryset, name, value): def filter_nodes(queryset, name, value):
@@ -61,13 +60,3 @@ class GatheredAccountFilterSet(BaseFilterSet):
class Meta: class Meta:
model = GatheredAccount model = GatheredAccount
fields = ['id', 'username'] fields = ['id', 'username']
class ChangeSecretRecordFilterSet(BaseFilterSet):
asset_name = drf_filters.CharFilter(field_name='asset__name', lookup_expr='icontains')
account_username = drf_filters.CharFilter(field_name='account__username', lookup_expr='icontains')
execution_id = drf_filters.CharFilter(field_name='execution_id', lookup_expr='exact')
class Meta:
model = ChangeSecretRecord
fields = ['id', 'status', 'asset_id', 'execution']

View File

@@ -1,8 +1,10 @@
# Generated by Django 4.1.13 on 2024-05-09 03:16 # Generated by Django 3.2.14 on 2022-12-28 07:29
import uuid import uuid
import django.db.models.deletion
import simple_history.models import simple_history.models
from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import common.db.encoder import common.db.encoder
@@ -13,6 +15,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('assets', '0098_auto_20220430_2126'),
] ]
operations = [ operations = [
@@ -28,81 +32,60 @@ class Migration(migrations.Migration):
('org_id', ('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('connectivity', ('connectivity',
models.CharField(choices=[('-', 'Unknown'), ('ok', 'OK'), ('err', 'Error')], default='-', models.CharField(choices=[('-', 'Unknown'), ('ok', 'Ok'), ('err', 'Error')], default='-',
max_length=16, verbose_name='Connectivity')), max_length=16, verbose_name='Connectivity')),
('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')),
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('secret_type', models.CharField( ('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
verbose_name='Secret type')), verbose_name='Secret type')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('privileged', models.BooleanField(default=False, verbose_name='Privileged')), ('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('version', models.IntegerField(default=0, verbose_name='Version')), ('version', models.IntegerField(default=0, verbose_name='Version')),
('source', models.CharField(default='local', max_length=30, verbose_name='Source')), ('source', models.CharField(default='local', max_length=30, verbose_name='Source')),
('source_id', models.CharField(blank=True, max_length=128, null=True, verbose_name='Source ID')), ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts',
to='assets.asset', verbose_name='Asset')),
('su_from',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to',
to='accounts.account', verbose_name='Su from')),
], ],
options={ options={
'verbose_name': 'Account', 'verbose_name': 'Account',
'permissions': [('view_accountsecret', 'Can view asset account secret'), 'permissions': [('view_accountsecret', 'Can view asset account secret'),
('view_historyaccount', 'Can view asset history account'), ('view_historyaccount', 'Can view asset history account'),
('view_historyaccountsecret', 'Can view asset history account secret'), ('view_historyaccountsecret', 'Can view asset history account secret')],
('verify_account', 'Can verify account'), ('push_account', 'Can push account'), 'unique_together': {('username', 'asset', 'secret_type'), ('name', 'asset')},
('remove_account', 'Can remove account')],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='AccountBackupAutomation', name='HistoricalAccount',
fields=[ fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), ('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), ('secret_type', models.CharField(
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
('comment', models.TextField(blank=True, default='', verbose_name='Comment')), verbose_name='Secret type')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('org_id', ('version', models.IntegerField(default=0, verbose_name='Version')),
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), ('history_id', models.AutoField(primary_key=True, serialize=False)),
('name', models.CharField(max_length=128, verbose_name='Name')), ('history_date', models.DateTimeField(db_index=True)),
('is_periodic', models.BooleanField(default=False, verbose_name='Periodic run')), ('history_change_reason', models.CharField(max_length=100, null=True)),
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Interval')), ('history_type',
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Crontab')), models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('types', models.JSONField(default=list)), ('history_user',
('backup_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+',
models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email', to=settings.AUTH_USER_MODEL)),
max_length=128, verbose_name='Backup type')),
('is_password_divided_by_email', models.BooleanField(default=True, verbose_name='Password divided')),
('is_password_divided_by_obj_storage',
models.BooleanField(default=True, verbose_name='Password divided')),
('zip_encrypt_password', common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True,
verbose_name='Zip encrypt password')),
], ],
options={ options={
'verbose_name': 'Account backup plan', 'verbose_name': 'historical Account',
'ordering': ['name'], 'verbose_name_plural': 'historical Accounts',
}, 'ordering': ('-history_date', '-history_id'),
), 'get_latest_by': ('history_date', 'history_id'),
migrations.CreateModel(
name='AccountBackupExecution',
fields=[
('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')),
('snapshot',
models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True,
verbose_name='Account backup snapshot')),
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')],
default='manual', max_length=128, verbose_name='Trigger mode')),
('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')),
('is_success', models.BooleanField(default=False, verbose_name='Is success')),
],
options={
'verbose_name': 'Account backup execution',
'ordering': ('-date_start',),
}, },
bases=(simple_history.models.HistoricalChanges, models.Model),
), ),
migrations.CreateModel( migrations.CreateModel(
name='AccountTemplate', name='AccountTemplate',
@@ -115,108 +98,21 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', ('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy',
models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')],
default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('name', models.CharField(max_length=128, verbose_name='Name')), ('name', models.CharField(max_length=128, verbose_name='Name')),
('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')),
('secret_type', models.CharField( ('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16, ('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
verbose_name='Secret type')), verbose_name='Secret type')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('privileged', models.BooleanField(default=False, verbose_name='Privileged')), ('privileged', models.BooleanField(default=False, verbose_name='Privileged')),
('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')),
('auto_push', models.BooleanField(default=False, verbose_name='Auto push')),
('push_params', models.JSONField(default=dict, verbose_name='Push params')),
], ],
options={ options={
'verbose_name': 'Account template', 'verbose_name': 'Account template',
'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret')], 'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'),
('change_accounttemplatesecret', 'Can change asset account template secret')],
'unique_together': {('name', 'org_id')},
}, },
), ),
migrations.CreateModel(
name='ChangeSecretRecord',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
('status', models.CharField(default='pending', max_length=16, verbose_name='Status')),
('error', models.TextField(blank=True, null=True, verbose_name='Error')),
],
options={
'verbose_name': 'Change secret record',
'ordering': ('-date_created',),
},
),
migrations.CreateModel(
name='GatheredAccount',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('present', models.BooleanField(default=True, verbose_name='Present')),
('date_last_login', models.DateTimeField(null=True, verbose_name='Date login')),
('username', models.CharField(blank=True, db_index=True, max_length=32, verbose_name='Username')),
('address_last_login', models.CharField(default='', max_length=39, verbose_name='Address login')),
],
options={
'verbose_name': 'Gather asset accounts',
'ordering': ['asset'],
},
),
migrations.CreateModel(
name='HistoricalAccount',
fields=[
('id', models.UUIDField(db_index=True, default=uuid.uuid4)),
('_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_type', models.CharField(
choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'),
('token', 'Token'), ('api_key', 'API key')], default='password', max_length=16,
verbose_name='Secret type')),
('version', models.IntegerField(default=0, verbose_name='Version')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type',
models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
],
options={
'verbose_name': 'historical Account',
'verbose_name_plural': 'historical Accounts',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='VirtualAccount',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('alias', models.CharField(
choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'),
('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')),
('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')),
],
options={'verbose_name': 'Virtual account'},
),
] ]

View File

@@ -1,103 +1,44 @@
# Generated by Django 4.1.13 on 2024-05-09 03:16 # Generated by Django 3.2.14 on 2022-12-28 10:39
import django.db.models.deletion
from django.db import migrations, models
import common.db.encoder
import common.db.fields 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): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
('assets', '0001_initial'), ('assets', '0106_auto_20221228_1838'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0001_initial'), ('accounts', '0001_initial'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='AccountBaseAutomation', name='AccountBackupAutomation',
fields=[ fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')),
('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')),
('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')),
('types', models.JSONField(default=list)),
('recipients', models.ManyToManyField(blank=True, related_name='recipient_escape_route_plans',
to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
], ],
options={ options={
'verbose_name': 'Account automation task', 'verbose_name': 'Account backup plan',
'proxy': True, 'ordering': ['name'],
'indexes': [], 'unique_together': {('name', 'org_id')},
'constraints': [],
}, },
bases=('assets.baseautomation',), )
),
migrations.CreateModel(
name='AutomationExecution',
fields=[
],
options={
'verbose_name': 'Automation execution',
'verbose_name_plural': 'Automation executions',
'permissions': [('view_changesecretexecution', 'Can view change secret execution'), ('add_changesecretexecution', 'Can add change secret execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('add_gatheraccountsexecution', 'Can add gather accounts execution'), ('view_pushaccountexecution', 'Can view push account execution'), ('add_pushaccountexecution', 'Can add push account execution')],
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('assets.automationexecution',),
),
migrations.CreateModel(
name='ChangeSecretAutomation',
fields=[
('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, 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')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
],
options={
'verbose_name': 'Change secret automation',
},
bases=('accounts.accountbaseautomation', models.Model),
),
migrations.CreateModel(
name='GatherAccountsAutomation',
fields=[
('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')),
('is_sync_account', models.BooleanField(blank=True, default=False, verbose_name='Is sync account')),
],
options={
'verbose_name': 'Gather account automation',
},
bases=('accounts.accountbaseautomation',),
),
migrations.CreateModel(
name='PushAccountAutomation',
fields=[
('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, 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')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(choices=[('set_jms', 'Replace (Replace only keys pushed by JumpServer) '), ('set', 'Empty and append SSH KEY'), ('add', 'Append SSH KEY')], default='set_jms', max_length=16, verbose_name='SSH key change strategy')),
('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('action', models.CharField(max_length=16, verbose_name='Action')),
],
options={
'verbose_name': 'Push asset account',
},
bases=('accounts.accountbaseautomation', models.Model),
),
migrations.CreateModel(
name='VerifyAccountAutomation',
fields=[
('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')),
],
options={
'verbose_name': 'Verify asset account',
},
bases=('accounts.accountbaseautomation',),
),
migrations.AlterUniqueTogether(
name='virtualaccount',
unique_together={('alias', 'org_id')},
),
] ]

View File

@@ -1,116 +1,198 @@
# Generated by Django 4.1.13 on 2024-05-09 03:16 # 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.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
import common.db.encoder
import common.db.fields
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True
dependencies = [ dependencies = [
('assets', '0001_initial'), ('assets', '0107_automation'),
('terminal', '0001_initial'),
('accounts', '0002_auto_20220616_0021'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0002_auto_20220616_0021'),
] ]
operations = [ operations = [
migrations.AddField( migrations.CreateModel(
model_name='historicalaccount', name='AccountBaseAutomation',
name='history_user', fields=[
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), ],
options={
'verbose_name': 'Account automation task',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('assets.baseautomation',),
), ),
migrations.AddField( migrations.CreateModel(
model_name='gatheredaccount', name='AutomationExecution',
name='asset', fields=[
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.asset', verbose_name='Asset'), ],
options={
'verbose_name': 'Automation execution',
'verbose_name_plural': 'Automation executions',
'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
('add_changesecretexecution', 'Can add change secret execution'),
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
('add_gatheraccountsexecution', 'Can add gather accounts execution')],
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('assets.automationexecution',),
), ),
migrations.AddField( migrations.CreateModel(
model_name='changesecretrecord', name='PushAccountAutomation',
name='account', fields=[
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account'), ('baseautomation_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
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')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
('random_one', 'All assets use the same random password'),
('random_all',
'All assets use different random password')],
default='specific', max_length=16,
verbose_name='Secret strategy')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(
choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'),
('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16,
verbose_name='SSH key change strategy')),
('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')),
('username', models.CharField(max_length=128, verbose_name='Username')),
('action', models.CharField(max_length=16, verbose_name='Action')),
],
options={
'verbose_name': 'Push asset account',
},
bases=('accounts.accountbaseautomation', models.Model),
), ),
migrations.AddField(
model_name='changesecretrecord', migrations.CreateModel(
name='asset', name='GatherAccountsAutomation',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset'), fields=[
('baseautomation_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='assets.baseautomation')),
],
options={
'verbose_name': 'Gather asset accounts',
},
bases=('accounts.accountbaseautomation',),
), ),
migrations.AddField( migrations.CreateModel(
model_name='changesecretrecord', name='VerifyAccountAutomation',
name='execution', fields=[
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.automationexecution'), ('baseautomation_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='assets.baseautomation')),
],
options={
'verbose_name': 'Verify asset account',
},
bases=('accounts.accountbaseautomation',),
), ),
migrations.AddField( migrations.CreateModel(
model_name='accounttemplate', name='ChangeSecretRecord',
name='platforms', fields=[
field=models.ManyToManyField(blank=True, related_name='account_templates', to='assets.platform', verbose_name='Platforms'), ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')),
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
('status', models.CharField(default='pending', max_length=16, verbose_name='Status')),
('error', models.TextField(blank=True, null=True, verbose_name='Error')),
('account',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')),
('asset', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset')),
('execution',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.automationexecution')),
],
options={
'verbose_name': 'Change secret record',
},
), ),
migrations.AddField( migrations.CreateModel(
model_name='accounttemplate', name='AccountBackupExecution',
name='su_from', fields=[
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='accounts.accounttemplate', verbose_name='Su from'), ('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')),
('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')),
('plan_snapshot',
models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True,
verbose_name='Account backup snapshot')),
('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')],
default='manual', max_length=128, verbose_name='Trigger mode')),
('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')),
('is_success', models.BooleanField(default=False, verbose_name='Is success')),
('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution',
to='accounts.accountbackupautomation', verbose_name='Account backup plan')),
],
options={
'verbose_name': 'Account backup execution',
'ordering': ('-date_start',),
},
), ),
migrations.AddField( migrations.CreateModel(
model_name='accountbackupexecution', name='ChangeSecretAutomation',
name='plan', fields=[
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution', to='accounts.accountbackupautomation', verbose_name='Account backup plan'), ('baseautomation_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
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')),
('secret_strategy', models.CharField(choices=[('specific', 'Specific password'),
('random_one', 'All assets use the same random password'),
('random_all',
'All assets use different random password')],
default='specific', max_length=16,
verbose_name='Secret strategy')),
('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')),
('password_rules', models.JSONField(default=dict, verbose_name='Password rules')),
('ssh_key_change_strategy', models.CharField(
choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'),
('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16,
verbose_name='SSH key change strategy')),
('recipients',
models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient')),
],
options={
'verbose_name': 'Change secret automation',
},
bases=('accounts.accountbaseautomation', models.Model),
), ),
migrations.AddField( migrations.AlterModelOptions(
model_name='accountbackupautomation', name='automationexecution',
name='obj_recipients_part_one', options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_one_plans', to='terminal.replaystorage', verbose_name='Object storage recipient part one'), ('add_changesecretexecution', 'Can add change secret execution'),
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
('view_pushaccountexecution', 'Can view push account execution'),
('add_pushaccountexecution', 'Can add push account execution')],
'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'},
), ),
migrations.AddField( migrations.AlterModelOptions(
model_name='accountbackupautomation', name='changesecretrecord',
name='obj_recipients_part_two', options={'ordering': ('-date_started',), 'verbose_name': 'Change secret record'},
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_two_plans', to='terminal.replaystorage', verbose_name='Object storage recipient part two'),
),
migrations.AddField(
model_name='accountbackupautomation',
name='recipients_part_one',
field=models.ManyToManyField(blank=True, related_name='recipient_part_one_plans', to=settings.AUTH_USER_MODEL, verbose_name='Recipient part one'),
),
migrations.AddField(
model_name='accountbackupautomation',
name='recipients_part_two',
field=models.ManyToManyField(blank=True, related_name='recipient_part_two_plans', to=settings.AUTH_USER_MODEL, verbose_name='Recipient part two'),
),
migrations.AddField(
model_name='account',
name='asset',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to='assets.asset', verbose_name='Asset'),
),
migrations.AddField(
model_name='account',
name='su_from',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', to='accounts.account', verbose_name='Su from'),
),
migrations.AlterUniqueTogether(
name='gatheredaccount',
unique_together={('username', 'asset')},
),
migrations.AddField(
model_name='gatheraccountsautomation',
name='recipients',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient'),
),
migrations.AddField(
model_name='changesecretautomation',
name='recipients',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient'),
),
migrations.AlterUniqueTogether(
name='accounttemplate',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='accountbackupautomation',
unique_together={('name', 'org_id')},
),
migrations.AlterUniqueTogether(
name='account',
unique_together={('name', 'asset'), ('username', 'asset', 'secret_type')},
), ),
] ]

View File

@@ -1,30 +0,0 @@
# Generated by Django 4.1.13 on 2024-08-26 09:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('assets', '0005_myasset'),
('accounts', '0003_automation'),
]
operations = [
migrations.AlterField(
model_name='changesecretrecord',
name='account',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.account'),
),
migrations.AlterField(
model_name='changesecretrecord',
name='asset',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset'),
),
migrations.AlterField(
model_name='changesecretrecord',
name='execution',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.automationexecution'),
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.16 on 2023-01-06 07:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0003_automation'),
]
operations = [
migrations.AlterField(
model_name='changesecretautomation',
name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
),
migrations.AlterField(
model_name='pushaccountautomation',
name='secret_strategy',
field=models.CharField(choices=[('specific', 'Specific secret'), ('random', 'Random generate')], default='specific', max_length=16, verbose_name='Secret strategy'),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.2.16 on 2023-01-10 06:45
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0004_auto_20230106_1507'),
]
operations = [
migrations.AlterModelOptions(
name='changesecretrecord',
options={'ordering': ('-date_created',), 'verbose_name': 'Change secret record'},
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.16 on 2023-02-07 04:41
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('assets', '0108_alter_platform_charset'),
('accounts', '0005_alter_changesecretrecord_options'),
]
operations = [
migrations.CreateModel(
name='GatheredAccount',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('present', models.BooleanField(default=True, verbose_name='Present')),
('date_last_login', models.DateTimeField(null=True, verbose_name='Date last login')),
('username', models.CharField(blank=True, db_index=True, max_length=32, verbose_name='Username')),
('address_last_login', models.CharField(default='', max_length=39, verbose_name='Address last login')),
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.asset', verbose_name='Asset')),
],
options={
'verbose_name': 'Gather account',
'ordering': ['asset'],
'unique_together': {('username', 'asset')},
},
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.16 on 2023-02-16 11:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_gatheredaccount'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'permissions': [
('view_accountsecret', 'Can view asset account secret'),
('view_historyaccount', 'Can view asset history account'),
('view_historyaccountsecret', 'Can view asset history account secret'),
('verify_account', 'Can verify account'),
('push_account', 'Can push account'),
('remove_account', 'Can remove account'),
], 'verbose_name': 'Account'},
),
]

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