mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-15 16:42:34 +00:00
Compare commits
19 Commits
revert-162
...
v3.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd50a1faff | ||
|
|
86dab4fc6e | ||
|
|
a85a80a945 | ||
|
|
349edc10aa | ||
|
|
44918e3cb5 | ||
|
|
9a2f6c0d70 | ||
|
|
934969a8f1 | ||
|
|
57162c1628 | ||
|
|
32fb36867f | ||
|
|
158b589028 | ||
|
|
d64277353c | ||
|
|
bff6f397ce | ||
|
|
0ad461a804 | ||
|
|
a1dcef0ba0 | ||
|
|
dbb1ee3a75 | ||
|
|
d6bd207a17 | ||
|
|
e69ba27ff4 | ||
|
|
adbe7c07c6 | ||
|
|
d1eacf53d4 |
@@ -1,4 +1,5 @@
|
||||
.git
|
||||
logs/*
|
||||
data/*
|
||||
.github
|
||||
tmp/*
|
||||
@@ -7,7 +8,4 @@ celerybeat.pid
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
apps/xpack/.git
|
||||
.history/
|
||||
.idea
|
||||
.venv/
|
||||
.env
|
||||
|
||||
|
||||
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -0,0 +1,4 @@
|
||||
*.mmdb filter=lfs diff=lfs merge=lfs -text
|
||||
*.mo filter=lfs diff=lfs merge=lfs -text
|
||||
*.ipdb filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/----.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/----.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: 需求建议
|
||||
about: 提出针对本项目的想法和建议
|
||||
title: "[Feature] "
|
||||
labels: 类型:需求
|
||||
assignees: ibuler
|
||||
|
||||
---
|
||||
|
||||
**请描述您的需求或者改进建议.**
|
||||
72
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
72
.github/ISSUE_TEMPLATE/1_bug_report.yml
vendored
@@ -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.
|
||||
60
.github/ISSUE_TEMPLATE/2_question.yml
vendored
60
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@@ -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.
|
||||
56
.github/ISSUE_TEMPLATE/3_feature_request.yml
vendored
56
.github/ISSUE_TEMPLATE/3_feature_request.yml
vendored
@@ -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.
|
||||
72
.github/ISSUE_TEMPLATE/4_bug_report_cn.yml
vendored
72
.github/ISSUE_TEMPLATE/4_bug_report_cn.yml
vendored
@@ -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: 如果你已经尝试解决问题,请在此列出你尝试过的解决方案。
|
||||
61
.github/ISSUE_TEMPLATE/5_question_cn.yml
vendored
61
.github/ISSUE_TEMPLATE/5_question_cn.yml
vendored
@@ -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: 在这里添加关于问题的任何其他背景信息。
|
||||
|
||||
56
.github/ISSUE_TEMPLATE/6_feature_request_cn.yml
vendored
56
.github/ISSUE_TEMPLATE/6_feature_request_cn.yml
vendored
@@ -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: 在这里添加关于问题的任何其他背景信息。
|
||||
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug---.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug 提交
|
||||
about: 提交产品缺陷帮助我们更好的改进
|
||||
title: "[Bug] "
|
||||
labels: 类型:bug
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
**JumpServer 版本(v1.5.9以下不再支持)**
|
||||
|
||||
|
||||
**浏览器版本**
|
||||
|
||||
|
||||
**Bug 描述**
|
||||
|
||||
|
||||
**Bug 重现步骤(有截图更好)**
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: 问题咨询
|
||||
about: 提出针对本项目安装部署、使用及其他方面的相关问题
|
||||
title: "[Question] "
|
||||
labels: 类型:提问
|
||||
assignees: wojiushixiaobai
|
||||
|
||||
---
|
||||
|
||||
**请描述您的问题.**
|
||||
14
.github/dependabot.yml.bak
vendored
14
.github/dependabot.yml.bak
vendored
@@ -1,14 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "uv"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "09:30"
|
||||
timezone: "Asia/Shanghai"
|
||||
target-branch: dev
|
||||
groups:
|
||||
python-dependencies:
|
||||
patterns:
|
||||
- "*"
|
||||
74
.github/workflows/build-base-image.yml
vendored
74
.github/workflows/build-base-image.yml
vendored
@@ -1,74 +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-22.04
|
||||
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
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||
|
||||
- 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 }}
|
||||
46
.github/workflows/build-python-image.yml
vendored
46
.github/workflows/build-python-image.yml
vendored
@@ -1,46 +0,0 @@
|
||||
name: Build and Push Python Base Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Tag to build'
|
||||
required: true
|
||||
default: '3.11-slim-bullseye-v1'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-22.04
|
||||
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
|
||||
with:
|
||||
image: tonistiigi/binfmt:qemu-v7.0.0-28
|
||||
|
||||
- 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 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-python
|
||||
tags: jumpserver/core-base:python-${{ inputs.tag }}
|
||||
|
||||
31
.github/workflows/check-compilemessages.yml
vendored
31
.github/workflows/check-compilemessages.yml
vendored
@@ -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
|
||||
123
.github/workflows/cleanup-branches.yml
vendored
123
.github/workflows/cleanup-branches.yml
vendored
@@ -1,123 +0,0 @@
|
||||
name: Cleanup PR Branches
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 每天凌晨2点运行
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch:
|
||||
# 允许手动触发
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run mode (default: true)'
|
||||
required: false
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
cleanup-branches:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # 获取所有分支和提交历史
|
||||
|
||||
- name: Setup Git
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
|
||||
- name: Get dry run setting
|
||||
id: dry-run
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "dry_run=${{ github.event.inputs.dry_run }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "dry_run=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Cleanup branches
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DRY_RUN: ${{ steps.dry-run.outputs.dry_run }}
|
||||
run: |
|
||||
echo "Starting branch cleanup..."
|
||||
echo "Dry run mode: $DRY_RUN"
|
||||
|
||||
# 获取所有本地分支
|
||||
git fetch --all --prune
|
||||
|
||||
# 获取以 pr 或 repr 开头的分支
|
||||
branches=$(git branch -r | grep -E 'origin/(pr|repr)' | sed 's/origin\///' | grep -v 'HEAD')
|
||||
|
||||
echo "Found branches matching pattern:"
|
||||
echo "$branches"
|
||||
|
||||
deleted_count=0
|
||||
skipped_count=0
|
||||
|
||||
for branch in $branches; do
|
||||
echo ""
|
||||
echo "Processing branch: $branch"
|
||||
|
||||
# 检查分支是否有未合并的PR
|
||||
pr_info=$(gh pr list --head "$branch" --state open --json number,title,state 2>/dev/null)
|
||||
|
||||
if [ $? -eq 0 ] && [ "$pr_info" != "[]" ]; then
|
||||
echo " ⚠️ Branch has open PR(s), skipping deletion"
|
||||
echo " PR info: $pr_info"
|
||||
skipped_count=$((skipped_count + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
# 检查分支是否有已合并的PR(可选:如果PR已合并也可以删除)
|
||||
merged_pr_info=$(gh pr list --head "$branch" --state merged --json number,title,state 2>/dev/null)
|
||||
|
||||
if [ $? -eq 0 ] && [ "$merged_pr_info" != "[]" ]; then
|
||||
echo " ✅ Branch has merged PR(s), safe to delete"
|
||||
echo " Merged PR info: $merged_pr_info"
|
||||
else
|
||||
echo " ℹ️ No PRs found for this branch"
|
||||
fi
|
||||
|
||||
# 执行删除操作
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo " 🔍 [DRY RUN] Would delete branch: $branch"
|
||||
deleted_count=$((deleted_count + 1))
|
||||
else
|
||||
echo " 🗑️ Deleting branch: $branch"
|
||||
|
||||
# 删除远程分支
|
||||
if git push origin --delete "$branch" 2>/dev/null; then
|
||||
echo " ✅ Successfully deleted remote branch: $branch"
|
||||
deleted_count=$((deleted_count + 1))
|
||||
else
|
||||
echo " ❌ Failed to delete remote branch: $branch"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== Cleanup Summary ==="
|
||||
echo "Branches processed: $(echo "$branches" | wc -l)"
|
||||
echo "Branches deleted: $deleted_count"
|
||||
echo "Branches skipped: $skipped_count"
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo ""
|
||||
echo "🔍 This was a DRY RUN - no branches were actually deleted"
|
||||
echo "To perform actual deletion, run this workflow manually with dry_run=false"
|
||||
fi
|
||||
|
||||
- name: Create summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Branch Cleanup Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Workflow:** ${{ github.workflow }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Run ID:** ${{ github.run_id }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Dry Run:** ${{ steps.dry-run.outputs.dry_run }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Triggered by:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Check the logs above for detailed information about processed branches." >> $GITHUB_STEP_SUMMARY
|
||||
24
.github/workflows/discord-release.yml
vendored
24
.github/workflows/discord-release.yml
vendored
@@ -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"
|
||||
24
.github/workflows/docs-release.yml
vendored
24
.github/workflows/docs-release.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: Auto update docs changelog
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
update_docs_changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.release.tag_name, 'v4.')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Update docs changelog
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
DOCS_TOKEN: ${{ secrets.DOCS_TOKEN }}
|
||||
run: |
|
||||
git config --global user.name 'BaiJiangJie'
|
||||
git config --global user.email 'jiangjie.bai@fit2cloud.com'
|
||||
|
||||
git clone https://$DOCS_TOKEN@github.com/jumpservice/documentation.git
|
||||
cd documentation/utils
|
||||
bash update_changelog.sh
|
||||
4
.github/workflows/issue-close-require.yml
vendored
4
.github/workflows/issue-close-require.yml
vendored
@@ -12,9 +12,7 @@ jobs:
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'close-issues'
|
||||
labels: '⏳ Pending feedback'
|
||||
labels: '状态:待反馈'
|
||||
inactive-day: 30
|
||||
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。
|
||||
|
||||
2
.github/workflows/issue-close.yml
vendored
2
.github/workflows/issue-close.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
labels: '🔔 Pending processing,⏳ Pending feedback'
|
||||
labels: '状态:待处理,状态:待反馈'
|
||||
39
.github/workflows/issue-comment.yml
vendored
39
.github/workflows/issue-comment.yml
vendored
@@ -13,53 +13,26 @@ jobs:
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '🔔 Pending processing'
|
||||
labels: '状态:待处理'
|
||||
|
||||
- name: Remove require reply label
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
labels: '⏳ Pending feedback'
|
||||
labels: '状态:待反馈'
|
||||
|
||||
add-label-if-is-member:
|
||||
add-label-if-not-author:
|
||||
runs-on: ubuntu-latest
|
||||
if: (github.event.issue.user.id != github.event.comment.user.id) && !github.event.issue.pull_request && (github.event.issue.state == 'open')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Get Organization name
|
||||
id: org_name
|
||||
run: echo "data=$(echo '${{ github.repository }}' | cut -d '/' -f 1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Organization public members
|
||||
uses: octokit/request-action@v2.x
|
||||
id: members
|
||||
with:
|
||||
route: GET /orgs/${{ steps.org_name.outputs.data }}/public_members
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Process public members data
|
||||
# 将 members 中的数据转化为 login 字段的拼接字符串
|
||||
id: member_names
|
||||
run: echo "data=$(echo '${{ steps.members.outputs.data }}' | jq '[.[].login] | join(",")')" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
- run: "echo members: '${{ steps.members.outputs.data }}'"
|
||||
- run: "echo member names: '${{ steps.member_names.outputs.data }}'"
|
||||
- run: "echo comment user: '${{ github.event.comment.user.login }}'"
|
||||
- run: "echo contains? : '${{ contains(steps.member_names.outputs.data, github.event.comment.user.login) }}'"
|
||||
|
||||
- name: Add require replay label
|
||||
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '⏳ Pending feedback'
|
||||
labels: '状态:待反馈'
|
||||
|
||||
- name: Remove require handle label
|
||||
if: contains(steps.member_names.outputs.data, github.event.comment.user.login)
|
||||
uses: actions-cool/issues-helper@v2
|
||||
with:
|
||||
actions: 'remove-labels'
|
||||
labels: '🔔 Pending processing'
|
||||
labels: '状态:待处理'
|
||||
|
||||
2
.github/workflows/issue-open.yml
vendored
2
.github/workflows/issue-open.yml
vendored
@@ -13,4 +13,4 @@ jobs:
|
||||
if: ${{ !github.event.issue.pull_request }}
|
||||
with:
|
||||
actions: 'add-labels'
|
||||
labels: '🔔 Pending processing'
|
||||
labels: '状态:待处理'
|
||||
35
.github/workflows/jms-build-test.yml
vendored
Normal file
35
.github/workflows/jms-build-test.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
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:test
|
||||
file: Dockerfile
|
||||
build-args: |
|
||||
APT_MIRROR=http://deb.debian.org
|
||||
PIP_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 }}
|
||||
63
.github/workflows/jms-build-test.yml.disabled
vendored
63
.github/workflows/jms-build-test.yml.disabled
vendored
@@ -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
|
||||
@@ -10,4 +10,3 @@ jobs:
|
||||
- uses: jumpserver/action-generic-handler@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
I18N_TOKEN: ${{ secrets.I18N_TOKEN }}
|
||||
|
||||
28
.github/workflows/llm-code-review.yml.bak
vendored
28
.github/workflows/llm-code-review.yml.bak
vendored
@@ -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"
|
||||
11
.github/workflows/sync-gitee.yml
vendored
11
.github/workflows/sync-gitee.yml
vendored
@@ -1,9 +1,11 @@
|
||||
name: 🔀 Sync mirror to Gitee
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 每天凌晨3点运行
|
||||
- cron: '0 3 * * *'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
create:
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
@@ -12,9 +14,10 @@ jobs:
|
||||
steps:
|
||||
- name: mirror
|
||||
continue-on-error: true
|
||||
if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag')
|
||||
uses: wearerequired/git-mirror-action@v1
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}
|
||||
with:
|
||||
source-repo: 'git@github.com:jumpserver/jumpserver.git'
|
||||
destination-repo: 'git@gitee.com:fit2cloud-feizhiyun/JumpServer.git'
|
||||
destination-repo: 'git@gitee.com:jumpserver/jumpserver.git'
|
||||
|
||||
45
.github/workflows/translate-readme.yml
vendored
45
.github/workflows/translate-readme.yml
vendored
@@ -1,45 +0,0 @@
|
||||
name: Translate README
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_readme:
|
||||
description: "Source README"
|
||||
required: false
|
||||
default: "./readmes/README.en.md"
|
||||
target_langs:
|
||||
description: "Target Languages"
|
||||
required: false
|
||||
default: "zh-hans,zh-hant,ja,pt-br,es,ru"
|
||||
gen_dir_path:
|
||||
description: "Generate Dir Name"
|
||||
required: false
|
||||
default: "readmes/"
|
||||
push_branch:
|
||||
description: "Push Branch"
|
||||
required: false
|
||||
default: "pr@dev@translate_readme"
|
||||
prompt:
|
||||
description: "AI Translate Prompt"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
gpt_mode:
|
||||
description: "GPT Mode"
|
||||
required: false
|
||||
default: "gpt-4o-mini"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Auto Translate
|
||||
uses: jumpserver-dev/action-translate-readme@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }}
|
||||
GPT_MODE: ${{ github.event.inputs.gpt_mode }}
|
||||
SOURCE_README: ${{ github.event.inputs.source_readme }}
|
||||
TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }}
|
||||
PUSH_BRANCH: ${{ github.event.inputs.push_branch }}
|
||||
GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }}
|
||||
PROMPT: ${{ github.event.inputs.prompt }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -35,6 +35,7 @@ celerybeat-schedule.db
|
||||
docs/_build/
|
||||
xpack
|
||||
xpack.bak
|
||||
logs/*
|
||||
### Vagrant ###
|
||||
.vagrant/
|
||||
release/*
|
||||
@@ -42,10 +43,3 @@ releashe
|
||||
/apps/script.py
|
||||
data/*
|
||||
test.py
|
||||
.history/
|
||||
.test/
|
||||
*.mo
|
||||
apps.iml
|
||||
*.db
|
||||
*.mmdb
|
||||
*.ipdb
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
[settings]
|
||||
line_length=120
|
||||
known_first_party=common,users,assets,perms,authentication,jumpserver,notification,ops,orgs,rbac,settings,terminal,tickets
|
||||
|
||||
|
||||
11
.prettierrc
11
.prettierrc
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
[MESSAGES CONTROL]
|
||||
disable=missing-module-docstring,missing-class-docstring,missing-function-docstring,too-many-ancestors
|
||||
@@ -1,10 +1,5 @@
|
||||
# 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
|
||||
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.
|
||||
|
||||
|
||||
111
Dockerfile
111
Dockerfile
@@ -1,69 +1,104 @@
|
||||
FROM jumpserver/core-base:20251029_031929 AS stage-build
|
||||
FROM python:3.9-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& \
|
||||
if [ -n "${VERSION}" ]; then \
|
||||
sed -i "s@VERSION = .*@VERSION = '${VERSION}'@g" apps/jumpserver/const.py; \
|
||||
fi
|
||||
FROM python:3.9-slim
|
||||
ARG TARGETARCH
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
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 jumpserver/core-base:python-3.11-slim-bullseye-v1
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libx11-dev"
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
cron \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
locales \
|
||||
openssh-client \
|
||||
procps \
|
||||
sshpass \
|
||||
nmap \
|
||||
bubblewrap"
|
||||
telnet \
|
||||
unzip \
|
||||
vim \
|
||||
git \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://deb.debian.org
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
|
||||
RUN set -ex \
|
||||
&& sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
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 > /dev/null \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& 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
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
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/
|
||||
ARG DOWNLOAD_URL=https://download.jumpserver.org
|
||||
|
||||
RUN mkdir -p /opt/oracle/ \
|
||||
&& cd /opt/oracle/ \
|
||||
&& wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
|
||||
&& unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip \
|
||||
&& sh -c "echo /opt/oracle/instantclient_19_10 > /etc/ld.so.conf.d/oracle-instantclient.conf" \
|
||||
&& ldconfig \
|
||||
&& rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip
|
||||
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
set -ex \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install --upgrade setuptools wheel \
|
||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& rm -rf /tmp/build
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
CMD ["start", "all"]
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1
|
||||
ARG TARGETARCH
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
|
||||
# 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.5
|
||||
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
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
PATH=/opt/py3/bin:$PATH
|
||||
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
--mount=type=bind,source=requirements/clean_site_packages.sh,target=clean_site_packages.sh \
|
||||
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \
|
||||
--mount=type=bind,source=requirements/static_files.sh,target=utils/static_files.sh \
|
||||
set -ex \
|
||||
&& uv venv \
|
||||
&& uv pip install -i${PIP_MIRROR} -r pyproject.toml \
|
||||
&& ln -sf $(pwd)/.venv /opt/py3 \
|
||||
&& bash utils/static_files.sh \
|
||||
&& bash clean_site_packages.sh
|
||||
@@ -1,32 +1,10 @@
|
||||
ARG VERSION=dev
|
||||
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} AS build-xpack
|
||||
FROM jumpserver/core:${VERSION}-ce
|
||||
|
||||
ARG VERSION
|
||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||
FROM jumpserver/core:${VERSION}
|
||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||
|
||||
ARG TOOLS=" \
|
||||
g++ \
|
||||
curl \
|
||||
iputils-ping \
|
||||
netcat-openbsd \
|
||||
nmap \
|
||||
telnet \
|
||||
vim \
|
||||
postgresql-client-13 \
|
||||
wget \
|
||||
poppler-utils"
|
||||
|
||||
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
|
||||
|
||||
RUN set -ex \
|
||||
&& uv pip install -i${PIP_MIRROR} --group xpack \
|
||||
&& playwright install chromium --with-deps --only-shell
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
set -ex \
|
||||
&& pip install -r requirements/requirements_xpack.txt
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
FROM python:3.11-slim-bullseye
|
||||
ARG TARGETARCH
|
||||
# Install APT dependencies
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get -y --no-install-recommends upgrade; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# upgrade pip and setuptools
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel
|
||||
96
Dockerfile.loong64
Normal file
96
Dockerfile.loong64
Normal file
@@ -0,0 +1,96 @@
|
||||
FROM python:3.9-slim as stage-build
|
||||
ARG TARGETARCH
|
||||
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
ADD . .
|
||||
RUN cd utils && bash -ixeu build.sh
|
||||
|
||||
FROM python:3.9-slim
|
||||
ARG TARGETARCH
|
||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
||||
|
||||
ARG BUILD_DEPENDENCIES=" \
|
||||
g++ \
|
||||
make \
|
||||
pkg-config"
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
freetds-dev \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
libjpeg-dev \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
libssl-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
libaio-dev"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
curl \
|
||||
default-libmysqlclient-dev \
|
||||
default-mysql-client \
|
||||
locales \
|
||||
openssh-client \
|
||||
procps \
|
||||
sshpass \
|
||||
telnet \
|
||||
unzip \
|
||||
vim \
|
||||
git \
|
||||
wget"
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
set -ex \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||
&& mkdir -p /root/.ssh/ \
|
||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \
|
||||
&& echo "set mouse-=a" > ~/.vimrc \
|
||||
&& echo "no" | dpkg-reconfigure dash \
|
||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /tmp/build
|
||||
COPY ./requirements ./requirements
|
||||
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
set -ex \
|
||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install --upgrade setuptools wheel \
|
||||
&& pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl \
|
||||
&& pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl \
|
||||
&& pip install $(grep 'PyNaCl' requirements/requirements.txt) \
|
||||
&& GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \
|
||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
||||
&& pip install -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN echo > /opt/jumpserver/config.yml \
|
||||
&& rm -rf /tmp/build
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
ENV LANG=zh_CN.UTF-8
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
188
README.md
188
README.md
@@ -1,125 +1,115 @@
|
||||
<div align="center">
|
||||
<a name="readme-top"></a>
|
||||
<a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
|
||||
|
||||
## An open-source PAM platform (Bastion Host)
|
||||
<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>
|
||||
|
||||
[![][license-shield]][license-link]
|
||||
[![][docs-shield]][docs-link]
|
||||
[![][deepwiki-shield]][deepwiki-link]
|
||||
[![][discord-shield]][discord-link]
|
||||
[![][docker-shield]][docker-link]
|
||||
[![][github-release-shield]][github-release-link]
|
||||
[![][github-stars-shield]][github-stars-link]
|
||||
<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>
|
||||
|
||||
[English](/README.md) · [中文(简体)](/readmes/README.zh-hans.md) · [中文(繁體)](/readmes/README.zh-hant.md) · [日本語](/readmes/README.ja.md) · [Português (Brasil)](/readmes/README.pt-br.md) · [Español](/readmes/README.es.md) · [Русский](/readmes/README.ru.md) · [한국어](/readmes/README.ko.md)
|
||||
--------------------------
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||
|
||||
## What is JumpServer?
|
||||
## 产品特色
|
||||
|
||||
JumpServer is an open-source Privileged Access Management (PAM) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
|
||||
- **开源**: 零门槛,线上快速获取和安装;
|
||||
- **无插件**: 仅需浏览器,极致的 Web Terminal 使用体验;
|
||||
- **分布式**: 支持分布式部署和横向扩展,轻松支持大规模并发访问;
|
||||
- **多云支持**: 一套系统,同时管理不同云上面的资产;
|
||||
- **多租户**: 一套系统,多个子公司或部门同时使用;
|
||||
- **云端存储**: 审计录像云端存储,永不丢失;
|
||||
- **多应用支持**: 全面支持各类资产,包括服务器、数据库、Windows RemoteApp、Kubernetes 等;
|
||||
- **安全可靠**: 被广泛使用、验证和信赖,连续 9 年的持续研发投入和产品更新升级。
|
||||
|
||||
## UI 展示
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://www.jumpserver.com/images/jumpserver-arch-light.png">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://www.jumpserver.com/images/jumpserver-arch-dark.png">
|
||||
<img src="https://github.com/user-attachments/assets/dd612f3d-c958-4f84-b164-f31b75454d7f" alt="Theme-based Image">
|
||||
</picture>
|
||||

|
||||
|
||||
## 在线体验
|
||||
|
||||
## Quickstart
|
||||
- 环境地址:<https://demo.jumpserver.org/>
|
||||
|
||||
Prepare a clean Linux Server ( 64 bit, >= 4c8g )
|
||||
| :warning: 注意 |
|
||||
|:-----------------------------|
|
||||
| 该环境仅作体验目的使用,我们会定时清理、重置数据! |
|
||||
| 请勿修改体验环境用户的密码! |
|
||||
| 请勿在环境中添加业务生产环境地址、用户名密码等敏感信息! |
|
||||
|
||||
```sh
|
||||
curl -sSL https://github.com/jumpserver/jumpserver/releases/latest/download/quick_start.sh | bash
|
||||
```
|
||||
## 快速开始
|
||||
|
||||
Access JumpServer in your browser at `http://your-jumpserver-ip/`
|
||||
- Username: `admin`
|
||||
- Password: `ChangeMe`
|
||||
- [极速安装](https://docs.jumpserver.org/zh/master/install/setup_by_fast/)
|
||||
- [手动安装](https://github.com/jumpserver/installer)
|
||||
- [产品文档](https://docs.jumpserver.org)
|
||||
- [知识库](https://kb.fit2cloud.com/categories/jumpserver)
|
||||
|
||||
[](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart")
|
||||
## 案例研究
|
||||
|
||||
## Screenshots
|
||||
<table style="border-collapse: collapse; border: 1px solid black;">
|
||||
<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/user-attachments/assets/7c1f81af-37e8-4f07-8ac9-182895e1062e" alt="JumpServer PAM" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/user-attachments/assets/eaa41f66-8cc8-4f01-a001-0d258501f1c9" alt="JumpServer RBAC" /></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>
|
||||
- [腾讯海外游戏:基于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)
|
||||
|
||||
## Components
|
||||
## 社区
|
||||
|
||||
JumpServer consists of multiple key components, which collectively form the functional framework of JumpServer, providing users with comprehensive capabilities for operations management and security control.
|
||||
如果您在使用过程中有任何疑问或对建议,欢迎提交 [GitHub Issue](https://github.com/jumpserver/jumpserver/issues/new/choose)
|
||||
或加入到我们的社区当中进行进一步交流沟通。
|
||||
|
||||
| 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 |
|
||||
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer Remote Application Connector (Windows) |
|
||||
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
|
||||
| [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
|
||||
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector |
|
||||
| [Nec](https://github.com/jumpserver/nec) | <img alt="Nec" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE VNC Proxy Connector |
|
||||
| [Facelive](https://github.com/jumpserver/facelive) | <img alt="Facelive" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Facial Recognition |
|
||||
### 微信交流群
|
||||
|
||||
## Third-party projects
|
||||
- [jumpserver-grafana-dashboard](https://github.com/acerrah/jumpserver-grafana-dashboard) JumpServer with grafana dashboard
|
||||
<img src="https://download.jumpserver.org/images/wecom-group.jpeg" alt="微信群二维码" width="200"/>
|
||||
|
||||
## Contributing
|
||||
### 参与贡献
|
||||
|
||||
Welcome to submit PR to contribute. Please refer to [CONTRIBUTING.md][contributing-link] for guidelines.
|
||||
欢迎提交 PR 参与贡献。感谢以下贡献者,他们让 JumpServer 变的越来越好。
|
||||
|
||||
## License
|
||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
Copyright (c) 2014-2025 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
|
||||
| 项目 | 状态 | 描述 |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
|
||||
| [Lina](https://github.com/jumpserver/lina) | <a href="https://github.com/jumpserver/lina/releases"><img alt="Lina release" src="https://img.shields.io/github/release/jumpserver/lina.svg" /></a> | JumpServer Web UI 项目 |
|
||||
| [Luna](https://github.com/jumpserver/luna) | <a href="https://github.com/jumpserver/luna/releases"><img alt="Luna release" src="https://img.shields.io/github/release/jumpserver/luna.svg" /></a> | JumpServer Web Terminal 项目 |
|
||||
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer 字符协议 Connector 项目,替代原来 Python 版本的 [Coco](https://github.com/jumpserver/coco) |
|
||||
| [Lion](https://github.com/jumpserver/lion-release) | <a href="https://github.com/jumpserver/lion-release/releases"><img alt="Lion release" src="https://img.shields.io/github/release/jumpserver/lion-release.svg" /></a> | JumpServer 图形协议 Connector 项目,依赖 [Apache Guacamole](https://guacamole.apache.org/) |
|
||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||
|
||||
## 安全说明
|
||||
|
||||
JumpServer是一款安全产品,请参考 [基本安全建议](https://docs.jumpserver.org/zh/master/install/install_security/)
|
||||
进行安装部署。如果您发现安全相关问题,请直接联系我们:
|
||||
|
||||
- 邮箱:support@fit2cloud.com
|
||||
- 电话:400-052-0755
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Apache Guacamole](https://guacamole.apache.org/): Web 页面连接 RDP、SSH、VNC 等协议资产,JumpServer Lion 组件使用到该项目;
|
||||
- [OmniDB](https://omnidb.org/): Web 页面连接使用数据库,JumpServer Web 数据库组件使用到该项目。
|
||||
|
||||
## License & Copyright
|
||||
|
||||
Copyright (c) 2014-2023 飞致云 FIT2CLOUD, All rights reserved.
|
||||
|
||||
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.
|
||||
|
||||
<!-- JumpServer official link -->
|
||||
[docs-link]: https://jumpserver.com/docs
|
||||
[discord-link]: https://discord.com/invite/W6vYXmAQG2
|
||||
[deepwiki-link]: https://deepwiki.com/jumpserver/jumpserver/
|
||||
[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-->
|
||||
[docs-shield]: https://img.shields.io/badge/documentation-148F76
|
||||
[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
|
||||
[deepwiki-shield]: https://img.shields.io/badge/deepwiki-devin?color=blue
|
||||
[discord-shield]: https://img.shields.io/discord/1194233267294052363?style=flat&logo=discord&logoColor=%23f5f5f5&labelColor=%235462eb&color=%235462eb
|
||||
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
94
README_EN.md
Normal 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 Connector,rely 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.
|
||||
@@ -5,7 +5,8 @@ JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议
|
||||
如果你发现安全问题,请直接联系我们,我们携手让世界更好:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@lxware.hk
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
|
||||
# Security Policy
|
||||
@@ -15,5 +16,6 @@ JumpServer is a security product, The installation and development should follow
|
||||
All security bugs should be reported to the contact as below:
|
||||
|
||||
- ibuler@fit2cloud.com
|
||||
- support@lxware.hk
|
||||
- support@fit2cloud.com
|
||||
- 400-052-0755
|
||||
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
from .account import *
|
||||
from .application import *
|
||||
from .pam_dashboard import *
|
||||
from .task import *
|
||||
from .template import *
|
||||
from .virtual import *
|
||||
|
||||
@@ -1,63 +1,35 @@
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.generics import ListAPIView, CreateAPIView
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_200_OK
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import ChangeSecretRecordStatusChoice
|
||||
from accounts.filters import AccountFilterSet, NodeFilterBackend
|
||||
from accounts.mixins import AccountRecordViewLogMixin
|
||||
from accounts.models import Account, ChangeSecretRecord
|
||||
from assets.const.gpt import create_or_update_chatx_resources
|
||||
from accounts.filters import AccountFilterSet
|
||||
from accounts.models import Account
|
||||
from assets.models import Asset, Node
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.api.mixin import ExtraFilterFieldsMixin
|
||||
from common.drf.filters import AttrRulesFilterBackend
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import lazyproperty, get_logger
|
||||
from common.permissions import UserConfirmation, ConfirmType
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = [
|
||||
'AccountViewSet', 'AccountSecretsViewSet',
|
||||
'AccountHistoriesSecretAPI', 'AssetAccountBulkCreateApi',
|
||||
'AccountHistoriesSecretAPI'
|
||||
]
|
||||
|
||||
|
||||
class AccountViewSet(OrgBulkModelViewSet):
|
||||
model = Account
|
||||
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
|
||||
extra_filter_backends = [AttrRulesFilterBackend, NodeFilterBackend]
|
||||
search_fields = ('username', 'asset__address', 'name')
|
||||
filterset_class = AccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountSerializer,
|
||||
'retrieve': serializers.AccountDetailSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'partial_update': ['accounts.change_account'],
|
||||
'su_from_accounts': 'accounts.view_account',
|
||||
'clear_secret': 'accounts.change_account',
|
||||
'move_to_assets': 'accounts.delete_account',
|
||||
'copy_to_assets': 'accounts.add_account',
|
||||
'chat': 'accounts.view_account',
|
||||
'username_suggestions': 'accounts.view_account',
|
||||
}
|
||||
export_as_zip = True
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
asset_id = self.request.query_params.get('asset') or self.request.query_params.get('asset_id')
|
||||
if not asset_id:
|
||||
return queryset
|
||||
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
queryset = asset.all_accounts.all()
|
||||
return queryset
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-accounts')
|
||||
def su_from_accounts(self, request, *args, **kwargs):
|
||||
@@ -71,36 +43,25 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
accounts = asset.accounts.all()
|
||||
else:
|
||||
accounts = Account.objects.none()
|
||||
accounts = []
|
||||
accounts = self.filter_queryset(accounts)
|
||||
serializer = serializers.AccountSerializer(accounts, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(
|
||||
methods=['post'], detail=False, url_path='username-suggestions',
|
||||
permission_classes=[IsValidUser]
|
||||
)
|
||||
@action(methods=['get'], detail=False, url_path='username-suggestions')
|
||||
def username_suggestions(self, request, *args, **kwargs):
|
||||
raw_asset_ids = request.data.get('assets', [])
|
||||
node_ids = request.data.get('nodes', [])
|
||||
username = request.data.get('username', '')
|
||||
|
||||
asset_ids = set(raw_asset_ids)
|
||||
|
||||
if node_ids:
|
||||
nodes = Node.objects.filter(id__in=node_ids)
|
||||
node_asset_qs = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
asset_ids |= {str(u) for u in node_asset_qs}
|
||||
asset_ids = request.query_params.get('assets')
|
||||
node_keys = request.query_params.get('keys')
|
||||
username = request.query_params.get('username')
|
||||
|
||||
assets = Asset.objects.all()
|
||||
if asset_ids:
|
||||
through = Asset.directory_services.through
|
||||
ds_qs = through.objects.filter(asset_id__in=asset_ids) \
|
||||
.values_list('directoryservice_id', flat=True)
|
||||
asset_ids |= {str(u) for u in ds_qs}
|
||||
accounts = Account.objects.filter(asset_id__in=list(asset_ids))
|
||||
else:
|
||||
accounts = Account.objects.all()
|
||||
assets = assets.filter(id__in=asset_ids.split(','))
|
||||
if node_keys:
|
||||
patten = Node.get_node_all_children_key_pattern(node_keys.split(','))
|
||||
assets = assets.filter(nodes__key__regex=patten)
|
||||
|
||||
accounts = Account.objects.filter(asset__in=assets)
|
||||
if username:
|
||||
accounts = accounts.filter(username__icontains=username)
|
||||
usernames = list(accounts.values_list('username', flat=True).distinct()[:10])
|
||||
@@ -110,60 +71,8 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
usernames = common + others
|
||||
return Response(data=usernames)
|
||||
|
||||
@action(methods=['patch'], detail=False, url_path='clear-secret')
|
||||
def clear_secret(self, request, *args, **kwargs):
|
||||
account_ids = request.data.get('account_ids', [])
|
||||
self.model.objects.filter(id__in=account_ids).update(secret=None)
|
||||
return Response(status=HTTP_200_OK)
|
||||
|
||||
def _copy_or_move_to_assets(self, request, move=False):
|
||||
account = self.get_object()
|
||||
asset_ids = request.data.get('assets', [])
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
field_names = [
|
||||
'name', 'username', 'secret_type', 'secret',
|
||||
'privileged', 'is_active', 'source', 'source_id', 'comment'
|
||||
]
|
||||
account_data = {field: getattr(account, field) for field in field_names}
|
||||
|
||||
creation_results = {}
|
||||
success_count = 0
|
||||
|
||||
for asset in assets:
|
||||
account_data['asset'] = asset
|
||||
creation_results[asset] = {'state': 'created'}
|
||||
try:
|
||||
with transaction.atomic():
|
||||
self.model.objects.create(**account_data)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
logger.debug(f'{"Move" if move else "Copy"} to assets error: {e}')
|
||||
creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'}
|
||||
|
||||
results = [{'asset': str(asset), **res} for asset, res in creation_results.items()]
|
||||
|
||||
if move and success_count > 0:
|
||||
account.delete()
|
||||
|
||||
return Response(results, status=HTTP_200_OK)
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='move-to-assets')
|
||||
def move_to_assets(self, request, *args, **kwargs):
|
||||
return self._copy_or_move_to_assets(request, move=True)
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='copy-to-assets')
|
||||
def copy_to_assets(self, request, *args, **kwargs):
|
||||
return self._copy_or_move_to_assets(request, move=False)
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='chat')
|
||||
def chat(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
__, account = create_or_update_chatx_resources()
|
||||
serializer = self.get_serializer(account)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||
class AccountSecretsViewSet(RecordViewLogMixin, AccountViewSet):
|
||||
"""
|
||||
因为可能要导出所有账号,所以单独建立了一个 viewset
|
||||
"""
|
||||
@@ -178,21 +87,7 @@ class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||
}
|
||||
|
||||
|
||||
class AssetAccountBulkCreateApi(CreateAPIView):
|
||||
serializer_class = serializers.AssetAccountBulkSerializer
|
||||
rbac_perms = {
|
||||
'POST': 'accounts.add_account',
|
||||
}
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.create(serializer.validated_data)
|
||||
serializer = serializers.AssetAccountBulkSerializerResultSerializer(data, many=True)
|
||||
return Response(data=serializer.data, status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixin, ListAPIView):
|
||||
class AccountHistoriesSecretAPI(RecordViewLogMixin, ListAPIView):
|
||||
model = Account.history.model
|
||||
serializer_class = serializers.AccountHistorySerializer
|
||||
http_method_names = ['get', 'options']
|
||||
@@ -200,60 +95,18 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
|
||||
rbac_perms = {
|
||||
'GET': 'accounts.view_accountsecret',
|
||||
}
|
||||
queryset = Account.history.model.objects.none()
|
||||
|
||||
@lazyproperty
|
||||
def account(self) -> Account:
|
||||
return get_object_or_404(Account, pk=self.kwargs.get('pk'))
|
||||
|
||||
def get_object(self):
|
||||
return self.account
|
||||
|
||||
@lazyproperty
|
||||
def latest_history(self):
|
||||
return self.account.history.first()
|
||||
|
||||
@property
|
||||
def latest_change_secret_record(self) -> ChangeSecretRecord:
|
||||
return self.account.changesecretrecords.filter(
|
||||
status=ChangeSecretRecordStatusChoice.pending
|
||||
).order_by('-date_created').first()
|
||||
|
||||
@staticmethod
|
||||
def filter_spm_queryset(resource_ids, queryset):
|
||||
return queryset.filter(history_id__in=resource_ids)
|
||||
return get_object_or_404(Account, pk=self.kwargs.get('pk'))
|
||||
|
||||
def get_queryset(self):
|
||||
account = self.account
|
||||
account = self.get_object()
|
||||
histories = account.history.all()
|
||||
latest_history = self.latest_history
|
||||
if not latest_history:
|
||||
last_history = account.history.first()
|
||||
if not last_history:
|
||||
return histories
|
||||
if account.secret != latest_history.secret:
|
||||
return histories
|
||||
if account.secret_type != latest_history.secret_type:
|
||||
return histories
|
||||
histories = histories.exclude(history_id=latest_history.history_id)
|
||||
|
||||
if account.secret == last_history.secret \
|
||||
and account.secret_type == last_history.secret_type:
|
||||
histories = histories.exclude(history_id=last_history.history_id)
|
||||
return histories
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
queryset = list(queryset)
|
||||
latest_history = self.latest_history
|
||||
if not latest_history:
|
||||
return queryset
|
||||
|
||||
latest_change_secret_record = self.latest_change_secret_record
|
||||
if not latest_change_secret_record:
|
||||
return queryset
|
||||
|
||||
if latest_change_secret_record.date_created > latest_history.history_date:
|
||||
temp_history = self.model(
|
||||
secret=latest_change_secret_record.new_secret,
|
||||
secret_type=self.account.secret_type,
|
||||
version=latest_history.version,
|
||||
history_date=latest_change_secret_record.date_created,
|
||||
)
|
||||
queryset = [temp_history] + queryset
|
||||
|
||||
return queryset
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _, get_language
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.models import IntegrationApplication
|
||||
from audits.models import IntegrationApplicationLog
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.exceptions import JMSException
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import get_request_ip
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
|
||||
class IntegrationApplicationViewSet(OrgBulkModelViewSet):
|
||||
model = IntegrationApplication
|
||||
search_fields = ('name', 'comment')
|
||||
serializer_classes = {
|
||||
'default': serializers.IntegrationApplicationSerializer,
|
||||
'get_account_secret': serializers.IntegrationAccountSecretSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
'get_once_secret': 'accounts.change_integrationapplication',
|
||||
'get_account_secret': 'accounts.view_integrationapplication',
|
||||
'get_sdks_info': 'accounts.view_integrationapplication'
|
||||
}
|
||||
|
||||
def read_file(self, path):
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r', encoding='utf-8') as file:
|
||||
return file.read()
|
||||
return ''
|
||||
|
||||
@action(
|
||||
['GET'], detail=False, url_path='sdks',
|
||||
)
|
||||
def get_sdks_info(self, request, *args, **kwargs):
|
||||
code_suffix_mapper = {
|
||||
'python': 'py',
|
||||
'java': 'java',
|
||||
'go': 'go',
|
||||
'node': 'js',
|
||||
'curl': 'sh',
|
||||
}
|
||||
sdk_language = request.query_params.get('language', 'python')
|
||||
sdk_path = os.path.join(settings.APPS_DIR, 'accounts', 'demos', sdk_language)
|
||||
readme_path = os.path.join(sdk_path, f'README.{get_language()}.md')
|
||||
demo_path = os.path.join(sdk_path, f'demo.{code_suffix_mapper[sdk_language]}')
|
||||
|
||||
readme_content = self.read_file(readme_path)
|
||||
demo_content = self.read_file(demo_path)
|
||||
|
||||
return Response(data={'readme': readme_content, 'code': demo_content})
|
||||
|
||||
@action(
|
||||
['GET'], detail=True, url_path='secret',
|
||||
permission_classes=[RBACPermission, UserConfirmation.require(ConfirmType.MFA)]
|
||||
)
|
||||
def get_once_secret(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
return Response(data={'id': instance.id, 'secret': instance.secret})
|
||||
|
||||
@action(['GET'], detail=False, url_path='account-secret',
|
||||
permission_classes=[RBACPermission])
|
||||
def get_account_secret(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.query_params)
|
||||
if not serializer.is_valid():
|
||||
return Response({'error': serializer.errors}, status=400)
|
||||
|
||||
service = request.user
|
||||
account = service.get_account(**serializer.data)
|
||||
if not account:
|
||||
msg = _('Account not found')
|
||||
raise JMSException(code='Not found', detail='%s' % msg)
|
||||
asset = account.asset
|
||||
IntegrationApplicationLog.objects.create(
|
||||
remote_addr=get_request_ip(request), service=service.name, service_id=service.id,
|
||||
account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})',
|
||||
)
|
||||
return Response(data={'id': request.user.id, 'secret': account.secret})
|
||||
@@ -1,130 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
from django.db.models import Count, F, Q
|
||||
from django.http.response import JsonResponse
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from accounts.models import (
|
||||
Account, GatherAccountsAutomation,
|
||||
PushAccountAutomation, BackupAccountAutomation,
|
||||
AccountRisk, IntegrationApplication, ChangeSecretAutomation
|
||||
)
|
||||
from assets.const import AllTypes
|
||||
from common.utils.timezone import local_monday
|
||||
|
||||
__all__ = ['PamDashboardApi']
|
||||
|
||||
|
||||
class PamDashboardApi(APIView):
|
||||
http_method_names = ['get']
|
||||
rbac_perms = {
|
||||
'GET': 'rbac.view_pam',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_type_to_accounts():
|
||||
result = Account.objects.annotate(type=F('asset__platform__type')) \
|
||||
.values('type').order_by('type').annotate(total=Count(1))
|
||||
all_types_dict = dict(AllTypes.choices())
|
||||
|
||||
return [
|
||||
{**i, 'label': all_types_dict.get(i['type'], i['type'])}
|
||||
for i in result
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_account_risk_data(_all, query_params):
|
||||
agg_map = {
|
||||
'total_long_time_no_login_accounts': ('long_time_no_login_count', Q(risk='long_time_no_login')),
|
||||
'total_new_found_accounts': ('new_found_count', Q(risk='new_found')),
|
||||
'total_groups_changed_accounts': ('groups_changed_count', Q(risk='groups_changed')),
|
||||
'total_sudoers_changed_accounts': ('sudoers_changed_count', Q(risk='sudoers_changed')),
|
||||
'total_authorized_keys_changed_accounts': (
|
||||
'authorized_keys_changed_count', Q(risk='authorized_keys_changed')),
|
||||
'total_account_deleted_accounts': ('account_deleted_count', Q(risk='account_deleted')),
|
||||
'total_password_expired_accounts': ('password_expired_count', Q(risk='password_expired')),
|
||||
'total_long_time_password_accounts': ('long_time_password_count', Q(risk='long_time_password')),
|
||||
'total_weak_password_accounts': ('weak_password_count', Q(risk='weak_password')),
|
||||
'total_leaked_password_accounts': ('leaked_password_count', Q(risk='leaked_password')),
|
||||
'total_repeated_password_accounts': ('repeated_password_count', Q(risk='repeated_password')),
|
||||
}
|
||||
|
||||
aggregations = {
|
||||
agg_key: Count('id', distinct=True, filter=agg_filter)
|
||||
for param_key, (agg_key, agg_filter) in agg_map.items()
|
||||
if _all or query_params.get(param_key)
|
||||
}
|
||||
|
||||
data = {}
|
||||
if aggregations:
|
||||
account_stats = AccountRisk.objects.aggregate(**aggregations)
|
||||
data = {param_key: account_stats.get(agg_key) for param_key, (agg_key, _) in agg_map.items() if
|
||||
agg_key in account_stats}
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_account_data(_all, query_params):
|
||||
agg_map = {
|
||||
'total_accounts': ('total_count', Count('id')),
|
||||
'total_privileged_accounts': ('privileged_count', Count('id', filter=Q(privileged=True))),
|
||||
'total_connectivity_ok_accounts': ('connectivity_ok_count', Count('id', filter=Q(connectivity='ok'))),
|
||||
'total_secret_reset_accounts': ('secret_reset_count', Count('id', filter=Q(secret_reset=True))),
|
||||
'total_valid_accounts': ('valid_count', Count('id', filter=Q(is_active=True))),
|
||||
'total_week_add_accounts': ('week_add_count', Count('id', filter=Q(date_created__gte=local_monday()))),
|
||||
}
|
||||
|
||||
aggregations = {
|
||||
agg_key: agg_expr
|
||||
for param_key, (agg_key, agg_expr) in agg_map.items()
|
||||
if _all or query_params.get(param_key)
|
||||
}
|
||||
|
||||
data = {}
|
||||
account_stats = Account.objects.aggregate(**aggregations)
|
||||
for param_key, (agg_key, __) in agg_map.items():
|
||||
if agg_key in account_stats:
|
||||
data[param_key] = account_stats[agg_key]
|
||||
|
||||
if _all or query_params.get('total_ordinary_accounts'):
|
||||
if 'total_count' in account_stats and 'privileged_count' in account_stats:
|
||||
data['total_ordinary_accounts'] = \
|
||||
account_stats['total_count'] - account_stats['privileged_count']
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_automation_counts(_all, query_params):
|
||||
automation_counts = defaultdict(int)
|
||||
automation_models = {
|
||||
'total_count_change_secret_automation': ChangeSecretAutomation,
|
||||
'total_count_gathered_account_automation': GatherAccountsAutomation,
|
||||
'total_count_push_account_automation': PushAccountAutomation,
|
||||
'total_count_backup_account_automation': BackupAccountAutomation,
|
||||
'total_count_integration_application': IntegrationApplication,
|
||||
}
|
||||
|
||||
for param_key, model in automation_models.items():
|
||||
if _all or query_params.get(param_key):
|
||||
automation_counts[param_key] = model.objects.count()
|
||||
|
||||
return automation_counts
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
query_params = self.request.query_params
|
||||
|
||||
_all = query_params.get('all')
|
||||
|
||||
data = {}
|
||||
data.update(self.get_account_data(_all, query_params))
|
||||
data.update(self.get_account_risk_data(_all, query_params))
|
||||
data.update(self.get_automation_counts(_all, query_params))
|
||||
|
||||
if _all or query_params.get('total_count_type_to_accounts'):
|
||||
data.update({
|
||||
'total_count_type_to_accounts': self.get_type_to_accounts(),
|
||||
})
|
||||
|
||||
return JsonResponse(data, status=200)
|
||||
@@ -1,13 +1,9 @@
|
||||
from django.db.models import Q
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.models import Account
|
||||
from accounts.permissions import AccountTaskActionPermission
|
||||
from accounts.tasks import (
|
||||
remove_accounts_task, verify_accounts_connectivity_task, push_accounts_to_assets_task
|
||||
)
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from accounts.tasks import verify_accounts_connectivity_task, push_accounts_to_assets_task
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
|
||||
__all__ = [
|
||||
'AccountsTaskCreateAPI',
|
||||
@@ -16,48 +12,37 @@ __all__ = [
|
||||
|
||||
class AccountsTaskCreateAPI(CreateAPIView):
|
||||
serializer_class = serializers.AccountTaskSerializer
|
||||
permission_classes = (AccountTaskActionPermission,)
|
||||
|
||||
def get_permissions(self):
|
||||
act = self.request.data.get('action')
|
||||
if act == 'remove':
|
||||
self.permission_classes = [
|
||||
AccountTaskActionPermission,
|
||||
UserConfirmation.require(ConfirmType.PASSWORD)
|
||||
]
|
||||
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 check_permissions(self, request):
|
||||
act = request.data.get('action')
|
||||
if act == 'push':
|
||||
code = 'accounts.push_account'
|
||||
else:
|
||||
code = 'accounts.verify_account'
|
||||
return request.user.has_perm(code)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
action = data['action']
|
||||
ids = self.get_account_ids(data, action)
|
||||
accounts = data.get('accounts', [])
|
||||
account_ids = [str(a.id) for a in accounts]
|
||||
|
||||
if action == 'push':
|
||||
task = push_accounts_to_assets_task.delay(ids, data.get('params'))
|
||||
elif action == 'remove':
|
||||
task = remove_accounts_task.delay(ids)
|
||||
elif action == 'verify':
|
||||
task = verify_accounts_connectivity_task.delay(ids)
|
||||
if data['action'] == 'push':
|
||||
task = push_accounts_to_assets_task.delay(account_ids)
|
||||
else:
|
||||
raise ValueError(f"Invalid action: {action}")
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
if not asset.auto_info['ansible_enabled'] or \
|
||||
not asset.auto_info['ping_enabled']:
|
||||
raise NotSupportedTemporarilyError()
|
||||
task = verify_accounts_connectivity_task.delay(account_ids)
|
||||
|
||||
data = getattr(serializer, '_data', {})
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=400)
|
||||
|
||||
return handler
|
||||
|
||||
@@ -1,72 +1,22 @@
|
||||
from django_filters import rest_framework as drf_filters
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.mixins import AccountRecordViewLogMixin
|
||||
from accounts.models import AccountTemplate
|
||||
from accounts.tasks import template_sync_related_accounts
|
||||
from assets.const import Protocol
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.drf.filters import BaseFilterSet
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from common.permissions import UserConfirmation, ConfirmType
|
||||
|
||||
|
||||
class AccountTemplateFilterSet(BaseFilterSet):
|
||||
protocols = drf_filters.CharFilter(method='filter_protocols')
|
||||
|
||||
class Meta:
|
||||
model = AccountTemplate
|
||||
fields = ('username', 'name')
|
||||
|
||||
@staticmethod
|
||||
def filter_protocols(queryset, name, value):
|
||||
secret_types = set()
|
||||
protocols = value.split(',')
|
||||
protocol_secret_type_map = Protocol.settings()
|
||||
for p in protocols:
|
||||
if p not in protocol_secret_type_map:
|
||||
continue
|
||||
_st = protocol_secret_type_map[p].get('secret_types', [])
|
||||
secret_types.update(_st)
|
||||
if not secret_types:
|
||||
secret_types = ['password']
|
||||
queryset = queryset.filter(secret_type__in=secret_types)
|
||||
return queryset
|
||||
from common.views.mixins import RecordViewLogMixin
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from accounts import serializers
|
||||
from accounts.models import AccountTemplate
|
||||
|
||||
|
||||
class AccountTemplateViewSet(OrgBulkModelViewSet):
|
||||
model = AccountTemplate
|
||||
filterset_class = AccountTemplateFilterSet
|
||||
filterset_fields = ("username", 'name')
|
||||
search_fields = ('username', 'name')
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountTemplateSerializer,
|
||||
'retrieve': serializers.AccountDetailTemplateSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'su_from_account_templates': 'accounts.view_accounttemplate',
|
||||
'sync_related_accounts': 'accounts.change_account',
|
||||
'default': serializers.AccountTemplateSerializer
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='su-from-account-templates')
|
||||
def su_from_account_templates(self, request, *args, **kwargs):
|
||||
pk = request.query_params.get('template_id')
|
||||
templates = AccountTemplate.get_su_from_account_templates(pk)
|
||||
templates = self.filter_queryset(templates)
|
||||
serializer = self.get_serializer(templates, many=True)
|
||||
return Response(data=serializer.data)
|
||||
|
||||
@action(methods=['patch'], detail=True, url_path='sync-related-accounts')
|
||||
def sync_related_accounts(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
user_id = str(request.user.id)
|
||||
task = template_sync_related_accounts.delay(str(instance.id), user_id)
|
||||
return Response({'task': task.id}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AccountTemplateSecretsViewSet(AccountRecordViewLogMixin, AccountTemplateViewSet):
|
||||
class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet):
|
||||
serializer_classes = {
|
||||
'default': serializers.AccountTemplateSecretSerializer,
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from accounts.models import VirtualAccount
|
||||
from accounts.serializers import VirtualAccountSerializer
|
||||
from common.utils import is_uuid
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
|
||||
class VirtualAccountViewSet(OrgBulkModelViewSet):
|
||||
serializer_class = VirtualAccountSerializer
|
||||
search_fields = ('alias',)
|
||||
filterset_fields = ('alias',)
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return VirtualAccount.objects.none()
|
||||
return VirtualAccount.get_or_init_queryset()
|
||||
|
||||
def get_object(self, ):
|
||||
pk = self.kwargs.get('pk')
|
||||
kwargs = {'pk': pk} if is_uuid(pk) else {'alias': pk}
|
||||
return get_object_or_404(VirtualAccount, **kwargs)
|
||||
@@ -1,7 +1,5 @@
|
||||
from .backup import *
|
||||
from .base import *
|
||||
from .change_secret import *
|
||||
from .change_secret_dashboard import *
|
||||
from .check_account import *
|
||||
from .gather_account import *
|
||||
from .gather_accounts import *
|
||||
from .push_account import *
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import (
|
||||
BackupAccountAutomation
|
||||
AccountBackupAutomation, AccountBackupExecution
|
||||
)
|
||||
from accounts.tasks import execute_account_backup_task
|
||||
from common.const.choices import Trigger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .base import AutomationExecutionViewSet
|
||||
|
||||
__all__ = [
|
||||
'BackupAccountViewSet', 'BackupAccountExecutionViewSet'
|
||||
'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet'
|
||||
]
|
||||
|
||||
|
||||
class BackupAccountViewSet(OrgBulkModelViewSet):
|
||||
model = BackupAccountAutomation
|
||||
filterset_fields = ('name',)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.BackupAccountSerializer
|
||||
class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
||||
model = AccountBackupAutomation
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
ordering = ('name',)
|
||||
serializer_class = serializers.AccountBackupSerializer
|
||||
|
||||
|
||||
class BackupAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_backupaccountexecution"),
|
||||
("retrieve", "accounts.view_backupaccountexecution"),
|
||||
("create", "accounts.add_backupaccountexecution"),
|
||||
("report", "accounts.view_backupaccountexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.backup_account
|
||||
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
||||
search_fields = ('trigger',)
|
||||
filterset_fields = ('trigger', 'plan_id')
|
||||
http_method_names = ['get', 'post', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
queryset = AccountBackupExecution.objects.all()
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
pid = serializer.data.get('plan')
|
||||
task = execute_account_backup_task.delay(pid=str(pid), trigger=Trigger.manual)
|
||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import status, mixins, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts.filters import AutomationExecutionFilterSet
|
||||
from accounts.models import AutomationExecution
|
||||
from accounts.tasks import execute_account_automation_task
|
||||
from assets import serializers
|
||||
@@ -17,15 +13,15 @@ from orgs.mixins import generics
|
||||
__all__ = [
|
||||
'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
|
||||
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi',
|
||||
'AutomationExecutionViewSet'
|
||||
'AutomationExecutionViewSet',
|
||||
]
|
||||
|
||||
|
||||
class AutomationAssetsListApi(generics.ListAPIView):
|
||||
model = BaseAutomation
|
||||
serializer_class = serializers.AutomationAssetsSerializer
|
||||
filterset_fields = ("name", "address")
|
||||
search_fields = filterset_fields
|
||||
filter_fields = ("name", "address")
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_object(self):
|
||||
pk = self.kwargs.get('pk')
|
||||
@@ -39,11 +35,9 @@ class AutomationAssetsListApi(generics.ListAPIView):
|
||||
return assets
|
||||
|
||||
|
||||
class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||
class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
|
||||
model = BaseAutomation
|
||||
queryset = BaseAutomation.objects.all()
|
||||
serializer_class = serializers.UpdateAssetSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@@ -58,11 +52,9 @@ class AutomationRemoveAssetApi(generics.UpdateAPIView):
|
||||
return Response({'msg': 'ok'})
|
||||
|
||||
|
||||
class AutomationAddAssetApi(generics.UpdateAPIView):
|
||||
class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
|
||||
model = BaseAutomation
|
||||
queryset = BaseAutomation.objects.all()
|
||||
serializer_class = serializers.UpdateAssetSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
@@ -76,10 +68,9 @@ class AutomationAddAssetApi(generics.UpdateAPIView):
|
||||
return Response({"error": serializer.errors})
|
||||
|
||||
|
||||
class AutomationNodeAddRemoveApi(generics.UpdateAPIView):
|
||||
class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView):
|
||||
model = BaseAutomation
|
||||
serializer_class = serializers.UpdateNodeSerializer
|
||||
http_method_names = ['patch']
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
action_params = ['add', 'remove']
|
||||
@@ -104,10 +95,10 @@ class AutomationExecutionViewSet(
|
||||
mixins.CreateModelMixin, mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
search_fields = ('trigger', 'automation__name')
|
||||
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
||||
filterset_class = AutomationExecutionFilterSet
|
||||
search_fields = ('trigger',)
|
||||
filterset_fields = ('trigger', 'automation_id')
|
||||
serializer_class = serializers.AutomationExecutionSerializer
|
||||
|
||||
tp: str
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -122,10 +113,3 @@ class AutomationExecutionViewSet(
|
||||
pid=str(automation.pk), trigger=Trigger.manual, tp=self.tp
|
||||
)
|
||||
return Response({'task': task.id}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@action(methods=['get'], detail=True, url_path='report')
|
||||
def report(self, request, *args, **kwargs):
|
||||
execution = self.get_object()
|
||||
report = execution.manager.gen_report()
|
||||
return HttpResponse(report)
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Max, Q, Subquery, OuterRef
|
||||
from rest_framework import status, mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from rest_framework import mixins
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import (
|
||||
AutomationTypes, ChangeSecretRecordStatusChoice
|
||||
)
|
||||
from accounts.filters import ChangeSecretRecordFilterSet, ChangeSecretStatusFilterSet
|
||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, Account
|
||||
from accounts.tasks import execute_automation_record_task
|
||||
from accounts.utils import account_secret_task_status
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.permissions import IsValidLicense
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution
|
||||
from common.utils import get_object_or_none
|
||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from .base import (
|
||||
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
||||
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
|
||||
@@ -26,118 +17,48 @@ __all__ = [
|
||||
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
|
||||
'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi',
|
||||
'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi',
|
||||
'ChangSecretNodeAddRemoveApi', 'ChangeSecretStatusViewSet'
|
||||
'ChangSecretNodeAddRemoveApi'
|
||||
]
|
||||
|
||||
|
||||
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = ChangeSecretAutomation
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
filterset_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filterset_fields
|
||||
filter_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filter_fields
|
||||
serializer_class = serializers.ChangeSecretAutomationSerializer
|
||||
|
||||
|
||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
filterset_class = ChangeSecretRecordFilterSet
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
search_fields = ('asset__address', 'account__username')
|
||||
ordering_fields = ('date_finished',)
|
||||
tp = AutomationTypes.change_secret
|
||||
serializer_classes = {
|
||||
'default': serializers.ChangeSecretRecordSerializer,
|
||||
'secret': serializers.ChangeSecretRecordViewSecretSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'execute': 'accounts.add_changesecretexecution',
|
||||
'secret': 'accounts.view_changesecretrecord',
|
||||
'dashboard': 'accounts.view_changesecretrecord',
|
||||
'ignore_fail': 'accounts.view_changesecretrecord',
|
||||
}
|
||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||
filter_fields = ['asset', 'execution_id']
|
||||
search_fields = ['asset__hostname']
|
||||
|
||||
def get_permissions(self):
|
||||
if self.action == 'secret':
|
||||
self.permission_classes = [
|
||||
RBACPermission,
|
||||
UserConfirmation.require(ConfirmType.MFA)
|
||||
]
|
||||
return super().get_permissions()
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.objects.filter(
|
||||
execution__automation__type=AutomationTypes.change_secret
|
||||
)
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
queryset = super().filter_queryset(queryset)
|
||||
|
||||
if self.action == 'dashboard':
|
||||
return self.get_dashboard_queryset(queryset)
|
||||
eid = self.request.query_params.get('execution_id')
|
||||
execution = get_object_or_none(AutomationExecution, pk=eid)
|
||||
if execution:
|
||||
queryset = queryset.filter(execution=execution)
|
||||
return queryset
|
||||
|
||||
@staticmethod
|
||||
def get_dashboard_queryset(queryset):
|
||||
recent_dates = queryset.values('account').annotate(
|
||||
max_date_finished=Max('date_finished')
|
||||
)
|
||||
|
||||
recent_success_accounts = queryset.filter(
|
||||
account=OuterRef('account'),
|
||||
date_finished=Subquery(
|
||||
recent_dates.filter(account=OuterRef('account')).values('max_date_finished')[:1]
|
||||
)
|
||||
).filter(Q(status=ChangeSecretRecordStatusChoice.success))
|
||||
|
||||
failed_records = queryset.filter(
|
||||
~Q(account__in=Subquery(recent_success_accounts.values('account'))),
|
||||
status=ChangeSecretRecordStatusChoice.failed,
|
||||
ignore_fail=False
|
||||
)
|
||||
return failed_records
|
||||
|
||||
def get_queryset(self):
|
||||
return ChangeSecretRecord.get_valid_records()
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='execute')
|
||||
def execute(self, request, *args, **kwargs):
|
||||
record_ids = request.data.get('record_ids')
|
||||
records = self.get_queryset().filter(id__in=record_ids)
|
||||
if not records.exists():
|
||||
return Response(
|
||||
{'detail': 'No valid records found'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
record_ids = [str(_id) for _id in records.values_list('id', flat=True)]
|
||||
task = execute_automation_record_task.delay(record_ids, self.tp)
|
||||
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)
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='dashboard')
|
||||
def dashboard(self, request, *args, **kwargs):
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
@action(methods=['patch'], detail=True, url_path='ignore-fail')
|
||||
def ignore_fail(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
instance.ignore_fail = True
|
||||
instance.save(update_fields=['ignore_fail'])
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_changesecretexecution"),
|
||||
("retrieve", "accounts.view_changesecretexecution"),
|
||||
("create", "accounts.add_changesecretexecution"),
|
||||
("report", "accounts.view_changesecretexecution"),
|
||||
)
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
|
||||
tp = AutomationTypes.change_secret
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
queryset = queryset.filter(automation__type=self.tp)
|
||||
return queryset
|
||||
|
||||
|
||||
@@ -154,27 +75,7 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
|
||||
model = ChangeSecretAutomation
|
||||
serializer_class = serializers.ChangeSecretUpdateAssetSerializer
|
||||
|
||||
|
||||
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
||||
model = ChangeSecretAutomation
|
||||
serializer_class = serializers.ChangeSecretUpdateNodeSerializer
|
||||
|
||||
class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
|
||||
perm_model = ChangeSecretAutomation
|
||||
filterset_class = ChangeSecretStatusFilterSet
|
||||
serializer_class = serializers.ChangeSecretAccountSerializer
|
||||
search_fields = ('username',)
|
||||
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
http_method_names = ["get", "delete", "options"]
|
||||
|
||||
def get_queryset(self):
|
||||
account_ids = list(account_secret_task_status.account_ids)
|
||||
return Account.objects.filter(id__in=account_ids).select_related('asset')
|
||||
|
||||
def bulk_destroy(self, request, *args, **kwargs):
|
||||
account_ids = request.data.get('account_ids')
|
||||
if isinstance(account_ids, str):
|
||||
account_ids = [account_ids]
|
||||
for _id in account_ids:
|
||||
account_secret_task_status.clear(_id)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from collections import defaultdict
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http.response import JsonResponse
|
||||
from django.utils import timezone
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
|
||||
from accounts.models import ChangeSecretAutomation, AutomationExecution, ChangeSecretRecord
|
||||
from assets.models import Node, Asset
|
||||
from common.const import Status
|
||||
from common.permissions import IsValidLicense
|
||||
from common.utils import lazyproperty
|
||||
from common.utils.timezone import local_zero_hour, local_now
|
||||
from ops.celery import app
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
__all__ = ['ChangeSecretDashboardApi']
|
||||
|
||||
|
||||
class ChangeSecretDashboardApi(APIView):
|
||||
http_method_names = ['get']
|
||||
rbac_perms = {
|
||||
'GET': 'accounts.view_changesecretautomation',
|
||||
}
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
tp = AutomationTypes.change_secret
|
||||
task_name = 'accounts.tasks.automation.execute_account_automation_task'
|
||||
ongoing_change_secret_cache_key = "ongoing_change_secret_cache_key"
|
||||
|
||||
@lazyproperty
|
||||
def days(self):
|
||||
count = self.request.query_params.get('days', 1)
|
||||
return int(count)
|
||||
|
||||
@property
|
||||
def days_to_datetime(self):
|
||||
if self.days == 1:
|
||||
return local_zero_hour()
|
||||
return local_now() - timezone.timedelta(days=self.days)
|
||||
|
||||
def get_queryset_date_filter(self, qs, query_field='date_updated'):
|
||||
return qs.filter(**{f'{query_field}__gte': self.days_to_datetime})
|
||||
|
||||
@lazyproperty
|
||||
def date_range_list(self):
|
||||
return [
|
||||
(local_now() - timezone.timedelta(days=i)).date()
|
||||
for i in range(self.days - 1, -1, -1)
|
||||
]
|
||||
|
||||
def filter_by_date_range(self, queryset, field_name):
|
||||
date_range_bounds = self.days_to_datetime.date(), (local_now() + timezone.timedelta(days=1)).date()
|
||||
return queryset.filter(**{f'{field_name}__range': date_range_bounds})
|
||||
|
||||
def calculate_daily_metrics(self, queryset, date_field):
|
||||
filtered_queryset = self.filter_by_date_range(queryset, date_field)
|
||||
results = filtered_queryset.values_list(date_field, 'status')
|
||||
|
||||
status_counts = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
for date_finished, status in results:
|
||||
dt_local = timezone.localtime(date_finished)
|
||||
date_str = str(dt_local.date())
|
||||
if status == ChangeSecretRecordStatusChoice.failed:
|
||||
status_counts[date_str]['failed'] += 1
|
||||
elif status == ChangeSecretRecordStatusChoice.success:
|
||||
status_counts[date_str]['success'] += 1
|
||||
|
||||
metrics = defaultdict(list)
|
||||
for date in self.date_range_list:
|
||||
date_str = str(date)
|
||||
for status in ['success', 'failed']:
|
||||
metrics[status].append(status_counts[date_str].get(status, 0))
|
||||
|
||||
return metrics
|
||||
|
||||
def get_daily_success_and_failure_metrics(self):
|
||||
metrics = self.calculate_daily_metrics(self.change_secret_records_queryset, 'date_finished')
|
||||
return metrics.get('success', []), metrics.get('failed', [])
|
||||
|
||||
@lazyproperty
|
||||
def change_secrets_queryset(self):
|
||||
return ChangeSecretAutomation.objects.all()
|
||||
|
||||
@lazyproperty
|
||||
def change_secret_records_queryset(self):
|
||||
return ChangeSecretRecord.get_valid_records()
|
||||
|
||||
def get_change_secret_asset_queryset(self):
|
||||
qs = self.change_secrets_queryset
|
||||
node_ids = qs.values_list('nodes', flat=True).distinct()
|
||||
nodes = Node.objects.filter(id__in=node_ids).only('id', 'key')
|
||||
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||
direct_asset_ids = qs.values_list('assets', flat=True).distinct()
|
||||
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
|
||||
return Asset.objects.filter(id__in=asset_ids)
|
||||
|
||||
def get_filtered_counts(self, qs, field=None):
|
||||
if field is None:
|
||||
return qs.count()
|
||||
return self.get_queryset_date_filter(qs, field).count()
|
||||
|
||||
def get_status_counts(self, executions):
|
||||
executions = executions.filter(type=self.tp)
|
||||
total, failed, success = 0, 0, 0
|
||||
for status in executions.values_list('status', flat=True):
|
||||
total += 1
|
||||
if status in [Status.failed, Status.error]:
|
||||
failed += 1
|
||||
elif status == Status.success:
|
||||
success += 1
|
||||
|
||||
return {
|
||||
'total_count_change_secret_executions': total,
|
||||
'total_count_success_change_secret_executions': success,
|
||||
'total_count_failed_change_secret_executions': failed,
|
||||
}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
query_params = self.request.query_params
|
||||
data = {}
|
||||
|
||||
_all = query_params.get('all')
|
||||
|
||||
if _all or query_params.get('total_count_change_secrets'):
|
||||
data['total_count_change_secrets'] = self.get_filtered_counts(
|
||||
self.change_secrets_queryset
|
||||
)
|
||||
|
||||
if _all or query_params.get('total_count_periodic_change_secrets'):
|
||||
data['total_count_periodic_change_secrets'] = self.get_filtered_counts(
|
||||
self.change_secrets_queryset.filter(is_periodic=True)
|
||||
)
|
||||
|
||||
if _all or query_params.get('total_count_change_secret_assets'):
|
||||
data['total_count_change_secret_assets'] = self.get_change_secret_asset_queryset().count()
|
||||
|
||||
if _all or query_params.get('total_count_change_secret_status'):
|
||||
executions = self.get_queryset_date_filter(AutomationExecution.objects.all(), 'date_start')
|
||||
data.update(self.get_status_counts(executions))
|
||||
|
||||
if _all or query_params.get('daily_success_and_failure_metrics'):
|
||||
success, failed = self.get_daily_success_and_failure_metrics()
|
||||
data.update({
|
||||
'dates_metrics_date': [date.strftime('%m-%d') for date in self.date_range_list] or ['0'],
|
||||
'dates_metrics_total_count_success': success,
|
||||
'dates_metrics_total_count_failed': failed,
|
||||
})
|
||||
|
||||
if _all or query_params.get('total_count_ongoing_change_secret'):
|
||||
ongoing_counts = cache.get(self.ongoing_change_secret_cache_key)
|
||||
if ongoing_counts is None:
|
||||
execution_ids = []
|
||||
inspect = app.control.inspect()
|
||||
try:
|
||||
active_tasks = inspect.active()
|
||||
except Exception:
|
||||
active_tasks = None
|
||||
if active_tasks:
|
||||
for tasks in active_tasks.values():
|
||||
for task in tasks:
|
||||
_id = task.get('id')
|
||||
name = task.get('name')
|
||||
tp = task.get('kwargs', {}).get('tp')
|
||||
if name == self.task_name and tp == self.tp:
|
||||
execution_ids.append(_id)
|
||||
|
||||
snapshots = AutomationExecution.objects.filter(id__in=execution_ids).values_list('snapshot', flat=True)
|
||||
|
||||
asset_ids = {asset for i in snapshots for asset in i.get('assets', [])}
|
||||
account_ids = {account for i in snapshots for account in i.get('accounts', [])}
|
||||
|
||||
ongoing_counts = (len(execution_ids), len(asset_ids), len(account_ids))
|
||||
data['total_count_ongoing_change_secret'] = ongoing_counts[0]
|
||||
data['total_count_ongoing_change_secret_assets'] = ongoing_counts[1]
|
||||
data['total_count_ongoing_change_secret_accounts'] = ongoing_counts[2]
|
||||
cache.set(self.ongoing_change_secret_cache_key, ongoing_counts, 60)
|
||||
else:
|
||||
data['total_count_ongoing_change_secret'] = ongoing_counts[0]
|
||||
data['total_count_ongoing_change_secret_assets'] = ongoing_counts[1]
|
||||
data['total_count_ongoing_change_secret_accounts'] = ongoing_counts[2]
|
||||
|
||||
return JsonResponse(data, status=200)
|
||||
@@ -1,165 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Q, Count
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import (
|
||||
CheckAccountAutomation,
|
||||
AccountRisk,
|
||||
RiskChoice,
|
||||
CheckAccountEngine,
|
||||
AutomationExecution,
|
||||
)
|
||||
from assets.models import Asset
|
||||
from common.api import JMSModelViewSet
|
||||
from common.permissions import IsValidLicense
|
||||
from common.utils import many_get
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from rbac.permissions import RBACPermission
|
||||
from .base import AutomationExecutionViewSet
|
||||
from ...filters import NodeFilterBackend
|
||||
from ...risk_handlers import RiskHandler
|
||||
|
||||
__all__ = [
|
||||
"CheckAccountAutomationViewSet",
|
||||
"CheckAccountExecutionViewSet",
|
||||
"AccountRiskViewSet",
|
||||
"CheckAccountEngineViewSet",
|
||||
]
|
||||
|
||||
|
||||
class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = CheckAccountAutomation
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
serializer_class = serializers.CheckAccountAutomationSerializer
|
||||
|
||||
|
||||
class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_checkaccountexecution"),
|
||||
("retrieve", "accounts.view_checkaccountexecution"),
|
||||
("create", "accounts.add_checkaccountexecution"),
|
||||
("adhoc", "accounts.add_checkaccountexecution"),
|
||||
("report", "accounts.view_checkaccountexecution"),
|
||||
)
|
||||
ordering = ("-date_created",)
|
||||
tp = AutomationTypes.check_account
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
return queryset
|
||||
|
||||
@action(methods=["get"], detail=False, url_path="adhoc")
|
||||
def adhoc(self, request, *args, **kwargs):
|
||||
asset_id = request.query_params.get("asset_id")
|
||||
if not asset_id:
|
||||
return Response(status=400, data={"asset_id": "This field is required."})
|
||||
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
name = "Check asset risk: {}".format(asset.name)
|
||||
execution = AutomationExecution()
|
||||
execution.snapshot = {
|
||||
"assets": [asset_id],
|
||||
"nodes": [],
|
||||
"type": AutomationTypes.check_account,
|
||||
"engines": "__all__",
|
||||
"name": name,
|
||||
}
|
||||
execution.save()
|
||||
execution.start()
|
||||
report = execution.manager.gen_report()
|
||||
return HttpResponse(report)
|
||||
|
||||
|
||||
class AccountRiskViewSet(OrgBulkModelViewSet):
|
||||
model = AccountRisk
|
||||
search_fields = ["username", "asset__name"]
|
||||
filterset_fields = ("risk", "status", "asset_id")
|
||||
extra_filter_backends = [NodeFilterBackend]
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
serializer_classes = {
|
||||
"default": serializers.AccountRiskSerializer,
|
||||
"assets": serializers.AssetRiskSerializer,
|
||||
"handle": serializers.HandleRiskSerializer,
|
||||
}
|
||||
ordering_fields = ("asset", "risk", "status", "username", "date_created")
|
||||
ordering = ("status", "asset", "date_created")
|
||||
rbac_perms = {
|
||||
"sync_accounts": "assets.add_accountrisk",
|
||||
"assets": "accounts.view_accountrisk",
|
||||
"handle": "accounts.change_accountrisk",
|
||||
}
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed("PUT")
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed("POST")
|
||||
|
||||
@action(methods=["get"], detail=False, url_path="assets")
|
||||
def assets(self, request, *args, **kwargs):
|
||||
annotations = {
|
||||
f"{risk[0]}_count": Count("id", filter=Q(risk=risk[0]))
|
||||
for risk in RiskChoice.choices
|
||||
}
|
||||
queryset = (
|
||||
AccountRisk.objects.select_related(
|
||||
"asset", "asset__platform"
|
||||
) # 使用 select_related 来优化 asset 和 asset__platform 的查询
|
||||
.values(
|
||||
"asset__id", "asset__name", "asset__address", "asset__platform__name"
|
||||
) # 添加需要的字段
|
||||
.annotate(risk_total=Count("id")) # 计算风险总数
|
||||
.annotate(**annotations) # 使用上面定义的 annotations 进行计数
|
||||
)
|
||||
return self.get_paginated_response_from_queryset(queryset)
|
||||
|
||||
@action(methods=["post"], detail=False, url_path="handle")
|
||||
def handle(self, request, *args, **kwargs):
|
||||
s = self.get_serializer(data=request.data)
|
||||
s.is_valid(raise_exception=True)
|
||||
|
||||
asset, username, act, risk = many_get(
|
||||
s.validated_data, ("asset", "username", "action", "risk")
|
||||
)
|
||||
handler = RiskHandler(asset=asset, username=username, request=self.request)
|
||||
|
||||
try:
|
||||
risk = handler.handle(act, risk)
|
||||
s = serializers.AccountRiskSerializer(instance=risk)
|
||||
return Response(data=s.data)
|
||||
except Exception as e:
|
||||
return Response(status=400, data=str(e))
|
||||
|
||||
|
||||
class CheckAccountEngineViewSet(JMSModelViewSet):
|
||||
search_fields = ("name",)
|
||||
serializer_class = serializers.CheckAccountEngineSerializer
|
||||
permission_classes = [RBACPermission, IsValidLicense]
|
||||
perm_model = CheckAccountEngine
|
||||
http_method_names = ['get', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
if getattr(self, "swagger_fake_view", False):
|
||||
return CheckAccountEngine.objects.none()
|
||||
|
||||
return CheckAccountEngine.get_default_engines()
|
||||
|
||||
def filter_queryset(self, queryset: list):
|
||||
search = self.request.GET.get('search')
|
||||
if search is not None:
|
||||
queryset = [
|
||||
item for item in queryset
|
||||
if search in item['name']
|
||||
]
|
||||
return queryset
|
||||
@@ -1,131 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.filters import GatheredAccountFilterSet, NodeFilterBackend
|
||||
from accounts.models import GatherAccountsAutomation, AutomationExecution, Account
|
||||
from accounts.models import GatheredAccount
|
||||
from assets.models import Asset
|
||||
from common.const import ConfirmOrIgnore
|
||||
from common.utils.http import is_true
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .base import AutomationExecutionViewSet
|
||||
|
||||
__all__ = [
|
||||
"DiscoverAccountsAutomationViewSet",
|
||||
"DiscoverAccountsExecutionViewSet",
|
||||
"GatheredAccountViewSet",
|
||||
]
|
||||
|
||||
from ...risk_handlers import RiskHandler
|
||||
|
||||
|
||||
class DiscoverAccountsAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = GatherAccountsAutomation
|
||||
filterset_fields = ("name",)
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.DiscoverAccountAutomationSerializer
|
||||
|
||||
|
||||
class DiscoverAccountsExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_gatheraccountsexecution"),
|
||||
("retrieve", "accounts.view_gatheraccountsexecution"),
|
||||
("create", "accounts.add_gatheraccountsexecution"),
|
||||
("adhoc", "accounts.add_gatheraccountsexecution"),
|
||||
("report", "accounts.view_gatheraccountsexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.gather_accounts
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
return queryset
|
||||
|
||||
@xframe_options_sameorigin
|
||||
@action(methods=["get"], detail=False, url_path="adhoc")
|
||||
def adhoc(self, request, *args, **kwargs):
|
||||
asset_id = request.query_params.get("asset_id")
|
||||
if not asset_id:
|
||||
return Response(status=400, data={"asset_id": "This field is required."})
|
||||
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
execution = AutomationExecution()
|
||||
execution.snapshot = {
|
||||
"assets": [asset_id],
|
||||
"nodes": [],
|
||||
"type": "gather_accounts",
|
||||
"is_sync_account": False,
|
||||
"check_risk": True,
|
||||
"name": "Adhoc gather accounts: {}".format(asset.name),
|
||||
}
|
||||
execution.save()
|
||||
execution.start()
|
||||
report = execution.manager.gen_report()
|
||||
return HttpResponse(report)
|
||||
|
||||
|
||||
class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||
model = GatheredAccount
|
||||
search_fields = ("username",)
|
||||
filterset_class = GatheredAccountFilterSet
|
||||
extra_filter_backends = [NodeFilterBackend]
|
||||
ordering = ("status",)
|
||||
serializer_classes = {
|
||||
"default": serializers.DiscoverAccountSerializer,
|
||||
"status": serializers.DiscoverAccountActionSerializer,
|
||||
"details": serializers.DiscoverAccountDetailsSerializer
|
||||
}
|
||||
rbac_perms = {
|
||||
"status": "assets.change_gatheredaccount",
|
||||
"details": "assets.view_gatheredaccount"
|
||||
}
|
||||
|
||||
@action(methods=["put"], detail=False, url_path="status")
|
||||
def status(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
validated_data = serializer.validated_data
|
||||
ids = validated_data.get('ids', [])
|
||||
new_status = validated_data.get('status')
|
||||
updated_instances = GatheredAccount.objects.filter(id__in=ids).select_related('asset')
|
||||
if new_status == ConfirmOrIgnore.confirmed:
|
||||
GatheredAccount.sync_accounts(updated_instances)
|
||||
updated_instances.update(present=True)
|
||||
updated_instances.update(status=new_status)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
request = self.request
|
||||
params = request.query_params
|
||||
is_delete_remote = params.get("is_delete_remote")
|
||||
is_delete_account = params.get("is_delete_account")
|
||||
asset_id = params.get("asset")
|
||||
username = params.get("username")
|
||||
if is_true(is_delete_remote):
|
||||
self._delete_remote(asset_id, username)
|
||||
if is_true(is_delete_account):
|
||||
account = get_object_or_404(Account, username=username, asset_id=asset_id)
|
||||
account.delete()
|
||||
super().perform_destroy(instance)
|
||||
|
||||
def _delete_remote(self, asset_id, username):
|
||||
asset = get_object_or_404(Asset, pk=asset_id)
|
||||
handler = RiskHandler(asset, username, request=self.request)
|
||||
handler.handle_delete_remote()
|
||||
|
||||
@action(methods=["get"], detail=True, url_path="details")
|
||||
def details(self, request, *args, **kwargs):
|
||||
pk = kwargs.get('pk')
|
||||
account = get_object_or_404(GatheredAccount, pk=pk)
|
||||
serializer = self.get_serializer(account.detail)
|
||||
return Response(data=serializer.data)
|
||||
71
apps/accounts/api/automations/gather_accounts.py
Normal file
71
apps/accounts/api/automations/gather_accounts.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.const import Source
|
||||
from accounts.filters import GatheredAccountFilterSet
|
||||
from accounts.models import GatherAccountsAutomation
|
||||
from accounts.models import GatheredAccount
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from .base import AutomationExecutionViewSet
|
||||
|
||||
__all__ = [
|
||||
'GatherAccountsAutomationViewSet', 'GatherAccountsExecutionViewSet',
|
||||
'GatheredAccountViewSet'
|
||||
]
|
||||
|
||||
|
||||
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = GatherAccountsAutomation
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
serializer_class = serializers.GatherAccountAutomationSerializer
|
||||
|
||||
|
||||
class GatherAccountsExecutionViewSet(AutomationExecutionViewSet):
|
||||
rbac_perms = (
|
||||
("list", "accounts.view_gatheraccountsexecution"),
|
||||
("retrieve", "accounts.view_gatheraccountsexecution"),
|
||||
("create", "accounts.add_gatheraccountsexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.gather_accounts
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(automation__type=self.tp)
|
||||
return queryset
|
||||
|
||||
|
||||
class GatheredAccountViewSet(OrgBulkModelViewSet):
|
||||
model = GatheredAccount
|
||||
search_fields = ('username',)
|
||||
filterset_class = GatheredAccountFilterSet
|
||||
serializer_classes = {
|
||||
'default': serializers.GatheredAccountSerializer,
|
||||
}
|
||||
rbac_perms = {
|
||||
'sync_account': 'assets.add_gatheredaccount',
|
||||
}
|
||||
|
||||
@action(methods=['post'], detail=True, url_path='sync')
|
||||
def sync_account(self, request, *args, **kwargs):
|
||||
gathered_account = super().get_object()
|
||||
asset = gathered_account.asset
|
||||
username = gathered_account.username
|
||||
accounts = asset.accounts.filter(username=username)
|
||||
|
||||
if accounts.exists():
|
||||
accounts.update(source=Source.COLLECTED)
|
||||
else:
|
||||
asset.accounts.model.objects.create(
|
||||
asset=asset, username=username,
|
||||
name=f'{username}-{_("Collected")}',
|
||||
source=Source.COLLECTED
|
||||
)
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
@@ -1,16 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import mixins
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.filters import PushAccountRecordFilterSet
|
||||
from accounts.models import PushAccountAutomation, PushSecretRecord
|
||||
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
|
||||
from accounts.models import PushAccountAutomation, ChangeSecretRecord
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .base import (
|
||||
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
|
||||
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
|
||||
)
|
||||
from .change_secret import ChangeSecretRecordViewSet
|
||||
|
||||
__all__ = [
|
||||
'PushAccountAutomationViewSet', 'PushAccountAssetsListApi', 'PushAccountRemoveAssetApi',
|
||||
@@ -21,8 +20,8 @@ __all__ = [
|
||||
|
||||
class PushAccountAutomationViewSet(OrgBulkModelViewSet):
|
||||
model = PushAccountAutomation
|
||||
filterset_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filterset_fields
|
||||
filter_fields = ('name', 'secret_type', 'secret_strategy')
|
||||
search_fields = filter_fields
|
||||
serializer_class = serializers.PushAccountAutomationSerializer
|
||||
|
||||
|
||||
@@ -31,28 +30,23 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
|
||||
("list", "accounts.view_pushaccountexecution"),
|
||||
("retrieve", "accounts.view_pushaccountexecution"),
|
||||
("create", "accounts.add_pushaccountexecution"),
|
||||
("report", "accounts.view_pushaccountexecution"),
|
||||
)
|
||||
|
||||
tp = AutomationTypes.push_account
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.filter(type=self.tp)
|
||||
queryset = queryset.filter(automation__type=self.tp)
|
||||
return queryset
|
||||
|
||||
|
||||
class PushAccountRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||
filterset_class = PushAccountRecordFilterSet
|
||||
search_fields = ('asset__address', 'account__username')
|
||||
ordering_fields = ('date_finished',)
|
||||
tp = AutomationTypes.push_account
|
||||
serializer_classes = {
|
||||
'default': serializers.PushSecretRecordSerializer,
|
||||
}
|
||||
class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
|
||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return PushSecretRecord.get_valid_records()
|
||||
return ChangeSecretRecord.objects.filter(
|
||||
execution__automation__type=AutomationTypes.push_account
|
||||
)
|
||||
|
||||
|
||||
class PushAccountAssetsListApi(AutomationAssetsListApi):
|
||||
@@ -63,10 +57,12 @@ class PushAccountRemoveAssetApi(AutomationRemoveAssetApi):
|
||||
model = PushAccountAutomation
|
||||
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
||||
|
||||
|
||||
class PushAccountAddAssetApi(AutomationAddAssetApi):
|
||||
model = PushAccountAutomation
|
||||
serializer_class = serializers.PushAccountUpdateAssetSerializer
|
||||
|
||||
|
||||
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
|
||||
model = PushAccountAutomation
|
||||
serializer_class = serializers.PushAccountUpdateNodeSerializer
|
||||
serializer_class = serializers.PushAccountUpdateNodeSerializer
|
||||
|
||||
@@ -4,8 +4,8 @@ from django.apps import AppConfig
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'accounts'
|
||||
verbose_name = 'App Accounts'
|
||||
|
||||
def ready(self):
|
||||
from . import signal_handlers # noqa
|
||||
from . import tasks # noqa
|
||||
from . import signal_handlers
|
||||
from . import tasks
|
||||
__all__ = signal_handlers
|
||||
|
||||
@@ -1,30 +1,24 @@
|
||||
import os
|
||||
import time
|
||||
from openpyxl import Workbook
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from xlsxwriter import Workbook
|
||||
|
||||
from accounts.const import AccountBackupType
|
||||
from accounts.models import BackupAccountAutomation, Account
|
||||
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
|
||||
from accounts.serializers import AccountSecretSerializer
|
||||
from accounts.models import Account
|
||||
from assets.const import AllTypes
|
||||
from common.const import Status
|
||||
from common.utils.file import encrypt_and_compress_zip_file, zip_files
|
||||
from common.utils.timezone import local_now_filename, local_now_display
|
||||
from terminal.models.component.storage import ReplayStorage
|
||||
from accounts.serializers import AccountSecretSerializer
|
||||
from accounts.notifications import AccountBackupExecutionTaskMsg
|
||||
from users.models import User
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
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):
|
||||
pass
|
||||
|
||||
|
||||
class BaseAccountHandler:
|
||||
@@ -36,26 +30,17 @@ class BaseAccountHandler:
|
||||
if isinstance(v, OrderedDict):
|
||||
cls.unpack_data(v, data)
|
||||
else:
|
||||
if isinstance(v, dict):
|
||||
v = v.get('label')
|
||||
elif v is None:
|
||||
v = ''
|
||||
data[k] = v
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def get_header_fields(cls, serializer: serializers.Serializer):
|
||||
try:
|
||||
exclude_backup_fields = getattr(serializer, 'Meta').exclude_backup_fields
|
||||
backup_fields = getattr(serializer, 'Meta').fields_backup
|
||||
except AttributeError:
|
||||
exclude_backup_fields = []
|
||||
backup_fields = serializer.fields.keys()
|
||||
|
||||
backup_fields = serializer.fields.keys()
|
||||
header_fields = {}
|
||||
for field in backup_fields:
|
||||
if field in exclude_backup_fields:
|
||||
continue
|
||||
|
||||
v = serializer.fields[field]
|
||||
if isinstance(v, serializers.Serializer):
|
||||
_fields = cls.get_header_fields(v)
|
||||
@@ -85,28 +70,14 @@ class BaseAccountHandler:
|
||||
|
||||
class AssetAccountHandler(BaseAccountHandler):
|
||||
@staticmethod
|
||||
def get_filename(name):
|
||||
def get_filename(plan_name):
|
||||
filename = os.path.join(
|
||||
PATH, f'{name}-{local_now_filename()}-{time.time()}.xlsx'
|
||||
PATH, f'{plan_name}-{local_now_display()}-{time.time()}.xlsx'
|
||||
)
|
||||
return filename
|
||||
|
||||
@staticmethod
|
||||
def handler_secret(data, section):
|
||||
for account_data in data:
|
||||
secret = account_data.get('secret')
|
||||
if not secret:
|
||||
continue
|
||||
length = len(secret)
|
||||
index = length // 2
|
||||
if section == "front":
|
||||
secret = secret[:index] + '*' * (length - index)
|
||||
elif section == "back":
|
||||
secret = '*' * (length - index) + secret[index:]
|
||||
account_data['secret'] = secret
|
||||
|
||||
@classmethod
|
||||
def create_data_map(cls, accounts, section):
|
||||
def create_data_map(cls, accounts):
|
||||
data_map = defaultdict(list)
|
||||
|
||||
if not accounts.exists():
|
||||
@@ -126,166 +97,117 @@ class AssetAccountHandler(BaseAccountHandler):
|
||||
for tp, _accounts in account_type_map.items():
|
||||
sheet_name = type_dict.get(tp, tp)
|
||||
data = AccountSecretSerializer(_accounts, many=True).data
|
||||
cls.handler_secret(data, section)
|
||||
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
||||
number_of_backup_accounts = _('Number of backup accounts')
|
||||
print('\033[33m- {}: {}\033[0m'.format(number_of_backup_accounts, accounts.count()))
|
||||
|
||||
logger.info('\n\033[33m- 共备份 {} 条账号\033[0m'.format(accounts.count()))
|
||||
return data_map
|
||||
|
||||
|
||||
class AccountBackupHandler:
|
||||
def __init__(self, manager, execution):
|
||||
self.manager = manager
|
||||
def __init__(self, execution):
|
||||
self.execution = execution
|
||||
self.name = self.execution.snapshot.get('name', '-')
|
||||
self.plan_name = self.execution.plan.name
|
||||
self.is_frozen = False # 任务状态冻结标志
|
||||
|
||||
def get_accounts(self):
|
||||
# TODO 可以优化一下查询 在账号上做 category 的缓存 避免数据量大时连表操作
|
||||
types = self.execution.snapshot.get('types', [])
|
||||
self.manager.summary['total_types'] = len(types)
|
||||
qs = Account.objects.filter(
|
||||
asset__platform__type__in=types
|
||||
).annotate(type=F('asset__platform__type'))
|
||||
return qs
|
||||
|
||||
def create_excel(self, section='complete'):
|
||||
hint = _('Generating asset related backup information files')
|
||||
print(
|
||||
f'\033[32m>>> {hint}\033[0m'
|
||||
def create_excel(self):
|
||||
logger.info(
|
||||
'\n'
|
||||
'\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
|
||||
''
|
||||
)
|
||||
|
||||
# Print task start date
|
||||
time_start = time.time()
|
||||
files = []
|
||||
accounts = self.get_accounts()
|
||||
self.manager.summary['total_accounts'] = accounts.count()
|
||||
data_map = AssetAccountHandler.create_data_map(accounts, section)
|
||||
accounts = self.execution.backup_accounts
|
||||
data_map = AssetAccountHandler.create_data_map(accounts)
|
||||
if not data_map:
|
||||
return files
|
||||
|
||||
filename = AssetAccountHandler.get_filename(self.name)
|
||||
filename = AssetAccountHandler.get_filename(self.plan_name)
|
||||
|
||||
wb = Workbook(filename)
|
||||
for sheet, data in data_map.items():
|
||||
ws = wb.add_worksheet(str(sheet))
|
||||
for row_index, row_data in enumerate(data):
|
||||
for col_index, col_data in enumerate(row_data):
|
||||
ws.write_string(row_index, col_index, col_data)
|
||||
wb.close()
|
||||
ws = wb.create_sheet(str(sheet))
|
||||
for row in data:
|
||||
ws.append(row)
|
||||
wb.save(filename)
|
||||
files.append(filename)
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
time_cost = _('Duration')
|
||||
file_created = _('Backup file creation completed')
|
||||
print('{}: {} {}s'.format(file_created, time_cost, timedelta))
|
||||
logger.info('步骤完成: 用时 {}s'.format(timedelta))
|
||||
return files
|
||||
|
||||
def send_backup_mail(self, files, recipients):
|
||||
if not files:
|
||||
return
|
||||
recipients = User.objects.filter(id__in=list(recipients))
|
||||
msg = _("Start sending backup emails")
|
||||
print(
|
||||
f'\033[32m>>> {msg}\033[0m'
|
||||
logger.info(
|
||||
'\n'
|
||||
'\033[32m>>> 发送备份邮件\033[0m'
|
||||
''
|
||||
)
|
||||
name = self.name
|
||||
plan_name = self.plan_name
|
||||
for user in recipients:
|
||||
if not user.secret_key:
|
||||
attachment_list = []
|
||||
else:
|
||||
attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, user.secret_key, files)
|
||||
attachment_list = [attachment]
|
||||
AccountBackupExecutionTaskMsg(name, user).publish(attachment_list)
|
||||
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
|
||||
def send_backup_obj_storage(self, files, recipients, password):
|
||||
if not files:
|
||||
return
|
||||
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
|
||||
print(
|
||||
'\033[32m>>> 📃 ---> sftp \033[0m'
|
||||
''
|
||||
)
|
||||
name = self.name
|
||||
encrypt_file = _('Encrypting files using encryption password')
|
||||
for rec in recipients:
|
||||
attachment = os.path.join(PATH, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||
if password:
|
||||
print(f'\033[32m>>> {encrypt_file}\033[0m')
|
||||
password = user.secret_key.encode('utf8')
|
||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_display()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, password, files)
|
||||
else:
|
||||
zip_files(attachment, files)
|
||||
attachment_list = attachment
|
||||
AccountBackupByObjStorageExecutionTaskMsg(name, rec).publish(attachment_list)
|
||||
file_sent_to = _('The backup file will be sent to')
|
||||
print('{}: {}({})'.format(file_sent_to, rec.name, rec.id))
|
||||
attachment_list = [attachment, ]
|
||||
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
||||
logger.info('邮件已发送至{}({})'.format(user, user.email))
|
||||
for file in files:
|
||||
os.remove(file)
|
||||
|
||||
def step_perform_task_update(self, is_success, reason):
|
||||
self.execution.reason = reason[:1024]
|
||||
self.execution.is_success = is_success
|
||||
self.execution.save()
|
||||
logger.info('已完成对任务状态的更新')
|
||||
|
||||
def step_finished(self, is_success):
|
||||
if is_success:
|
||||
logger.info('任务执行成功')
|
||||
else:
|
||||
logger.error('任务执行失败')
|
||||
|
||||
def _run(self):
|
||||
is_success = False
|
||||
error = '-'
|
||||
try:
|
||||
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email)
|
||||
if backup_type == AccountBackupType.email:
|
||||
self.backup_by_email()
|
||||
elif backup_type == AccountBackupType.object_storage:
|
||||
self.backup_by_obj_storage()
|
||||
recipients = self.execution.plan_snapshot.get('recipients')
|
||||
if not recipients:
|
||||
logger.info(
|
||||
'\n'
|
||||
'\033[32m>>> 该备份任务未分配收件人\033[0m'
|
||||
''
|
||||
)
|
||||
else:
|
||||
files = self.create_excel()
|
||||
self.send_backup_mail(files, recipients)
|
||||
except Exception as e:
|
||||
self.is_frozen = True
|
||||
logger.error('任务执行被异常中断')
|
||||
logger.info('下面打印发生异常的 Traceback 信息 : ')
|
||||
logger.error(e, exc_info=True)
|
||||
error = str(e)
|
||||
print(f'\033[31m>>> {error}\033[0m')
|
||||
self.manager.status = Status.error
|
||||
self.manager.summary['error'] = error
|
||||
|
||||
def backup_by_obj_storage(self):
|
||||
object_id = self.execution.snapshot.get('id')
|
||||
zip_encrypt_password = BackupAccountAutomation.objects.get(id=object_id).zip_encrypt_password
|
||||
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
|
||||
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:
|
||||
print(
|
||||
'\n'
|
||||
f'\033[31m>>> {no_assigned_sftp_server}\033[0m'
|
||||
''
|
||||
)
|
||||
raise RecipientsNotFound('Not Found Recipients')
|
||||
if obj_recipients_part_one and obj_recipients_part_two:
|
||||
print(f'\033[32m>>> {split_help_text}\033[0m')
|
||||
files = self.create_excel(section='front')
|
||||
self.send_backup_obj_storage(files, obj_recipients_part_one, zip_encrypt_password)
|
||||
|
||||
files = self.create_excel(section='back')
|
||||
self.send_backup_obj_storage(files, obj_recipients_part_two, zip_encrypt_password)
|
||||
else:
|
||||
recipients = obj_recipients_part_one or obj_recipients_part_two
|
||||
files = self.create_excel()
|
||||
self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
|
||||
|
||||
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_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||
if not recipients_part_one and not recipients_part_two:
|
||||
print(
|
||||
'\n'
|
||||
f'\033[31m>>> {warn_text}\033[0m'
|
||||
''
|
||||
)
|
||||
return
|
||||
if recipients_part_one and recipients_part_two:
|
||||
print(f'\033[32m>>> {split_help_text}\033[0m')
|
||||
files = self.create_excel(section='front')
|
||||
self.send_backup_mail(files, recipients_part_one)
|
||||
|
||||
files = self.create_excel(section='back')
|
||||
self.send_backup_mail(files, recipients_part_two)
|
||||
else:
|
||||
recipients = recipients_part_one or recipients_part_two
|
||||
files = self.create_excel()
|
||||
self.send_backup_mail(files, recipients)
|
||||
is_success = True
|
||||
finally:
|
||||
reason = error
|
||||
self.step_perform_task_update(is_success, reason)
|
||||
self.step_finished(is_success)
|
||||
|
||||
def run(self):
|
||||
print('{}: {}'.format(_('Plan start'), local_now_display()))
|
||||
self._run()
|
||||
logger.info('任务开始: {}'.format(local_now_display()))
|
||||
time_start = time.time()
|
||||
try:
|
||||
self._run()
|
||||
except Exception as e:
|
||||
logger.error('任务运行出现异常')
|
||||
logger.error('下面显示异常 Traceback 信息: ')
|
||||
logger.error(e, exc_info=True)
|
||||
finally:
|
||||
logger.info('\n任务结束: {}'.format(local_now_display()))
|
||||
timedelta = round((time.time() - time_start), 2)
|
||||
logger.info('用时: {}'.format(timedelta))
|
||||
|
||||
@@ -1,30 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import time
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils import timezone
|
||||
|
||||
from assets.automations.base.manager import BaseManager
|
||||
from common.utils import get_logger
|
||||
from common.utils.timezone import local_now_display
|
||||
|
||||
from .handlers import AccountBackupHandler
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AccountBackupManager:
|
||||
def __init__(self, execution):
|
||||
self.execution = execution
|
||||
self.date_start = timezone.now()
|
||||
self.time_start = time.time()
|
||||
self.date_end = None
|
||||
self.time_end = None
|
||||
self.timedelta = 0
|
||||
|
||||
class AccountBackupManager(BaseManager):
|
||||
def do_run(self):
|
||||
execution = self.execution
|
||||
account_backup_execution_being_executed = _('The account backup plan is being executed')
|
||||
print(f'\033[33m# {account_backup_execution_being_executed}\033[0m')
|
||||
handler = AccountBackupHandler(self, execution)
|
||||
logger.info('\n\033[33m# 账号备份计划正在执行\033[0m')
|
||||
handler = AccountBackupHandler(execution)
|
||||
handler.run()
|
||||
|
||||
def send_report_if_need(self):
|
||||
pass
|
||||
def pre_run(self):
|
||||
self.execution.date_start = self.date_start
|
||||
self.execution.save()
|
||||
|
||||
def print_summary(self):
|
||||
print('\n\n' + '-' * 80)
|
||||
plan_execution_end = _('Plan execution end')
|
||||
print('{} {}\n'.format(plan_execution_end, local_now_display()))
|
||||
time_cost = _('Duration')
|
||||
print('{}: {}s'.format(time_cost, self.duration))
|
||||
def post_run(self):
|
||||
self.time_end = time.time()
|
||||
self.date_end = timezone.now()
|
||||
|
||||
def get_report_template(self):
|
||||
return "accounts/backup_account_report.html"
|
||||
logger.info('\n\n' + '-' * 80)
|
||||
logger.info('计划执行结束 {}\n'.format(local_now_display()))
|
||||
self.timedelta = self.time_end - self.time_start
|
||||
logger.info('用时: {}s'.format(self.timedelta))
|
||||
self.execution.timedelta = self.timedelta
|
||||
self.execution.save()
|
||||
|
||||
def run(self):
|
||||
self.pre_run()
|
||||
self.do_run()
|
||||
self.post_run()
|
||||
|
||||
@@ -1,235 +1,12 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.automations.methods import platform_automation_methods
|
||||
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice, \
|
||||
ChangeSecretAccountStatus
|
||||
from accounts.models import BaseAccountQuerySet
|
||||
from accounts.utils import SecretGenerator, account_secret_task_status
|
||||
from assets.automations.base.manager import BasePlaybookManager
|
||||
from assets.const import HostTypes
|
||||
from common.db.utils import safe_atomic_db_connection
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AccountBasePlaybookManager(BasePlaybookManager):
|
||||
template_path = ''
|
||||
|
||||
@property
|
||||
def platform_automation_methods(self):
|
||||
return platform_automation_methods
|
||||
|
||||
|
||||
class BaseChangeSecretPushManager(AccountBasePlaybookManager):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||
self.secret_strategy = self.execution.snapshot.get(
|
||||
'secret_strategy', SecretStrategy.custom
|
||||
)
|
||||
self.ssh_key_change_strategy = self.execution.snapshot.get(
|
||||
'ssh_key_change_strategy', SSHKeyStrategy.set_jms
|
||||
)
|
||||
self.account_ids = self.execution.snapshot['accounts']
|
||||
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
|
||||
self.name_record_mapper = {} # 做个映射,方便后面处理
|
||||
|
||||
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_ssh_params(self, secret, secret_type):
|
||||
kwargs = {}
|
||||
if secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
kwargs['strategy'] = self.ssh_key_change_strategy
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
|
||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||
return kwargs
|
||||
|
||||
def get_secret(self, account):
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
new_secret = self.execution.snapshot['secret']
|
||||
else:
|
||||
generator = SecretGenerator(
|
||||
self.secret_strategy, self.secret_type,
|
||||
self.execution.snapshot.get('password_rules')
|
||||
)
|
||||
new_secret = generator.get_secret()
|
||||
return new_secret
|
||||
|
||||
def get_accounts(self, privilege_account) -> BaseAccountQuerySet | None:
|
||||
if not privilege_account:
|
||||
print('Not privilege account')
|
||||
return
|
||||
|
||||
asset = privilege_account.asset
|
||||
accounts = asset.all_accounts.all()
|
||||
accounts = accounts.filter(id__in=self.account_ids, secret_reset=True)
|
||||
|
||||
if self.secret_type:
|
||||
accounts = accounts.filter(secret_type=self.secret_type)
|
||||
|
||||
if settings.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED:
|
||||
accounts = accounts.filter(privileged=False).exclude(
|
||||
username__in=['root', 'administrator', privilege_account.username]
|
||||
)
|
||||
return accounts
|
||||
|
||||
def handle_ssh_secret(self, secret_type, new_secret, path_dir):
|
||||
private_key_path = None
|
||||
if secret_type == SecretType.SSH_KEY:
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
return new_secret, private_key_path
|
||||
|
||||
def gen_inventory(self, h, account, new_secret, private_key_path, asset):
|
||||
secret_type = account.secret_type
|
||||
h['ssh_params'].update(self.get_ssh_params(new_secret, secret_type))
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'full_username': account.full_username,
|
||||
'secret_type': secret_type,
|
||||
'secret': account.escape_jinja2_syntax(new_secret),
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
return h
|
||||
|
||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||
host = super().host_callback(
|
||||
host, asset=asset, account=account, automation=automation,
|
||||
path_dir=path_dir, **kwargs
|
||||
)
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
inventory_hosts = []
|
||||
if asset.type == HostTypes.WINDOWS:
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
host['error'] = _("Windows does not support SSH key authentication")
|
||||
return host
|
||||
new_secret = self.get_secret(account)
|
||||
if '>' in new_secret or '^' in new_secret:
|
||||
host['error'] = _("Windows password cannot contain special characters like > ^")
|
||||
return host
|
||||
|
||||
host['ssh_params'] = {}
|
||||
|
||||
accounts = self.get_accounts(account)
|
||||
existing_ids = set(map(str, accounts.values_list('id', flat=True)))
|
||||
missing_ids = set(map(str, self.account_ids)) - existing_ids
|
||||
|
||||
for account_id in missing_ids:
|
||||
self.clear_account_queue_status(account_id)
|
||||
|
||||
error_msg = _("No pending accounts found")
|
||||
if not accounts:
|
||||
print(f'{asset}: {error_msg}')
|
||||
return []
|
||||
|
||||
if asset.type == HostTypes.WINDOWS:
|
||||
accounts = accounts.filter(secret_type=SecretType.PASSWORD)
|
||||
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
h['name'] += '(' + account.username + ')' # To distinguish different accounts
|
||||
|
||||
account_status = account_secret_task_status.get_status(account.id)
|
||||
if account_status == ChangeSecretAccountStatus.PROCESSING:
|
||||
h['error'] = f'Account is already being processed, skipping: {account}'
|
||||
inventory_hosts.append(h)
|
||||
continue
|
||||
|
||||
try:
|
||||
h, record = self.gen_account_inventory(account, asset, h, path_dir)
|
||||
h['check_conn_after_change'] = record.execution.snapshot.get('check_conn_after_change', True)
|
||||
account_secret_task_status.set_status(
|
||||
account.id,
|
||||
ChangeSecretAccountStatus.PROCESSING,
|
||||
metadata={'execution_id': self.execution.id}
|
||||
)
|
||||
except Exception as e:
|
||||
h['error'] = str(e)
|
||||
self.clear_account_queue_status(account.id)
|
||||
|
||||
inventory_hosts.append(h)
|
||||
|
||||
return inventory_hosts
|
||||
|
||||
@staticmethod
|
||||
def save_record(record):
|
||||
record.save(update_fields=['error', 'status', 'date_finished'])
|
||||
|
||||
@staticmethod
|
||||
def clear_account_queue_status(account_id):
|
||||
account_secret_task_status.clear(account_id)
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
record = self.name_record_mapper.get(host)
|
||||
if not record:
|
||||
return
|
||||
record.status = ChangeSecretRecordStatusChoice.success.value
|
||||
record.date_finished = timezone.now()
|
||||
|
||||
account = record.account
|
||||
if not account:
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
|
||||
account.secret = getattr(record, 'new_secret', account.secret)
|
||||
account.date_updated = timezone.now()
|
||||
account.date_change_secret = timezone.now()
|
||||
account.change_secret_status = ChangeSecretRecordStatusChoice.success
|
||||
|
||||
self.summary['ok_accounts'] += 1
|
||||
self.result['ok_accounts'].append(
|
||||
{
|
||||
"asset": str(account.asset),
|
||||
"username": account.username,
|
||||
}
|
||||
)
|
||||
super().on_host_success(host, result)
|
||||
|
||||
with safe_atomic_db_connection():
|
||||
account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status'])
|
||||
self.save_record(record)
|
||||
self.clear_account_queue_status(account.id)
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
record = self.name_record_mapper.get(host)
|
||||
if not record:
|
||||
return
|
||||
record.status = ChangeSecretRecordStatusChoice.failed.value
|
||||
record.date_finished = timezone.now()
|
||||
record.error = error
|
||||
account = record.account
|
||||
if not account:
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
account.date_updated = timezone.now()
|
||||
account.date_change_secret = timezone.now()
|
||||
account.change_secret_status = ChangeSecretRecordStatusChoice.failed
|
||||
|
||||
self.summary['fail_accounts'] += 1
|
||||
self.result['fail_accounts'].append(
|
||||
{
|
||||
"asset": str(record.asset),
|
||||
"username": record.account.username,
|
||||
}
|
||||
)
|
||||
super().on_host_error(host, error, result)
|
||||
|
||||
with safe_atomic_db_connection():
|
||||
account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated'])
|
||||
self.save_record(record)
|
||||
self.clear_account_queue_status(account.id)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
|
||||
tasks:
|
||||
- name: Test privileged account (paramiko)
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ jms_custom_become | default(False) }}"
|
||||
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ jms_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) }}"
|
||||
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||
register: ping_info
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Change asset password (paramiko)
|
||||
custom_command:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ jms_custom_become | default(False) }}"
|
||||
become_method: "{{ jms_custom_become_method | default('su') }}"
|
||||
become_user: "{{ jms_custom_become_user | default('') }}"
|
||||
become_password: "{{ jms_custom_become_password | default('') }}"
|
||||
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
commands: "{{ params.commands }}"
|
||||
answers: "{{ params.answers }}"
|
||||
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||
delay_time: "{{ params.delay_time | default(2) }}"
|
||||
prompt: "{{ params.prompt | default('.*') }}"
|
||||
ignore_errors: true
|
||||
when: ping_info is succeeded and check_conn_after_change
|
||||
register: change_info
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Verify password (paramiko)
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
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) }}"
|
||||
recv_timeout: "{{ params.recv_timeout | default(30) }}"
|
||||
delegate_to: localhost
|
||||
when: check_conn_after_change
|
||||
@@ -1,132 +0,0 @@
|
||||
id: change_secret_by_ssh
|
||||
name: "{{ 'SSH account change secret' | trans }}"
|
||||
category:
|
||||
- device
|
||||
- host
|
||||
type:
|
||||
- all
|
||||
method: change_secret
|
||||
protocol: ssh
|
||||
priority: 50
|
||||
params:
|
||||
- name: commands
|
||||
type: text
|
||||
label: "{{ 'Params commands label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params commands help text' | trans }}"
|
||||
- name: recv_timeout
|
||||
type: int
|
||||
label: "{{ 'Params recv_timeout label' | trans }}"
|
||||
default: 30
|
||||
help_text: "{{ 'Params recv_timeout help text' | trans }}"
|
||||
- name: delay_time
|
||||
type: int
|
||||
label: "{{ 'Params delay_time label' | trans }}"
|
||||
default: 2
|
||||
help_text: "{{ 'Params delay_time help text' | trans }}"
|
||||
- name: prompt
|
||||
type: str
|
||||
label: "{{ 'Params prompt label' | trans }}"
|
||||
default: '.*'
|
||||
help_text: "{{ 'Params prompt help text' | trans }}"
|
||||
- name: answers
|
||||
type: text
|
||||
label: "{{ 'Params answer label' | trans }}"
|
||||
default: '.*'
|
||||
help_text: "{{ 'Params answer help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
SSH account change secret:
|
||||
zh: '使用 SSH 命令行自定义改密'
|
||||
ja: 'SSH コマンドライン方式でカスタムパスワード変更'
|
||||
en: 'Custom password change by SSH command line'
|
||||
|
||||
Params commands help text:
|
||||
zh: |
|
||||
请将命令中的指定位置改成特殊符号 <br />
|
||||
1. 改密账号 -> {username} <br />
|
||||
2. 改密密码 -> {password} <br />
|
||||
3. 登录用户密码 -> {login_password} <br />
|
||||
<strong>多条命令使用换行分割,</strong>执行任务时系统会根据特殊符号替换真实数据。<br />
|
||||
比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />
|
||||
enable <br />
|
||||
{login_password} <br />
|
||||
configure terminal <br />
|
||||
username {username} privilege 0 password {password} <br />
|
||||
end <br />
|
||||
ja: |
|
||||
コマンド内の指定された位置を特殊記号に変更してください。<br />
|
||||
新しいパスワード(アカウント変更) -> {username} <br />
|
||||
新しいパスワード(パスワード変更) -> {password} <br />
|
||||
ログインユーザーパスワード -> {login_password} <br />
|
||||
<strong>複数のコマンドは改行で区切り、</strong>タスクを実行するときにシステムは特殊記号を使用して実際のデータを置き換えます。<br />
|
||||
例えば、Cisco機器のパスワードを変更する場合、一般的には5つのコマンドを設定する必要があります:<br />
|
||||
enable <br />
|
||||
{login_password} <br />
|
||||
configure terminal <br />
|
||||
username {username} privilege 0 password {password} <br />
|
||||
end <br />
|
||||
en: |
|
||||
Please change the specified positions in the command to special symbols. <br />
|
||||
Change password account -> {username} <br />
|
||||
Change password -> {password} <br />
|
||||
Login user password -> {login_password} <br />
|
||||
<strong>Multiple commands are separated by new lines,</strong> and when executing tasks, <br />
|
||||
the system will replace the special symbols with real data. <br />
|
||||
For example, to change the password for a Cisco device, you generally need to configure five commands: <br />
|
||||
enable <br />
|
||||
{login_password} <br />
|
||||
configure terminal <br />
|
||||
username {username} privilege 0 password {password} <br />
|
||||
end <br />
|
||||
|
||||
Params commands label:
|
||||
zh: '自定义命令'
|
||||
ja: 'カスタムコマンド'
|
||||
en: 'Custom command'
|
||||
|
||||
Params recv_timeout label:
|
||||
zh: '超时时间'
|
||||
ja: 'タイムアウト'
|
||||
en: 'Timeout'
|
||||
|
||||
Params recv_timeout help text:
|
||||
zh: '等待命令结果返回的超时时间(秒)'
|
||||
ja: 'コマンドの結果を待つタイムアウト時間(秒)'
|
||||
en: 'The timeout for waiting for the command result to return (Seconds)'
|
||||
|
||||
Params delay_time label:
|
||||
zh: '延迟发送时间'
|
||||
ja: '遅延送信時間'
|
||||
en: 'Delayed send time'
|
||||
|
||||
Params delay_time help text:
|
||||
zh: '每条命令延迟发送的时间间隔(秒)'
|
||||
ja: '各コマンド送信の遅延間隔(秒)'
|
||||
en: 'Time interval for each command delay in sending (Seconds)'
|
||||
|
||||
Params prompt label:
|
||||
zh: '提示符'
|
||||
ja: 'ヒント'
|
||||
en: 'Prompt'
|
||||
|
||||
Params prompt help text:
|
||||
zh: '终端连接后显示的提示符信息(正则表达式)'
|
||||
ja: 'ターミナル接続後に表示されるプロンプト情報(正規表現)'
|
||||
en: 'Prompt information displayed after terminal connection (Regular expression)'
|
||||
|
||||
Params answer label:
|
||||
zh: '命令结果'
|
||||
ja: 'コマンド結果'
|
||||
en: 'Command result'
|
||||
|
||||
Params answer help text:
|
||||
zh: |
|
||||
根据结果匹配度决定是否执行下一条命令,输入框的内容和上方 “自定义命令” 内容按行一一对应(正则表达式)
|
||||
ja: |
|
||||
結果の一致度に基づいて次のコマンドを実行するかどうかを決定します。
|
||||
入力欄の内容は、上の「カスタムコマンド」の内容と行ごとに対応しています(せいきひょうげん)
|
||||
en: |
|
||||
Decide whether to execute the next command based on the result match.
|
||||
The input content corresponds line by line with the content
|
||||
of the `Custom command` above. (Regular expression)
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test MongoDB connection
|
||||
@@ -11,9 +11,9 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl | default('') }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
register: db_info
|
||||
@@ -31,15 +31,15 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
mongodb_ping:
|
||||
@@ -49,10 +49,10 @@
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
when: check_conn_after_change
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
id: change_secret_mongodb
|
||||
name: "{{ 'MongoDB account change secret' | trans }}"
|
||||
name: Change secret for MongoDB
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
MongoDB account change secret:
|
||||
zh: 使用 Ansible 模块 mongodb 执行 MongoDB 账号改密
|
||||
ja: Ansible mongodb モジュールを使用して MongoDB アカウントのパスワード変更
|
||||
en: Using Ansible module mongodb to change MongoDB account secret
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
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 }}"
|
||||
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:
|
||||
- name: Test MySQL connection
|
||||
@@ -15,10 +11,6 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@@ -32,17 +24,12 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}"
|
||||
append_privs: "{{ db_name != '' | bool }}"
|
||||
ignore_errors: true
|
||||
priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.mysql.mysql_info:
|
||||
@@ -50,9 +37,7 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: version
|
||||
when: check_conn_after_change
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
@@ -1,13 +1,7 @@
|
||||
id: change_secret_mysql
|
||||
name: "{{ 'MySQL account change secret' | trans }}"
|
||||
name: Change secret for MySQL
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
MySQL account change secret:
|
||||
zh: 使用 Ansible 模块 mysql 执行 MySQL 账号改密
|
||||
ja: Ansible mysql モジュールを使用して MySQL アカウントのパスワード変更
|
||||
en: Using Ansible module mysql to change MySQL account secret
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test Oracle connection
|
||||
@@ -29,8 +29,8 @@
|
||||
mode: "{{ jms_account.mode }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
ignore_errors: true
|
||||
when: db_info is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
oracle_ping:
|
||||
@@ -39,5 +39,6 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ account.mode }}"
|
||||
when: check_conn_after_change
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: change_secret_oracle
|
||||
name: "{{ 'Oracle account change secret' | trans }}"
|
||||
name: Change secret for Oracle
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
Oracle account change secret:
|
||||
zh: Oracle 账号改密
|
||||
ja: Oracle アカウントのパスワード変更
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
- hosts: postgre
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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('') }}"
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test PostgreSQL connection
|
||||
@@ -15,10 +11,6 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
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
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -36,13 +28,9 @@
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
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
|
||||
ignore_errors: true
|
||||
when: result is succeeded
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
community.postgresql.postgresql_ping:
|
||||
@@ -51,10 +39,8 @@
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
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: check_conn_after_change
|
||||
when:
|
||||
- result is succeeded
|
||||
- change_info is succeeded
|
||||
register: result
|
||||
failed_when: not result.is_available
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: change_secret_postgresql
|
||||
name: "{{ 'PostgreSQL account change secret' | trans }}"
|
||||
name: Change secret for PostgreSQL
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
PostgreSQL account change secret:
|
||||
zh: PostgreSQL 账号改密
|
||||
ja: PostgreSQL アカウントのパスワード変更
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
ansible_python_interpreter: /usr/local/bin/python
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
register: db_info
|
||||
@@ -25,53 +23,47 @@
|
||||
var: info
|
||||
|
||||
- name: Check whether SQLServer User exist
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
|
||||
when: db_info is succeeded
|
||||
register: user_exist
|
||||
|
||||
- name: Change SQLServer password
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
|
||||
ignore_errors: true
|
||||
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
when: user_exist.query_results[0] | length != 0
|
||||
register: change_info
|
||||
|
||||
- name: Add SQLServer user
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version"
|
||||
ignore_errors: true
|
||||
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}'; select @@version"
|
||||
when: user_exist.query_results[0] | length == 0
|
||||
register: change_info
|
||||
|
||||
- name: Verify password
|
||||
mssql_script:
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT @@version
|
||||
when: check_conn_after_change
|
||||
when:
|
||||
- db_info is succeeded
|
||||
- change_info is succeeded
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
id: change_secret_sqlserver
|
||||
name: "{{ 'SQLServer account change secret' | trans }}"
|
||||
name: Change secret for SQLServer
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: change_secret
|
||||
|
||||
i18n:
|
||||
SQLServer account change secret:
|
||||
zh: SQLServer 账号改密
|
||||
ja: SQLServer アカウントのパスワード変更
|
||||
|
||||
@@ -1,123 +1,54 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Test privileged {{ jms_account.username }} account"
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: "Check if {{ account.username }} user exists"
|
||||
getent:
|
||||
database: passwd
|
||||
key: "{{ account.username }}"
|
||||
register: user_info
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
group: "{{ params.group if params.group | length > 0 else omit }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: "{{ true if params.groups | length > 0 else false }}"
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.msg is defined
|
||||
|
||||
- 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.msg is defined or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
- name: Change password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('des') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
- name: create user If it already exists, no operation will be performed
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- 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
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
- secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: "Verify {{ account.username }} password (paramiko)"
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
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"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
@@ -1,108 +1,6 @@
|
||||
id: change_secret_aix
|
||||
name: "{{ 'AIX account change secret' | trans }}"
|
||||
name: Change secret for aix
|
||||
category: host
|
||||
type:
|
||||
- AIX
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: group
|
||||
type: str
|
||||
label: "{{ 'Params group label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params group help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
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:
|
||||
AIX account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (DES)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントのパスワード変更 (DES)'
|
||||
en: 'Using Ansible module user to change account secret (DES)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
Params home help text:
|
||||
zh: '默认家目录 /home/{账号用户名}'
|
||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||
en: 'Default home directory /home/{account username}'
|
||||
|
||||
Params group help text:
|
||||
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
|
||||
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
|
||||
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
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:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
en: 'Home'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params group label:
|
||||
zh: '主组'
|
||||
ja: '主组'
|
||||
en: 'Main group'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -1,123 +1,54 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Test privileged {{ jms_account.username }} account"
|
||||
- name: Test privileged account
|
||||
ansible.builtin.ping:
|
||||
|
||||
- name: "Check if {{ account.username }} user exists"
|
||||
getent:
|
||||
database: passwd
|
||||
key: "{{ account.username }}"
|
||||
register: user_info
|
||||
failed_when: false
|
||||
changed_when: false
|
||||
|
||||
- name: "Add {{ account.username }} user"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
|
||||
shell: "{{ params.shell if params.shell | length > 0 else omit }}"
|
||||
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
|
||||
group: "{{ params.group if params.group | length > 0 else omit }}"
|
||||
groups: "{{ params.groups if params.groups | length > 0 else omit }}"
|
||||
append: "{{ true if params.groups | length > 0 else false }}"
|
||||
expires: -1
|
||||
state: present
|
||||
when: user_info.msg is defined
|
||||
|
||||
- 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.msg is defined or params.modify_sudo
|
||||
- params.sudo
|
||||
|
||||
- name: "Change {{ account.username }} password"
|
||||
- name: Change password
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret | password_hash('sha512') }}"
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "password"
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: "Get home directory for {{ account.username }}"
|
||||
ansible.builtin.shell: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: home_dir
|
||||
when: account.secret_type == "ssh_key"
|
||||
ignore_errors: yes
|
||||
- name: create user If it already exists, no operation will be performed
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- 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
|
||||
- name: remove jumpserver ssh key
|
||||
ansible.builtin.lineinfile:
|
||||
dest: "{{ home_dir.stdout.strip() }}/.ssh/authorized_keys"
|
||||
regexp: "{{ ssh_params.regexp }}"
|
||||
dest: "{{ kwargs.dest }}"
|
||||
regexp: "{{ kwargs.regexp }}"
|
||||
state: absent
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- ssh_params.strategy == "set_jms"
|
||||
ignore_errors: yes
|
||||
- secret_type == "ssh_key"
|
||||
- kwargs.strategy == "set_jms"
|
||||
|
||||
- name: "Change {{ account.username }} SSH key"
|
||||
- name: Change SSH key
|
||||
ansible.builtin.authorized_key:
|
||||
user: "{{ account.username }}"
|
||||
key: "{{ account.secret }}"
|
||||
exclusive: "{{ ssh_params.exclusive }}"
|
||||
register: change_secret_result
|
||||
when: account.secret_type == "ssh_key"
|
||||
exclusive: "{{ kwargs.exclusive }}"
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: "Verify {{ account.username }} password (paramiko)"
|
||||
ssh_ping:
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||
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"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
- name: Verify password
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
ansible_become: no
|
||||
when: secret_type == "password"
|
||||
|
||||
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_private_key_path: "{{ account.private_key_path }}"
|
||||
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
|
||||
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
|
||||
when:
|
||||
- account.secret_type == "ssh_key"
|
||||
- check_conn_after_change or change_secret_result.failed | default(false)
|
||||
delegate_to: localhost
|
||||
- name: Verify SSH key
|
||||
ansible.builtin.ping:
|
||||
become: no
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
||||
ansible_become: no
|
||||
when: secret_type == "ssh_key"
|
||||
|
||||
@@ -1,110 +1,7 @@
|
||||
id: change_secret_posix
|
||||
name: "{{ 'Posix account change secret' | trans }}"
|
||||
name: Change secret for posix
|
||||
category: host
|
||||
type:
|
||||
- unix
|
||||
- linux
|
||||
method: change_secret
|
||||
params:
|
||||
- name: modify_sudo
|
||||
type: bool
|
||||
label: "{{ 'Modify sudo label' | trans }}"
|
||||
default: False
|
||||
help_text: "{{ 'Modify params sudo help text' | trans }}"
|
||||
|
||||
- name: sudo
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
label: 'Shell'
|
||||
default: '/bin/bash'
|
||||
help_text: ''
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: group
|
||||
type: str
|
||||
label: "{{ 'Params group label' | trans }}"
|
||||
default: ''
|
||||
help_text: "{{ 'Params group help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
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:
|
||||
Posix account change secret:
|
||||
zh: '使用 Ansible 模块 user 执行账号改密 (SHA512)'
|
||||
ja: 'Ansible user モジュールを使用して アカウントのパスワード変更 (SHA512)'
|
||||
en: 'Using Ansible module user to change account secret (SHA512)'
|
||||
|
||||
Modify params sudo help text:
|
||||
zh: '如果用户存在,可以修改sudo权限'
|
||||
ja: 'ユーザーが存在する場合、sudo権限を変更できます'
|
||||
en: 'If the user exists, sudo permissions can be modified'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
Params home help text:
|
||||
zh: '默认家目录 /home/{账号用户名}'
|
||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||
en: 'Default home directory /home/{account username}'
|
||||
|
||||
Params group help text:
|
||||
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
|
||||
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
|
||||
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
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:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
en: 'Home'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
Params group label:
|
||||
zh: '主组'
|
||||
ja: '主组'
|
||||
en: 'Main group'
|
||||
|
||||
Params uid label:
|
||||
zh: '用户ID'
|
||||
ja: 'ユーザーID'
|
||||
en: 'User ID'
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
# - name: Print variables
|
||||
# debug:
|
||||
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||
|
||||
|
||||
- name: Get groups of a Windows user
|
||||
ansible.windows.win_user:
|
||||
name: "{{ jms_account.username }}"
|
||||
register: user_info
|
||||
|
||||
- name: Change password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups: "{{ user_info.groups[0].name }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
@@ -24,4 +31,4 @@
|
||||
vars:
|
||||
ansible_user: "{{ account.username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password" and check_conn_after_change
|
||||
when: account.secret_type == "password"
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
id: change_secret_local_windows
|
||||
name: "{{ 'Windows account change secret' | trans }}"
|
||||
name: Change secret local account for Windows
|
||||
version: 1
|
||||
method: change_secret
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account change secret:
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密'
|
||||
ja: 'Ansible win_user モジュールを使用して Windows アカウントのパスワード変更'
|
||||
en: 'Using Ansible module win_user to change Windows account secret'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
- name: Change password
|
||||
community.windows.win_domain_user:
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
update_password: always
|
||||
password_never_expires: yes
|
||||
state: present
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password
|
||||
ansible.windows.win_ping:
|
||||
vars:
|
||||
ansible_user: "{{ account.full_username }}"
|
||||
ansible_password: "{{ account.secret }}"
|
||||
when: account.secret_type == "password" and check_conn_after_change
|
||||
@@ -1,32 +0,0 @@
|
||||
id: change_secret_ad_windows
|
||||
name: "{{ 'Windows account change secret' | trans }}"
|
||||
version: 1
|
||||
method: change_secret
|
||||
category:
|
||||
- ds
|
||||
type:
|
||||
- windows_ad
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account change secret:
|
||||
zh: '使用 Ansible 模块 win_domain_user 执行 Windows 账号改密'
|
||||
ja: 'Ansible win_domain_user モジュールを使用して Windows アカウントのパスワード変更'
|
||||
en: 'Using Ansible module win_domain_user to change Windows account secret'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Test privileged account
|
||||
ansible.windows.win_ping:
|
||||
|
||||
- name: Change password
|
||||
ansible.windows.win_user:
|
||||
fullname: "{{ account.username}}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
password_never_expires: yes
|
||||
groups: "{{ params.groups }}"
|
||||
groups_action: add
|
||||
update_password: always
|
||||
ignore_errors: true
|
||||
when: account.secret_type == "password"
|
||||
|
||||
- name: Refresh connection
|
||||
ansible.builtin.meta: reset_connection
|
||||
|
||||
- name: Verify password (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.origin_address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
gateway_args: "{{ jms_gateway | default({}) }}"
|
||||
when: account.secret_type == "password" and check_conn_after_change
|
||||
delegate_to: localhost
|
||||
@@ -1,32 +0,0 @@
|
||||
id: change_secret_windows_rdp_verify
|
||||
name: "{{ 'Windows account change secret rdp verify' | trans }}"
|
||||
version: 1
|
||||
method: change_secret
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
priority: 49
|
||||
params:
|
||||
- name: groups
|
||||
type: str
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
|
||||
i18n:
|
||||
Windows account change secret rdp verify:
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密(最后使用 Python 模块 pyfreerdp 验证账号的可连接性)'
|
||||
ja: 'Ansible モジュール win_user を使用して Windows アカウントのパスワードを変更します (最後に Python モジュール pyfreerdp を使用してアカウントの接続を確認します)'
|
||||
en: 'Use the Ansible module win_user to change the Windows account password (finally use the Python module pyfreerdp to verify the account connectivity)'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
@@ -1,57 +1,166 @@
|
||||
import os
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from xlsxwriter import Workbook
|
||||
from django.utils import timezone
|
||||
from openpyxl import Workbook
|
||||
|
||||
from accounts.const import (
|
||||
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
|
||||
)
|
||||
from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy
|
||||
from accounts.models import ChangeSecretRecord
|
||||
from accounts.notifications import ChangeSecretExecutionTaskMsg
|
||||
from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
||||
from common.utils import get_logger
|
||||
from assets.const import HostTypes
|
||||
from common.utils import get_logger, lazyproperty
|
||||
from common.utils.file import encrypt_and_compress_zip_file
|
||||
from common.utils.timezone import local_now_filename
|
||||
from ..base.manager import BaseChangeSecretPushManager
|
||||
from common.utils.timezone import local_now_display
|
||||
from users.models import User
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
from ...utils import SecretGenerator
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||
class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
ansible_account_prefer = ''
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.method_hosts_mapper = defaultdict(list)
|
||||
self.secret_type = self.execution.snapshot['secret_type']
|
||||
self.secret_strategy = self.execution.snapshot.get(
|
||||
'secret_strategy', SecretStrategy.custom
|
||||
)
|
||||
self.ssh_key_change_strategy = self.execution.snapshot.get(
|
||||
'ssh_key_change_strategy', SSHKeyStrategy.add
|
||||
)
|
||||
self.snapshot_account_usernames = self.execution.snapshot['accounts']
|
||||
self.name_recorder_mapper = {} # 做个映射,方便后面处理
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.change_secret
|
||||
|
||||
def gen_account_inventory(self, account, asset, h, path_dir):
|
||||
record = self.get_or_create_record(asset, account, h['name'])
|
||||
new_secret, private_key_path = self.handle_ssh_secret(account.secret_type, record.new_secret, path_dir)
|
||||
h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
|
||||
return h, record
|
||||
def get_kwargs(self, account, secret):
|
||||
kwargs = {}
|
||||
if self.secret_type != SecretType.SSH_KEY:
|
||||
return kwargs
|
||||
kwargs['strategy'] = self.ssh_key_change_strategy
|
||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||
|
||||
def get_or_create_record(self, asset, account, name):
|
||||
asset_account_id = f'{asset.id}-{account.id}'
|
||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||
kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username)
|
||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||
return kwargs
|
||||
|
||||
if asset_account_id in self.record_map:
|
||||
record_id = self.record_map[asset_account_id]
|
||||
record = ChangeSecretRecord.objects.filter(id=record_id).first()
|
||||
else:
|
||||
new_secret = self.get_secret(account)
|
||||
record = self.create_record(asset, account, new_secret)
|
||||
|
||||
self.name_record_mapper[name] = record
|
||||
return record
|
||||
|
||||
def create_record(self, asset, account, new_secret):
|
||||
record = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
comment=f'{account.username}@{asset.address}'
|
||||
@lazyproperty
|
||||
def secret_generator(self):
|
||||
return SecretGenerator(
|
||||
self.secret_strategy, self.secret_type,
|
||||
self.execution.snapshot.get('password_rules')
|
||||
)
|
||||
return record
|
||||
|
||||
def get_secret(self):
|
||||
if self.secret_strategy == SecretStrategy.custom:
|
||||
return self.execution.snapshot['secret']
|
||||
else:
|
||||
return self.secret_generator.get_secret()
|
||||
|
||||
def host_callback(
|
||||
self, host, asset=None, account=None,
|
||||
automation=None, path_dir=None, **kwargs
|
||||
):
|
||||
host = super().host_callback(
|
||||
host, asset=asset, account=account, automation=automation,
|
||||
path_dir=path_dir, **kwargs
|
||||
)
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
accounts = asset.accounts.all()
|
||||
if account:
|
||||
accounts = accounts.exclude(username=account.username)
|
||||
|
||||
if '*' not in self.snapshot_account_usernames:
|
||||
accounts = accounts.filter(username__in=self.snapshot_account_usernames)
|
||||
|
||||
accounts = accounts.filter(secret_type=self.secret_type)
|
||||
if not accounts:
|
||||
print('没有发现待改密账号: %s 用户名: %s 类型: %s' % (
|
||||
asset.name, self.snapshot_account_usernames, self.secret_type
|
||||
))
|
||||
return []
|
||||
|
||||
method_attr = getattr(automation, self.method_type() + '_method')
|
||||
method_hosts = self.method_hosts_mapper[method_attr]
|
||||
method_hosts = [h for h in method_hosts if h != host['name']]
|
||||
inventory_hosts = []
|
||||
records = []
|
||||
host['secret_type'] = self.secret_type
|
||||
|
||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||
print(f'Windows {asset} does not support ssh key push \n')
|
||||
return inventory_hosts
|
||||
|
||||
for account in accounts:
|
||||
h = deepcopy(host)
|
||||
h['name'] += '(' + account.username + ')'
|
||||
new_secret = self.get_secret()
|
||||
|
||||
recorder = ChangeSecretRecord(
|
||||
asset=asset, account=account, execution=self.execution,
|
||||
old_secret=account.secret, new_secret=new_secret,
|
||||
)
|
||||
records.append(recorder)
|
||||
self.name_recorder_mapper[h['name']] = recorder
|
||||
|
||||
private_key_path = None
|
||||
if self.secret_type == SecretType.SSH_KEY:
|
||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
||||
new_secret = self.generate_public_key(new_secret)
|
||||
|
||||
h['kwargs'] = self.get_kwargs(account, new_secret)
|
||||
h['account'] = {
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': account.secret_type,
|
||||
'secret': new_secret,
|
||||
'private_key_path': private_key_path
|
||||
}
|
||||
if asset.platform.type == 'oracle':
|
||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||
inventory_hosts.append(h)
|
||||
method_hosts.append(h['name'])
|
||||
self.method_hosts_mapper[method_attr] = method_hosts
|
||||
ChangeSecretRecord.objects.bulk_create(records)
|
||||
return inventory_hosts
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = 'success'
|
||||
recorder.date_finished = timezone.now()
|
||||
recorder.save()
|
||||
account = recorder.account
|
||||
if not account:
|
||||
print("Account not found, deleted ?")
|
||||
return
|
||||
account.secret = recorder.new_secret
|
||||
account.save(update_fields=['secret'])
|
||||
|
||||
def on_host_error(self, host, error, result):
|
||||
recorder = self.name_recorder_mapper.get(host)
|
||||
if not recorder:
|
||||
return
|
||||
recorder.status = 'failed'
|
||||
recorder.date_finished = timezone.now()
|
||||
recorder.error = error
|
||||
recorder.save()
|
||||
|
||||
def on_runner_failed(self, runner, e):
|
||||
logger.error("Change secret error: ", e)
|
||||
|
||||
def check_secret(self):
|
||||
if self.secret_strategy == SecretStrategy.custom \
|
||||
@@ -60,66 +169,41 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def get_summary(records):
|
||||
total, succeed, failed = 0, 0, 0
|
||||
for record in records:
|
||||
if record.status == ChangeSecretRecordStatusChoice.success.value:
|
||||
succeed += 1
|
||||
else:
|
||||
failed += 1
|
||||
total += 1
|
||||
summary = _('Success: %s, Failed: %s, Total: %s') % (succeed, failed, total)
|
||||
return summary
|
||||
|
||||
def print_summary(self):
|
||||
records = list(self.name_record_mapper.values())
|
||||
summary = self.get_summary(records)
|
||||
print('\n\n' + '-' * 80)
|
||||
plan_execution_end = _('Plan execution end')
|
||||
print('{} {}\n'.format(plan_execution_end, local_now_filename()))
|
||||
time_cost = _('Duration')
|
||||
print('{}: {}s'.format(time_cost, self.duration))
|
||||
print(summary)
|
||||
|
||||
def send_report_if_need(self, *args, **kwargs):
|
||||
if self.secret_type and not self.check_secret():
|
||||
return
|
||||
|
||||
records = list(self.name_record_mapper.values())
|
||||
if self.record_map:
|
||||
def run(self, *args, **kwargs):
|
||||
if not self.check_secret():
|
||||
return
|
||||
super().run(*args, **kwargs)
|
||||
recorders = self.name_recorder_mapper.values()
|
||||
recorders = list(recorders)
|
||||
self.send_recorder_mail(recorders)
|
||||
|
||||
def send_recorder_mail(self, recorders):
|
||||
recipients = self.execution.recipients
|
||||
if not recipients:
|
||||
if not recorders or not recipients:
|
||||
return
|
||||
|
||||
if not records:
|
||||
return
|
||||
recipients = User.objects.filter(id__in=list(recipients.keys()))
|
||||
|
||||
summary = self.get_summary(records)
|
||||
self.send_record_mail(recipients, records, summary)
|
||||
|
||||
def send_record_mail(self, recipients, records, summary):
|
||||
name = self.execution.snapshot['name']
|
||||
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
|
||||
if not self.create_file(records, filename):
|
||||
filename = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.xlsx')
|
||||
if not self.create_file(recorders, filename):
|
||||
return
|
||||
|
||||
for user in recipients:
|
||||
attachments = []
|
||||
if user.secret_key:
|
||||
attachment = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, user.secret_key, [filename])
|
||||
password = user.secret_key.encode('utf8')
|
||||
attachment = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.zip')
|
||||
encrypt_and_compress_zip_file(attachment, password, [filename])
|
||||
attachments = [attachment]
|
||||
ChangeSecretExecutionTaskMsg(name, user, summary).publish(attachments)
|
||||
ChangeSecretExecutionTaskMsg(name, user).publish(attachments)
|
||||
os.remove(filename)
|
||||
|
||||
@staticmethod
|
||||
def create_file(records, filename):
|
||||
def create_file(recorders, filename):
|
||||
serializer_cls = ChangeSecretRecordBackUpSerializer
|
||||
serializer = serializer_cls(records, many=True)
|
||||
serializer = serializer_cls(recorders, many=True)
|
||||
|
||||
header = [str(v.label) for v in serializer.child.fields.values()]
|
||||
rows = [[str(i) for i in row.values()] for row in serializer.data]
|
||||
@@ -128,12 +212,8 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
|
||||
|
||||
rows.insert(0, header)
|
||||
wb = Workbook(filename)
|
||||
ws = wb.add_worksheet('Sheet1')
|
||||
for row_index, row_data in enumerate(rows):
|
||||
for col_index, col_data in enumerate(row_data):
|
||||
ws.write_string(row_index, col_index, col_data)
|
||||
wb.close()
|
||||
ws = wb.create_sheet('Sheet1')
|
||||
for row in rows:
|
||||
ws.append(row)
|
||||
wb.save(filename)
|
||||
return True
|
||||
|
||||
def get_report_template(self):
|
||||
return "accounts/change_secret_report.html"
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
|
||||
def is_weak_password(password):
|
||||
if len(password) < 8:
|
||||
return True
|
||||
|
||||
# 判断是否只有一种字符类型
|
||||
if password.isdigit() or password.isalpha():
|
||||
return True
|
||||
|
||||
# 判断是否只包含数字或字母
|
||||
if password.islower() or password.isupper():
|
||||
return True
|
||||
|
||||
# 判断是否包含常见弱密码
|
||||
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
|
||||
if password.lower() in common_passwords:
|
||||
return True
|
||||
|
||||
# 正则表达式判断字符多样性(数字、字母、特殊字符)
|
||||
if (
|
||||
not re.search(r"[A-Za-z]", password)
|
||||
or not re.search(r"[0-9]", password)
|
||||
or not re.search(r"[\W_]", password)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_it(fname):
|
||||
count = 0
|
||||
lines = []
|
||||
with open(fname, 'rb') as f:
|
||||
for line in f:
|
||||
try:
|
||||
line = line.decode().strip()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
if len(line) > 32:
|
||||
continue
|
||||
|
||||
if is_weak_password(line):
|
||||
continue
|
||||
|
||||
lines.append(line)
|
||||
count += 0
|
||||
print(line)
|
||||
return lines
|
||||
|
||||
|
||||
def insert_to_db(lines):
|
||||
conn = sqlite3.connect('./leak_passwords.db')
|
||||
cursor = conn.cursor()
|
||||
create_table_sql = '''
|
||||
CREATE TABLE IF NOT EXISTS passwords (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
password CHAR(32)
|
||||
)
|
||||
'''
|
||||
create_index_sql = 'CREATE INDEX IF NOT EXISTS idx_password ON passwords(password)'
|
||||
cursor.execute(create_table_sql)
|
||||
cursor.execute(create_index_sql)
|
||||
|
||||
for line in lines:
|
||||
cursor.execute('INSERT INTO passwords (password) VALUES (?)', [line])
|
||||
conn.commit()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filename = sys.argv[1]
|
||||
lines = parse_it(filename)
|
||||
insert_to_db(lines)
|
||||
@@ -1,292 +0,0 @@
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import Account, AccountRisk, RiskChoice
|
||||
from assets.automations.base.manager import BaseManager
|
||||
from common.const import ConfirmOrIgnore
|
||||
from common.decorators import bulk_create_decorator, bulk_update_decorator
|
||||
from settings.models import LeakPasswords
|
||||
|
||||
|
||||
# 已设置手动 finish
|
||||
@bulk_create_decorator(AccountRisk)
|
||||
def create_risk(data):
|
||||
return AccountRisk(**data)
|
||||
|
||||
|
||||
# 已设置手动 finish
|
||||
@bulk_update_decorator(AccountRisk, update_fields=["details", "status"])
|
||||
def update_risk(risk):
|
||||
return risk
|
||||
|
||||
|
||||
class BaseCheckHandler:
|
||||
risk = ''
|
||||
|
||||
def __init__(self, assets):
|
||||
self.assets = assets
|
||||
|
||||
def check(self, account):
|
||||
pass
|
||||
|
||||
def clean(self):
|
||||
pass
|
||||
|
||||
|
||||
class CheckSecretHandler(BaseCheckHandler):
|
||||
risk = RiskChoice.weak_password
|
||||
|
||||
@staticmethod
|
||||
def is_weak_password(password):
|
||||
# 判断密码长度
|
||||
if len(password) < 8:
|
||||
return True
|
||||
|
||||
# 判断是否只有一种字符类型
|
||||
if password.isdigit() or password.isalpha():
|
||||
return True
|
||||
|
||||
# 判断是否只包含数字或字母
|
||||
if password.islower() or password.isupper():
|
||||
return True
|
||||
|
||||
# 判断是否包含常见弱密码
|
||||
common_passwords = ["123456", "password", "12345678", "qwerty", "abc123"]
|
||||
if password.lower() in common_passwords:
|
||||
return True
|
||||
|
||||
# 正则表达式判断字符多样性(数字、字母、特殊字符)
|
||||
if (
|
||||
not re.search(r"[A-Za-z]", password)
|
||||
or not re.search(r"[0-9]", password)
|
||||
or not re.search(r"[\W_]", password)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def check(self, account):
|
||||
if not account.secret:
|
||||
return False
|
||||
return self.is_weak_password(account.secret)
|
||||
|
||||
|
||||
class CheckRepeatHandler(BaseCheckHandler):
|
||||
risk = RiskChoice.repeated_password
|
||||
|
||||
def __init__(self, assets):
|
||||
super().__init__(assets)
|
||||
self.path, self.conn, self.cursor = self.init_repeat_check_db()
|
||||
self.add_password_for_check_repeat()
|
||||
|
||||
@staticmethod
|
||||
def init_repeat_check_db():
|
||||
path = os.path.join('/tmp', 'accounts_' + str(uuid.uuid4()) + '.db')
|
||||
sql = """
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
digest CHAR(32)
|
||||
)
|
||||
"""
|
||||
index = "CREATE INDEX IF NOT EXISTS idx_digest ON accounts(digest)"
|
||||
conn = sqlite3.connect(path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(sql)
|
||||
cursor.execute(index)
|
||||
return path, conn, cursor
|
||||
|
||||
def check(self, account):
|
||||
if not account.secret:
|
||||
return False
|
||||
|
||||
digest = self.digest(account.secret)
|
||||
sql = 'SELECT COUNT(*) FROM accounts WHERE digest = ?'
|
||||
self.cursor.execute(sql, [digest])
|
||||
result = self.cursor.fetchone()
|
||||
if not result:
|
||||
return False
|
||||
return result[0] > 1
|
||||
|
||||
@staticmethod
|
||||
def digest(secret):
|
||||
return hashlib.md5(secret.encode()).hexdigest()
|
||||
|
||||
def add_password_for_check_repeat(self):
|
||||
accounts = Account.objects.all().only('id', '_secret', 'secret_type')
|
||||
sql = "INSERT INTO accounts (digest) VALUES (?)"
|
||||
|
||||
for account in accounts:
|
||||
secret = account.secret
|
||||
if not secret:
|
||||
continue
|
||||
digest = self.digest(secret)
|
||||
self.cursor.execute(sql, [digest])
|
||||
self.conn.commit()
|
||||
|
||||
def clean(self):
|
||||
self.cursor.close()
|
||||
self.conn.close()
|
||||
os.remove(self.path)
|
||||
|
||||
|
||||
class CheckLeakHandler(BaseCheckHandler):
|
||||
risk = RiskChoice.leaked_password
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.conn, self.cursor = self.init_leak_password_db()
|
||||
|
||||
@staticmethod
|
||||
def init_leak_password_db():
|
||||
db_path = os.path.join(
|
||||
settings.APPS_DIR, 'accounts', 'automations',
|
||||
'check_account', 'leak_passwords.db'
|
||||
)
|
||||
|
||||
if settings.LEAK_PASSWORD_DB_PATH and os.path.isfile(settings.LEAK_PASSWORD_DB_PATH):
|
||||
db_path = settings.LEAK_PASSWORD_DB_PATH
|
||||
|
||||
db_conn = sqlite3.connect(db_path)
|
||||
db_cursor = db_conn.cursor()
|
||||
return db_conn, db_cursor
|
||||
|
||||
def check(self, account):
|
||||
if not account.secret:
|
||||
return False
|
||||
|
||||
is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists()
|
||||
return is_exist
|
||||
|
||||
def clean(self):
|
||||
self.cursor.close()
|
||||
self.conn.close()
|
||||
|
||||
|
||||
class CheckAccountManager(BaseManager):
|
||||
batch_size = 100
|
||||
tmpl = 'Checked the status of account %s: %s'
|
||||
|
||||
def __init__(self, execution):
|
||||
super().__init__(execution)
|
||||
self.assets = []
|
||||
self.batch_risks = []
|
||||
self.handlers = []
|
||||
|
||||
def add_risk(self, risk, account):
|
||||
self.summary[risk] += 1
|
||||
self.result[risk].append({
|
||||
'asset': str(account.asset), 'username': account.username,
|
||||
})
|
||||
risk_obj = {'account': account, 'risk': risk}
|
||||
self.batch_risks.append(risk_obj)
|
||||
|
||||
def commit_risks(self, assets):
|
||||
account_risks = AccountRisk.objects.filter(asset__in=assets)
|
||||
ori_risk_map = {}
|
||||
|
||||
for risk in account_risks:
|
||||
key = f'{risk.account_id}_{risk.risk}'
|
||||
ori_risk_map[key] = risk
|
||||
|
||||
now = timezone.now().isoformat()
|
||||
for d in self.batch_risks:
|
||||
account = d["account"]
|
||||
key = f'{account.id}_{d["risk"]}'
|
||||
origin_risk = ori_risk_map.get(key)
|
||||
|
||||
if origin_risk and origin_risk.status != ConfirmOrIgnore.pending:
|
||||
details = origin_risk.details or []
|
||||
details.append({"datetime": now, 'type': 'refind'})
|
||||
|
||||
if len(details) > 10:
|
||||
details = [*details[:5], *details[-5:]]
|
||||
|
||||
origin_risk.details = details
|
||||
origin_risk.status = ConfirmOrIgnore.pending
|
||||
update_risk(origin_risk)
|
||||
else:
|
||||
create_risk({
|
||||
"account": account,
|
||||
"asset": account.asset,
|
||||
"username": account.username,
|
||||
"risk": d["risk"],
|
||||
"details": [{"datetime": now, 'type': 'init'}],
|
||||
})
|
||||
|
||||
create_risk.finish()
|
||||
update_risk.finish()
|
||||
|
||||
def pre_run(self):
|
||||
super().pre_run()
|
||||
self.assets = self.execution.get_all_assets()
|
||||
|
||||
def batch_check(self, handler):
|
||||
print("Engine: {}".format(handler.__class__.__name__))
|
||||
for i in range(0, len(self.assets), self.batch_size):
|
||||
_assets = self.assets[i: i + self.batch_size]
|
||||
accounts = Account.objects.filter(asset__in=_assets)
|
||||
|
||||
print("Start to check accounts: {}".format(len(accounts)))
|
||||
|
||||
for account in accounts:
|
||||
error = handler.check(account)
|
||||
msg = handler.risk if error else 'ok'
|
||||
|
||||
print("Check: {} => {}".format(account, msg))
|
||||
if not error:
|
||||
AccountRisk.objects.filter(
|
||||
asset=account.asset,
|
||||
username=account.username,
|
||||
risk=handler.risk
|
||||
).delete()
|
||||
continue
|
||||
self.add_risk(handler.risk, account)
|
||||
self.commit_risks(_assets)
|
||||
|
||||
def do_run(self, *args, **kwargs):
|
||||
engines = self.execution.snapshot.get("engines", [])
|
||||
if engines == '__all__':
|
||||
engines = ['check_account_secret', 'check_account_repeat', 'check_account_leak']
|
||||
|
||||
for engine in engines:
|
||||
if engine == "check_account_secret":
|
||||
handler = CheckSecretHandler(self.assets)
|
||||
elif engine == "check_account_repeat":
|
||||
handler = CheckRepeatHandler(self.assets)
|
||||
elif engine == "check_account_leak":
|
||||
handler = CheckLeakHandler(self.assets)
|
||||
else:
|
||||
print("Unknown engine: {}".format(engine))
|
||||
continue
|
||||
|
||||
self.handlers.append(handler)
|
||||
self.batch_check(handler)
|
||||
|
||||
def post_run(self):
|
||||
super().post_run()
|
||||
for handler in self.handlers:
|
||||
handler.clean()
|
||||
|
||||
def get_report_subject(self):
|
||||
return _("Check account report of {}").format(self.execution.id)
|
||||
|
||||
def get_report_template(self):
|
||||
return "accounts/check_account_report.html"
|
||||
|
||||
def print_summary(self):
|
||||
tmpl = _("---\nSummary: \nok: {}, weak password: {}, leaked password: {}, "
|
||||
"repeated password: {}, no secret: {}, using time: {}s").format(
|
||||
self.summary["ok"],
|
||||
self.summary[RiskChoice.weak_password],
|
||||
self.summary[RiskChoice.leaked_password],
|
||||
self.summary[RiskChoice.repeated_password],
|
||||
self.summary["no_secret"],
|
||||
self.duration
|
||||
)
|
||||
print(tmpl)
|
||||
@@ -1,10 +1,8 @@
|
||||
from .backup_account.manager import AccountBackupManager
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .check_account.manager import CheckAccountManager
|
||||
from .gather_account.manager import GatherAccountsManager
|
||||
from .push_account.manager import PushAccountManager
|
||||
from .remove_account.manager import RemoveAccountManager
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .verify_account.manager import VerifyAccountManager
|
||||
from .backup_account.manager import AccountBackupManager
|
||||
from .gather_accounts.manager import GatherAccountsManager
|
||||
from .verify_gateway_account.manager import VerifyGatewayAccountManager
|
||||
from ..const import AutomationTypes
|
||||
|
||||
@@ -14,11 +12,10 @@ class ExecutionManager:
|
||||
AutomationTypes.push_account: PushAccountManager,
|
||||
AutomationTypes.change_secret: ChangeSecretManager,
|
||||
AutomationTypes.verify_account: VerifyAccountManager,
|
||||
AutomationTypes.remove_account: RemoveAccountManager,
|
||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
||||
AutomationTypes.check_account: CheckAccountManager,
|
||||
AutomationTypes.backup_account: AccountBackupManager,
|
||||
# TODO 后期迁移到自动化策略中
|
||||
'backup_account': AccountBackupManager,
|
||||
}
|
||||
|
||||
def __init__(self, execution):
|
||||
@@ -27,6 +24,3 @@ class ExecutionManager:
|
||||
|
||||
def run(self, *args, **kwargs):
|
||||
return self._runner.run(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._runner, item)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
id: gather_accounts_mongodb
|
||||
name: "{{ 'MongoDB account gather' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
MongoDB account gather:
|
||||
zh: MongoDB 账号收集
|
||||
ja: MongoDB アカウントの収集
|
||||
@@ -1,29 +0,0 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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:
|
||||
- name: Get info
|
||||
community.mysql.mysql_info:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ ca_cert if check_ssl and ca_cert | length > 0 else omit }}"
|
||||
client_cert: "{{ ssl_cert if check_ssl and ssl_cert | length > 0 else omit }}"
|
||||
client_key: "{{ ssl_key if check_ssl and ssl_key | length > 0 else omit }}"
|
||||
filter: users
|
||||
register: db_info
|
||||
|
||||
- name: Define info by set_fact
|
||||
set_fact:
|
||||
info: "{{ db_info.users }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
||||
@@ -1,12 +0,0 @@
|
||||
id: gather_accounts_mysql
|
||||
name: "{{ 'MySQL account gather' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
MySQL account gather:
|
||||
zh: MySQL 账号收集
|
||||
ja: MySQL アカウントの収集
|
||||
@@ -1,11 +0,0 @@
|
||||
id: gather_accounts_oracle
|
||||
name: "{{ 'Oracle account gather' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
Oracle account gather:
|
||||
zh: Oracle 账号收集
|
||||
ja: Oracle アカウントの収集
|
||||
@@ -1,30 +0,0 @@
|
||||
- hosts: postgresql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
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:
|
||||
- name: Get info
|
||||
community.postgresql.postgresql_info:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_db: "{{ jms_asset.spec_info.db_name }}"
|
||||
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"
|
||||
register: db_info
|
||||
|
||||
- name: Define info by set_fact
|
||||
set_fact:
|
||||
info: "{{ db_info.roles }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
||||
@@ -1,11 +0,0 @@
|
||||
id: gather_accounts_postgresql
|
||||
name: "{{ 'PostgreSQL account gather' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
PostgreSQL account gather:
|
||||
zh: PostgreSQL 账号收集
|
||||
ja: PostgreSQL アカウントの収集
|
||||
@@ -1,45 +0,0 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: "{{ local_python_interpreter }}"
|
||||
|
||||
tasks:
|
||||
- name: Test SQLServer connection
|
||||
mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: '{{ jms_asset.spec_info.db_name }}'
|
||||
encryption: "{{ jms_asset.encryption | default(None) }}"
|
||||
tds_version: "{{ jms_asset.tds_version | default(None) }}"
|
||||
script: |
|
||||
SELECT
|
||||
l.name,
|
||||
l.modify_date,
|
||||
l.is_disabled,
|
||||
l.create_date,
|
||||
l.default_database_name,
|
||||
LOGINPROPERTY(name, 'DaysUntilExpiration') AS days_until_expiration,
|
||||
MAX(s.login_time) AS last_login_time
|
||||
FROM
|
||||
sys.sql_logins l
|
||||
LEFT JOIN
|
||||
sys.dm_exec_sessions s
|
||||
ON
|
||||
l.name = s.login_name
|
||||
WHERE
|
||||
s.is_user_process = 1 OR s.login_name IS NULL
|
||||
GROUP BY
|
||||
l.name, l.create_date, l.modify_date, l.is_disabled, l.default_database_name
|
||||
ORDER BY
|
||||
last_login_time DESC;
|
||||
output: dict
|
||||
register: db_info
|
||||
|
||||
- name: Define info by set_fact
|
||||
set_fact:
|
||||
info: "{{ db_info.query_results_dict }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
||||
@@ -1,10 +0,0 @@
|
||||
id: gather_accounts_sqlserver
|
||||
name: "{{ 'SQLServer account gather' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: gather_accounts
|
||||
i18n:
|
||||
SQLServer account gather:
|
||||
zh: SQLServer 账号收集
|
||||
ja: SQLServer アカウントの収集
|
||||
@@ -1,270 +0,0 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
__all__ = ['GatherAccountsFilter']
|
||||
|
||||
|
||||
def parse_date(date_str, default=None):
|
||||
if not date_str:
|
||||
return default
|
||||
if date_str in ['Never', 'null']:
|
||||
return default
|
||||
formats = [
|
||||
'%Y/%m/%d %H:%M:%S',
|
||||
'%Y-%m-%dT%H:%M:%S',
|
||||
'%Y-%m-%d %H:%M:%S',
|
||||
'%d-%m-%Y %H:%M:%S',
|
||||
'%Y/%m/%d',
|
||||
'%d-%m-%Y',
|
||||
]
|
||||
for fmt in formats:
|
||||
try:
|
||||
dt = datetime.strptime(date_str, fmt)
|
||||
return timezone.make_aware(dt, timezone.get_current_timezone())
|
||||
except ValueError:
|
||||
continue
|
||||
return default
|
||||
|
||||
|
||||
class GatherAccountsFilter:
|
||||
def __init__(self, tp):
|
||||
self.tp = tp
|
||||
|
||||
@staticmethod
|
||||
def mysql_filter(info):
|
||||
result = {}
|
||||
for host, user_dict in info.items():
|
||||
for username, user_info in user_dict.items():
|
||||
password_last_changed = parse_date(user_info.get('password_last_changed'))
|
||||
password_lifetime = user_info.get('password_lifetime')
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': password_last_changed,
|
||||
'date_password_expired': password_last_changed + timezone.timedelta(
|
||||
days=password_lifetime) if password_last_changed and password_lifetime else None,
|
||||
'date_last_login': None,
|
||||
'groups': '',
|
||||
}
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def postgresql_filter(info):
|
||||
result = {}
|
||||
for username, user_info in info.items():
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': None,
|
||||
'date_password_expired': parse_date(user_info.get('valid_until')),
|
||||
'date_last_login': None,
|
||||
'groups': '',
|
||||
}
|
||||
detail = {
|
||||
'can_login': user_info.get('canlogin'),
|
||||
'superuser': user_info.get('superuser'),
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def sqlserver_filter(info):
|
||||
if not info:
|
||||
return {}
|
||||
result = {}
|
||||
for user_info in info[0][0]:
|
||||
days_until_expiration = user_info.get('days_until_expiration')
|
||||
date_password_expired = timezone.now() + timezone.timedelta(
|
||||
days=int(days_until_expiration)) if days_until_expiration else None
|
||||
user = {
|
||||
'username': user_info.get('name', ''),
|
||||
'date_password_change': parse_date(user_info.get('modify_date')),
|
||||
'date_password_expired': date_password_expired,
|
||||
'date_last_login': parse_date(user_info.get('last_login_time')),
|
||||
'groups': '',
|
||||
}
|
||||
detail = {
|
||||
'create_date': user_info.get('create_date', ''),
|
||||
'is_disabled': user_info.get('is_disabled', ''),
|
||||
'default_database_name': user_info.get('default_database_name', ''),
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[user['username']] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def oracle_filter(info):
|
||||
result = {}
|
||||
for default_tablespace, users in info.items():
|
||||
for username, user_info in users.items():
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': parse_date(user_info.get('password_change_date')),
|
||||
'date_password_expired': parse_date(user_info.get('expiry_date')),
|
||||
'date_last_login': parse_date(user_info.get('last_login')),
|
||||
'groups': '',
|
||||
}
|
||||
detail = {
|
||||
'uid': user_info.get('user_id', ''),
|
||||
'create_date': user_info.get('created', ''),
|
||||
'account_status': user_info.get('account_status', ''),
|
||||
'default_tablespace': default_tablespace,
|
||||
'roles': user_info.get('roles', []),
|
||||
'privileges': user_info.get('privileges', []),
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[user['username']] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def posix_filter(info):
|
||||
user_groups = info.pop('user_groups', [])
|
||||
username_groups = {}
|
||||
for line in user_groups:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, groups = line.split(':', 1)
|
||||
username_groups[username.strip()] = groups.strip()
|
||||
|
||||
user_sudo = info.pop('user_sudo', [])
|
||||
username_sudo = {}
|
||||
for line in user_sudo:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, sudo = line.split(':', 1)
|
||||
if not sudo.strip():
|
||||
continue
|
||||
username_sudo[username.strip()] = sudo.strip()
|
||||
|
||||
last_login = info.pop('last_login', '')
|
||||
user_last_login = {}
|
||||
for line in last_login:
|
||||
if not line.strip() or ' ' not in line:
|
||||
continue
|
||||
username, login = line.split(' ', 1)
|
||||
user_last_login[username] = login.split()
|
||||
|
||||
user_authorized = info.pop('user_authorized', [])
|
||||
username_authorized = {}
|
||||
for line in user_authorized:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, authorized = line.split(':', 1)
|
||||
username_authorized[username.strip()] = authorized.strip()
|
||||
|
||||
passwd_date = info.pop('passwd_date', [])
|
||||
username_password_date = {}
|
||||
for line in passwd_date:
|
||||
if ':' not in line:
|
||||
continue
|
||||
username, password_date = line.split(':', 1)
|
||||
username_password_date[username.strip()] = password_date.strip().split()
|
||||
|
||||
result = {}
|
||||
users = info.pop('users', '')
|
||||
|
||||
for username in users:
|
||||
if not username:
|
||||
continue
|
||||
user = dict()
|
||||
|
||||
login = user_last_login.get(username) or ''
|
||||
if login and len(login) == 3:
|
||||
user['address_last_login'] = login[0][:32]
|
||||
try:
|
||||
login_date = timezone.datetime.fromisoformat(login[1])
|
||||
user['date_last_login'] = login_date
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
start_date = timezone.make_aware(timezone.datetime(1970, 1, 1))
|
||||
_password_date = username_password_date.get(username) or ''
|
||||
if _password_date and len(_password_date) == 2:
|
||||
if _password_date[0]:
|
||||
user['date_password_change'] = start_date + timezone.timedelta(days=int(_password_date[0]))
|
||||
if _password_date[1]:
|
||||
user['date_password_expired'] = start_date + timezone.timedelta(days=int(_password_date[1]))
|
||||
detail = {
|
||||
'groups': username_groups.get(username) or '',
|
||||
'sudoers': username_sudo.get(username) or '',
|
||||
'authorized_keys': username_authorized.get(username) or ''
|
||||
}
|
||||
user['detail'] = detail
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def windows_filter(info):
|
||||
result = {}
|
||||
for user_details in info['user_details']:
|
||||
user_info = {}
|
||||
lines = user_details['stdout_lines']
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split(' ', 1)
|
||||
if len(parts) == 2:
|
||||
key, value = parts
|
||||
user_info[key.strip()] = value.strip()
|
||||
detail = {'groups': user_info.get('Global Group memberships', ''), }
|
||||
|
||||
username = user_info.get('User name')
|
||||
if not username:
|
||||
continue
|
||||
|
||||
result[username] = {
|
||||
'username': username,
|
||||
'date_password_change': parse_date(user_info.get('Password last set')),
|
||||
'date_password_expired': parse_date(user_info.get('Password expires')),
|
||||
'date_last_login': parse_date(user_info.get('Last logon')),
|
||||
'groups': detail,
|
||||
}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def windows_ad_filter(info):
|
||||
result = {}
|
||||
for user_info in info['user_details']:
|
||||
detail = {'groups': user_info.get('GlobalGroupMemberships', ''), }
|
||||
username = user_info.get('SamAccountName')
|
||||
if not username:
|
||||
continue
|
||||
result[username] = {
|
||||
'username': username,
|
||||
'date_password_change': parse_date(user_info.get('PasswordLastSet')),
|
||||
'date_password_expired': parse_date(user_info.get('PasswordExpires')),
|
||||
'date_last_login': parse_date(user_info.get('LastLogonDate')),
|
||||
'groups': detail,
|
||||
}
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def mongodb_filter(info):
|
||||
result = {}
|
||||
for db, users in info.items():
|
||||
for username, user_info in users.items():
|
||||
user = {
|
||||
'username': username,
|
||||
'date_password_change': None,
|
||||
'date_password_expired': None,
|
||||
'date_last_login': None,
|
||||
'groups': '',
|
||||
}
|
||||
result['detail'] = {'db': db, 'roles': user_info.get('roles', [])}
|
||||
result[username] = user
|
||||
return result
|
||||
|
||||
def run(self, method_id_meta_mapper, info):
|
||||
run_method_name = None
|
||||
for k, v in method_id_meta_mapper.items():
|
||||
if self.tp not in v['type']:
|
||||
continue
|
||||
run_method_name = k.replace(f'{v["method"]}_', '')
|
||||
|
||||
if not run_method_name:
|
||||
return info
|
||||
|
||||
if hasattr(self, f'{run_method_name}_filter'):
|
||||
return getattr(self, f'{run_method_name}_filter')(info)
|
||||
return info
|
||||
@@ -1,61 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Get users
|
||||
ansible.builtin.shell:
|
||||
cmd: >
|
||||
getent passwd | awk -F: '$7 !~ /(false|nologin|true|sync)$/' | grep -v '^$' | awk -F":" '{ print $1 }'
|
||||
register: users
|
||||
|
||||
- name: Gather posix account last login
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
last -wi --time-format iso -n 1 ${user} | awk '{ print $1,$3,$4, $NF }' | head -1 | awk 'NF'
|
||||
done
|
||||
register: last_login
|
||||
|
||||
- name: Get user password change date and expiry
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
k=$(getent shadow $user | awk -F: '{ print $3, $5 }')
|
||||
echo "$user:$k"
|
||||
done
|
||||
register: passwd_date
|
||||
|
||||
- name: Get user groups
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
echo "$(groups $user)" | sed 's@ : @:@g'
|
||||
done
|
||||
register: user_groups
|
||||
|
||||
- name: Get sudoers
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
echo "$user: $(grep "^$user " /etc/sudoers | tr '\n' ';' || echo '')"
|
||||
done
|
||||
register: user_sudo
|
||||
|
||||
- name: Get authorized keys
|
||||
ansible.builtin.shell: |
|
||||
for user in {{ users.stdout_lines | join(" ") }}; do
|
||||
home=$(getent passwd $user | cut -d: -f6)
|
||||
echo -n "$user:"
|
||||
if [ -f "${home}/.ssh/authorized_keys" ]; then
|
||||
cat ${home}/.ssh/authorized_keys | tr '\n' ';'
|
||||
fi
|
||||
echo
|
||||
done
|
||||
register: user_authorized
|
||||
|
||||
- set_fact:
|
||||
info:
|
||||
users: "{{ users.stdout_lines }}"
|
||||
last_login: "{{ last_login.stdout_lines }}"
|
||||
user_groups: "{{ user_groups.stdout_lines }}"
|
||||
user_sudo: "{{ user_sudo.stdout_lines }}"
|
||||
user_authorized: "{{ user_authorized.stdout_lines }}"
|
||||
passwd_date: "{{ passwd_date.stdout_lines }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
||||
@@ -1,13 +0,0 @@
|
||||
id: gather_accounts_posix
|
||||
name: "{{ 'Posix account gather' | trans }}"
|
||||
category: host
|
||||
type:
|
||||
- linux
|
||||
- unix
|
||||
method: gather_accounts
|
||||
|
||||
i18n:
|
||||
Posix account gather:
|
||||
zh: 使用命令 getent passwd 收集 Posix 资产账号
|
||||
ja: コマンド getent を使用してアセットアカウントを収集する
|
||||
en: Using command getent to gather accounts
|
||||
@@ -1,33 +0,0 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: Run net user command to get all users
|
||||
win_shell: net user
|
||||
register: user_list_output
|
||||
failed_when: false
|
||||
|
||||
- name: Parse all users from net user command
|
||||
set_fact:
|
||||
all_users: >-
|
||||
{%- set users = [] -%}
|
||||
{%- for line in user_list_output.stdout_lines -%}
|
||||
{%- if loop.index > 4 and line.strip() != "" and not line.startswith("The command completed") -%}
|
||||
{%- for user in line.split() -%}
|
||||
{%- set _ = users.append(user) -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ users }}
|
||||
|
||||
- name: Run net user command for each user to get details
|
||||
win_shell: net user {{ item }}
|
||||
loop: "{{ all_users }}"
|
||||
register: user_details
|
||||
ignore_errors: yes
|
||||
|
||||
- set_fact:
|
||||
info:
|
||||
user_details: "{{ user_details.results }}"
|
||||
|
||||
- debug:
|
||||
var: info
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user