Compare commits

..

37 Commits

Author SHA1 Message Date
feng
3043b08a00 perf: Login language 2025-03-24 18:56:09 +08:00
feng
245963a6f7 perf: Translate 2025-03-24 16:51:42 +08:00
feng
c1b04e077a perf: Translate 2025-03-24 16:12:56 +08:00
fit2bot
d14e52edb8 perf: update tk create (#15103) 2025-03-24 14:02:56 +08:00
feng
133a216c04 perf: Translate 2025-03-21 14:24:10 +08:00
Bryan
ad5460dab8 Merge pull request #15086 from jumpserver/dev
v4.8.0
2025-03-20 18:44:44 +08:00
Bryan
4d37dca0de Merge pull request #14901 from jumpserver/dev
v4.7.0
2025-02-20 10:21:16 +08:00
Bryan
2ca4002624 Merge pull request #14813 from jumpserver/dev
v4.6.0
2025-01-15 14:38:17 +08:00
Bryan
053d640e4c Merge pull request #14699 from jumpserver/dev
v4.5.0
2024-12-19 16:04:45 +08:00
Bryan
f3acc28ded Merge pull request #14697 from jumpserver/dev
v4.5.0
2024-12-19 15:57:11 +08:00
Bryan
25987545db Merge pull request #14511 from jumpserver/dev
v4.4.0
2024-11-21 19:00:35 +08:00
Bryan
6720ecc6e0 Merge pull request #14319 from jumpserver/dev
v4.3.0
2024-10-17 14:55:38 +08:00
老广
0b3a7bb020 Merge pull request #14203 from jumpserver/dev
merge: from dev to master
2024-09-19 19:37:19 +08:00
Bryan
56373e362b Merge pull request #13988 from jumpserver/dev
v4.1.0
2024-08-16 18:40:35 +08:00
Bryan
02fc045370 Merge pull request #13600 from jumpserver/dev
v4.0.0
2024-07-03 19:04:35 +08:00
Bryan
e4ac73896f Merge pull request #13452 from jumpserver/dev
v3.10.11-lts
2024-06-19 16:01:26 +08:00
Bryan
1518f792d6 Merge pull request #13236 from jumpserver/dev
v3.10.10-lts
2024-05-16 16:04:07 +08:00
Bai
67277dd622 fix: 修复仪表盘会话排序数量都是 1 的问题 2024-04-22 19:42:33 +08:00
Bryan
82e7f020ea Merge pull request #13094 from jumpserver/dev
v3.10.9 (dev to master)
2024-04-22 19:39:53 +08:00
Bryan
f20b9e01ab Merge pull request #13062 from jumpserver/dev
v3.10.8 dev to master
2024-04-18 18:01:20 +08:00
Bryan
8cf8a3701b Merge pull request #13059 from jumpserver/dev
v3.10.8
2024-04-18 17:16:37 +08:00
Bryan
7ba24293d1 Merge pull request #12736 from jumpserver/pr@dev@master_fix
fix: 解决冲突
2024-02-29 16:38:43 +08:00
Bai
f10114c9ed fix: 解决冲突 2024-02-29 16:37:10 +08:00
Bryan
cf31cbfb07 Merge pull request #12729 from jumpserver/dev
v3.10.4
2024-02-29 16:19:59 +08:00
wangruidong
0edad24d5d fix: 资产过期消息提示发送失败 2024-02-04 11:41:48 +08:00
ibuler
1f1c1a9157 fix: 修复定时检测用户是否活跃任务无法执行的问题 2024-01-23 09:28:38 +00:00
feng
6c9d271ae1 fix: redis 密码有特殊字符celery beat启动失败 2024-01-22 06:18:34 +00:00
Bai
6ff852e225 perf: 修复 Count 时没有去重的问题 2024-01-22 06:16:25 +00:00
Bryan
baa75dc735 Merge pull request #12566 from jumpserver/master
v3.10.2
2024-01-17 07:34:28 -04:00
Bryan
8a9f0436b8 Merge pull request #12565 from jumpserver/dev
v3.10.2
2024-01-17 07:23:30 -04:00
Bryan
a9620a3cbe Merge pull request #12461 from jumpserver/master
v3.10.1
2023-12-29 11:33:05 +05:00
Bryan
769e7dc8a0 Merge pull request #12460 from jumpserver/dev
v3.10.1
2023-12-29 11:20:36 +05:00
Bryan
2a70449411 Merge pull request #12458 from jumpserver/dev
v3.10.1
2023-12-29 11:01:13 +05:00
Bryan
8df720f19e Merge pull request #12401 from jumpserver/dev
v3.10
2023-12-21 15:14:19 +05:00
老广
dabbb45f6e Merge pull request #12144 from jumpserver/dev
v3.9.0
2023-11-16 18:23:05 +08:00
Bryan
ce24c1c3fd Merge pull request #11914 from jumpserver/dev
v3.8.0
2023-10-19 03:37:39 -05:00
Bryan
3c54c82ce9 Merge pull request #11636 from jumpserver/dev
v3.7.0
2023-09-21 17:02:48 +08:00
618 changed files with 23479 additions and 64595 deletions

View File

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

4
.gitattributes vendored
View File

@@ -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
leak_passwords.db filter=lfs diff=lfs merge=lfs -text

View File

@@ -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:
- "*"

View File

@@ -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 }}

View File

@@ -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

View File

@@ -1,9 +1,11 @@
name: 🔀 Sync mirror to Gitee name: 🔀 Sync mirror to Gitee
on: on:
schedule: push:
# 每天凌晨3点运行 branches:
- cron: '0 3 * * *' - master
- dev
create:
jobs: jobs:
mirror: mirror:
@@ -12,6 +14,7 @@ jobs:
steps: steps:
- name: mirror - name: mirror
continue-on-error: true continue-on-error: true
if: github.event_name == 'push' || (github.event_name == 'create' && github.event.ref_type == 'tag')
uses: wearerequired/git-mirror-action@v1 uses: wearerequired/git-mirror-action@v1
env: env:
SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.GITEE_SSH_PRIVATE_KEY }}

View File

@@ -2,14 +2,10 @@ name: Translate README
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
source_readme:
description: "Source README"
required: false
default: "./readmes/README.en.md"
target_langs: target_langs:
description: "Target Languages" description: "Target Languages"
required: false required: false
default: "zh-hans,zh-hant,ja,pt-br,es,ru" default: "zh-hans,zh-hant,ja,pt-br"
gen_dir_path: gen_dir_path:
description: "Generate Dir Name" description: "Generate Dir Name"
required: false required: false
@@ -38,7 +34,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }} GITHUB_TOKEN: ${{ secrets.PRIVATE_TOKEN }}
OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }} OPENAI_API_KEY: ${{ secrets.GPT_API_TOKEN }}
GPT_MODE: ${{ github.event.inputs.gpt_mode }} GPT_MODE: ${{ github.event.inputs.gpt_mode }}
SOURCE_README: ${{ github.event.inputs.source_readme }}
TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }} TARGET_LANGUAGES: ${{ github.event.inputs.target_langs }}
PUSH_BRANCH: ${{ github.event.inputs.push_branch }} PUSH_BRANCH: ${{ github.event.inputs.push_branch }}
GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }} GEN_DIR_PATH: ${{ github.event.inputs.gen_dir_path }}

3
.gitignore vendored
View File

@@ -46,6 +46,3 @@ test.py
.test/ .test/
*.mo *.mo
apps.iml apps.iml
*.db
*.mmdb
*.ipdb

View File

@@ -1,11 +0,0 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 100,
"endOfLine": "lf"
}

View File

@@ -1,4 +1,4 @@
FROM jumpserver/core-base:20251029_031929 AS stage-build FROM jumpserver/core-base:20250224_065619 AS stage-build
ARG VERSION ARG VERSION
@@ -19,7 +19,7 @@ RUN set -ex \
&& python manage.py compilemessages && python manage.py compilemessages
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1 FROM python:3.11-slim-bullseye
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH PATH=/opt/py3/bin:$PATH
@@ -33,7 +33,6 @@ ARG TOOLS=" \
default-libmysqlclient-dev \ default-libmysqlclient-dev \
openssh-client \ openssh-client \
sshpass \ sshpass \
nmap \
bubblewrap" bubblewrap"
ARG APT_MIRROR=http://deb.debian.org ARG APT_MIRROR=http://deb.debian.org

View File

@@ -1,6 +1,6 @@
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1 FROM python:3.11-slim-bullseye
ARG TARGETARCH ARG TARGETARCH
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
# Install APT dependencies # Install APT dependencies
ARG DEPENDENCIES=" \ ARG DEPENDENCIES=" \
ca-certificates \ ca-certificates \
@@ -28,7 +28,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& echo "no" | dpkg-reconfigure dash && echo "no" | dpkg-reconfigure dash
# Install bin tools # Install bin tools
ARG CHECK_VERSION=v1.0.5 ARG CHECK_VERSION=v1.0.4
RUN set -ex \ RUN set -ex \
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \ && 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 \ && tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
@@ -43,19 +43,18 @@ WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR} ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
ENV ANSIBLE_COLLECTIONS_PATHS=/opt/py3/lib/python3.11/site-packages/ansible_collections 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 \ RUN --mount=type=cache,target=/root/.cache \
--mount=type=bind,source=poetry.lock,target=poetry.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=requirements/clean_site_packages.sh,target=clean_site_packages.sh \ --mount=type=bind,source=utils/clean_site_packages.sh,target=clean_site_packages.sh \
--mount=type=bind,source=requirements/collections.yml,target=collections.yml \ --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 \ set -ex \
&& uv venv \ && python3 -m venv /opt/py3 \
&& uv pip install -i${PIP_MIRROR} -r pyproject.toml \ && pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& ln -sf $(pwd)/.venv /opt/py3 \ && . /opt/py3/bin/activate \
&& bash utils/static_files.sh \ && poetry config virtualenvs.create false \
&& bash clean_site_packages.sh && poetry install --no-cache --only main \
&& ansible-galaxy collection install -r collections.yml --force --ignore-certs \
&& bash clean_site_packages.sh \
&& poetry cache clear pypi --all

View File

@@ -13,9 +13,7 @@ ARG TOOLS=" \
nmap \ nmap \
telnet \ telnet \
vim \ vim \
postgresql-client-13 \ wget"
wget \
poppler-utils"
RUN set -ex \ RUN set -ex \
&& apt-get update \ && apt-get update \
@@ -26,7 +24,11 @@ RUN set -ex \
WORKDIR /opt/jumpserver WORKDIR /opt/jumpserver
ARG PIP_MIRROR=https://pypi.org/simple ARG PIP_MIRROR=https://pypi.org/simple
ENV POETRY_PYPI_MIRROR_URL=${PIP_MIRROR}
COPY poetry.lock pyproject.toml ./
RUN set -ex \ RUN set -ex \
&& uv pip install -i${PIP_MIRROR} --group xpack \ && . /opt/py3/bin/activate \
&& playwright install chromium --with-deps --only-shell && pip install poetry poetry-plugin-pypi-mirror -i ${PIP_MIRROR} \
&& poetry install --only xpack \
&& poetry cache clear pypi --all

View File

@@ -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

View File

@@ -1,33 +1,25 @@
<div align="center"> <div align="center">
<a name="readme-top"></a> <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> <a href="https://jumpserver.org/index-en.html"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
## An open-source PAM platform (Bastion Host) ## An open-source PAM tool (Bastion Host)
[![][license-shield]][license-link] [![][license-shield]][license-link]
[![][docs-shield]][docs-link]
[![][deepwiki-shield]][deepwiki-link]
[![][discord-shield]][discord-link] [![][discord-shield]][discord-link]
[![][docker-shield]][docker-link] [![][docker-shield]][docker-link]
[![][github-release-shield]][github-release-link] [![][github-release-shield]][github-release-link]
[![][github-stars-shield]][github-stars-link] [![][github-stars-shield]][github-stars-link]
[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) [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)
</div> </div>
<br/> <br/>
## What is JumpServer? ## 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. JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
<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>
![JumpServer Overview](https://github.com/jumpserver/jumpserver/assets/32935519/35a371cb-8590-40ed-88ec-f351f8cf9045)
## Quickstart ## Quickstart
@@ -44,19 +36,18 @@ Access JumpServer in your browser at `http://your-jumpserver-ip/`
[![JumpServer Quickstart](https://github.com/user-attachments/assets/0f32f52b-9935-485e-8534-336c63389612)](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart") [![JumpServer Quickstart](https://github.com/user-attachments/assets/0f32f52b-9935-485e-8534-336c63389612)](https://www.youtube.com/watch?v=UlGYRbKrpgY "JumpServer Quickstart")
## Screenshots ## Screenshots
<table style="border-collapse: collapse; border: 1px solid black;"> <table style="border-collapse: collapse; border: 1px solid black;">
<tr> <tr>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/99fabe5b-0475-4a53-9116-4c370a1426c4" alt="JumpServer Console" /></td> <td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/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/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>
<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/393d2c27-a2d0-4dea-882d-00ed509e00c9" alt="JumpServer Workbench" /></td>
<td style="padding: 5px;background-color:#fff;"><img src= "https://github.com/jumpserver/jumpserver/assets/32935519/3a2611cd-8902-49b8-b82b-2a6dac851f3e" alt="JumpServer Settings" /></td> <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>
<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/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> <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>
@@ -78,20 +69,24 @@ JumpServer consists of multiple key components, which collectively form the func
| [KoKo](https://github.com/jumpserver/koko) | <a href="https://github.com/jumpserver/koko/releases"><img alt="Koko release" src="https://img.shields.io/github/release/jumpserver/koko.svg" /></a> | JumpServer Character Protocol Connector | | [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 | | [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 | | [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 | | [Razor](https://github.com/jumpserver/razor) | <img alt="Chen" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE RDP Proxy Connector |
| [Tinker](https://github.com/jumpserver/tinker) | <img alt="Tinker" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Windows) |
| [Panda](https://github.com/jumpserver/Panda) | <img alt="Panda" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Remote Application Connector (Linux) |
| [Magnus](https://github.com/jumpserver/magnus) | <img alt="Magnus" src="https://img.shields.io/badge/release-private-red" /> | JumpServer EE Database Proxy Connector | | [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 | | [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 | | [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
## Contributing ## Contributing
Welcome to submit PR to contribute. Please refer to [CONTRIBUTING.md][contributing-link] for guidelines. Welcome to submit PR to contribute. Please refer to [CONTRIBUTING.md][contributing-link] for guidelines.
## Security
JumpServer is a mission critical product. Please refer to the Basic Security Recommendations for installation and deployment. If you encounter any security-related issues, please contact us directly:
- Email: support@fit2cloud.com
## License ## License
Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved. Copyright (c) 2014-2025 FIT2CLOUD, All rights reserved.
@@ -105,7 +100,6 @@ Unless required by applicable law or agreed to in writing, software distributed
<!-- JumpServer official link --> <!-- JumpServer official link -->
[docs-link]: https://jumpserver.com/docs [docs-link]: https://jumpserver.com/docs
[discord-link]: https://discord.com/invite/W6vYXmAQG2 [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 [contributing-link]: https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md
<!-- JumpServer Other link--> <!-- JumpServer Other link-->
@@ -116,10 +110,10 @@ Unless required by applicable law or agreed to in writing, software distributed
[github-issues-link]: https://github.com/jumpserver/jumpserver/issues [github-issues-link]: https://github.com/jumpserver/jumpserver/issues
<!-- Shield link--> <!-- Shield link-->
[docs-shield]: https://img.shields.io/badge/documentation-148F76
[github-release-shield]: https://img.shields.io/github/v/release/jumpserver/jumpserver [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    [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 [docker-shield]: https://img.shields.io/docker/pulls/jumpserver/jms_all.svg
[license-shield]: https://img.shields.io/github/license/jumpserver/jumpserver [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 [discord-shield]: https://img.shields.io/discord/1194233267294052363?style=flat&logo=discord&logoColor=%23f5f5f5&labelColor=%235462eb&color=%235462eb
<!-- Image link -->

View File

@@ -5,7 +5,8 @@ JumpServer 是一款正在成长的安全产品, 请参考 [基本安全建议
如果你发现安全问题,请直接联系我们,我们携手让世界更好: 如果你发现安全问题,请直接联系我们,我们携手让世界更好:
- ibuler@fit2cloud.com - ibuler@fit2cloud.com
- support@lxware.hk - support@fit2cloud.com
- 400-052-0755
# Security Policy # 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: All security bugs should be reported to the contact as below:
- ibuler@fit2cloud.com - ibuler@fit2cloud.com
- support@lxware.hk - support@fit2cloud.com
- 400-052-0755

View File

@@ -11,7 +11,6 @@ from accounts.const import ChangeSecretRecordStatusChoice
from accounts.filters import AccountFilterSet, NodeFilterBackend from accounts.filters import AccountFilterSet, NodeFilterBackend
from accounts.mixins import AccountRecordViewLogMixin from accounts.mixins import AccountRecordViewLogMixin
from accounts.models import Account, ChangeSecretRecord from accounts.models import Account, ChangeSecretRecord
from assets.const.gpt import create_or_update_chatx_resources
from assets.models import Asset, Node from assets.models import Asset, Node
from authentication.permissions import UserConfirmation, ConfirmType from authentication.permissions import UserConfirmation, ConfirmType
from common.api.mixin import ExtraFilterFieldsMixin from common.api.mixin import ExtraFilterFieldsMixin
@@ -19,7 +18,6 @@ from common.drf.filters import AttrRulesFilterBackend
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.utils import lazyproperty, get_logger from common.utils import lazyproperty, get_logger
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -43,22 +41,11 @@ class AccountViewSet(OrgBulkModelViewSet):
'partial_update': ['accounts.change_account'], 'partial_update': ['accounts.change_account'],
'su_from_accounts': 'accounts.view_account', 'su_from_accounts': 'accounts.view_account',
'clear_secret': 'accounts.change_account', 'clear_secret': 'accounts.change_account',
'move_to_assets': 'accounts.delete_account', 'move_to_assets': 'accounts.create_account',
'copy_to_assets': 'accounts.add_account', 'copy_to_assets': 'accounts.create_account',
'chat': 'accounts.view_account',
} }
export_as_zip = True 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') @action(methods=['get'], detail=False, url_path='su-from-accounts')
def su_from_accounts(self, request, *args, **kwargs): def su_from_accounts(self, request, *args, **kwargs):
account_id = request.query_params.get('account') account_id = request.query_params.get('account')
@@ -81,25 +68,18 @@ class AccountViewSet(OrgBulkModelViewSet):
permission_classes=[IsValidUser] permission_classes=[IsValidUser]
) )
def username_suggestions(self, request, *args, **kwargs): def username_suggestions(self, request, *args, **kwargs):
raw_asset_ids = request.data.get('assets', []) asset_ids = request.data.get('assets', [])
node_ids = request.data.get('nodes', []) node_ids = request.data.get('nodes', [])
username = request.data.get('username', '') username = request.data.get('username', '')
asset_ids = set(raw_asset_ids) accounts = Account.objects.all()
if node_ids: if node_ids:
nodes = Node.objects.filter(id__in=node_ids) nodes = Node.objects.filter(id__in=node_ids)
node_asset_qs = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
asset_ids |= {str(u) for u in node_asset_qs} asset_ids.extend(node_asset_ids)
if asset_ids: if asset_ids:
through = Asset.directory_services.through accounts = accounts.filter(asset_id__in=list(set(asset_ids)))
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()
if username: if username:
accounts = accounts.filter(username__icontains=username) accounts = accounts.filter(username__icontains=username)
@@ -137,7 +117,7 @@ class AccountViewSet(OrgBulkModelViewSet):
self.model.objects.create(**account_data) self.model.objects.create(**account_data)
success_count += 1 success_count += 1
except Exception as e: except Exception as e:
logger.debug(f'{"Move" if move else "Copy"} to assets error: {e}') logger.debug(f'{ "Move" if move else "Copy" } to assets error: {e}')
creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'} creation_results[asset] = {'error': _('Account already exists'), 'state': 'error'}
results = [{'asset': str(asset), **res} for asset, res in creation_results.items()] results = [{'asset': str(asset), **res} for asset, res in creation_results.items()]
@@ -155,13 +135,6 @@ class AccountViewSet(OrgBulkModelViewSet):
def copy_to_assets(self, request, *args, **kwargs): def copy_to_assets(self, request, *args, **kwargs):
return self._copy_or_move_to_assets(request, move=False) 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(AccountRecordViewLogMixin, AccountViewSet):
""" """
@@ -200,7 +173,6 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
rbac_perms = { rbac_perms = {
'GET': 'accounts.view_accountsecret', 'GET': 'accounts.view_accountsecret',
} }
queryset = Account.history.model.objects.none()
@lazyproperty @lazyproperty
def account(self) -> Account: def account(self) -> Account:

View File

@@ -25,8 +25,7 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
} }
rbac_perms = { rbac_perms = {
'get_once_secret': 'accounts.change_integrationapplication', 'get_once_secret': 'accounts.change_integrationapplication',
'get_account_secret': 'accounts.view_integrationapplication', 'get_account_secret': 'accounts.view_integrationapplication'
'get_sdks_info': 'accounts.view_integrationapplication'
} }
def read_file(self, path): def read_file(self, path):
@@ -37,6 +36,7 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
@action( @action(
['GET'], detail=False, url_path='sdks', ['GET'], detail=False, url_path='sdks',
permission_classes=[IsValidUser]
) )
def get_sdks_info(self, request, *args, **kwargs): def get_sdks_info(self, request, *args, **kwargs):
code_suffix_mapper = { code_suffix_mapper = {
@@ -62,7 +62,8 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet):
) )
def get_once_secret(self, request, *args, **kwargs): def get_once_secret(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
return Response(data={'id': instance.id, 'secret': instance.secret}) secret = instance.get_secret()
return Response(data={'id': instance.id, 'secret': secret})
@action(['GET'], detail=False, url_path='account-secret', @action(['GET'], detail=False, url_path='account-secret',
permission_classes=[RBACPermission]) permission_classes=[RBACPermission])

View File

@@ -20,7 +20,7 @@ __all__ = ['PamDashboardApi']
class PamDashboardApi(APIView): class PamDashboardApi(APIView):
http_method_names = ['get'] http_method_names = ['get']
rbac_perms = { rbac_perms = {
'GET': 'rbac.view_pam', 'GET': 'accounts.view_account',
} }
@staticmethod @staticmethod

View File

@@ -43,7 +43,6 @@ class AccountTemplateViewSet(OrgBulkModelViewSet):
search_fields = ('username', 'name') search_fields = ('username', 'name')
serializer_classes = { serializer_classes = {
'default': serializers.AccountTemplateSerializer, 'default': serializers.AccountTemplateSerializer,
'retrieve': serializers.AccountDetailTemplateSerializer,
} }
rbac_perms = { rbac_perms = {
'su_from_account_templates': 'accounts.view_accounttemplate', 'su_from_account_templates': 'accounts.view_accounttemplate',

View File

@@ -12,8 +12,6 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
filterset_fields = ('alias',) filterset_fields = ('alias',)
def get_queryset(self): def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return VirtualAccount.objects.none()
return VirtualAccount.get_or_init_queryset() return VirtualAccount.get_or_init_queryset()
def get_object(self, ): def get_object(self, ):

View File

@@ -17,7 +17,7 @@ from orgs.mixins import generics
__all__ = [ __all__ = [
'AutomationAssetsListApi', 'AutomationRemoveAssetApi', 'AutomationAssetsListApi', 'AutomationRemoveAssetApi',
'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi', 'AutomationAddAssetApi', 'AutomationNodeAddRemoveApi',
'AutomationExecutionViewSet' 'AutomationExecutionViewSet', 'RecordListMixin'
] ]
@@ -39,11 +39,9 @@ class AutomationAssetsListApi(generics.ListAPIView):
return assets return assets
class AutomationRemoveAssetApi(generics.UpdateAPIView): class AutomationRemoveAssetApi(generics.RetrieveUpdateAPIView):
model = BaseAutomation model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@@ -58,11 +56,9 @@ class AutomationRemoveAssetApi(generics.UpdateAPIView):
return Response({'msg': 'ok'}) return Response({'msg': 'ok'})
class AutomationAddAssetApi(generics.UpdateAPIView): class AutomationAddAssetApi(generics.RetrieveUpdateAPIView):
model = BaseAutomation model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
@@ -76,10 +72,9 @@ class AutomationAddAssetApi(generics.UpdateAPIView):
return Response({"error": serializer.errors}) return Response({"error": serializer.errors})
class AutomationNodeAddRemoveApi(generics.UpdateAPIView): class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView):
model = BaseAutomation model = BaseAutomation
serializer_class = serializers.UpdateNodeSerializer serializer_class = serializers.UpdateNodeSerializer
http_method_names = ['patch']
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
action_params = ['add', 'remove'] action_params = ['add', 'remove']
@@ -129,3 +124,12 @@ class AutomationExecutionViewSet(
execution = self.get_object() execution = self.get_object()
report = execution.manager.gen_report() report = execution.manager.gen_report()
return HttpResponse(report) return HttpResponse(report)
class RecordListMixin:
def list(self, request, *args, **kwargs):
try:
response = super().list(request, *args, **kwargs)
except Exception as e:
response = Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
return response

View File

@@ -6,27 +6,24 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from accounts import serializers from accounts import serializers
from accounts.const import ( from accounts.const import AutomationTypes, ChangeSecretRecordStatusChoice
AutomationTypes, ChangeSecretRecordStatusChoice from accounts.filters import ChangeSecretRecordFilterSet
) from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
from accounts.filters import ChangeSecretRecordFilterSet, ChangeSecretStatusFilterSet
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, Account
from accounts.tasks import execute_automation_record_task from accounts.tasks import execute_automation_record_task
from accounts.utils import account_secret_task_status
from authentication.permissions import UserConfirmation, ConfirmType from authentication.permissions import UserConfirmation, ConfirmType
from common.permissions import IsValidLicense from common.permissions import IsValidLicense
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from .base import ( from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet AutomationNodeAddRemoveApi, AutomationExecutionViewSet, RecordListMixin
) )
__all__ = [ __all__ = [
'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet',
'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi', 'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi',
'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi', 'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi',
'ChangSecretNodeAddRemoveApi', 'ChangeSecretStatusViewSet' 'ChangSecretNodeAddRemoveApi'
] ]
@@ -38,7 +35,7 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
serializer_class = serializers.ChangeSecretAutomationSerializer serializer_class = serializers.ChangeSecretAutomationSerializer
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): class ChangeSecretRecordViewSet(RecordListMixin, mixins.ListModelMixin, OrgGenericViewSet):
filterset_class = ChangeSecretRecordFilterSet filterset_class = ChangeSecretRecordFilterSet
permission_classes = [RBACPermission, IsValidLicense] permission_classes = [RBACPermission, IsValidLicense]
search_fields = ('asset__address', 'account__username') search_fields = ('asset__address', 'account__username')
@@ -97,13 +94,12 @@ class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
def execute(self, request, *args, **kwargs): def execute(self, request, *args, **kwargs):
record_ids = request.data.get('record_ids') record_ids = request.data.get('record_ids')
records = self.get_queryset().filter(id__in=record_ids) records = self.get_queryset().filter(id__in=record_ids)
if not records.exists(): execution_count = records.values_list('execution_id', flat=True).distinct().count()
if execution_count != 1:
return Response( return Response(
{'detail': 'No valid records found'}, {'detail': 'Only one execution is allowed to execute'},
status=status.HTTP_400_BAD_REQUEST 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) task = execute_automation_record_task.delay(record_ids, self.tp)
return Response({'task': task.id}, status=status.HTTP_200_OK) return Response({'task': task.id}, status=status.HTTP_200_OK)
@@ -154,27 +150,7 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
model = ChangeSecretAutomation model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateAssetSerializer serializer_class = serializers.ChangeSecretUpdateAssetSerializer
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi): class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = ChangeSecretAutomation model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateNodeSerializer 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)

View File

@@ -62,8 +62,7 @@ class ChangeSecretDashboardApi(APIView):
status_counts = defaultdict(lambda: defaultdict(int)) status_counts = defaultdict(lambda: defaultdict(int))
for date_finished, status in results: for date_finished, status in results:
dt_local = timezone.localtime(date_finished) date_str = str(date_finished.date())
date_str = str(dt_local.date())
if status == ChangeSecretRecordStatusChoice.failed: if status == ChangeSecretRecordStatusChoice.failed:
status_counts[date_str]['failed'] += 1 status_counts[date_str]['failed'] += 1
elif status == ChangeSecretRecordStatusChoice.success: elif status == ChangeSecretRecordStatusChoice.success:
@@ -91,10 +90,10 @@ class ChangeSecretDashboardApi(APIView):
def get_change_secret_asset_queryset(self): def get_change_secret_asset_queryset(self):
qs = self.change_secrets_queryset qs = self.change_secrets_queryset
node_ids = qs.values_list('nodes', flat=True).distinct() node_ids = qs.filter(nodes__isnull=False).values_list('nodes', flat=True).distinct()
nodes = Node.objects.filter(id__in=node_ids).only('id', 'key') nodes = Node.objects.filter(id__in=node_ids)
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
direct_asset_ids = qs.values_list('assets', flat=True).distinct() direct_asset_ids = qs.filter(assets__isnull=False).values_list('assets', flat=True).distinct()
asset_ids = set(list(direct_asset_ids) + list(node_asset_ids)) asset_ids = set(list(direct_asset_ids) + list(node_asset_ids))
return Asset.objects.filter(id__in=asset_ids) return Asset.objects.filter(id__in=asset_ids)

View File

@@ -45,10 +45,10 @@ class CheckAccountAutomationViewSet(OrgBulkModelViewSet):
class CheckAccountExecutionViewSet(AutomationExecutionViewSet): class CheckAccountExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = ( rbac_perms = (
("list", "accounts.view_checkaccountexecution"), ("list", "accounts.view_checkaccountexecution"),
("retrieve", "accounts.view_checkaccountexecution"), ("retrieve", "accounts.view_checkaccountsexecution"),
("create", "accounts.add_checkaccountexecution"), ("create", "accounts.add_checkaccountexecution"),
("adhoc", "accounts.add_checkaccountexecution"), ("adhoc", "accounts.add_checkaccountexecution"),
("report", "accounts.view_checkaccountexecution"), ("report", "accounts.view_checkaccountsexecution"),
) )
ordering = ("-date_created",) ordering = ("-date_created",)
tp = AutomationTypes.check_account tp = AutomationTypes.check_account
@@ -147,12 +147,8 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
serializer_class = serializers.CheckAccountEngineSerializer serializer_class = serializers.CheckAccountEngineSerializer
permission_classes = [RBACPermission, IsValidLicense] permission_classes = [RBACPermission, IsValidLicense]
perm_model = CheckAccountEngine perm_model = CheckAccountEngine
http_method_names = ['get', 'options']
def get_queryset(self): def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return CheckAccountEngine.objects.none()
return CheckAccountEngine.get_default_engines() return CheckAccountEngine.get_default_engines()
def filter_queryset(self, queryset: list): def filter_queryset(self, queryset: list):

View File

@@ -9,7 +9,7 @@ from accounts.models import PushAccountAutomation, PushSecretRecord
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
from .base import ( from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet AutomationNodeAddRemoveApi, AutomationExecutionViewSet, RecordListMixin
) )
__all__ = [ __all__ = [
@@ -42,7 +42,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
return queryset return queryset
class PushAccountRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): class PushAccountRecordViewSet(RecordListMixin, mixins.ListModelMixin, OrgGenericViewSet):
filterset_class = PushAccountRecordFilterSet filterset_class = PushAccountRecordFilterSet
search_fields = ('asset__address', 'account__username') search_fields = ('asset__address', 'account__username')
ordering_fields = ('date_finished',) ordering_fields = ('date_finished',)
@@ -63,10 +63,12 @@ class PushAccountRemoveAssetApi(AutomationRemoveAssetApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountAddAssetApi(AutomationAddAssetApi): class PushAccountAddAssetApi(AutomationAddAssetApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi): class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateNodeSerializer serializer_class = serializers.PushAccountUpdateNodeSerializer

View File

@@ -235,8 +235,8 @@ class AccountBackupHandler:
except Exception as e: except Exception as e:
error = str(e) error = str(e)
print(f'\033[31m>>> {error}\033[0m') print(f'\033[31m>>> {error}\033[0m')
self.manager.status = Status.error self.execution.status = Status.error
self.manager.summary['error'] = error self.execution.summary['error'] = error
def backup_by_obj_storage(self): def backup_by_obj_storage(self):
object_id = self.execution.snapshot.get('id') object_id = self.execution.snapshot.get('id')

View File

@@ -5,13 +5,12 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.automations.methods import platform_automation_methods from accounts.automations.methods import platform_automation_methods
from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice, \ from accounts.const import SSHKeyStrategy, SecretStrategy, SecretType, ChangeSecretRecordStatusChoice
ChangeSecretAccountStatus
from accounts.models import BaseAccountQuerySet from accounts.models import BaseAccountQuerySet
from accounts.utils import SecretGenerator, account_secret_task_status from accounts.utils import SecretGenerator
from assets.automations.base.manager import BasePlaybookManager from assets.automations.base.manager import BasePlaybookManager
from assets.const import HostTypes from assets.const import HostTypes
from common.db.utils import safe_atomic_db_connection from common.db.utils import safe_db_connection
from common.utils import get_logger from common.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -37,7 +36,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
) )
self.account_ids = self.execution.snapshot['accounts'] self.account_ids = self.execution.snapshot['accounts']
self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试 self.record_map = self.execution.snapshot.get('record_map', {}) # 这个是某个失败的记录重试
self.name_record_mapper = {} # 做个映射,方便后面处理 self.name_recorder_mapper = {} # 做个映射,方便后面处理
def gen_account_inventory(self, account, asset, h, path_dir): def gen_account_inventory(self, account, asset, h, path_dir):
raise NotImplementedError raise NotImplementedError
@@ -70,7 +69,7 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
return return
asset = privilege_account.asset asset = privilege_account.asset
accounts = asset.all_accounts.all() accounts = asset.accounts.all()
accounts = accounts.filter(id__in=self.account_ids, secret_reset=True) accounts = accounts.filter(id__in=self.account_ids, secret_reset=True)
if self.secret_type: if self.secret_type:
@@ -95,7 +94,6 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
h['account'] = { h['account'] = {
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,
'full_username': account.full_username,
'secret_type': secret_type, 'secret_type': secret_type,
'secret': account.escape_jinja2_syntax(new_secret), 'secret': account.escape_jinja2_syntax(new_secret),
'private_key_path': private_key_path, 'private_key_path': private_key_path,
@@ -113,25 +111,10 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if host.get('error'): if host.get('error'):
return host return host
inventory_hosts = [] host['check_conn_after_change'] = self.execution.snapshot.get('check_conn_after_change', True)
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'] = {} host['ssh_params'] = {}
accounts = self.get_accounts(account) 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") error_msg = _("No pending accounts found")
if not accounts: if not accounts:
print(f'{asset}: {error_msg}') print(f'{asset}: {error_msg}')
@@ -140,53 +123,39 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if asset.type == HostTypes.WINDOWS: if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD) accounts = accounts.filter(secret_type=SecretType.PASSWORD)
inventory_hosts = []
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
print(f'Windows {asset} does not support ssh key push')
return inventory_hosts
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
h['name'] += '(' + account.username + ')' # To distinguish different accounts 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: try:
h, record = self.gen_account_inventory(account, asset, h, path_dir) h = 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: except Exception as e:
h['error'] = str(e) h['error'] = str(e)
self.clear_account_queue_status(account.id)
inventory_hosts.append(h) inventory_hosts.append(h)
return inventory_hosts return inventory_hosts
@staticmethod @staticmethod
def save_record(record): def save_record(recorder):
record.save(update_fields=['error', 'status', 'date_finished']) recorder.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): def on_host_success(self, host, result):
record = self.name_record_mapper.get(host) recorder = self.name_recorder_mapper.get(host)
if not record: if not recorder:
return return
record.status = ChangeSecretRecordStatusChoice.success.value recorder.status = ChangeSecretRecordStatusChoice.success.value
record.date_finished = timezone.now() recorder.date_finished = timezone.now()
account = record.account account = recorder.account
if not account: if not account:
print("Account not found, deleted ?") print("Account not found, deleted ?")
return return
account.secret = getattr(record, 'new_secret', account.secret) account.secret = getattr(recorder, 'new_secret', account.secret)
account.date_updated = timezone.now() account.date_updated = timezone.now()
account.date_change_secret = timezone.now() account.date_change_secret = timezone.now()
account.change_secret_status = ChangeSecretRecordStatusChoice.success account.change_secret_status = ChangeSecretRecordStatusChoice.success
@@ -200,19 +169,18 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
) )
super().on_host_success(host, result) super().on_host_success(host, result)
with safe_atomic_db_connection(): with safe_db_connection():
account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status']) account.save(update_fields=['secret', 'date_updated', 'date_change_secret', 'change_secret_status'])
self.save_record(record) self.save_record(recorder)
self.clear_account_queue_status(account.id)
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
record = self.name_record_mapper.get(host) recorder = self.name_recorder_mapper.get(host)
if not record: if not recorder:
return return
record.status = ChangeSecretRecordStatusChoice.failed.value recorder.status = ChangeSecretRecordStatusChoice.failed.value
record.date_finished = timezone.now() recorder.date_finished = timezone.now()
record.error = error recorder.error = error
account = record.account account = recorder.account
if not account: if not account:
print("Account not found, deleted ?") print("Account not found, deleted ?")
return return
@@ -223,13 +191,12 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
self.summary['fail_accounts'] += 1 self.summary['fail_accounts'] += 1
self.result['fail_accounts'].append( self.result['fail_accounts'].append(
{ {
"asset": str(record.asset), "asset": str(recorder.asset),
"username": record.account.username, "username": recorder.account.username,
} }
) )
super().on_host_error(host, error, result) super().on_host_error(host, error, result)
with safe_atomic_db_connection(): with safe_db_connection():
account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated']) account.save(update_fields=['change_secret_status', 'date_change_secret', 'date_updated'])
self.save_record(record) self.save_record(recorder)
self.clear_account_queue_status(account.id)

View File

@@ -53,6 +53,4 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}" ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options: connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
when: check_conn_after_change when: check_conn_after_change
register: result
failed_when: not result.is_available

View File

@@ -39,8 +39,7 @@
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}" priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true ignore_errors: true
when: db_info is succeeded when: db_info is succeeded

View File

@@ -56,5 +56,3 @@
ssl_key: "{{ ssl_key if check_ssl and ssl_key | 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 }}" ssl_mode: "{{ jms_asset.spec_info.pg_ssl_mode }}"
when: check_conn_after_change when: check_conn_after_change
register: result
failed_when: not result.is_available

View File

@@ -5,14 +5,12 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
register: db_info register: db_info
@@ -25,53 +23,45 @@
var: info var: info
- name: Check whether SQLServer User exist - name: Check whether SQLServer User exist
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' 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 }}';" script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded when: db_info is succeeded
register: user_exist register: user_exist
- name: Change SQLServer password - name: Change SQLServer password
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' 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" script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length != 0 when: user_exist.query_results[0] | length != 0
- name: Add SQLServer user - name: Add SQLServer user
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' 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" 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 ignore_errors: true
when: user_exist.query_results[0] | length == 0 when: user_exist.query_results[0] | length == 0
- name: Verify password - name: Verify password
mssql_script: community.general.mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
when: check_conn_after_change when: check_conn_after_change

View File

@@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | 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 }}" 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 }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1
@@ -42,7 +41,6 @@
password: "{{ account.secret | password_hash('des') }}" password: "{{ account.secret | password_hash('des') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -85,7 +83,6 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -104,9 +101,7 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "password" and check_conn_after_change
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -117,7 +112,5 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "ssh_key" and check_conn_after_change
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -28,12 +28,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" 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 - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -67,11 +61,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' 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: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -97,11 +86,6 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | 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 }}" 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 }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1
@@ -42,7 +41,6 @@
password: "{{ account.secret | password_hash('sha512') }}" password: "{{ account.secret | password_hash('sha512') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -85,7 +83,6 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -104,9 +101,7 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "password" and check_conn_after_change
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -117,7 +112,5 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "ssh_key" and check_conn_after_change
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -30,12 +30,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" 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 - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -69,11 +63,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' 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: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -99,11 +88,6 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@@ -8,7 +8,7 @@ type:
params: params:
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: '用户组'
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
@@ -24,7 +24,3 @@ i18n:
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -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

View File

@@ -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'

View File

@@ -9,24 +9,19 @@ priority: 49
params: params:
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: '用户组'
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
i18n: i18n:
Windows account change secret rdp verify: Windows account change secret rdp verify:
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密(最后使用 Python 模块 pyfreerdp 验证账号的可连接性' zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密 RDP 协议测试最后的可连接性'
ja: 'Ansible モジュール win_user を使用して Windows アカウントのパスワードを変更します (最後に Python モジュール pyfreerdp を使用してアカウントの接続を確認します)' ja: 'Ansibleモジュールwin_userWindowsアカウントの改密RDPプロトコルテストの最後の接続性を実行する'
en: 'Use the Ansible module win_user to change the Windows account password (finally use the Python module pyfreerdp to verify the account connectivity)' en: 'Using the Ansible module win_user performs Windows account encryption RDP protocol testing for final connectivity'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -9,7 +9,7 @@ from accounts.const import (
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
) )
from accounts.models import ChangeSecretRecord from accounts.models import ChangeSecretRecord
from accounts.notifications import ChangeSecretExecutionTaskMsg from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer from accounts.serializers import ChangeSecretRecordBackUpSerializer
from common.utils import get_logger from common.utils import get_logger
from common.utils.file import encrypt_and_compress_zip_file from common.utils.file import encrypt_and_compress_zip_file
@@ -30,28 +30,28 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
record = self.get_or_create_record(asset, account, h['name']) 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) 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) h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
return h, record return h
def get_or_create_record(self, asset, account, name): def get_or_create_record(self, asset, account, name):
asset_account_id = f'{asset.id}-{account.id}' asset_account_id = f'{asset.id}-{account.id}'
if asset_account_id in self.record_map: if asset_account_id in self.record_map:
record_id = self.record_map[asset_account_id] record_id = self.record_map[asset_account_id]
record = ChangeSecretRecord.objects.filter(id=record_id).first() recorder = ChangeSecretRecord.objects.filter(id=record_id).first()
else: else:
new_secret = self.get_secret(account) new_secret = self.get_secret(account)
record = self.create_record(asset, account, new_secret) recorder = self.create_record(asset, account, new_secret)
self.name_record_mapper[name] = record self.name_recorder_mapper[name] = recorder
return record return recorder
def create_record(self, asset, account, new_secret): def create_record(self, asset, account, new_secret):
record = ChangeSecretRecord( recorder = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution, asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret, old_secret=account.secret, new_secret=new_secret,
comment=f'{account.username}@{asset.address}' comment=f'{account.username}@{asset.address}'
) )
return record return recorder
def check_secret(self): def check_secret(self):
if self.secret_strategy == SecretStrategy.custom \ if self.secret_strategy == SecretStrategy.custom \
@@ -61,10 +61,10 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
return True return True
@staticmethod @staticmethod
def get_summary(records): def get_summary(recorders):
total, succeed, failed = 0, 0, 0 total, succeed, failed = 0, 0, 0
for record in records: for recorder in recorders:
if record.status == ChangeSecretRecordStatusChoice.success.value: if recorder.status == ChangeSecretRecordStatusChoice.success.value:
succeed += 1 succeed += 1
else: else:
failed += 1 failed += 1
@@ -73,8 +73,8 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
return summary return summary
def print_summary(self): def print_summary(self):
records = list(self.name_record_mapper.values()) recorders = list(self.name_recorder_mapper.values())
summary = self.get_summary(records) summary = self.get_summary(recorders)
print('\n\n' + '-' * 80) print('\n\n' + '-' * 80)
plan_execution_end = _('Plan execution end') plan_execution_end = _('Plan execution end')
print('{} {}\n'.format(plan_execution_end, local_now_filename())) print('{} {}\n'.format(plan_execution_end, local_now_filename()))
@@ -86,7 +86,7 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
if self.secret_type and not self.check_secret(): if self.secret_type and not self.check_secret():
return return
records = list(self.name_record_mapper.values()) recorders = list(self.name_recorder_mapper.values())
if self.record_map: if self.record_map:
return return
@@ -94,17 +94,21 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
if not recipients: if not recipients:
return return
if not records: context = self.get_report_context()
for user in recipients:
ChangeSecretReportMsg(user, context).publish()
if not recorders:
return return
summary = self.get_summary(records) summary = self.get_summary(recorders)
self.send_record_mail(recipients, records, summary) self.send_recorder_mail(recipients, recorders, summary)
def send_record_mail(self, recipients, records, summary): def send_recorder_mail(self, recipients, recorders, summary):
name = self.execution.snapshot['name'] name = self.execution.snapshot['name']
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx') filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
if not self.create_file(records, filename): if not self.create_file(recorders, filename):
return return
for user in recipients: for user in recipients:
@@ -117,9 +121,9 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
os.remove(filename) os.remove(filename)
@staticmethod @staticmethod
def create_file(records, filename): def create_file(recorders, filename):
serializer_cls = ChangeSecretRecordBackUpSerializer 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()] header = [str(v.label) for v in serializer.child.fields.values()]
rows = [[str(i) for i in row.values()] for row in serializer.data] rows = [[str(i) for i in row.values()] for row in serializer.data]

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a2805a0264fc07ae597704841ab060edef8bf74654f525bc778cb9195d8cad0e
size 2547712

View File

@@ -12,16 +12,13 @@ from accounts.models import Account, AccountRisk, RiskChoice
from assets.automations.base.manager import BaseManager from assets.automations.base.manager import BaseManager
from common.const import ConfirmOrIgnore from common.const import ConfirmOrIgnore
from common.decorators import bulk_create_decorator, bulk_update_decorator from common.decorators import bulk_create_decorator, bulk_update_decorator
from settings.models import LeakPasswords
# 已设置手动 finish
@bulk_create_decorator(AccountRisk) @bulk_create_decorator(AccountRisk)
def create_risk(data): def create_risk(data):
return AccountRisk(**data) return AccountRisk(**data)
# 已设置手动 finish
@bulk_update_decorator(AccountRisk, update_fields=["details", "status"]) @bulk_update_decorator(AccountRisk, update_fields=["details", "status"])
def update_risk(risk): def update_risk(risk):
return risk return risk
@@ -160,8 +157,10 @@ class CheckLeakHandler(BaseCheckHandler):
if not account.secret: if not account.secret:
return False return False
is_exist = LeakPasswords.objects.using('sqlite').filter(password=account.secret).exists() sql = 'SELECT 1 FROM passwords WHERE password = ? LIMIT 1'
return is_exist self.cursor.execute(sql, (account.secret,))
leak = self.cursor.fetchone() is not None
return leak
def clean(self): def clean(self):
self.cursor.close() self.cursor.close()
@@ -219,9 +218,6 @@ class CheckAccountManager(BaseManager):
"details": [{"datetime": now, 'type': 'init'}], "details": [{"datetime": now, 'type': 'init'}],
}) })
create_risk.finish()
update_risk.finish()
def pre_run(self): def pre_run(self):
super().pre_run() super().pre_run()
self.assets = self.execution.get_all_assets() self.assets = self.execution.get_all_assets()
@@ -240,11 +236,6 @@ class CheckAccountManager(BaseManager):
print("Check: {} => {}".format(account, msg)) print("Check: {} => {}".format(account, msg))
if not error: if not error:
AccountRisk.objects.filter(
asset=account.asset,
username=account.username,
risk=handler.risk
).delete()
continue continue
self.add_risk(handler.risk, account) self.add_risk(handler.risk, account)
self.commit_risks(_assets) self.commit_risks(_assets)
@@ -274,7 +265,7 @@ class CheckAccountManager(BaseManager):
handler.clean() handler.clean()
def get_report_subject(self): def get_report_subject(self):
return _("Check account report of {}").format(self.execution.id) return "Check account report of %s" % self.execution.id
def get_report_template(self): def get_report_template(self):
return "accounts/check_account_report.html" return "accounts/check_account_report.html"

View File

@@ -5,14 +5,12 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT SELECT
l.name, l.name,

View File

@@ -13,7 +13,6 @@ def parse_date(date_str, default=None):
formats = [ formats = [
'%Y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S',
'%Y-%m-%dT%H:%M:%S', '%Y-%m-%dT%H:%M:%S',
'%Y-%m-%d %H:%M:%S',
'%d-%m-%Y %H:%M:%S', '%d-%m-%Y %H:%M:%S',
'%Y/%m/%d', '%Y/%m/%d',
'%d-%m-%Y', '%d-%m-%Y',
@@ -27,6 +26,7 @@ def parse_date(date_str, default=None):
return default return default
# TODO 后期会挪到 playbook 中
class GatherAccountsFilter: class GatherAccountsFilter:
def __init__(self, tp): def __init__(self, tp):
self.tp = tp self.tp = tp
@@ -208,35 +208,14 @@ class GatherAccountsFilter:
key, value = parts key, value = parts
user_info[key.strip()] = value.strip() user_info[key.strip()] = value.strip()
detail = {'groups': user_info.get('Global Group memberships', ''), } detail = {'groups': user_info.get('Global Group memberships', ''), }
user = {
username = user_info.get('User name') 'username': user_info.get('User name', ''),
if not username: 'date_password_change': parse_date(user_info.get('Password last set', '')),
continue 'date_password_expired': parse_date(user_info.get('Password expires', '')),
'date_last_login': parse_date(user_info.get('Last logon', '')),
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, 'groups': detail,
} }
result[user['username']] = user
return result return result
@staticmethod @staticmethod

View File

@@ -4,7 +4,6 @@
- name: Run net user command to get all users - name: Run net user command to get all users
win_shell: net user win_shell: net user
register: user_list_output register: user_list_output
failed_when: false
- name: Parse all users from net user command - name: Parse all users from net user command
set_fact: set_fact:

View File

@@ -2,13 +2,10 @@ id: gather_accounts_windows
name: "{{ 'Windows account gather' | trans }}" name: "{{ 'Windows account gather' | trans }}"
version: 1 version: 1
method: gather_accounts method: gather_accounts
category: category: host
- host
type: type:
- windows - windows
i18n: i18n:
Windows account gather: Windows account gather:
zh: 使用命令 net user 收集 Windows 账号 zh: 使用命令 net user 收集 Windows 账号

View File

@@ -1,74 +0,0 @@
- hosts: demo
gather_facts: no
tasks:
- name: Import ActiveDirectory module
win_shell: Import-Module ActiveDirectory
args:
warn: false
- name: Get the SamAccountName list of all AD users
win_shell: |
Import-Module ActiveDirectory
Get-ADUser -Filter * | Select-Object -ExpandProperty SamAccountName
register: ad_user_list
- name: Set the all_users variable
set_fact:
all_users: "{{ ad_user_list.stdout_lines }}"
- name: Get detailed information for each user
win_shell: |
Import-Module ActiveDirectory
$user = Get-ADUser -Identity {{ item }} -Properties Name, SamAccountName, Enabled, LastLogonDate, PasswordLastSet, msDS-UserPasswordExpiryTimeComputed, MemberOf
$globalGroups = @()
if ($user.MemberOf) {
$globalGroups = $user.MemberOf | ForEach-Object {
try {
$group = Get-ADGroup $_ -ErrorAction Stop
if ($group.GroupScope -eq 'Global') { $group.Name }
} catch {
}
}
}
$passwordExpiry = $null
$expiryRaw = $user.'msDS-UserPasswordExpiryTimeComputed'
if ($expiryRaw) {
try {
$passwordExpiry = [datetime]::FromFileTime($expiryRaw)
} catch {
$passwordExpiry = $null
}
}
$output = [PSCustomObject]@{
Name = $user.Name
SamAccountName = $user.SamAccountName
Enabled = $user.Enabled
LastLogonDate = if ($user.LastLogonDate) { $user.LastLogonDate.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
PasswordLastSet = if ($user.PasswordLastSet) { $user.PasswordLastSet.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
PasswordExpires = if ($passwordExpiry) { $passwordExpiry.ToString("yyyy-MM-dd HH:mm:ss") } else { $null }
GlobalGroupMemberships = $globalGroups
}
$output | ConvertTo-Json -Depth 3
loop: "{{ all_users }}"
register: ad_user_details
ignore_errors: yes
- set_fact:
info:
user_details: >-
{{
ad_user_details.results
| selectattr('rc', 'equalto', 0)
| map(attribute='stdout')
| select('truthy')
| map('from_json')
}}
- debug:
var: info

View File

@@ -1,15 +0,0 @@
id: gather_accounts_windows_ad
name: "{{ 'Windows account gather' | trans }}"
version: 1
method: gather_accounts
category:
- ds
type:
- windows_ad
i18n:
Windows account gather:
zh: 使用命令 Get-ADUser 收集 Windows 账号
ja: コマンド Get-ADUser を使用して Windows アカウントを収集する
en: Using command Get-ADUser to gather accounts

View File

@@ -1,6 +1,6 @@
import time
from collections import defaultdict from collections import defaultdict
import time
from django.utils import timezone from django.utils import timezone
from accounts.const import AutomationTypes from accounts.const import AutomationTypes
@@ -30,16 +30,6 @@ common_risk_items = [
diff_items = risk_items + common_risk_items diff_items = risk_items + common_risk_items
@bulk_create_decorator(AccountRisk)
def _create_risk(data):
return AccountRisk(**data)
@bulk_update_decorator(AccountRisk, update_fields=["details"])
def _update_risk(account):
return account
def format_datetime(value): def format_datetime(value):
if isinstance(value, timezone.datetime): if isinstance(value, timezone.datetime):
return value.strftime("%Y-%m-%d %H:%M:%S") return value.strftime("%Y-%m-%d %H:%M:%S")
@@ -151,17 +141,25 @@ class AnalyseAccountRisk:
found = assets_risks.get(key) found = assets_risks.get(key)
if not found: if not found:
_create_risk(dict(**d, details=[detail])) self._create_risk(dict(**d, details=[detail]))
continue continue
found.details.append(detail) found.details.append(detail)
_update_risk(found) self._update_risk(found)
@bulk_create_decorator(AccountRisk)
def _create_risk(self, data):
return AccountRisk(**data)
@bulk_update_decorator(AccountRisk, update_fields=["details"])
def _update_risk(self, account):
return account
def lost_accounts(self, asset, lost_users): def lost_accounts(self, asset, lost_users):
if not self.check_risk: if not self.check_risk:
return return
for user in lost_users: for user in lost_users:
_create_risk( self._create_risk(
dict( dict(
asset_id=str(asset.id), asset_id=str(asset.id),
username=user, username=user,
@@ -178,7 +176,7 @@ class AnalyseAccountRisk:
self._analyse_item_changed(ga, d) self._analyse_item_changed(ga, d)
if not sys_found: if not sys_found:
basic = {"asset": asset, "username": d["username"], 'gathered_account': ga} basic = {"asset": asset, "username": d["username"], 'gathered_account': ga}
_create_risk( self._create_risk(
dict( dict(
**basic, **basic,
risk=RiskChoice.new_found, risk=RiskChoice.new_found,
@@ -224,7 +222,6 @@ class GatherAccountsManager(AccountBasePlaybookManager):
def _collect_asset_account_info(self, asset, info): def _collect_asset_account_info(self, asset, info):
result = self._filter_success_result(asset.type, info) result = self._filter_success_result(asset.type, info)
accounts = [] accounts = []
for username, info in result.items(): for username, info in result.items():
self.asset_usernames_mapper[str(asset.id)].add(username) self.asset_usernames_mapper[str(asset.id)].add(username)
@@ -376,7 +373,6 @@ class GatherAccountsManager(AccountBasePlaybookManager):
for asset, accounts_data in self.asset_account_info.items(): for asset, accounts_data in self.asset_account_info.items():
ori_users = self.ori_asset_usernames[str(asset.id)] ori_users = self.ori_asset_usernames[str(asset.id)]
need_analyser_gather_account = []
with tmp_to_org(asset.org_id): with tmp_to_org(asset.org_id):
for d in accounts_data: for d in accounts_data:
username = d["username"] username = d["username"]
@@ -389,12 +385,10 @@ class GatherAccountsManager(AccountBasePlaybookManager):
ga = ori_account ga = ori_account
self.update_gathered_account(ori_account, d) self.update_gathered_account(ori_account, d)
ori_found = username in ori_users ori_found = username in ori_users
need_analyser_gather_account.append((asset, ga, d, ori_found)) risk_analyser.analyse_risk(asset, ga, d, ori_found)
# 这里顺序不能调整risk 外键关联了 gathered_account 主键 id所以在创建 risk 需要保证 gathered_account 已经创建完成
self.create_gathered_account.finish() self.create_gathered_account.finish()
self.update_gathered_account.finish() self.update_gathered_account.finish()
for analysis_data in need_analyser_gather_account:
risk_analyser.analyse_risk(*analysis_data)
self.update_gather_accounts_status(asset) self.update_gather_accounts_status(asset)
if not self.is_sync_account: if not self.is_sync_account:
continue continue
@@ -406,9 +400,6 @@ class GatherAccountsManager(AccountBasePlaybookManager):
present=True present=True
) )
# 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步 # 因为有 bulk create, bulk update, 所以这里需要 sleep 一下,等待数据同步
_update_risk.finish()
_create_risk.finish()
time.sleep(0.5) time.sleep(0.5)
def get_report_template(self): def get_report_template(self):

View File

@@ -20,11 +20,10 @@
become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}" become_private_key_path: "{{ jms_custom_become_private_key_path | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
register: ping_info register: ping_info
delegate_to: localhost delegate_to: localhost
- name: Push asset password (paramiko) - name: Change asset password (paramiko)
custom_command: custom_command:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
@@ -40,10 +39,7 @@
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
commands: "{{ params.commands }}" commands: "{{ params.commands }}"
answers: "{{ params.answers }}" first_conn_delay_time: "{{ first_conn_delay_time | default(0.5) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
delay_time: "{{ params.delay_time | default(2) }}"
prompt: "{{ params.prompt | default('.*') }}"
ignore_errors: true ignore_errors: true
when: ping_info is succeeded and check_conn_after_change when: ping_info is succeeded and check_conn_after_change
register: change_info register: change_info
@@ -62,6 +58,5 @@
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
recv_timeout: "{{ params.recv_timeout | default(30) }}"
delegate_to: localhost delegate_to: localhost
when: check_conn_after_change when: check_conn_after_change

View File

@@ -10,30 +10,10 @@ protocol: ssh
priority: 50 priority: 50
params: params:
- name: commands - name: commands
type: text type: list
label: "{{ 'Params commands label' | trans }}" label: "{{ 'Params commands label' | trans }}"
default: '' default: [ '' ]
help_text: "{{ 'Params commands help text' | trans }}" 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: i18n:
SSH account push: SSH account push:
@@ -42,91 +22,11 @@ i18n:
en: 'Custom push using SSH command line' en: 'Custom push using SSH command line'
Params commands help text: Params commands help text:
zh: | zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 &#123;username&#125;、&#123;password&#125;、&#123;login_password&#125;格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br />4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
请将命令中的指定位置改成特殊符号 <br /> ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />&#123;ユーザー名&#125;、&#123;パスワード&#125;、&#123;login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.&#123;login_password&#125;<br />3 .ターミナルの設定<br / >4. ユーザー名 &#123;ユーザー名&#125; 権限 0 パスワード &#123;パスワード&#125; <br />5. 終了'
1. 推送账号 -> {username} <br /> en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use &#123;username&#125;, &#123;password&#125;, &#123;login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. &#123;login_password&#125;<br />3. configure terminal<br / >4. username &#123;username&#125; privilege 0 password &#123;password&#125; <br />5. end'
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 push 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: Params commands label:
zh: '自定义命令' zh: '自定义命令'
ja: 'カスタムコマンド' ja: 'カスタムコマンド'
en: 'Custom command' 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)

View File

@@ -54,5 +54,3 @@
connection_options: connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
when: check_conn_after_change when: check_conn_after_change
register: result
failed_when: not result.is_available

View File

@@ -39,8 +39,7 @@
name: "{{ account.username }}" name: "{{ account.username }}"
password: "{{ account.secret }}" password: "{{ account.secret }}"
host: "%" host: "%"
priv: "{{ omit if db_name == '' else db_name + '.*:ALL' }}" priv: "{{ account.username + '.*:USAGE' if db_name == '' else db_name + '.*:ALL' }}"
append_privs: "{{ db_name != '' | bool }}"
ignore_errors: true ignore_errors: true
when: db_info is succeeded when: db_info is succeeded

View File

@@ -5,14 +5,12 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
register: db_info register: db_info
@@ -25,55 +23,47 @@
var: info var: info
- name: Check whether SQLServer User exist - name: Check whether SQLServer User exist
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' 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 }}';" script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded when: db_info is succeeded
register: user_exist register: user_exist
- name: Change SQLServer password - name: Change SQLServer password
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' 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" script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length != 0 when: user_exist.query_results[0] | length != 0
register: change_info register: change_info
- name: Add SQLServer user - name: Add SQLServer user
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' 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 }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version" script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length == 0 when: user_exist.query_results[0] | length == 0
register: change_info register: change_info
- name: Verify password - name: Verify password
mssql_script: community.general.mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
when: check_conn_after_change when: check_conn_after_change

View File

@@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | 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 }}" 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 }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1
@@ -42,7 +41,6 @@
password: "{{ account.secret | password_hash('des') }}" password: "{{ account.secret | password_hash('des') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -85,7 +83,6 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -104,9 +101,7 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "password" and check_conn_after_change
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -117,8 +112,6 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "ssh_key" and check_conn_after_change
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -28,12 +28,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" 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 - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -67,11 +61,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' 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: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -97,11 +86,6 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | 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 }}" 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 }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1
@@ -42,7 +41,6 @@
password: "{{ account.secret | password_hash('sha512') }}" password: "{{ account.secret | password_hash('sha512') }}"
update_password: always update_password: always
ignore_errors: true ignore_errors: true
register: change_secret_result
when: account.secret_type == "password" when: account.secret_type == "password"
- name: "Get home directory for {{ account.username }}" - name: "Get home directory for {{ account.username }}"
@@ -85,7 +83,6 @@
user: "{{ account.username }}" user: "{{ account.username }}"
key: "{{ account.secret }}" key: "{{ account.secret }}"
exclusive: "{{ ssh_params.exclusive }}" exclusive: "{{ ssh_params.exclusive }}"
register: change_secret_result
when: account.secret_type == "ssh_key" when: account.secret_type == "ssh_key"
- name: Refresh connection - name: Refresh connection
@@ -104,9 +101,7 @@
become_password: "{{ account.become.ansible_password | default('') }}" become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}" become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "password" and check_conn_after_change
- account.secret_type == "password"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost
- name: "Verify {{ account.username }} SSH KEY (paramiko)" - name: "Verify {{ account.username }} SSH KEY (paramiko)"
@@ -117,8 +112,6 @@
login_private_key_path: "{{ account.private_key_path }}" login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}" gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}" old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: when: account.secret_type == "ssh_key" and check_conn_after_change
- account.secret_type == "ssh_key"
- check_conn_after_change or change_secret_result.failed | default(false)
delegate_to: localhost delegate_to: localhost

View File

@@ -30,12 +30,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" 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 - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@@ -69,11 +63,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' 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: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@@ -95,14 +84,9 @@ i18n:
en: 'Home' en: 'Home'
Params groups label: Params groups label:
zh: '附加组' zh: '用户组'
ja: '追加グループ' ja: 'グループ'
en: 'Additional Group' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'

View File

@@ -8,7 +8,7 @@ type:
params: params:
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: '用户组'
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
@@ -22,8 +22,3 @@ i18n:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -1,27 +0,0 @@
- hosts: demo
gather_facts: no
tasks:
- name: Test privileged account
ansible.windows.win_ping:
- name: Push user 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

View File

@@ -1,30 +0,0 @@
id: push_account_ad_windows
name: "{{ 'Windows account push' | trans }}"
version: 1
method: push_account
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 push:
zh: '使用 Ansible 模块 win_domain_user 执行 Windows 账号推送'
ja: 'Ansible win_domain_user モジュールを使用して Windows アカウントをプッシュする'
en: 'Using Ansible module win_domain_user to push account'
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'

View File

@@ -9,7 +9,7 @@ priority: 49
params: params:
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: '用户组'
default: 'Users,Remote Desktop Users' default: 'Users,Remote Desktop Users'
help_text: "{{ 'Params groups help text' | trans }}" help_text: "{{ 'Params groups help text' | trans }}"
@@ -23,8 +23,3 @@ i18n:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
Params groups label:
zh: '用户组'
ja: 'グループ'
en: 'Groups'

View File

@@ -12,7 +12,7 @@ logger = get_logger(__name__)
class PushAccountManager(BaseChangeSecretPushManager): class PushAccountManager(BaseChangeSecretPushManager):
@staticmethod @staticmethod
def require_update_version(account, record): def require_update_version(account, recorder):
account.skip_history_when_saving = True account.skip_history_when_saving = True
return False return False
@@ -31,29 +31,29 @@ class PushAccountManager(BaseChangeSecretPushManager):
secret_type = account.secret_type secret_type = account.secret_type
if not secret: if not secret:
raise ValueError(_('Secret cannot be empty')) raise ValueError(_('Secret cannot be empty'))
record = self.get_or_create_record(asset, account, h['name']) self.get_or_create_record(asset, account, h['name'])
new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir) new_secret, private_key_path = self.handle_ssh_secret(secret_type, secret, path_dir)
h = self.gen_inventory(h, account, new_secret, private_key_path, asset) h = self.gen_inventory(h, account, new_secret, private_key_path, asset)
return h, record return h
def get_or_create_record(self, asset, account, name): def get_or_create_record(self, asset, account, name):
asset_account_id = f'{asset.id}-{account.id}' asset_account_id = f'{asset.id}-{account.id}'
if asset_account_id in self.record_map: if asset_account_id in self.record_map:
record_id = self.record_map[asset_account_id] record_id = self.record_map[asset_account_id]
record = PushSecretRecord.objects.filter(id=record_id).first() recorder = PushSecretRecord.objects.filter(id=record_id).first()
else: else:
record = self.create_record(asset, account) recorder = self.create_record(asset, account)
self.name_record_mapper[name] = record self.name_recorder_mapper[name] = recorder
return record return recorder
def create_record(self, asset, account): def create_record(self, asset, account):
record = PushSecretRecord( recorder = PushSecretRecord(
asset=asset, account=account, execution=self.execution, asset=asset, account=account, execution=self.execution,
comment=f'{account.username}@{asset.address}' comment=f'{account.username}@{asset.address}'
) )
return record return recorder
def print_summary(self): def print_summary(self):
print('\n\n' + '-' * 80) print('\n\n' + '-' * 80)

View File

@@ -5,13 +5,10 @@
tasks: tasks:
- name: "Remove account" - name: "Remove account"
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: "{{ jms_asset.spec_info.db_name }}" name: "{{ jms_asset.spec_info.db_name }}"
encryption: "{{ jms_asset.encryption | default(None) }}" script: "DROP USER {{ account.username }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "DROP LOGIN {{ account.username }}; select @@version"

View File

@@ -1,9 +0,0 @@
- hosts: windows
gather_facts: no
tasks:
- name: "Remove account"
ansible.windows.win_domain_user:
name: "{{ account.username }}"
state: absent

View File

@@ -1,14 +0,0 @@
id: remove_account_ad_windows
name: "{{ 'Windows account remove' | trans }}"
version: 1
method: remove_account
category:
- ds
type:
- windows_ad
i18n:
Windows account remove:
zh: 使用 Ansible 模块 win_domain_user 删除账号
ja: Ansible モジュール win_domain_user を使用してアカウントを削除する
en: Use the Ansible module win_domain_user to delete an account

View File

@@ -3,13 +3,13 @@
vars: vars:
ansible_shell_type: sh ansible_shell_type: sh
ansible_connection: local ansible_connection: local
ansible_python_interpreter: "{{ local_python_interpreter }}" ansible_python_interpreter: /opt/py3/bin/python
tasks: tasks:
- name: Verify account (pyfreerdp) - name: Verify account (pyfreerdp)
rdp_ping: rdp_ping:
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
login_user: "{{ account.full_username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_secret_type: "{{ account.secret_type }}" login_secret_type: "{{ account.secret_type }}"

View File

@@ -2,10 +2,8 @@ id: verify_account_by_rdp
name: "{{ 'Windows rdp account verify' | trans }}" name: "{{ 'Windows rdp account verify' | trans }}"
category: category:
- host - host
- ds
type: type:
- windows - windows
- windows_ad
method: verify_account method: verify_account
protocol: rdp protocol: rdp
priority: 1 priority: 1

View File

@@ -16,5 +16,3 @@
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}" ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
connection_options: connection_options:
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}" - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert }}"
register: result
failed_when: not result.is_available

View File

@@ -5,13 +5,11 @@
tasks: tasks:
- name: Verify account - name: Verify account
mssql_script: community.general.mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version

View File

@@ -8,7 +8,6 @@
ansible_user: "{{ account.username }}" ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}" ansible_password: "{{ account.secret }}"
ansible_ssh_private_key_file: "{{ account.private_key_path }}" ansible_ssh_private_key_file: "{{ account.private_key_path }}"
ansible_timeout: 30
when: not account.become.ansible_become when: not account.become.ansible_become
- name: Verify account connectivity(Switch) - name: Verify account connectivity(Switch)
@@ -21,5 +20,4 @@
ansible_become_method: "{{ account.become.ansible_become_method }}" ansible_become_method: "{{ account.become.ansible_become_method }}"
ansible_become_user: "{{ account.become.ansible_become_user }}" ansible_become_user: "{{ account.become.ansible_become_user }}"
ansible_become_password: "{{ account.become.ansible_become_password }}" ansible_become_password: "{{ account.become.ansible_become_password }}"
ansible_timeout: 30
when: account.become.ansible_become when: account.become.ansible_become

View File

@@ -7,6 +7,5 @@
- name: Verify account - name: Verify account
ansible.windows.win_ping: ansible.windows.win_ping:
vars: vars:
ansible_user: "{{ account.full_username }}" ansible_user: "{{ account.username }}"
ansible_password: "{{ account.secret }}" ansible_password: "{{ account.secret }}"
ansible_timeout: 30

View File

@@ -2,12 +2,9 @@ id: verify_account_windows
name: "{{ 'Windows account verify' | trans }}" name: "{{ 'Windows account verify' | trans }}"
version: 1 version: 1
method: verify_account method: verify_account
category: category: host
- host
- ds
type: type:
- windows - windows
- windows_ad
i18n: i18n:
Windows account verify: Windows account verify:

View File

@@ -42,7 +42,7 @@ class VerifyAccountManager(AccountBasePlaybookManager):
if host.get('error'): if host.get('error'):
return host return host
accounts = asset.all_accounts.all() accounts = asset.accounts.all()
accounts = self.get_accounts(account, accounts) accounts = self.get_accounts(account, accounts)
inventory_hosts = [] inventory_hosts = []
@@ -64,7 +64,6 @@ class VerifyAccountManager(AccountBasePlaybookManager):
h['account'] = { h['account'] = {
'name': account.name, 'name': account.name,
'username': account.username, 'username': account.username,
'full_username': account.full_username,
'secret_type': account.secret_type, 'secret_type': account.secret_type,
'secret': account.escape_jinja2_syntax(secret), 'secret': account.escape_jinja2_syntax(secret),
'private_key_path': private_key_path, 'private_key_path': private_key_path,
@@ -85,7 +84,6 @@ class VerifyAccountManager(AccountBasePlaybookManager):
def on_host_error(self, host, error, result): def on_host_error(self, host, error, result):
account = self.host_account_mapper.get(host) account = self.host_account_mapper.get(host)
try: try:
error_tp = account.get_err_connectivity(error) account.set_connectivity(Connectivity.ERR)
account.set_connectivity(error_tp)
except Exception as e: except Exception as e:
print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n') print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')

View File

@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
from common.utils import get_logger from common.utils import get_logger
@@ -11,9 +14,6 @@ __all__ = ['AZUREVaultClient']
class AZUREVaultClient(object): class AZUREVaultClient(object):
def __init__(self, vault_url, tenant_id, client_id, client_secret): def __init__(self, vault_url, tenant_id, client_id, client_secret):
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
authentication_endpoint = 'https://login.microsoftonline.com/' \ authentication_endpoint = 'https://login.microsoftonline.com/' \
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/' if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
@@ -23,8 +23,6 @@ class AZUREVaultClient(object):
self.client = SecretClient(vault_url=vault_url, credential=credentials) self.client = SecretClient(vault_url=vault_url, credential=credentials)
def is_active(self): def is_active(self):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try: try:
self.client.set_secret('jumpserver', '666') self.client.set_secret('jumpserver', '666')
except (ResourceNotFoundError, ClientAuthenticationError) as e: except (ResourceNotFoundError, ClientAuthenticationError) as e:
@@ -34,8 +32,6 @@ class AZUREVaultClient(object):
return True, '' return True, ''
def get(self, name, version=None): def get(self, name, version=None):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try: try:
secret = self.client.get_secret(name, version) secret = self.client.get_secret(name, version)
return secret.value return secret.value

View File

@@ -17,7 +17,7 @@ __all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity', 'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice', 'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice', 'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
'GatherAccountDetailField', 'ChangeSecretAccountStatus' 'GatherAccountDetailField'
] ]
@@ -117,12 +117,6 @@ class ChangeSecretRecordStatusChoice(models.TextChoices):
pending = 'pending', _('Pending') pending = 'pending', _('Pending')
class ChangeSecretAccountStatus(models.TextChoices):
QUEUED = 'queued', _('Queued')
READY = 'ready', _('Ready')
PROCESSING = 'processing', _('Processing')
class GatherAccountDetailField(models.TextChoices): class GatherAccountDetailField(models.TextChoices):
can_login = 'can_login', _('Can login') can_login = 'can_login', _('Can login')
superuser = 'superuser', _('Superuser') superuser = 'superuser', _('Superuser')

View File

@@ -5,6 +5,7 @@ import uuid
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_filters import rest_framework as drf_filters from django_filters import rest_framework as drf_filters
from rest_framework import filters from rest_framework import filters
from rest_framework.compat import coreapi from rest_framework.compat import coreapi
@@ -12,26 +13,10 @@ from rest_framework.compat import coreapi
from assets.models import Node from assets.models import Node
from assets.utils import get_node_from_request from assets.utils import get_node_from_request
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from common.utils import get_logger
from common.utils.timezone import local_zero_hour, local_now from common.utils.timezone import local_zero_hour, local_now
from .const.automation import ChangeSecretRecordStatusChoice from .const.automation import ChangeSecretRecordStatusChoice
from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \ from .models import Account, GatheredAccount, ChangeSecretRecord, PushSecretRecord, IntegrationApplication, \
AutomationExecution AutomationExecution
from .utils import account_secret_task_status
logger = get_logger(__file__)
class UUIDFilterMixin:
@staticmethod
def filter_uuid(queryset, name, value):
try:
uuid.UUID(value)
except ValueError:
logger.warning(f"Invalid UUID: {value}")
return queryset.none()
return queryset.filter(**{name: value})
class NodeFilterBackend(filters.BaseFilterBackend): class NodeFilterBackend(filters.BaseFilterBackend):
@@ -58,15 +43,14 @@ class NodeFilterBackend(filters.BaseFilterBackend):
return queryset return queryset
class AccountFilterSet(UUIDFilterMixin, BaseFilterSet): class AccountFilterSet(BaseFilterSet):
ip = drf_filters.CharFilter(field_name="address", lookup_expr="exact") ip = drf_filters.CharFilter(field_name="address", lookup_expr="exact")
name = drf_filters.CharFilter(field_name="name", lookup_expr="exact")
hostname = drf_filters.CharFilter(field_name="name", lookup_expr="exact") hostname = drf_filters.CharFilter(field_name="name", lookup_expr="exact")
username = drf_filters.CharFilter(field_name="username", lookup_expr="exact") username = drf_filters.CharFilter(field_name="username", lookup_expr="exact")
address = drf_filters.CharFilter(field_name="asset__address", lookup_expr="exact") address = drf_filters.CharFilter(field_name="asset__address", lookup_expr="exact")
asset_name = drf_filters.CharFilter(field_name="asset__name", lookup_expr="exact") asset_id = drf_filters.CharFilter(field_name="asset", lookup_expr="exact")
asset_id = drf_filters.CharFilter(field_name="asset", method="filter_uuid") asset = drf_filters.CharFilter(field_name="asset", lookup_expr="exact")
assets = drf_filters.CharFilter(field_name="asset_id", method="filter_uuid") assets = drf_filters.CharFilter(field_name="asset_id", lookup_expr="exact")
has_secret = drf_filters.BooleanFilter(method="filter_has_secret") has_secret = drf_filters.BooleanFilter(method="filter_has_secret")
platform = drf_filters.CharFilter( platform = drf_filters.CharFilter(
field_name="asset__platform_id", lookup_expr="exact" field_name="asset__platform_id", lookup_expr="exact"
@@ -151,9 +135,8 @@ class AccountFilterSet(UUIDFilterMixin, BaseFilterSet):
kwargs.update({"date_change_secret__gt": date}) kwargs.update({"date_change_secret__gt": date})
if name == "latest_secret_change_failed": if name == "latest_secret_change_failed":
queryset = ( queryset = queryset.filter(date_change_secret__gt=date).exclude(
queryset.filter(date_change_secret__gt=date) change_secret_status=ChangeSecretRecordStatusChoice.success
.exclude(change_secret_status=ChangeSecretRecordStatusChoice.success)
) )
if kwargs: if kwargs:
@@ -163,8 +146,8 @@ class AccountFilterSet(UUIDFilterMixin, BaseFilterSet):
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
"id", "source_id", "secret_type", "category", "type", "id", "asset", "source_id", "secret_type", "category",
"privileged", "secret_reset", "connectivity", "is_active" "type", "privileged", "secret_reset", "connectivity", 'is_active'
] ]
@@ -202,6 +185,16 @@ class SecretRecordMixin(drf_filters.FilterSet):
return queryset.filter(date_finished__gte=dt) return queryset.filter(date_finished__gte=dt)
class UUIDExecutionFilterMixin:
@staticmethod
def filter_execution(queryset, name, value):
try:
uuid.UUID(value)
except ValueError:
raise ValueError(_('Enter a valid UUID.'))
return queryset.filter(**{name: value})
class DaysExecutionFilterMixin: class DaysExecutionFilterMixin:
days = drf_filters.NumberFilter(method="filter_days") days = drf_filters.NumberFilter(method="filter_days")
field: str field: str
@@ -216,10 +209,10 @@ class DaysExecutionFilterMixin:
class ChangeSecretRecordFilterSet( class ChangeSecretRecordFilterSet(
SecretRecordMixin, UUIDFilterMixin, SecretRecordMixin, UUIDExecutionFilterMixin,
DaysExecutionFilterMixin, BaseFilterSet DaysExecutionFilterMixin, BaseFilterSet
): ):
execution_id = django_filters.CharFilter(method="filter_uuid") execution_id = django_filters.CharFilter(method="filter_execution")
days = drf_filters.NumberFilter(method="filter_days") days = drf_filters.NumberFilter(method="filter_days")
field = 'date_finished' field = 'date_finished'
@@ -234,34 +227,12 @@ class AutomationExecutionFilterSet(DaysExecutionFilterMixin, BaseFilterSet):
class Meta: class Meta:
model = AutomationExecution model = AutomationExecution
fields = ["days", 'trigger', 'automation__name'] fields = ["days", 'trigger', 'automation_id', 'automation__name']
class PushAccountRecordFilterSet(SecretRecordMixin, UUIDFilterMixin, BaseFilterSet): class PushAccountRecordFilterSet(SecretRecordMixin, UUIDExecutionFilterMixin, BaseFilterSet):
execution_id = django_filters.CharFilter(method="filter_uuid") execution_id = django_filters.CharFilter(method="filter_execution")
class Meta: class Meta:
model = PushSecretRecord model = PushSecretRecord
fields = ["id", "status", "asset_id", "execution_id"] fields = ["id", "status", "asset_id", "execution_id"]
class ChangeSecretStatusFilterSet(BaseFilterSet):
asset_name = drf_filters.CharFilter(
field_name="asset__name", lookup_expr="icontains"
)
status = drf_filters.CharFilter(method='filter_dynamic')
execution_id = drf_filters.CharFilter(method='filter_dynamic')
class Meta:
model = Account
fields = ["username"]
@staticmethod
def filter_dynamic(queryset, name, value):
_ids = list(queryset.values_list('id', flat=True))
data_map = {
_id: account_secret_task_status.get(str(_id)).get(name)
for _id in _ids
}
matched = [_id for _id, v in data_map.items() if v == value]
return queryset.filter(id__in=matched)

View File

@@ -46,16 +46,11 @@ class Migration(migrations.Migration):
], ],
options={ options={
'verbose_name': 'Account', 'verbose_name': 'Account',
'permissions': [ 'permissions': [('view_accountsecret', 'Can view asset account secret'),
('view_accountsecret', 'Can view asset account secret'), ('view_historyaccount', 'Can view asset history account'),
('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret'),
('view_historyaccountsecret', 'Can view asset history account secret'), ('verify_account', 'Can verify account'), ('push_account', 'Can push account'),
('verify_account', 'Can verify account'), ('remove_account', 'Can remove account')],
('push_account', 'Can push account'),
('remove_account', 'Can remove account'),
('view_accountsession', 'Can view session'),
('view_accountactivity', 'Can view activity')
],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(

View File

@@ -335,7 +335,6 @@ class Migration(migrations.Migration):
], ],
options={ options={
"abstract": False, "abstract": False,
"verbose_name": "Check engine",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@@ -630,15 +629,10 @@ class Migration(migrations.Migration):
name="connectivity", name="connectivity",
field=models.CharField( field=models.CharField(
choices=[ choices=[
('-', 'Unknown'), ("-", "Unknown"),
('na', 'N/A'), ("na", "N/A"),
('ok', 'OK'), ("ok", "OK"),
('err', 'Error'), ("err", "Error"),
('auth_err', 'Authentication error'),
('password_err', 'Invalid password error'),
('openssh_key_err', 'OpenSSH key error'),
('ntlm_err', 'NTLM credentials rejected error'),
('create_temp_err', 'Create temporary error')
], ],
default="-", default="-",
max_length=16, max_length=16,

View File

@@ -1,29 +0,0 @@
# Generated by Django 4.1.13 on 2025-05-06 10:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0006_alter_accountrisk_username_and_more'),
]
operations = [
migrations.AlterField(
model_name='account',
name='connectivity',
field=models.CharField(choices=[
('-', 'Unknown'),
('na', 'N/A'),
('ok', 'OK'),
('err', 'Error'),
('rdp_err', 'RDP error'),
('auth_err', 'Authentication error'),
('password_err', 'Invalid password error'),
('openssh_key_err', 'OpenSSH key error'),
('ntlm_err', 'NTLM credentials rejected error'),
('create_temp_err', 'Create temporary error')
],
default='-', max_length=16, verbose_name='Connectivity'),
),
]

View File

@@ -1,15 +1,65 @@
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from django.db.models import Model
from django.utils import translation from django.utils import translation
from django.utils.translation import gettext_noop
from audits.const import ActionChoices from audits.const import ActionChoices
from audits.handler import create_or_update_operate_log from common.views.mixins import RecordViewLogMixin
from common.utils import i18n_fmt
class AccountRecordViewLogMixin(object): class AccountRecordViewLogMixin(RecordViewLogMixin):
get_object: callable get_object: callable
model: Model get_queryset: callable
@staticmethod
def _filter_params(params):
new_params = {}
need_pop_params = ('format', 'order')
for key, value in params.items():
if key in need_pop_params:
continue
if isinstance(value, list):
value = list(filter(None, value))
if value:
new_params[key] = value
return new_params
def get_resource_display(self, request):
query_params = dict(request.query_params)
params = self._filter_params(query_params)
spm_filter = params.pop("spm", None)
if not params and not spm_filter:
display_message = gettext_noop("Export all")
elif spm_filter:
display_message = gettext_noop("Export only selected items")
else:
query = ",".join(
["%s=%s" % (key, value) for key, value in params.items()]
)
display_message = i18n_fmt(gettext_noop("Export filtered: %s"), query)
return display_message
@property
def detail_msg(self):
return i18n_fmt(
gettext_noop('User %s view/export secret'), self.request.user
)
def list(self, request, *args, **kwargs):
list_func = getattr(super(), 'list')
if not callable(list_func):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
response = list_func(request, *args, **kwargs)
with translation.override('en'):
resource_display = self.get_resource_display(request)
ids = [q.id for q in self.get_queryset()]
self.record_logs(
ids, ActionChoices.view, self.detail_msg, resource_display=resource_display
)
return response
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
retrieve_func = getattr(super(), 'retrieve') retrieve_func = getattr(super(), 'retrieve')
@@ -17,9 +67,9 @@ class AccountRecordViewLogMixin(object):
return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED)
response = retrieve_func(request, *args, **kwargs) response = retrieve_func(request, *args, **kwargs)
with translation.override('en'): with translation.override('en'):
create_or_update_operate_log( resource = self.get_object()
ActionChoices.view, self.model._meta.verbose_name, self.record_logs(
force=True, resource=self.get_object(), [resource.id], ActionChoices.view, self.detail_msg, resource=resource
) )
return response return response

View File

@@ -116,8 +116,6 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
('verify_account', _('Can verify account')), ('verify_account', _('Can verify account')),
('push_account', _('Can push account')), ('push_account', _('Can push account')),
('remove_account', _('Can remove account')), ('remove_account', _('Can remove account')),
('view_accountsession', _('Can view session')),
('view_accountactivity', _('Can view activity')),
] ]
def __str__(self): def __str__(self):
@@ -132,57 +130,17 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.asset.platform return self.asset.platform
@lazyproperty @lazyproperty
def alias(self) -> str: def alias(self):
"""
别称,因为有虚拟账号,@INPUT @MANUAL @USER, 否则为 id
"""
if self.username.startswith('@'): if self.username.startswith('@'):
return self.username return self.username
return str(self.id) return self.name
def is_virtual(self) -> bool:
"""
不要用 username 去判断,因为可能是构造的 account 对象,设置了同名账号的用户名,
"""
return self.alias.startswith('@')
def is_ds_account(self) -> bool:
if self.is_virtual():
return ''
if not self.asset.is_directory_service:
return False
return True
@lazyproperty @lazyproperty
def ds(self): def has_secret(self):
if not self.is_ds_account():
return None
return self.asset.ds
@lazyproperty
def ds_domain(self) -> str:
"""这个不能去掉perm_account 会动态设置这个值,以更改 full_username"""
if self.is_virtual():
return ''
if self.ds and self.ds.domain_name:
return self.ds.domain_name
return ''
def username_has_domain(self):
return '@' in self.username or '\\' in self.username
@property
def full_username(self) -> str:
if not self.username_has_domain() and self.ds_domain:
return '{}@{}'.format(self.username, self.ds_domain)
return self.username
@lazyproperty
def has_secret(self) -> bool:
return bool(self.secret) return bool(self.secret)
@lazyproperty @lazyproperty
def versions(self) -> int: def versions(self):
return self.history.count() return self.history.count()
def get_su_from_accounts(self): def get_su_from_accounts(self):

View File

@@ -33,7 +33,7 @@ class IntegrationApplication(JMSOrgBaseModel):
return qs.filter(*query) return qs.filter(*query)
@property @property
def accounts_amount(self) -> int: def accounts_amount(self):
return self.get_accounts().count() return self.get_accounts().count()
@property @property

View File

@@ -68,10 +68,8 @@ class AccountRisk(JMSOrgBaseModel):
related_name='risks', null=True related_name='risks', null=True
) )
risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices) risk = models.CharField(max_length=128, verbose_name=_('Risk'), choices=RiskChoice.choices)
status = models.CharField( status = models.CharField(max_length=32, choices=ConfirmOrIgnore.choices, default=ConfirmOrIgnore.pending,
max_length=32, choices=ConfirmOrIgnore.choices, default=ConfirmOrIgnore.pending, blank=True, verbose_name=_('Status'))
blank=True, verbose_name=_('Status')
)
details = models.JSONField(default=list, verbose_name=_('Detail')) details = models.JSONField(default=list, verbose_name=_('Detail'))
class Meta: class Meta:
@@ -121,9 +119,6 @@ class CheckAccountEngine(JMSBaseModel):
def __str__(self): def __str__(self):
return self.name return self.name
class Meta:
verbose_name = _('Check engine')
@staticmethod @staticmethod
def get_default_engines(): def get_default_engines():
data = [ data = [
@@ -133,7 +128,7 @@ class CheckAccountEngine(JMSBaseModel):
"name": _("Check the discovered accounts"), "name": _("Check the discovered accounts"),
"comment": _( "comment": _(
"Perform checks and analyses based on automatically discovered account results, " "Perform checks and analyses based on automatically discovered account results, "
"including user groups, public keys, sudoers, and other information." "including user groups, public keys, sudoers, and other information"
) )
}, },
{ {
@@ -149,13 +144,13 @@ class CheckAccountEngine(JMSBaseModel):
"id": "00000000-0000-0000-0000-000000000003", "id": "00000000-0000-0000-0000-000000000003",
"slug": "check_account_repeat", "slug": "check_account_repeat",
"name": _("Check if the account and password are repeated"), "name": _("Check if the account and password are repeated"),
"comment": _("Check if the account is the same as other accounts.") "comment": _("Check if the account is the same as other accounts")
}, },
{ {
"id": "00000000-0000-0000-0000-000000000004", "id": "00000000-0000-0000-0000-000000000004",
"slug": "check_account_leak", "slug": "check_account_leak",
"name": _("Check whether the account password is a common password"), "name": _("Check whether the account password is a common password"),
"comment": _("Check whether the account password is a commonly leaked password.") "comment": _("Check whether the account password is a commonly leaked password")
}, },
] ]
return data return data

View File

@@ -75,11 +75,11 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return bool(self.secret) return bool(self.secret)
@property @property
def has_username(self) -> bool: def has_username(self):
return bool(self.username) return bool(self.username)
@property @property
def spec_info(self) -> dict: def spec_info(self):
data = {} data = {}
if self.secret_type != SecretType.SSH_KEY: if self.secret_type != SecretType.SSH_KEY:
return data return data
@@ -87,13 +87,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return data return data
@property @property
def password(self) -> str: def password(self):
if self.secret_type == SecretType.PASSWORD: if self.secret_type == SecretType.PASSWORD:
return self.secret return self.secret
return None return None
@property @property
def private_key(self) -> str: def private_key(self):
if self.secret_type == SecretType.SSH_KEY: if self.secret_type == SecretType.SSH_KEY:
return self.secret return self.secret
return None return None
@@ -110,7 +110,7 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return None return None
@property @property
def ssh_key_fingerprint(self) -> str: def ssh_key_fingerprint(self):
if self.public_key: if self.public_key:
public_key = self.public_key public_key = self.public_key
elif self.private_key: elif self.private_key:

View File

@@ -56,7 +56,7 @@ class VaultModelMixin(models.Model):
__secret = None __secret = None
@property @property
def secret(self) -> str: def secret(self):
if self.__secret: if self.__secret:
return self.__secret return self.__secret
from accounts.backends import vault_client from accounts.backends import vault_client

View File

@@ -18,11 +18,11 @@ class VirtualAccount(JMSOrgBaseModel):
verbose_name = _('Virtual account') verbose_name = _('Virtual account')
@property @property
def name(self) -> str: def name(self):
return self.get_alias_display() return self.get_alias_display()
@property @property
def username(self) -> str: def username(self):
usernames_map = { usernames_map = {
AliasAccount.INPUT: _("Manual input"), AliasAccount.INPUT: _("Manual input"),
AliasAccount.USER: _("Same with user"), AliasAccount.USER: _("Same with user"),
@@ -32,7 +32,7 @@ class VirtualAccount(JMSOrgBaseModel):
return usernames_map.get(self.alias, '') return usernames_map.get(self.alias, '')
@property @property
def comment(self) -> str: def comment(self):
comments_map = { comments_map = {
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'), AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
AliasAccount.USER: _('The account username name same with user on connect'), AliasAccount.USER: _('The account username name same with user on connect'),
@@ -92,9 +92,8 @@ class VirtualAccount(JMSOrgBaseModel):
from .account import Account from .account import Account
username = user.username username = user.username
alias = AliasAccount.USER.value
with tmp_to_org(asset.org): with tmp_to_org(asset.org):
same_account = cls.objects.filter(alias=alias).first() same_account = cls.objects.filter(alias='@USER').first()
secret = '' secret = ''
if same_account and same_account.secret_from_login: if same_account and same_account.secret_from_login:
@@ -102,6 +101,4 @@ class VirtualAccount(JMSOrgBaseModel):
if not secret and not from_permed: if not secret and not from_permed:
secret = input_secret secret = input_secret
account = Account(name=AliasAccount.USER.label, username=username, secret=secret) return Account(name=AliasAccount.USER.label, username=username, secret=secret)
account.alias = alias
return account

View File

@@ -6,7 +6,6 @@ from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storag
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
from terminal.models.component.storage import ReplayStorage from terminal.models.component.storage import ReplayStorage
from users.models import User from users.models import User
from users.utils import activate_user_language
class AccountBackupExecutionTaskMsg: class AccountBackupExecutionTaskMsg:
@@ -29,10 +28,9 @@ class AccountBackupExecutionTaskMsg:
).format(name) ).format(name)
def publish(self, attachment_list=None): def publish(self, attachment_list=None):
with activate_user_language(self.user): send_mail_attachment_async(
send_mail_attachment_async( self.subject, self.message, [self.user.email], attachment_list
self.subject, self.message, [self.user.email], attachment_list )
)
class AccountBackupByObjStorageExecutionTaskMsg: class AccountBackupByObjStorageExecutionTaskMsg:
@@ -76,10 +74,9 @@ class ChangeSecretExecutionTaskMsg:
return self.summary + '\n' + default_message return self.summary + '\n' + default_message
def publish(self, attachments=None): def publish(self, attachments=None):
with activate_user_language(self.user): send_mail_attachment_async(
send_mail_attachment_async( self.subject, self.message, [self.user.email], attachments
self.subject, self.message, [self.user.email], attachments )
)
class GatherAccountChangeMsg(UserMessage): class GatherAccountChangeMsg(UserMessage):

View File

@@ -23,7 +23,7 @@ TYPE_CHOICES = [
("delete_both", _("Delete remote")), ("delete_both", _("Delete remote")),
("add_account", _("Add account")), ("add_account", _("Add account")),
("change_password_add", _("Change password and Add")), ("change_password_add", _("Change password and Add")),
("change_password", _("Change secret")), ("change_password", _("Change password")),
] ]

View File

@@ -233,7 +233,6 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
required=False, queryset=Account.objects, allow_null=True, allow_empty=True, required=False, queryset=Account.objects, allow_null=True, allow_empty=True,
label=_('Su from'), attrs=('id', 'name', 'username') label=_('Su from'), attrs=('id', 'name', 'username')
) )
ds = ObjectRelatedField(read_only=True, label=_('Directory service'), attrs=('id', 'name', 'domain_name'))
class Meta(BaseAccountSerializer.Meta): class Meta(BaseAccountSerializer.Meta):
model = Account model = Account
@@ -242,19 +241,16 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
'date_change_secret', 'change_secret_status' 'date_change_secret', 'change_secret_status'
] ]
fields = BaseAccountSerializer.Meta.fields + [ fields = BaseAccountSerializer.Meta.fields + [
'su_from', 'asset', 'version', 'ds', 'su_from', 'asset', 'version',
'source', 'source_id', 'secret_reset', 'source', 'source_id', 'secret_reset',
] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields ] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields
fields = [f for f in fields if f not in ['spec_info']]
extra_kwargs = { extra_kwargs = {
**BaseAccountSerializer.Meta.extra_kwargs, **BaseAccountSerializer.Meta.extra_kwargs,
'name': {'required': False}, 'name': {'required': False},
'source_id': {'required': False, 'allow_null': True}, 'source_id': {'required': False, 'allow_null': True},
} }
fields_unimport_template = ['params'] fields_unimport_template = ['params']
# 手动判断唯一性校验
validators = []
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
@@ -262,31 +258,16 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
queryset = queryset.prefetch_related( queryset = queryset.prefetch_related(
'asset', 'asset__platform', 'asset', 'asset__platform',
'asset__platform__automation' 'asset__platform__automation'
) ).prefetch_related('labels', 'labels__label')
return queryset return queryset
def validate(self, attrs):
instance = getattr(self, "instance", None)
if instance:
return super().validate(attrs)
field_errors = {}
for _fields in Account._meta.unique_together:
lookup = {field: attrs.get(field) for field in _fields}
if Account.objects.filter(**lookup).exists():
verbose_names = ', '.join([str(Account._meta.get_field(f).verbose_name) for f in _fields])
msg_template = _('Account already exists. Field(s): {fields} must be unique.')
field_errors[_fields[0]] = msg_template.format(fields=verbose_names)
raise serializers.ValidationError(field_errors)
return attrs
class AccountDetailSerializer(AccountSerializer): class AccountDetailSerializer(AccountSerializer):
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
model = Account model = Account
fields = AccountSerializer.Meta.fields + ['has_secret', 'spec_info'] fields = AccountSerializer.Meta.fields + ['has_secret']
read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret'] read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']
@@ -309,10 +290,10 @@ class AssetAccountBulkSerializer(
class Meta: class Meta:
model = Account model = Account
fields = [ fields = [
'name', 'username', 'secret', 'secret_type', 'secret_reset', 'name', 'username', 'secret', 'secret_type', 'passphrase',
'passphrase', 'privileged', 'is_active', 'comment', 'template', 'privileged', 'is_active', 'comment', 'template',
'on_invalid', 'push_now', 'params', 'assets', 'su_from_username', 'on_invalid', 'push_now', 'params', 'assets',
'source', 'source_id', 'su_from_username', 'source', 'source_id',
] ]
extra_kwargs = { extra_kwargs = {
'name': {'required': False}, 'name': {'required': False},
@@ -473,8 +454,6 @@ class AssetAccountBulkSerializer(
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
fields = AccountSerializer.Meta.fields + ['spec_info'] fields = AccountSerializer.Meta.fields + ['spec_info']
extra_kwargs = { extra_kwargs = {
@@ -489,7 +468,6 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class AccountHistorySerializer(serializers.ModelSerializer): class AccountHistorySerializer(serializers.ModelSerializer):
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
secret = serializers.CharField(label=_('Secret'), read_only=True)
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True) id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
class Meta: class Meta:

View File

@@ -70,14 +70,12 @@ class AuthValidateMixin(serializers.Serializer):
class BaseAccountSerializer( class BaseAccountSerializer(
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
): ):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta: class Meta:
model = BaseAccount model = BaseAccount
fields_mini = ["id", "name", "username"] fields_mini = ["id", "name", "username"]
fields_small = fields_mini + [ fields_small = fields_mini + [
"secret_type", "secret", "passphrase", "secret_type", "secret", "passphrase",
"privileged", "is_active", "privileged", "is_active", "spec_info",
] ]
fields_other = ["created_by", "date_created", "date_updated", "comment"] fields_other = ["created_by", "date_created", "date_updated", "comment"]
fields = fields_small + fields_other + ["labels"] fields = fields_small + fields_other + ["labels"]

View File

@@ -1,11 +1,9 @@
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from accounts.models import IntegrationApplication from accounts.models import IntegrationApplication
from acls.serializers.rules import ip_group_child_validator, ip_group_help_text from acls.serializers.rules import ip_group_child_validator, ip_group_help_text
from common.serializers.fields import JSONManyToManyField from common.serializers.fields import JSONManyToManyField
from common.utils import random_string
from orgs.mixins.serializers import BulkOrgResourceModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
@@ -29,18 +27,13 @@ class IntegrationApplicationSerializer(BulkOrgResourceModelSerializer):
'name': {'label': _('Name')}, 'name': {'label': _('Name')},
'accounts_amount': {'label': _('Accounts amount')}, 'accounts_amount': {'label': _('Accounts amount')},
'is_active': {'default': True}, 'is_active': {'default': True},
'logo': {'required': False},
} }
def to_representation(self, instance): def __init__(self, *args, **kwargs):
data = super().to_representation(instance) super().__init__(*args, **kwargs)
if not data.get('logo'): request_method = self.context.get('request').method
data['logo'] = static('img/logo.png') if request_method == 'PUT':
return data self.fields['logo'].required = False
def validate(self, attrs):
attrs['secret'] = random_string(36)
return attrs
class IntegrationAccountSecretSerializer(serializers.Serializer): class IntegrationAccountSecretSerializer(serializers.Serializer):

View File

@@ -57,15 +57,11 @@ class AccountTemplateSerializer(BaseAccountSerializer):
fields_unimport_template = ['push_params'] fields_unimport_template = ['push_params']
class AccountDetailTemplateSerializer(AccountTemplateSerializer): class AccountTemplateSecretSerializer(SecretReadableMixin, AccountTemplateSerializer):
class Meta(AccountTemplateSerializer.Meta): class Meta(AccountTemplateSerializer.Meta):
fields = AccountTemplateSerializer.Meta.fields + ['spec_info'] fields = AccountTemplateSerializer.Meta.fields + ['spec_info']
class AccountTemplateSecretSerializer(SecretReadableMixin, AccountDetailTemplateSerializer):
class Meta(AccountDetailTemplateSerializer.Meta):
fields = AccountDetailTemplateSerializer.Meta.fields
extra_kwargs = { extra_kwargs = {
**AccountDetailTemplateSerializer.Meta.extra_kwargs, **AccountTemplateSerializer.Meta.extra_kwargs,
'secret': {'write_only': False}, 'secret': {'write_only': False},
'spec_info': {'label': _('Spec info')},
} }

View File

@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.const import AutomationTypes, AccountBackupType from accounts.const import AutomationTypes
from accounts.models import BackupAccountAutomation from accounts.models import BackupAccountAutomation
from common.serializers.fields import EncryptedField from common.serializers.fields import EncryptedField
from common.utils import get_logger from common.utils import get_logger
@@ -42,17 +41,6 @@ class BackupAccountSerializer(BaseAutomationSerializer):
'types': {'label': _('Asset type')} 'types': {'label': _('Asset type')}
} }
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_backup_type_choices()
def set_backup_type_choices(self):
field_backup_type = self.fields.get("backup_type")
if not field_backup_type:
return
if not settings.XPACK_LICENSE_IS_VALID:
field_backup_type._choices.pop(AccountBackupType.object_storage, None)
@property @property
def model_type(self): def model_type(self):
return AutomationTypes.backup_account return AutomationTypes.backup_account

View File

@@ -16,7 +16,6 @@ from assets.models import Asset
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
from common.utils import get_logger from common.utils import get_logger
from .base import BaseAutomationSerializer from .base import BaseAutomationSerializer
from ...utils import account_secret_task_status
logger = get_logger(__file__) logger = get_logger(__file__)
@@ -27,7 +26,6 @@ __all__ = [
'ChangeSecretRecordBackUpSerializer', 'ChangeSecretRecordBackUpSerializer',
'ChangeSecretUpdateAssetSerializer', 'ChangeSecretUpdateAssetSerializer',
'ChangeSecretUpdateNodeSerializer', 'ChangeSecretUpdateNodeSerializer',
'ChangeSecretAccountSerializer'
] ]
@@ -130,7 +128,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
@staticmethod @staticmethod
def get_is_success(obj) -> bool: def get_is_success(obj):
return obj.status == ChangeSecretRecordStatusChoice.success return obj.status == ChangeSecretRecordStatusChoice.success
@@ -157,7 +155,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
@staticmethod @staticmethod
def get_asset(instance) -> str: def get_asset(instance):
return str(instance.asset) return str(instance.asset)
@staticmethod @staticmethod
@@ -165,7 +163,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
return str(instance.account) return str(instance.account)
@staticmethod @staticmethod
def get_is_success(obj) -> str: def get_is_success(obj):
if obj.status == ChangeSecretRecordStatusChoice.success.value: if obj.status == ChangeSecretRecordStatusChoice.success.value:
return _("Success") return _("Success")
return _("Failed") return _("Failed")
@@ -181,24 +179,3 @@ class ChangeSecretUpdateNodeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = ChangeSecretAutomation model = ChangeSecretAutomation
fields = ['id', 'nodes'] fields = ['id', 'nodes']
class ChangeSecretAccountSerializer(serializers.ModelSerializer):
asset = ObjectRelatedField(
queryset=Asset.objects.all(), required=False, label=_("Asset")
)
ttl = serializers.SerializerMethodField(label=_('TTL'))
meta = serializers.SerializerMethodField(label=_('Meta'))
class Meta:
model = Account
fields = ['id', 'username', 'asset', 'meta', 'ttl']
read_only_fields = fields
@staticmethod
def get_meta(obj) -> dict:
return account_secret_task_status.get(str(obj.id))
@staticmethod
def get_ttl(obj) -> int:
return account_secret_task_status.get_ttl(str(obj.id))

View File

@@ -69,7 +69,7 @@ class AssetRiskSerializer(serializers.Serializer):
risk_summary = serializers.SerializerMethodField() risk_summary = serializers.SerializerMethodField()
@staticmethod @staticmethod
def get_risk_summary(obj) -> dict: def get_risk_summary(obj):
summary = {} summary = {}
for risk in RiskChoice.choices: for risk in RiskChoice.choices:
summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0) summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)

View File

@@ -28,7 +28,7 @@ class DiscoverAccountAutomationSerializer(BaseAutomationSerializer):
+ read_only_fields) + read_only_fields)
extra_kwargs = { extra_kwargs = {
'check_risk': { 'check_risk': {
'help_text': _('Whether to check the risk of the discovered accounts.'), 'help_text': _('Whether to check the risk of the gathered accounts.'),
}, },
**BaseAutomationSerializer.Meta.extra_kwargs **BaseAutomationSerializer.Meta.extra_kwargs
} }

View File

@@ -1,5 +1,4 @@
import datetime import datetime
from collections import defaultdict
from celery import shared_task from celery import shared_task
from django.db.models import Q from django.db.models import Q
@@ -73,43 +72,24 @@ def execute_automation_record_task(record_ids, tp):
task_name = gettext_noop('Execute automation record') task_name = gettext_noop('Execute automation record')
with tmp_to_root_org(): with tmp_to_root_org():
records = ChangeSecretRecord.objects.filter(id__in=record_ids).order_by('-date_updated') records = ChangeSecretRecord.objects.filter(id__in=record_ids)
if not records: if not records:
logger.error(f'No automation record found: {record_ids}') logger.error('No automation record found: {}'.format(record_ids))
return return
seen_accounts = set() record = records[0]
unique_records = [] record_map = {f'{record.asset_id}-{record.account_id}': str(record.id) for record in records}
for rec in records: task_snapshot = {
acct = str(rec.account_id) 'params': {},
if acct not in seen_accounts: 'record_map': record_map,
seen_accounts.add(acct) 'secret': record.new_secret,
unique_records.append(rec) 'secret_type': record.execution.snapshot.get('secret_type'),
'assets': [str(instance.asset_id) for instance in records],
exec_groups = defaultdict(list) 'accounts': [str(instance.account_id) for instance in records],
for rec in unique_records: }
exec_groups[rec.execution_id].append(rec) with tmp_to_org(record.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
for __, group in exec_groups.items():
latest_rec = group[0]
snapshot = getattr(latest_rec.execution, 'snapshot', {}) or {}
record_map = {f"{r.asset_id}-{r.account_id}": str(r.id) for r in group}
assets = [str(r.asset_id) for r in group]
accounts = [str(r.account_id) for r in group]
task_snapshot = {
'params': {},
'record_map': record_map,
'secret': latest_rec.new_secret,
'secret_type': snapshot.get('secret_type'),
'assets': assets,
'accounts': accounts,
}
with tmp_to_org(latest_rec.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
@shared_task( @shared_task(
@@ -127,18 +107,16 @@ def execute_automation_record_task(record_ids, tp):
) )
@register_as_period_task(crontab=CRONTAB_AT_AM_THREE) @register_as_period_task(crontab=CRONTAB_AT_AM_THREE)
def clean_change_secret_and_push_record_period(): def clean_change_secret_and_push_record_period():
from accounts.models import ChangeSecretRecord, PushSecretRecord from accounts.models import ChangeSecretRecord
print('Start clean change secret and push record period') print('Start clean change secret and push record period')
with tmp_to_root_org(): with tmp_to_root_org():
now = timezone.now() now = timezone.now()
days = get_log_keep_day('ACCOUNT_CHANGE_SECRET_RECORD_KEEP_DAYS') days = get_log_keep_day('ACCOUNT_CHANGE_SECRET_RECORD_KEEP_DAYS')
expired_time = now - datetime.timedelta(days=days) expired_day = now - datetime.timedelta(days=days)
records = ChangeSecretRecord.objects.filter(
date_updated__lt=expired_day
).filter(
Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True)
)
null_related_q = Q(execution__isnull=True) | Q(asset__isnull=True) | Q(account__isnull=True) records.delete()
expired_q = Q(date_updated__lt=expired_time)
ChangeSecretRecord.objects.filter(null_related_q).delete()
ChangeSecretRecord.objects.filter(expired_q).delete()
PushSecretRecord.objects.filter(null_related_q).delete()
PushSecretRecord.objects.filter(expired_q).delete()

View File

@@ -1,107 +1,37 @@
from collections import defaultdict
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_noop, gettext_lazy as _ from django.utils.translation import gettext_noop, gettext_lazy as _
from accounts.const import AutomationTypes, ChangeSecretAccountStatus from accounts.const import AutomationTypes
from accounts.tasks.common import quickstart_automation_by_snapshot from accounts.tasks.common import quickstart_automation_by_snapshot
from accounts.utils import account_secret_task_status
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import tmp_to_org
logger = get_logger(__file__) logger = get_logger(__file__)
__all__ = [ __all__ = [
'push_accounts_to_assets_task', 'change_secret_accounts_to_assets_task' 'push_accounts_to_assets_task',
] ]
def _process_accounts(account_ids, automation_model, default_name, automation_type, snapshot=None):
from accounts.models import Account
accounts = Account.objects.filter(id__in=account_ids)
if not accounts:
logger.warning(
"No accounts found for automation task %s with ids %s",
automation_type, account_ids
)
return
task_name = automation_model.generate_unique_name(gettext_noop(default_name))
snapshot = snapshot or {}
snapshot.update({
'accounts': [str(a.id) for a in accounts],
'assets': [str(a.asset_id) for a in accounts],
})
quickstart_automation_by_snapshot(task_name, automation_type, snapshot)
@shared_task( @shared_task(
queue="ansible", queue="ansible",
verbose_name=_('Push accounts to assets'), verbose_name=_('Push accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None), activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_( description=_(
"Whenever an account is created or modified and needs pushing to assets, run this task" "When creating or modifying an account requires account push, this task is executed"
) )
) )
def push_accounts_to_assets_task(account_ids, params=None): def push_accounts_to_assets_task(account_ids, params=None):
from accounts.models import PushAccountAutomation from accounts.models import PushAccountAutomation
snapshot = { from accounts.models import Account
'params': params or {},
}
_process_accounts(
account_ids,
PushAccountAutomation,
_("Push accounts to assets"),
AutomationTypes.push_account,
snapshot=snapshot
)
@shared_task(
queue="ansible",
verbose_name=_('Change secret accounts to assets'),
activity_callback=lambda self, account_ids, *args, **kwargs: (account_ids, None),
description=_(
"When a secret on an account changes and needs pushing to assets, run this task"
)
)
def change_secret_accounts_to_assets_task(account_ids, params=None, snapshot=None, trigger='manual'):
from accounts.models import ChangeSecretAutomation, Account
manager = account_secret_task_status
if trigger == 'delay':
for _id in manager.account_ids:
status = manager.get_status(_id)
# Check if the account is in QUEUED status
if status == ChangeSecretAccountStatus.QUEUED:
account_ids.append(_id)
manager.set_status(_id, ChangeSecretAccountStatus.READY)
if not account_ids:
return
accounts = Account.objects.filter(id__in=account_ids) accounts = Account.objects.filter(id__in=account_ids)
if not accounts: task_name = gettext_noop("Push accounts to assets")
logger.warning( task_name = PushAccountAutomation.generate_unique_name(task_name)
"No accounts found for change secret automation task with ids %s",
account_ids
)
return
grouped_ids = defaultdict(lambda: defaultdict(list)) task_snapshot = {
for account in accounts: 'accounts': [str(account.id) for account in accounts],
grouped_ids[account.org_id][account.secret_type].append(str(account.id)) 'assets': [str(account.asset_id) for account in accounts],
'params': params or {},
}
snapshot = snapshot or {} tp = AutomationTypes.push_account
for org_id, secret_map in grouped_ids.items(): quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
with tmp_to_org(org_id):
for secret_type, ids in secret_map.items():
snapshot['secret_type'] = secret_type
_process_accounts(
ids,
ChangeSecretAutomation,
_("Change secret accounts to assets"),
AutomationTypes.change_secret,
snapshot=snapshot
)

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