mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-12-26 05:52:36 +00:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43dc58c06c | ||
|
|
2a23af5030 | ||
|
|
0fbbdceec2 | ||
|
|
b10ee436e8 | ||
|
|
24987c7f60 | ||
|
|
73bed4d33d | ||
|
|
4c389c05c1 | ||
|
|
e744c4c8af | ||
|
|
06d1c9f420 | ||
|
|
a92023840a | ||
|
|
2d1bf866fa | ||
|
|
c606c3eb21 | ||
|
|
dabbb45f6e | ||
|
|
ded1b4bba1 | ||
|
|
2630ea39a1 | ||
|
|
9e10029bdd | ||
|
|
d1391cb5d5 | ||
|
|
44f029774d | ||
|
|
23fce9e426 | ||
|
|
0778a39894 | ||
|
|
9cc6d6a9af | ||
|
|
8f309dee92 | ||
|
|
d166b26252 | ||
|
|
1ef51563b5 | ||
|
|
3e7b4682e4 | ||
|
|
994b42aa93 | ||
|
|
d6aea54722 | ||
|
|
88afabdd1d | ||
|
|
b2327c0c5a | ||
|
|
7610f64433 | ||
|
|
b15c314384 | ||
|
|
7a5cffac91 | ||
|
|
8667943443 | ||
|
|
7c51d90a3d | ||
|
|
9996b200f9 | ||
|
|
ae364ac373 | ||
|
|
fef4a97931 | ||
|
|
d63c4d6cc4 | ||
|
|
4e5a44bd98 | ||
|
|
fcce03f7bd | ||
|
|
5f121934a7 | ||
|
|
521c1f0dfa | ||
|
|
5673698a57 | ||
|
|
d6b75ac700 | ||
|
|
0ee14e6d85 | ||
|
|
9babe977d8 | ||
|
|
0f9223331c | ||
|
|
f8a4a0e108 | ||
|
|
ba76f30af9 | ||
|
|
e5e0c841a2 | ||
|
|
c41fdf1786 | ||
|
|
69c0eb2f50 | ||
|
|
e077afe2cc | ||
|
|
c1f572df05 | ||
|
|
d60fe464ca | ||
|
|
f47895b8a8 | ||
|
|
3eb1583c69 | ||
|
|
5ab8ff4fde | ||
|
|
7746491e19 | ||
|
|
5e54792d94 | ||
|
|
621c7a31fe | ||
|
|
75bab70ccf | ||
|
|
30683ed859 | ||
|
|
7c52cec5fb | ||
|
|
f01bfc44b8 | ||
|
|
54b89f6fee | ||
|
|
c0de0b0d8e | ||
|
|
06275a09ac | ||
|
|
7b86938b58 | ||
|
|
44624d0ce0 | ||
|
|
9b8c817a16 | ||
|
|
927fe1f128 | ||
|
|
eee119eba1 | ||
|
|
53d8f716eb | ||
|
|
f48aec2bcb | ||
|
|
78e9f51786 | ||
|
|
af33ad6631 | ||
|
|
864da49ae6 | ||
|
|
e6b8b3982d | ||
|
|
49b3df218e | ||
|
|
0858d67098 | ||
|
|
ffa242e635 | ||
|
|
4021b1955e | ||
|
|
204258f058 | ||
|
|
dc841650cf | ||
|
|
bc54685a31 | ||
|
|
ee586954f8 | ||
|
|
e56a37afd2 | ||
|
|
7669744312 | ||
|
|
ad8aba88a3 | ||
|
|
7659846df4 | ||
|
|
f93979eb2d | ||
|
|
badf83c560 | ||
|
|
f6466a3a20 | ||
|
|
996394ba29 | ||
|
|
09f8470d34 | ||
|
|
fdb3f6409c | ||
|
|
73b0b23910 | ||
|
|
c1185e989a | ||
|
|
1239082649 | ||
|
|
ff073185f1 | ||
|
|
d7a682b462 | ||
|
|
4df2bdd9b6 | ||
|
|
2437072768 | ||
|
|
08a2d96213 | ||
|
|
de7d7b41c0 | ||
|
|
b04c7f022f | ||
|
|
bf0d9f4b80 | ||
|
|
314257f790 | ||
|
|
6d2a62e413 | ||
|
|
1734ddc2bd | ||
|
|
7c796e8201 | ||
|
|
62a74418ea | ||
|
|
32461078fe | ||
|
|
939b517e34 | ||
|
|
66eac762ff | ||
|
|
6f4082f800 | ||
|
|
edd65f965b |
4
.github/workflows/jms-build-test.yml
vendored
4
.github/workflows/jms-build-test.yml
vendored
@@ -19,8 +19,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: false
|
push: false
|
||||||
tags: jumpserver/core:test
|
tags: jumpserver/core-ce:test
|
||||||
file: Dockerfile
|
file: Dockerfile-ce
|
||||||
build-args: |
|
build-args: |
|
||||||
APT_MIRROR=http://deb.debian.org
|
APT_MIRROR=http://deb.debian.org
|
||||||
PIP_MIRROR=https://pypi.org/simple
|
PIP_MIRROR=https://pypi.org/simple
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-slim-bullseye as stage-build
|
FROM python:3.11-slim-bullseye as stage-1
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
@@ -6,9 +6,10 @@ ENV VERSION=$VERSION
|
|||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
WORKDIR /opt/jumpserver
|
||||||
ADD . .
|
ADD . .
|
||||||
RUN cd utils && bash -ixeu build.sh
|
RUN echo > /opt/jumpserver/config.yml \
|
||||||
|
&& cd utils && bash -ixeu build.sh
|
||||||
|
|
||||||
FROM python:3.11-slim-bullseye
|
FROM python:3.11-slim-bullseye as stage-2
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
ARG BUILD_DEPENDENCIES=" \
|
ARG BUILD_DEPENDENCIES=" \
|
||||||
@@ -31,6 +32,53 @@ ARG DEPENDENCIES=" \
|
|||||||
freerdp2-dev \
|
freerdp2-dev \
|
||||||
libaio-dev"
|
libaio-dev"
|
||||||
|
|
||||||
|
ARG TOOLS=" \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
default-libmysqlclient-dev \
|
||||||
|
default-mysql-client \
|
||||||
|
git \
|
||||||
|
git-lfs \
|
||||||
|
unzip \
|
||||||
|
xz-utils \
|
||||||
|
wget"
|
||||||
|
|
||||||
|
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||||
|
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||||
|
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||||
|
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||||
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
|
&& apt-get update \
|
||||||
|
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
||||||
|
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||||
|
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||||
|
&& echo "no" | dpkg-reconfigure dash
|
||||||
|
|
||||||
|
WORKDIR /opt/jumpserver
|
||||||
|
|
||||||
|
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
|
--mount=type=bind,source=poetry.lock,target=/opt/jumpserver/poetry.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
|
||||||
|
set -ex \
|
||||||
|
&& python3 -m venv /opt/py3 \
|
||||||
|
&& pip install poetry -i ${PIP_MIRROR} \
|
||||||
|
&& poetry config virtualenvs.create false \
|
||||||
|
&& . /opt/py3/bin/activate \
|
||||||
|
&& poetry install
|
||||||
|
|
||||||
|
FROM python:3.11-slim-bullseye
|
||||||
|
ARG TARGETARCH
|
||||||
|
ENV LANG=zh_CN.UTF-8 \
|
||||||
|
PATH=/opt/py3/bin:$PATH
|
||||||
|
|
||||||
|
ARG DEPENDENCIES=" \
|
||||||
|
libjpeg-dev \
|
||||||
|
libx11-dev \
|
||||||
|
freerdp2-dev \
|
||||||
|
libxmlsec1-openssl"
|
||||||
|
|
||||||
ARG TOOLS=" \
|
ARG TOOLS=" \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
@@ -47,39 +95,30 @@ ARG TOOLS=" \
|
|||||||
wget"
|
wget"
|
||||||
|
|
||||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \
|
|
||||||
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
&& apt-get -y install --no-install-recommends ${DEPENDENCIES} \
|
||||||
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
&& apt-get -y install --no-install-recommends ${TOOLS} \
|
||||||
&& mkdir -p /root/.ssh/ \
|
&& mkdir -p /root/.ssh/ \
|
||||||
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
&& echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n\tCiphers +aes128-cbc\n\tKexAlgorithms +diffie-hellman-group1-sha1\n\tHostKeyAlgorithms +ssh-rsa" > /root/.ssh/config \
|
||||||
&& echo "set mouse-=a" > ~/.vimrc \
|
|
||||||
&& echo "no" | dpkg-reconfigure dash \
|
&& echo "no" | dpkg-reconfigure dash \
|
||||||
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
&& echo "zh_CN.UTF-8" | dpkg-reconfigure locales \
|
||||||
&& sed -i "s@# export @export @g" ~/.bashrc \
|
&& sed -i "s@# export @export @g" ~/.bashrc \
|
||||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
&& sed -i "s@# alias @alias @g" ~/.bashrc
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
COPY --from=stage-2 /opt/py3 /opt/py3
|
||||||
|
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||||
|
|
||||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
|
||||||
WORKDIR /opt/jumpserver
|
WORKDIR /opt/jumpserver
|
||||||
|
|
||||||
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
ARG VERSION
|
||||||
RUN --mount=type=cache,target=/root/.cache \
|
ENV VERSION=$VERSION
|
||||||
set -ex \
|
|
||||||
&& echo > /opt/jumpserver/config.yml \
|
|
||||||
&& pip install poetry -i ${PIP_MIRROR} \
|
|
||||||
&& poetry config virtualenvs.create false \
|
|
||||||
&& poetry install --only=main
|
|
||||||
|
|
||||||
VOLUME /opt/jumpserver/data
|
VOLUME /opt/jumpserver/data
|
||||||
VOLUME /opt/jumpserver/logs
|
|
||||||
|
|
||||||
ENV LANG=zh_CN.UTF-8
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
ARG VERSION
|
ARG VERSION
|
||||||
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
FROM registry.fit2cloud.com/jumpserver/xpack:${VERSION} as build-xpack
|
||||||
FROM jumpserver/core:${VERSION}
|
FROM registry.fit2cloud.com/jumpserver/core-ce:${VERSION}
|
||||||
|
|
||||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache \
|
|
||||||
set -ex \
|
|
||||||
&& poetry install --only=xpack
|
|
||||||
@@ -61,6 +61,7 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
|||||||
|
|
||||||
## 案例研究
|
## 案例研究
|
||||||
|
|
||||||
|
- [腾讯音乐娱乐集团:基于JumpServer的安全运维审计解决方案](https://blog.fit2cloud.com/?p=a04cdf0d-6704-4d18-9b40-9180baecd0e2)
|
||||||
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
|
- [腾讯海外游戏:基于JumpServer构建游戏安全运营能力](https://blog.fit2cloud.com/?p=3704)
|
||||||
- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
|
- [万华化学:通过JumpServer管理全球化分布式IT资产,并且实现与云管平台的联动](https://blog.fit2cloud.com/?p=3504)
|
||||||
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
|
- [雪花啤酒:JumpServer堡垒机使用体会](https://blog.fit2cloud.com/?p=3412)
|
||||||
@@ -97,7 +98,7 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
|||||||
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
| [Magnus](https://github.com/jumpserver/magnus-release) | <a href="https://github.com/jumpserver/magnus-release/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/magnus-release.svg" /> | JumpServer 数据库代理 Connector 项目 |
|
||||||
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
|
| [Chen](https://github.com/jumpserver/chen-release) | <a href="https://github.com/jumpserver/chen-release/releases"><img alt="Chen release" src="https://img.shields.io/github/release/jumpserver/chen-release.svg" /> | JumpServer Web DB 项目,替代原来的 OmniDB |
|
||||||
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
|
| [Kael](https://github.com/jumpserver/kael) | <a href="https://github.com/jumpserver/kael/releases"><img alt="Kael release" src="https://img.shields.io/github/release/jumpserver/kael.svg" /> | JumpServer 连接 GPT 资产的组件项目 |
|
||||||
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core Api 通信的组件项目 |
|
| [Wisp](https://github.com/jumpserver/wisp) | <a href="https://github.com/jumpserver/wisp/releases"><img alt="Magnus release" src="https://img.shields.io/github/release/jumpserver/wisp.svg" /> | JumpServer 各系统终端组件和 Core API 通信的组件项目 |
|
||||||
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
| [Clients](https://github.com/jumpserver/clients) | <a href="https://github.com/jumpserver/clients/releases"><img alt="Clients release" src="https://img.shields.io/github/release/jumpserver/clients.svg" /> | JumpServer 客户端 项目 |
|
||||||
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
| [Installer](https://github.com/jumpserver/installer) | <a href="https://github.com/jumpserver/installer/releases"><img alt="Installer release" src="https://img.shields.io/github/release/jumpserver/installer.svg" /> | JumpServer 安装包 项目 |
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
|
from rest_framework import status, mixins
|
||||||
from rest_framework import mixins
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from accounts import serializers
|
from accounts import serializers
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
|
||||||
|
from accounts.tasks import execute_automation_record_task
|
||||||
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,
|
||||||
@@ -29,18 +31,27 @@ class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
|
|||||||
|
|
||||||
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
|
||||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||||
filter_fields = ('asset', 'execution_id')
|
filterset_fields = ('asset_id', 'execution_id')
|
||||||
search_fields = ('asset__address',)
|
search_fields = ('asset__address',)
|
||||||
|
tp = AutomationTypes.change_secret
|
||||||
|
rbac_perms = {
|
||||||
|
'execute': 'accounts.add_changesecretexecution',
|
||||||
|
}
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ChangeSecretRecord.objects.filter(
|
return ChangeSecretRecord.objects.all()
|
||||||
execution__automation__type=AutomationTypes.change_secret
|
|
||||||
)
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
@action(methods=['post'], detail=False, url_path='execute')
|
||||||
queryset = super().filter_queryset(queryset)
|
def execute(self, request, *args, **kwargs):
|
||||||
eid = self.request.query_params.get('execution_id')
|
record_id = request.data.get('record_id')
|
||||||
return queryset.filter(execution_id=eid)
|
record = self.get_queryset().filter(pk=record_id)
|
||||||
|
if not record:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'record not found'},
|
||||||
|
status=status.HTTP_404_NOT_FOUND
|
||||||
|
)
|
||||||
|
task = execute_automation_record_task.delay(record_id, self.tp)
|
||||||
|
return Response({'task': task.id}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class PushAccountExecutionViewSet(AutomationExecutionViewSet):
|
|||||||
|
|
||||||
class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
|
class PushAccountRecordViewSet(ChangeSecretRecordViewSet):
|
||||||
serializer_class = serializers.ChangeSecretRecordSerializer
|
serializer_class = serializers.ChangeSecretRecordSerializer
|
||||||
|
tp = AutomationTypes.push_account
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ChangeSecretRecord.objects.filter(
|
return ChangeSecretRecord.objects.filter(
|
||||||
|
|||||||
@@ -6,16 +6,23 @@ from django.conf import settings
|
|||||||
from openpyxl import Workbook
|
from openpyxl import Workbook
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.notifications import AccountBackupExecutionTaskMsg
|
from accounts.const.automation import AccountBackupType
|
||||||
|
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
|
||||||
from accounts.serializers import AccountSecretSerializer
|
from accounts.serializers import AccountSecretSerializer
|
||||||
|
from accounts.models.automations.backup_account import AccountBackupAutomation
|
||||||
from assets.const import AllTypes
|
from assets.const import AllTypes
|
||||||
from common.utils.file import encrypt_and_compress_zip_file
|
from common.utils.file import encrypt_and_compress_zip_file, zip_files
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_filename, local_now_display
|
||||||
|
from terminal.models.component.storage import ReplayStorage
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
|
||||||
|
|
||||||
|
|
||||||
|
class RecipientsNotFound(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountHandler:
|
class BaseAccountHandler:
|
||||||
@classmethod
|
@classmethod
|
||||||
def unpack_data(cls, serializer_data, data=None):
|
def unpack_data(cls, serializer_data, data=None):
|
||||||
@@ -67,7 +74,7 @@ class AssetAccountHandler(BaseAccountHandler):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_filename(plan_name):
|
def get_filename(plan_name):
|
||||||
filename = os.path.join(
|
filename = os.path.join(
|
||||||
PATH, f'{plan_name}-{local_now_display()}-{time.time()}.xlsx'
|
PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.xlsx'
|
||||||
)
|
)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
@@ -143,7 +150,7 @@ class AccountBackupHandler:
|
|||||||
wb.save(filename)
|
wb.save(filename)
|
||||||
files.append(filename)
|
files.append(filename)
|
||||||
timedelta = round((time.time() - time_start), 2)
|
timedelta = round((time.time() - time_start), 2)
|
||||||
print('步骤完成: 用时 {}s'.format(timedelta))
|
print('创建备份文件完成: 用时 {}s'.format(timedelta))
|
||||||
return files
|
return files
|
||||||
|
|
||||||
def send_backup_mail(self, files, recipients):
|
def send_backup_mail(self, files, recipients):
|
||||||
@@ -152,7 +159,7 @@ class AccountBackupHandler:
|
|||||||
recipients = User.objects.filter(id__in=list(recipients))
|
recipients = User.objects.filter(id__in=list(recipients))
|
||||||
print(
|
print(
|
||||||
'\n'
|
'\n'
|
||||||
'\033[32m>>> 发送备份邮件\033[0m'
|
'\033[32m>>> 开始发送备份邮件\033[0m'
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
plan_name = self.plan_name
|
plan_name = self.plan_name
|
||||||
@@ -161,7 +168,7 @@ class AccountBackupHandler:
|
|||||||
attachment_list = []
|
attachment_list = []
|
||||||
else:
|
else:
|
||||||
password = user.secret_key.encode('utf8')
|
password = user.secret_key.encode('utf8')
|
||||||
attachment = os.path.join(PATH, f'{plan_name}-{local_now_display()}-{time.time()}.zip')
|
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
||||||
encrypt_and_compress_zip_file(attachment, password, files)
|
encrypt_and_compress_zip_file(attachment, password, files)
|
||||||
attachment_list = [attachment, ]
|
attachment_list = [attachment, ]
|
||||||
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list)
|
||||||
@@ -169,11 +176,35 @@ class AccountBackupHandler:
|
|||||||
for file in files:
|
for file in files:
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
|
|
||||||
|
def send_backup_obj_storage(self, files, recipients, password):
|
||||||
|
if not files:
|
||||||
|
return
|
||||||
|
recipients = ReplayStorage.objects.filter(id__in=list(recipients))
|
||||||
|
print(
|
||||||
|
'\n'
|
||||||
|
'\033[32m>>> 开始发送备份文件到sftp服务器\033[0m'
|
||||||
|
''
|
||||||
|
)
|
||||||
|
plan_name = self.plan_name
|
||||||
|
for rec in recipients:
|
||||||
|
attachment = os.path.join(PATH, f'{plan_name}-{local_now_filename()}-{time.time()}.zip')
|
||||||
|
if password:
|
||||||
|
print('\033[32m>>> 使用加密密码对文件进行加密中\033[0m')
|
||||||
|
password = password.encode('utf8')
|
||||||
|
encrypt_and_compress_zip_file(attachment, password, files)
|
||||||
|
else:
|
||||||
|
zip_files(attachment, files)
|
||||||
|
attachment_list = attachment
|
||||||
|
AccountBackupByObjStorageExecutionTaskMsg(plan_name, rec).publish(attachment_list)
|
||||||
|
print('备份文件将发送至{}({})'.format(rec.name, rec.id))
|
||||||
|
for file in files:
|
||||||
|
os.remove(file)
|
||||||
|
|
||||||
def step_perform_task_update(self, is_success, reason):
|
def step_perform_task_update(self, is_success, reason):
|
||||||
self.execution.reason = reason[:1024]
|
self.execution.reason = reason[:1024]
|
||||||
self.execution.is_success = is_success
|
self.execution.is_success = is_success
|
||||||
self.execution.save()
|
self.execution.save()
|
||||||
print('已完成对任务状态的更新')
|
print('\n已完成对任务状态的更新\n')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def step_finished(is_success):
|
def step_finished(is_success):
|
||||||
@@ -186,24 +217,11 @@ class AccountBackupHandler:
|
|||||||
is_success = False
|
is_success = False
|
||||||
error = '-'
|
error = '-'
|
||||||
try:
|
try:
|
||||||
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
backup_type = self.execution.snapshot.get('backup_type', AccountBackupType.email.value)
|
||||||
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
if backup_type == AccountBackupType.email.value:
|
||||||
if not recipients_part_one and not recipients_part_two:
|
self.backup_by_email()
|
||||||
print(
|
elif backup_type == AccountBackupType.object_storage.value:
|
||||||
'\n'
|
self.backup_by_obj_storage()
|
||||||
'\033[32m>>> 该备份任务未分配收件人\033[0m'
|
|
||||||
''
|
|
||||||
)
|
|
||||||
if recipients_part_one and recipients_part_two:
|
|
||||||
files = self.create_excel(section='front')
|
|
||||||
self.send_backup_mail(files, recipients_part_one)
|
|
||||||
|
|
||||||
files = self.create_excel(section='back')
|
|
||||||
self.send_backup_mail(files, recipients_part_two)
|
|
||||||
else:
|
|
||||||
recipients = recipients_part_one or recipients_part_two
|
|
||||||
files = self.create_excel()
|
|
||||||
self.send_backup_mail(files, recipients)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.is_frozen = True
|
self.is_frozen = True
|
||||||
print('任务执行被异常中断')
|
print('任务执行被异常中断')
|
||||||
@@ -217,6 +235,52 @@ class AccountBackupHandler:
|
|||||||
self.step_perform_task_update(is_success, reason)
|
self.step_perform_task_update(is_success, reason)
|
||||||
self.step_finished(is_success)
|
self.step_finished(is_success)
|
||||||
|
|
||||||
|
def backup_by_obj_storage(self):
|
||||||
|
object_id = self.execution.snapshot.get('id')
|
||||||
|
zip_encrypt_password = AccountBackupAutomation.objects.get(id=object_id).zip_encrypt_password
|
||||||
|
obj_recipients_part_one = self.execution.snapshot.get('obj_recipients_part_one', [])
|
||||||
|
obj_recipients_part_two = self.execution.snapshot.get('obj_recipients_part_two', [])
|
||||||
|
if not obj_recipients_part_one and not obj_recipients_part_two:
|
||||||
|
print(
|
||||||
|
'\n'
|
||||||
|
'\033[31m>>> 该备份任务未分配sftp服务器\033[0m'
|
||||||
|
''
|
||||||
|
)
|
||||||
|
raise RecipientsNotFound('Not Found Recipients')
|
||||||
|
if obj_recipients_part_one and obj_recipients_part_two:
|
||||||
|
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
|
||||||
|
files = self.create_excel(section='front')
|
||||||
|
self.send_backup_obj_storage(files, obj_recipients_part_one, zip_encrypt_password)
|
||||||
|
|
||||||
|
files = self.create_excel(section='back')
|
||||||
|
self.send_backup_obj_storage(files, obj_recipients_part_two, zip_encrypt_password)
|
||||||
|
else:
|
||||||
|
recipients = obj_recipients_part_one or obj_recipients_part_two
|
||||||
|
files = self.create_excel()
|
||||||
|
self.send_backup_obj_storage(files, recipients, zip_encrypt_password)
|
||||||
|
|
||||||
|
def backup_by_email(self):
|
||||||
|
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
||||||
|
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||||
|
if not recipients_part_one and not recipients_part_two:
|
||||||
|
print(
|
||||||
|
'\n'
|
||||||
|
'\033[31m>>> 该备份任务未分配收件人\033[0m'
|
||||||
|
''
|
||||||
|
)
|
||||||
|
raise RecipientsNotFound('Not Found Recipients')
|
||||||
|
if recipients_part_one and recipients_part_two:
|
||||||
|
print('\033[32m>>> 账号的密钥将被拆分成前后两部分发送\033[0m')
|
||||||
|
files = self.create_excel(section='front')
|
||||||
|
self.send_backup_mail(files, recipients_part_one)
|
||||||
|
|
||||||
|
files = self.create_excel(section='back')
|
||||||
|
self.send_backup_mail(files, recipients_part_two)
|
||||||
|
else:
|
||||||
|
recipients = recipients_part_one or recipients_part_two
|
||||||
|
files = self.create_excel()
|
||||||
|
self.send_backup_mail(files, recipients)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
print('任务开始: {}'.format(local_now_display()))
|
print('任务开始: {}'.format(local_now_display()))
|
||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
@@ -229,4 +293,4 @@ class AccountBackupHandler:
|
|||||||
finally:
|
finally:
|
||||||
print('\n任务结束: {}'.format(local_now_display()))
|
print('\n任务结束: {}'.format(local_now_display()))
|
||||||
timedelta = round((time.time() - time_start), 2)
|
timedelta = round((time.time() - time_start), 2)
|
||||||
print('用时: {}'.format(timedelta))
|
print('用时: {}s'.format(timedelta))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
- hosts: custom
|
- hosts: custom
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
|
asset_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
- name: Test privileged account (paramiko)
|
- name: Test privileged account (paramiko)
|
||||||
ssh_ping:
|
ssh_ping:
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ asset_port }}"
|
||||||
login_user: "{{ jms_account.username }}"
|
login_user: "{{ jms_account.username }}"
|
||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
@@ -19,13 +20,14 @@
|
|||||||
become_password: "{{ custom_become_password | default('') }}"
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
register: ping_info
|
register: ping_info
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Change 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 }}"
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ asset_port }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
become: "{{ custom_become | default(False) }}"
|
become: "{{ custom_become | default(False) }}"
|
||||||
@@ -40,15 +42,17 @@
|
|||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: ping_info is succeeded
|
when: ping_info is succeeded
|
||||||
register: change_info
|
register: change_info
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify password (paramiko)
|
- name: Verify password (paramiko)
|
||||||
ssh_ping:
|
ssh_ping:
|
||||||
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: "{{ asset_port }}"
|
||||||
become: "{{ account.become.ansible_become | default(False) }}"
|
become: "{{ account.become.ansible_become | default(False) }}"
|
||||||
become_method: su
|
become_method: su
|
||||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||||
become_password: "{{ account.become.ansible_password | default('') }}"
|
become_password: "{{ account.become.ansible_password | default('') }}"
|
||||||
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
|
||||||
|
delegate_to: localhost
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MongoDB connection
|
- name: Test MongoDB connection
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||||
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MySQL connection
|
- name: Test MySQL connection
|
||||||
@@ -11,10 +12,10 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
@@ -28,10 +29,10 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
host: "%"
|
host: "%"
|
||||||
@@ -45,8 +46,8 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test Oracle connection
|
- name: Test Oracle connection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: postgre
|
- hosts: postgre
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test PostgreSQL connection
|
- name: Test PostgreSQL connection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
- hosts: demo
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- name: Test privileged account
|
||||||
|
ansible.windows.win_ping:
|
||||||
|
|
||||||
|
# - name: Print variables
|
||||||
|
# debug:
|
||||||
|
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||||
|
|
||||||
|
- name: Change password
|
||||||
|
ansible.windows.win_user:
|
||||||
|
fullname: "{{ account.username}}"
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
password: "{{ account.secret }}"
|
||||||
|
password_never_expires: yes
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
groups_action: add
|
||||||
|
update_password: always
|
||||||
|
ignore_errors: true
|
||||||
|
when: account.secret_type == "password"
|
||||||
|
|
||||||
|
- name: Refresh connection
|
||||||
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
|
- name: Verify password (pyfreerdp)
|
||||||
|
rdp_ping:
|
||||||
|
login_host: "{{ jms_asset.address }}"
|
||||||
|
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||||
|
login_user: "{{ account.username }}"
|
||||||
|
login_password: "{{ account.secret }}"
|
||||||
|
login_secret_type: "{{ account.secret_type }}"
|
||||||
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
id: change_secret_windows_rdp_verify
|
||||||
|
name: "{{ 'Windows account change secret rdp verify' | trans }}"
|
||||||
|
version: 1
|
||||||
|
method: change_secret
|
||||||
|
category: host
|
||||||
|
type:
|
||||||
|
- windows
|
||||||
|
params:
|
||||||
|
- name: groups
|
||||||
|
type: str
|
||||||
|
label: '用户组'
|
||||||
|
default: 'Users,Remote Desktop Users'
|
||||||
|
help_text: "{{ 'Params groups help text' | trans }}"
|
||||||
|
|
||||||
|
|
||||||
|
i18n:
|
||||||
|
Windows account change secret rdp verify:
|
||||||
|
zh: '使用 Ansible 模块 win_user 执行 Windows 账号改密 RDP 协议测试最后的可连接性'
|
||||||
|
ja: 'Ansibleモジュールwin_userはWindowsアカウントの改密RDPプロトコルテストの最後の接続性を実行する'
|
||||||
|
en: 'Using the Ansible module win_user performs Windows account encryption RDP protocol testing for final connectivity'
|
||||||
|
|
||||||
|
Params groups help text:
|
||||||
|
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
|
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||||
|
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -14,7 +13,7 @@ from accounts.serializers import ChangeSecretRecordBackUpSerializer
|
|||||||
from assets.const import HostTypes
|
from assets.const import HostTypes
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.file import encrypt_and_compress_zip_file
|
from common.utils.file import encrypt_and_compress_zip_file
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_filename
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from ..base.manager import AccountBasePlaybookManager
|
from ..base.manager import AccountBasePlaybookManager
|
||||||
from ...utils import SecretGenerator
|
from ...utils import SecretGenerator
|
||||||
@@ -27,7 +26,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.method_hosts_mapper = defaultdict(list)
|
self.record_id = self.execution.snapshot.get('record_id')
|
||||||
self.secret_type = self.execution.snapshot.get('secret_type')
|
self.secret_type = self.execution.snapshot.get('secret_type')
|
||||||
self.secret_strategy = self.execution.snapshot.get(
|
self.secret_strategy = self.execution.snapshot.get(
|
||||||
'secret_strategy', SecretStrategy.custom
|
'secret_strategy', SecretStrategy.custom
|
||||||
@@ -50,7 +49,9 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no'
|
||||||
|
|
||||||
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
if kwargs['strategy'] == SSHKeyStrategy.set_jms:
|
||||||
kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username)
|
username = account.username
|
||||||
|
path = f'/{username}' if username == "root" else f'/home/{username}'
|
||||||
|
kwargs['dest'] = f'{path}/.ssh/authorized_keys'
|
||||||
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip())
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@@ -96,17 +97,13 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
|
|
||||||
accounts = self.get_accounts(account)
|
accounts = self.get_accounts(account)
|
||||||
if not accounts:
|
if not accounts:
|
||||||
print('没有发现待改密账号: %s 用户ID: %s 类型: %s' % (
|
print('没有发现待处理的账号: %s 用户ID: %s 类型: %s' % (
|
||||||
asset.name, self.account_ids, self.secret_type
|
asset.name, self.account_ids, self.secret_type
|
||||||
))
|
))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
method_attr = getattr(automation, self.method_type() + '_method')
|
|
||||||
method_hosts = self.method_hosts_mapper[method_attr]
|
|
||||||
method_hosts = [h for h in method_hosts if h != host['name']]
|
|
||||||
inventory_hosts = []
|
|
||||||
records = []
|
records = []
|
||||||
|
inventory_hosts = []
|
||||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
||||||
print(f'Windows {asset} does not support ssh key push')
|
print(f'Windows {asset} does not support ssh key push')
|
||||||
return inventory_hosts
|
return inventory_hosts
|
||||||
@@ -116,13 +113,20 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
h = deepcopy(host)
|
h = deepcopy(host)
|
||||||
secret_type = account.secret_type
|
secret_type = account.secret_type
|
||||||
h['name'] += '(' + account.username + ')'
|
h['name'] += '(' + account.username + ')'
|
||||||
new_secret = self.get_secret(secret_type)
|
if self.secret_type is None:
|
||||||
|
new_secret = account.secret
|
||||||
|
else:
|
||||||
|
new_secret = self.get_secret(secret_type)
|
||||||
|
|
||||||
|
if self.record_id is None:
|
||||||
|
recorder = ChangeSecretRecord(
|
||||||
|
asset=asset, account=account, execution=self.execution,
|
||||||
|
old_secret=account.secret, new_secret=new_secret,
|
||||||
|
)
|
||||||
|
records.append(recorder)
|
||||||
|
else:
|
||||||
|
recorder = ChangeSecretRecord.objects.get(id=self.record_id)
|
||||||
|
|
||||||
recorder = ChangeSecretRecord(
|
|
||||||
asset=asset, account=account, execution=self.execution,
|
|
||||||
old_secret=account.secret, new_secret=new_secret,
|
|
||||||
)
|
|
||||||
records.append(recorder)
|
|
||||||
self.name_recorder_mapper[h['name']] = recorder
|
self.name_recorder_mapper[h['name']] = recorder
|
||||||
|
|
||||||
private_key_path = None
|
private_key_path = None
|
||||||
@@ -136,13 +140,12 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
'username': account.username,
|
'username': account.username,
|
||||||
'secret_type': secret_type,
|
'secret_type': secret_type,
|
||||||
'secret': new_secret,
|
'secret': new_secret,
|
||||||
'private_key_path': private_key_path
|
'private_key_path': private_key_path,
|
||||||
|
'become': account.get_ansible_become_auth(),
|
||||||
}
|
}
|
||||||
if asset.platform.type == 'oracle':
|
if asset.platform.type == 'oracle':
|
||||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
h['account']['mode'] = 'sysdba' if account.privileged else None
|
||||||
inventory_hosts.append(h)
|
inventory_hosts.append(h)
|
||||||
method_hosts.append(h['name'])
|
|
||||||
self.method_hosts_mapper[method_attr] = method_hosts
|
|
||||||
ChangeSecretRecord.objects.bulk_create(records)
|
ChangeSecretRecord.objects.bulk_create(records)
|
||||||
return inventory_hosts
|
return inventory_hosts
|
||||||
|
|
||||||
@@ -170,7 +173,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
recorder.save()
|
recorder.save()
|
||||||
|
|
||||||
def on_runner_failed(self, runner, e):
|
def on_runner_failed(self, runner, e):
|
||||||
logger.error("Change secret error: ", e)
|
logger.error("Account error: ", e)
|
||||||
|
|
||||||
def check_secret(self):
|
def check_secret(self):
|
||||||
if self.secret_strategy == SecretStrategy.custom \
|
if self.secret_strategy == SecretStrategy.custom \
|
||||||
@@ -180,9 +183,11 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
def run(self, *args, **kwargs):
|
||||||
if not self.check_secret():
|
if self.secret_type and not self.check_secret():
|
||||||
return
|
return
|
||||||
super().run(*args, **kwargs)
|
super().run(*args, **kwargs)
|
||||||
|
if self.record_id:
|
||||||
|
return
|
||||||
recorders = self.name_recorder_mapper.values()
|
recorders = self.name_recorder_mapper.values()
|
||||||
recorders = list(recorders)
|
recorders = list(recorders)
|
||||||
self.send_recorder_mail(recorders)
|
self.send_recorder_mail(recorders)
|
||||||
@@ -196,7 +201,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
|
|
||||||
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_display()}-{time.time()}.xlsx')
|
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
|
||||||
if not self.create_file(recorders, filename):
|
if not self.create_file(recorders, filename):
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -204,7 +209,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
|||||||
attachments = []
|
attachments = []
|
||||||
if user.secret_key:
|
if user.secret_key:
|
||||||
password = user.secret_key.encode('utf8')
|
password = user.secret_key.encode('utf8')
|
||||||
attachment = os.path.join(path, f'{name}-{local_now_display()}-{time.time()}.zip')
|
attachment = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.zip')
|
||||||
encrypt_and_compress_zip_file(attachment, password, [filename])
|
encrypt_and_compress_zip_file(attachment, password, [filename])
|
||||||
attachments = [attachment]
|
attachments = [attachment]
|
||||||
ChangeSecretExecutionTaskMsg(name, user).publish(attachments)
|
ChangeSecretExecutionTaskMsg(name, user).publish(attachments)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
@@ -10,10 +11,10 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: users
|
filter: users
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: oralce
|
- hosts: oralce
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: postgresql
|
- hosts: postgresql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import GatheredAccount
|
from accounts.models import GatheredAccount
|
||||||
|
from assets.models import Asset
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from orgs.utils import tmp_to_org
|
from orgs.utils import tmp_to_org
|
||||||
|
from users.models import User
|
||||||
from .filter import GatherAccountsFilter
|
from .filter import GatherAccountsFilter
|
||||||
from ..base.manager import AccountBasePlaybookManager
|
from ..base.manager import AccountBasePlaybookManager
|
||||||
|
from ...notifications import GatherAccountChangeMsg
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -12,6 +17,9 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.host_asset_mapper = {}
|
self.host_asset_mapper = {}
|
||||||
|
self.asset_account_info = {}
|
||||||
|
|
||||||
|
self.asset_username_mapper = defaultdict(set)
|
||||||
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
self.is_sync_account = self.execution.snapshot.get('is_sync_account')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -26,10 +34,11 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
|||||||
def filter_success_result(self, tp, result):
|
def filter_success_result(self, tp, result):
|
||||||
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
result = GatherAccountsFilter(tp).run(self.method_id_meta_mapper, result)
|
||||||
return result
|
return result
|
||||||
@staticmethod
|
|
||||||
def generate_data(asset, result):
|
def generate_data(self, asset, result):
|
||||||
data = []
|
data = []
|
||||||
for username, info in result.items():
|
for username, info in result.items():
|
||||||
|
self.asset_username_mapper[str(asset.id)].add(username)
|
||||||
d = {'asset': asset, 'username': username, 'present': True}
|
d = {'asset': asset, 'username': username, 'present': True}
|
||||||
if info.get('date'):
|
if info.get('date'):
|
||||||
d['date_last_login'] = info['date']
|
d['date_last_login'] = info['date']
|
||||||
@@ -38,26 +47,85 @@ class GatherAccountsManager(AccountBasePlaybookManager):
|
|||||||
data.append(d)
|
data.append(d)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def update_or_create_accounts(self, asset, result):
|
def collect_asset_account_info(self, asset, result):
|
||||||
data = self.generate_data(asset, result)
|
data = self.generate_data(asset, result)
|
||||||
with tmp_to_org(asset.org_id):
|
self.asset_account_info[asset] = data
|
||||||
gathered_accounts = []
|
|
||||||
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
|
||||||
for d in data:
|
|
||||||
username = d['username']
|
|
||||||
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
|
||||||
defaults=d, asset=asset, username=username,
|
|
||||||
)
|
|
||||||
gathered_accounts.append(gathered_account)
|
|
||||||
if not self.is_sync_account:
|
|
||||||
return
|
|
||||||
GatheredAccount.sync_accounts(gathered_accounts)
|
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
def on_host_success(self, host, result):
|
||||||
info = result.get('debug', {}).get('res', {}).get('info', {})
|
info = result.get('debug', {}).get('res', {}).get('info', {})
|
||||||
asset = self.host_asset_mapper.get(host)
|
asset = self.host_asset_mapper.get(host)
|
||||||
if asset and info:
|
if asset and info:
|
||||||
result = self.filter_success_result(asset.type, info)
|
result = self.filter_success_result(asset.type, info)
|
||||||
self.update_or_create_accounts(asset, result)
|
self.collect_asset_account_info(asset, result)
|
||||||
else:
|
else:
|
||||||
logger.error("Not found info".format(host))
|
logger.error(f'Not found {host} info')
|
||||||
|
|
||||||
|
def update_or_create_accounts(self):
|
||||||
|
for asset, data in self.asset_account_info.items():
|
||||||
|
with tmp_to_org(asset.org_id):
|
||||||
|
gathered_accounts = []
|
||||||
|
GatheredAccount.objects.filter(asset=asset, present=True).update(present=False)
|
||||||
|
for d in data:
|
||||||
|
username = d['username']
|
||||||
|
gathered_account, __ = GatheredAccount.objects.update_or_create(
|
||||||
|
defaults=d, asset=asset, username=username,
|
||||||
|
)
|
||||||
|
gathered_accounts.append(gathered_account)
|
||||||
|
if not self.is_sync_account:
|
||||||
|
return
|
||||||
|
GatheredAccount.sync_accounts(gathered_accounts)
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
super().run(*args, **kwargs)
|
||||||
|
users, change_info = self.generate_send_users_and_change_info()
|
||||||
|
self.update_or_create_accounts()
|
||||||
|
self.send_email_if_need(users, change_info)
|
||||||
|
|
||||||
|
def generate_send_users_and_change_info(self):
|
||||||
|
recipients = self.execution.recipients
|
||||||
|
if not self.asset_username_mapper or not recipients:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
users = User.objects.filter(id__in=recipients)
|
||||||
|
if not users:
|
||||||
|
return users, None
|
||||||
|
|
||||||
|
asset_ids = self.asset_username_mapper.keys()
|
||||||
|
assets = Asset.objects.filter(id__in=asset_ids)
|
||||||
|
gather_accounts = GatheredAccount.objects.filter(asset_id__in=asset_ids, present=True)
|
||||||
|
asset_id_map = {str(asset.id): asset for asset in assets}
|
||||||
|
asset_id_username = list(assets.values_list('id', 'accounts__username'))
|
||||||
|
asset_id_username.extend(list(gather_accounts.values_list('asset_id', 'username')))
|
||||||
|
|
||||||
|
system_asset_username_mapper = defaultdict(set)
|
||||||
|
for asset_id, username in asset_id_username:
|
||||||
|
system_asset_username_mapper[str(asset_id)].add(username)
|
||||||
|
|
||||||
|
change_info = {}
|
||||||
|
for asset_id, usernames in self.asset_username_mapper.items():
|
||||||
|
system_usernames = system_asset_username_mapper.get(asset_id)
|
||||||
|
|
||||||
|
if not system_usernames:
|
||||||
|
continue
|
||||||
|
|
||||||
|
add_usernames = usernames - system_usernames
|
||||||
|
remove_usernames = system_usernames - usernames
|
||||||
|
k = f'{asset_id_map[asset_id]}[{asset_id}]'
|
||||||
|
|
||||||
|
if not add_usernames and not remove_usernames:
|
||||||
|
continue
|
||||||
|
|
||||||
|
change_info[k] = {
|
||||||
|
'add_usernames': ', '.join(add_usernames),
|
||||||
|
'remove_usernames': ', '.join(remove_usernames),
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, change_info
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_email_if_need(users, change_info):
|
||||||
|
if not users or not change_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
for user in users:
|
||||||
|
GatherAccountChangeMsg(user, change_info).publish_async()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MongoDB connection
|
- name: Test MongoDB connection
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||||
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MySQL connection
|
- name: Test MySQL connection
|
||||||
@@ -11,10 +12,10 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
@@ -28,10 +29,10 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
host: "%"
|
host: "%"
|
||||||
@@ -45,8 +46,8 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test Oracle connection
|
- name: Test Oracle connection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: postgre
|
- hosts: postgre
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test PostgreSQL connection
|
- name: Test PostgreSQL connection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
- hosts: demo
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- name: Test privileged account
|
||||||
|
ansible.windows.win_ping:
|
||||||
|
|
||||||
|
# - name: Print variables
|
||||||
|
# debug:
|
||||||
|
# msg: "Username: {{ account.username }}, Password: {{ account.secret }}"
|
||||||
|
|
||||||
|
- name: Push user password
|
||||||
|
ansible.windows.win_user:
|
||||||
|
fullname: "{{ account.username}}"
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
password: "{{ account.secret }}"
|
||||||
|
password_never_expires: yes
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
groups_action: add
|
||||||
|
update_password: always
|
||||||
|
ignore_errors: true
|
||||||
|
when: account.secret_type == "password"
|
||||||
|
|
||||||
|
- name: Refresh connection
|
||||||
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
|
- name: Verify password (pyfreerdp)
|
||||||
|
rdp_ping:
|
||||||
|
login_host: "{{ jms_asset.address }}"
|
||||||
|
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||||
|
login_user: "{{ account.username }}"
|
||||||
|
login_password: "{{ account.secret }}"
|
||||||
|
login_secret_type: "{{ account.secret_type }}"
|
||||||
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
id: push_account_windows_rdp_verify
|
||||||
|
name: "{{ 'Windows account push rdp verify' | trans }}"
|
||||||
|
version: 1
|
||||||
|
method: push_account
|
||||||
|
category: host
|
||||||
|
type:
|
||||||
|
- windows
|
||||||
|
params:
|
||||||
|
- name: groups
|
||||||
|
type: str
|
||||||
|
label: '用户组'
|
||||||
|
default: 'Users,Remote Desktop Users'
|
||||||
|
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||||
|
|
||||||
|
i18n:
|
||||||
|
Windows account push rdp verify:
|
||||||
|
zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送 RDP 协议测试最后的可连接性
|
||||||
|
ja: Ansibleモジュールwin_userがWindowsアカウントプッシュRDPプロトコルテストを実行する最後の接続性
|
||||||
|
en: Using the Ansible module win_user performs Windows account push RDP protocol testing for final connectivity
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
from copy import deepcopy
|
from accounts.const import AutomationTypes
|
||||||
|
|
||||||
from accounts.const import AutomationTypes, SecretType, Connectivity
|
|
||||||
from assets.const import HostTypes
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from ..base.manager import AccountBasePlaybookManager
|
from ..base.manager import AccountBasePlaybookManager
|
||||||
from ..change_secret.manager import ChangeSecretManager
|
from ..change_secret.manager import ChangeSecretManager
|
||||||
@@ -10,83 +7,11 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
class PushAccountManager(ChangeSecretManager, AccountBasePlaybookManager):
|
||||||
ansible_account_prefer = ''
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def method_type(cls):
|
def method_type(cls):
|
||||||
return AutomationTypes.push_account
|
return AutomationTypes.push_account
|
||||||
|
|
||||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
|
||||||
host = super(ChangeSecretManager, self).host_callback(
|
|
||||||
host, asset=asset, account=account, automation=automation,
|
|
||||||
path_dir=path_dir, **kwargs
|
|
||||||
)
|
|
||||||
if host.get('error'):
|
|
||||||
return host
|
|
||||||
|
|
||||||
accounts = self.get_accounts(account)
|
|
||||||
inventory_hosts = []
|
|
||||||
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
|
|
||||||
msg = f'Windows {asset} does not support ssh key push'
|
|
||||||
print(msg)
|
|
||||||
return inventory_hosts
|
|
||||||
|
|
||||||
host['ssh_params'] = {}
|
|
||||||
for account in accounts:
|
|
||||||
h = deepcopy(host)
|
|
||||||
secret_type = account.secret_type
|
|
||||||
h['name'] += '(' + account.username + ')'
|
|
||||||
if self.secret_type is None:
|
|
||||||
new_secret = account.secret
|
|
||||||
else:
|
|
||||||
new_secret = self.get_secret(secret_type)
|
|
||||||
|
|
||||||
self.name_recorder_mapper[h['name']] = {
|
|
||||||
'account': account, 'new_secret': new_secret,
|
|
||||||
}
|
|
||||||
|
|
||||||
private_key_path = None
|
|
||||||
if secret_type == SecretType.SSH_KEY:
|
|
||||||
private_key_path = self.generate_private_key_path(new_secret, path_dir)
|
|
||||||
new_secret = self.generate_public_key(new_secret)
|
|
||||||
|
|
||||||
h['ssh_params'].update(self.get_ssh_params(account, new_secret, secret_type))
|
|
||||||
h['account'] = {
|
|
||||||
'name': account.name,
|
|
||||||
'username': account.username,
|
|
||||||
'secret_type': secret_type,
|
|
||||||
'secret': new_secret,
|
|
||||||
'private_key_path': private_key_path
|
|
||||||
}
|
|
||||||
if asset.platform.type == 'oracle':
|
|
||||||
h['account']['mode'] = 'sysdba' if account.privileged else None
|
|
||||||
inventory_hosts.append(h)
|
|
||||||
return inventory_hosts
|
|
||||||
|
|
||||||
def on_host_success(self, host, result):
|
|
||||||
account_info = self.name_recorder_mapper.get(host)
|
|
||||||
if not account_info:
|
|
||||||
return
|
|
||||||
|
|
||||||
account = account_info['account']
|
|
||||||
new_secret = account_info['new_secret']
|
|
||||||
if not account:
|
|
||||||
return
|
|
||||||
account.secret = new_secret
|
|
||||||
account.save(update_fields=['secret'])
|
|
||||||
account.set_connectivity(Connectivity.OK)
|
|
||||||
|
|
||||||
def on_host_error(self, host, error, result):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def on_runner_failed(self, runner, e):
|
|
||||||
logger.error("Pust account error: {}".format(e))
|
|
||||||
|
|
||||||
def run(self, *args, **kwargs):
|
|
||||||
if self.secret_type and not self.check_secret():
|
|
||||||
return
|
|
||||||
super(ChangeSecretManager, self).run(*args, **kwargs)
|
|
||||||
|
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# def trigger_by_asset_create(cls, asset):
|
# def trigger_by_asset_create(cls, asset):
|
||||||
# automations = PushAccountAutomation.objects.filter(
|
# automations = PushAccountAutomation.objects.filter(
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
ansible_shell_type: sh
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account (paramiko)
|
- name: Verify account (paramiko)
|
||||||
ssh_ping:
|
ssh_ping:
|
||||||
login_host: "{{ jms_asset.address }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
|
||||||
login_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_secret_type: "{{ account.secret_type }}"
|
login_secret_type: "{{ account.secret_type }}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: mongdb
|
- hosts: mongdb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
@@ -10,8 +11,8 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: postgresql
|
- hosts: postgresql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account
|
- name: Verify account
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ DEFAULT_PASSWORD_RULES = {
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
|
||||||
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
|
||||||
'PushAccountActionChoice',
|
'PushAccountActionChoice', 'AccountBackupType'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -95,3 +95,10 @@ class TriggerChoice(models.TextChoices, TreeChoices):
|
|||||||
class PushAccountActionChoice(models.TextChoices):
|
class PushAccountActionChoice(models.TextChoices):
|
||||||
create_and_push = 'create_and_push', _('Create and push')
|
create_and_push = 'create_and_push', _('Create and push')
|
||||||
only_create = 'only_create', _('Only create')
|
only_create = 'only_create', _('Only create')
|
||||||
|
|
||||||
|
|
||||||
|
class AccountBackupType(models.TextChoices):
|
||||||
|
"""Backup type"""
|
||||||
|
email = 'email', _('Email')
|
||||||
|
# 目前只支持sftp方式
|
||||||
|
object_storage = 'object_storage', _('SFTP')
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class Migration(migrations.Migration):
|
|||||||
'verbose_name': 'Automation execution',
|
'verbose_name': 'Automation execution',
|
||||||
'verbose_name_plural': 'Automation executions',
|
'verbose_name_plural': 'Automation executions',
|
||||||
'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
||||||
('add_changesecretexection', 'Can add change secret execution'),
|
('add_changesecretexecution', 'Can add change secret execution'),
|
||||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||||
('add_gatheraccountsexecution', 'Can add gather accounts execution')],
|
('add_gatheraccountsexecution', 'Can add gather accounts execution')],
|
||||||
'proxy': True,
|
'proxy': True,
|
||||||
@@ -116,7 +116,7 @@ class Migration(migrations.Migration):
|
|||||||
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
|
('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='New secret')),
|
||||||
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')),
|
||||||
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')),
|
||||||
('status', models.CharField(default='pending', max_length=16)),
|
('status', models.CharField(default='pending', max_length=16, verbose_name='Status')),
|
||||||
('error', models.TextField(blank=True, null=True, verbose_name='Error')),
|
('error', models.TextField(blank=True, null=True, verbose_name='Error')),
|
||||||
('account',
|
('account',
|
||||||
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')),
|
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')),
|
||||||
@@ -184,7 +184,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='automationexecution',
|
name='automationexecution',
|
||||||
options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'),
|
||||||
('add_changesecretexection', 'Can add change secret execution'),
|
('add_changesecretexecution', 'Can add change secret execution'),
|
||||||
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||||
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
|
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
|
||||||
('view_pushaccountexecution', 'Can view push account execution'),
|
('view_pushaccountexecution', 'Can view push account execution'),
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-10-24 05:59
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0016_accounttemplate_password_rules'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='automationexecution',
|
||||||
|
options={
|
||||||
|
'permissions': [
|
||||||
|
('view_changesecretexecution', 'Can view change secret execution'),
|
||||||
|
('add_changesecretexecution', 'Can add change secret execution'),
|
||||||
|
('view_gatheraccountsexecution', 'Can view gather accounts execution'),
|
||||||
|
('add_gatheraccountsexecution', 'Can add gather accounts execution'),
|
||||||
|
('view_pushaccountexecution', 'Can view push account execution'),
|
||||||
|
('add_pushaccountexecution', 'Can add push account execution')
|
||||||
|
],
|
||||||
|
'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-11-03 07:10
|
||||||
|
|
||||||
|
import common.db.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('terminal', '0067_alter_replaystorage_type'),
|
||||||
|
('accounts', '0017_alter_automationexecution_options'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='backup_type',
|
||||||
|
field=models.CharField(choices=[('email', 'Email'), ('object_storage', 'Object Storage')], default='email', max_length=128),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='is_password_divided_by_email',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='is_password_divided_by_obj_storage',
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='obj_recipients_part_one',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_one_plans', to='terminal.replaystorage', verbose_name='Object Storage Recipient part one'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='obj_recipients_part_two',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='obj_recipient_part_two_plans', to='terminal.replaystorage', verbose_name='Object Storage Recipient part two'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='zip_encrypt_password',
|
||||||
|
field=common.db.fields.EncryptCharField(blank=True, max_length=4096, null=True, verbose_name='Zip Encrypt Password'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-10-31 06:12
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('accounts', '0018_accountbackupautomation_backup_type_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gatheraccountsautomation',
|
||||||
|
name='recipients',
|
||||||
|
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-11-16 02:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0019_gatheraccountsautomation_recipients'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='backup_type',
|
||||||
|
field=models.CharField(choices=[('email', 'Email'), ('object_storage', 'SFTP')], default='email', max_length=128, verbose_name='Backup Type'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='is_password_divided_by_email',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Is Password Divided'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='is_password_divided_by_obj_storage',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Is Password Divided'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -114,7 +114,7 @@ class Account(AbsConnectivity, BaseAccount):
|
|||||||
return auth
|
return auth
|
||||||
|
|
||||||
auth.update(self.make_account_ansible_vars(su_from))
|
auth.update(self.make_account_ansible_vars(su_from))
|
||||||
become_method = 'sudo' if platform.su_method != 'su' else 'su'
|
become_method = platform.su_method if platform.su_method else 'sudo'
|
||||||
password = su_from.secret if become_method == 'sudo' else self.secret
|
password = su_from.secret if become_method == 'sudo' else self.secret
|
||||||
auth['ansible_become'] = True
|
auth['ansible_become'] = True
|
||||||
auth['ansible_become_method'] = become_method
|
auth['ansible_become_method'] = become_method
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ from django.db import models
|
|||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.const.automation import AccountBackupType
|
||||||
from common.const.choices import Trigger
|
from common.const.choices import Trigger
|
||||||
|
from common.db import fields
|
||||||
from common.db.encoder import ModelJSONFieldEncoder
|
from common.db.encoder import ModelJSONFieldEncoder
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger, lazyproperty
|
||||||
from common.utils import lazyproperty
|
|
||||||
from ops.mixin import PeriodTaskModelMixin
|
from ops.mixin import PeriodTaskModelMixin
|
||||||
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
|
from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ logger = get_logger(__file__)
|
|||||||
|
|
||||||
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||||
types = models.JSONField(default=list)
|
types = models.JSONField(default=list)
|
||||||
|
backup_type = models.CharField(max_length=128, choices=AccountBackupType.choices,
|
||||||
|
default=AccountBackupType.email.value, verbose_name=_('Backup Type'))
|
||||||
|
is_password_divided_by_email = models.BooleanField(default=True, verbose_name=_('Is Password Divided'))
|
||||||
|
is_password_divided_by_obj_storage = models.BooleanField(default=True, verbose_name=_('Is Password Divided'))
|
||||||
recipients_part_one = models.ManyToManyField(
|
recipients_part_one = models.ManyToManyField(
|
||||||
'users.User', related_name='recipient_part_one_plans', blank=True,
|
'users.User', related_name='recipient_part_one_plans', blank=True,
|
||||||
verbose_name=_("Recipient part one")
|
verbose_name=_("Recipient part one")
|
||||||
@@ -30,6 +35,16 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||||||
'users.User', related_name='recipient_part_two_plans', blank=True,
|
'users.User', related_name='recipient_part_two_plans', blank=True,
|
||||||
verbose_name=_("Recipient part two")
|
verbose_name=_("Recipient part two")
|
||||||
)
|
)
|
||||||
|
obj_recipients_part_one = models.ManyToManyField(
|
||||||
|
'terminal.ReplayStorage', related_name='obj_recipient_part_one_plans', blank=True,
|
||||||
|
verbose_name=_("Object Storage Recipient part one")
|
||||||
|
)
|
||||||
|
obj_recipients_part_two = models.ManyToManyField(
|
||||||
|
'terminal.ReplayStorage', related_name='obj_recipient_part_two_plans', blank=True,
|
||||||
|
verbose_name=_("Object Storage Recipient part two")
|
||||||
|
)
|
||||||
|
zip_encrypt_password = fields.EncryptCharField(max_length=4096, blank=True, null=True,
|
||||||
|
verbose_name=_('Zip Encrypt Password'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.name}({self.org_id})'
|
return f'{self.name}({self.org_id})'
|
||||||
@@ -49,6 +64,7 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||||||
|
|
||||||
def to_attr_json(self):
|
def to_attr_json(self):
|
||||||
return {
|
return {
|
||||||
|
'id': self.id,
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'is_periodic': self.is_periodic,
|
'is_periodic': self.is_periodic,
|
||||||
'interval': self.interval,
|
'interval': self.interval,
|
||||||
@@ -56,6 +72,10 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||||||
'org_id': self.org_id,
|
'org_id': self.org_id,
|
||||||
'created_by': self.created_by,
|
'created_by': self.created_by,
|
||||||
'types': self.types,
|
'types': self.types,
|
||||||
|
'backup_type': self.backup_type,
|
||||||
|
'is_password_divided_by_email': self.is_password_divided_by_email,
|
||||||
|
'is_password_divided_by_obj_storage': self.is_password_divided_by_obj_storage,
|
||||||
|
'zip_encrypt_password': self.zip_encrypt_password,
|
||||||
'recipients_part_one': {
|
'recipients_part_one': {
|
||||||
str(user.id): (str(user), bool(user.secret_key))
|
str(user.id): (str(user), bool(user.secret_key))
|
||||||
for user in self.recipients_part_one.all()
|
for user in self.recipients_part_one.all()
|
||||||
@@ -63,7 +83,15 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||||||
'recipients_part_two': {
|
'recipients_part_two': {
|
||||||
str(user.id): (str(user), bool(user.secret_key))
|
str(user.id): (str(user), bool(user.secret_key))
|
||||||
for user in self.recipients_part_two.all()
|
for user in self.recipients_part_two.all()
|
||||||
}
|
},
|
||||||
|
'obj_recipients_part_one': {
|
||||||
|
str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type))
|
||||||
|
for obj_storage in self.obj_recipients_part_one.all()
|
||||||
|
},
|
||||||
|
'obj_recipients_part_two': {
|
||||||
|
str(obj_storage.id): (str(obj_storage.name), str(obj_storage.type))
|
||||||
|
for obj_storage in self.obj_recipients_part_two.all()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class AutomationExecution(AssetAutomationExecution):
|
|||||||
verbose_name_plural = _("Automation executions")
|
verbose_name_plural = _("Automation executions")
|
||||||
permissions = [
|
permissions = [
|
||||||
('view_changesecretexecution', _('Can view change secret execution')),
|
('view_changesecretexecution', _('Can view change secret execution')),
|
||||||
('add_changesecretexection', _('Can add change secret execution')),
|
('add_changesecretexecution', _('Can add change secret execution')),
|
||||||
|
|
||||||
('view_gatheraccountsexecution', _('Can view gather accounts execution')),
|
('view_gatheraccountsexecution', _('Can view gather accounts execution')),
|
||||||
('add_gatheraccountsexecution', _('Can add gather accounts execution')),
|
('add_gatheraccountsexecution', _('Can add gather accounts execution')),
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class ChangeSecretRecord(JMSBaseModel):
|
|||||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
|
||||||
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
|
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
|
||||||
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
|
||||||
status = models.CharField(max_length=16, default='pending')
|
status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
|
||||||
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -49,9 +49,3 @@ class ChangeSecretRecord(JMSBaseModel):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.account.__str__()
|
return self.account.__str__()
|
||||||
|
|
||||||
@property
|
|
||||||
def timedelta(self):
|
|
||||||
if self.date_started and self.date_finished:
|
|
||||||
return self.date_finished - self.date_started
|
|
||||||
return None
|
|
||||||
|
|||||||
@@ -55,11 +55,15 @@ class GatherAccountsAutomation(AccountBaseAutomation):
|
|||||||
is_sync_account = models.BooleanField(
|
is_sync_account = models.BooleanField(
|
||||||
default=False, blank=True, verbose_name=_("Is sync account")
|
default=False, blank=True, verbose_name=_("Is sync account")
|
||||||
)
|
)
|
||||||
|
recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True)
|
||||||
|
|
||||||
def to_attr_json(self):
|
def to_attr_json(self):
|
||||||
attr_json = super().to_attr_json()
|
attr_json = super().to_attr_json()
|
||||||
attr_json.update({
|
attr_json.update({
|
||||||
'is_sync_account': self.is_sync_account,
|
'is_sync_account': self.is_sync_account,
|
||||||
|
'recipients': [
|
||||||
|
str(recipient.id) for recipient in self.recipients.all()
|
||||||
|
]
|
||||||
})
|
})
|
||||||
return attr_json
|
return attr_json
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.tasks import send_mail_attachment_async
|
from common.tasks import send_mail_attachment, upload_backup_to_obj_storage
|
||||||
|
from notifications.notifications import UserMessage
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
from terminal.models.component.storage import ReplayStorage
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupExecutionTaskMsg(object):
|
class AccountBackupExecutionTaskMsg(object):
|
||||||
@@ -24,11 +27,30 @@ class AccountBackupExecutionTaskMsg(object):
|
|||||||
"to set the encryption password").format(name)
|
"to set the encryption password").format(name)
|
||||||
|
|
||||||
def publish(self, attachment_list=None):
|
def publish(self, attachment_list=None):
|
||||||
send_mail_attachment_async(
|
send_mail_attachment(
|
||||||
self.subject, self.message, [self.user.email], attachment_list
|
self.subject, self.message, [self.user.email], attachment_list
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountBackupByObjStorageExecutionTaskMsg(object):
|
||||||
|
subject = _('Notification of account backup route task results')
|
||||||
|
|
||||||
|
def __init__(self, name: str, obj_storage: ReplayStorage):
|
||||||
|
self.name = name
|
||||||
|
self.obj_storage = obj_storage
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self):
|
||||||
|
name = self.name
|
||||||
|
return _('{} - The account backup passage task has been completed.'
|
||||||
|
' See the attachment for details').format(name)
|
||||||
|
|
||||||
|
def publish(self, attachment_list=None):
|
||||||
|
upload_backup_to_obj_storage(
|
||||||
|
self.obj_storage, attachment_list
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChangeSecretExecutionTaskMsg(object):
|
class ChangeSecretExecutionTaskMsg(object):
|
||||||
subject = _('Notification of implementation result of encryption change plan')
|
subject = _('Notification of implementation result of encryption change plan')
|
||||||
|
|
||||||
@@ -48,6 +70,28 @@ class ChangeSecretExecutionTaskMsg(object):
|
|||||||
"file encryption password to set the encryption password").format(name)
|
"file encryption password to set the encryption password").format(name)
|
||||||
|
|
||||||
def publish(self, attachments=None):
|
def publish(self, attachments=None):
|
||||||
send_mail_attachment_async(
|
send_mail_attachment(
|
||||||
self.subject, self.message, [self.user.email], attachments
|
self.subject, self.message, [self.user.email], attachments
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GatherAccountChangeMsg(UserMessage):
|
||||||
|
subject = _('Gather account change information')
|
||||||
|
|
||||||
|
def __init__(self, user, change_info: dict):
|
||||||
|
self.change_info = change_info
|
||||||
|
super().__init__(user)
|
||||||
|
|
||||||
|
def get_html_msg(self) -> dict:
|
||||||
|
context = {'change_info': self.change_info}
|
||||||
|
message = render_to_string('accounts/asset_account_change_info.html', context)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'subject': str(self.subject),
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_test_msg(cls):
|
||||||
|
user = User.objects.first()
|
||||||
|
return cls(user, {})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
||||||
from common.const.choices import Trigger
|
from common.const.choices import Trigger
|
||||||
from common.serializers.fields import LabeledChoiceField
|
from common.serializers.fields import LabeledChoiceField, EncryptedField
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from ops.mixin import PeriodTaskSerializerMixin
|
from ops.mixin import PeriodTaskSerializerMixin
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
@@ -16,6 +16,11 @@ __all__ = ['AccountBackupSerializer', 'AccountBackupPlanExecutionSerializer']
|
|||||||
|
|
||||||
|
|
||||||
class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
|
||||||
|
zip_encrypt_password = EncryptedField(
|
||||||
|
label=_('Zip Encrypt Password'), required=False, max_length=40960, allow_blank=True,
|
||||||
|
allow_null=True, write_only=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AccountBackupAutomation
|
model = AccountBackupAutomation
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
@@ -24,7 +29,9 @@ class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSer
|
|||||||
]
|
]
|
||||||
fields = read_only_fields + [
|
fields = read_only_fields + [
|
||||||
'id', 'name', 'is_periodic', 'interval', 'crontab',
|
'id', 'name', 'is_periodic', 'interval', 'crontab',
|
||||||
'comment', 'types', 'recipients_part_one', 'recipients_part_two'
|
'comment', 'types', 'recipients_part_one', 'recipients_part_two', 'backup_type',
|
||||||
|
'is_password_divided_by_email', 'is_password_divided_by_obj_storage', 'obj_recipients_part_one',
|
||||||
|
'obj_recipients_part_two', 'zip_encrypt_password'
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'required': True},
|
'name': {'required': True},
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
|||||||
return password_rules
|
return password_rules
|
||||||
|
|
||||||
length = password_rules.get('length')
|
length = password_rules.get('length')
|
||||||
symbol_set = password_rules.get('symbol_set', '')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
length = int(length)
|
length = int(length)
|
||||||
@@ -84,10 +83,6 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
|
|||||||
msg = _('* Password length range 6-30 bits')
|
msg = _('* Password length range 6-30 bits')
|
||||||
raise serializers.ValidationError(msg)
|
raise serializers.ValidationError(msg)
|
||||||
|
|
||||||
if not isinstance(symbol_set, str):
|
|
||||||
symbol_set = str(symbol_set)
|
|
||||||
|
|
||||||
password_rules = {'length': length, 'symbol_set': ''.join(symbol_set)}
|
|
||||||
return password_rules
|
return password_rules
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
@@ -117,8 +112,8 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = ChangeSecretRecord
|
model = ChangeSecretRecord
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'asset', 'account', 'date_started', 'date_finished',
|
'id', 'asset', 'account', 'date_finished',
|
||||||
'timedelta', 'is_success', 'error', 'execution',
|
'status', 'is_success', 'error', 'execution',
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class GatherAccountAutomationSerializer(BaseAutomationSerializer):
|
|||||||
model = GatherAccountsAutomation
|
model = GatherAccountsAutomation
|
||||||
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
read_only_fields = BaseAutomationSerializer.Meta.read_only_fields
|
||||||
fields = BaseAutomationSerializer.Meta.fields \
|
fields = BaseAutomationSerializer.Meta.fields \
|
||||||
+ ['is_sync_account'] + read_only_fields
|
+ ['is_sync_account', 'recipients'] + read_only_fields
|
||||||
|
|
||||||
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _, gettext_noop
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
|
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||||
from common.utils import get_logger, get_object_or_none
|
from common.utils import get_logger, get_object_or_none
|
||||||
from orgs.utils import tmp_to_org, tmp_to_root_org
|
from orgs.utils import tmp_to_org, tmp_to_root_org
|
||||||
|
|
||||||
@@ -33,3 +34,39 @@ def execute_account_automation_task(pid, trigger, tp):
|
|||||||
return
|
return
|
||||||
with tmp_to_org(instance.org):
|
with tmp_to_org(instance.org):
|
||||||
instance.execute(trigger)
|
instance.execute(trigger)
|
||||||
|
|
||||||
|
|
||||||
|
def record_task_activity_callback(self, record_id, *args, **kwargs):
|
||||||
|
from accounts.models import ChangeSecretRecord
|
||||||
|
with tmp_to_root_org():
|
||||||
|
record = get_object_or_none(ChangeSecretRecord, id=record_id)
|
||||||
|
if not record:
|
||||||
|
return
|
||||||
|
resource_ids = [record.id]
|
||||||
|
org_id = record.execution.org_id
|
||||||
|
return resource_ids, org_id
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(
|
||||||
|
queue='ansible', verbose_name=_('Execute automation record'),
|
||||||
|
activity_callback=record_task_activity_callback
|
||||||
|
)
|
||||||
|
def execute_automation_record_task(record_id, tp):
|
||||||
|
from accounts.models import ChangeSecretRecord
|
||||||
|
with tmp_to_root_org():
|
||||||
|
instance = get_object_or_none(ChangeSecretRecord, pk=record_id)
|
||||||
|
if not instance:
|
||||||
|
logger.error("No automation record found: {}".format(record_id))
|
||||||
|
return
|
||||||
|
|
||||||
|
task_name = gettext_noop('Execute automation record')
|
||||||
|
task_snapshot = {
|
||||||
|
'secret': instance.new_secret,
|
||||||
|
'secret_type': instance.execution.snapshot.get('secret_type'),
|
||||||
|
'accounts': [str(instance.account_id)],
|
||||||
|
'assets': [str(instance.asset_id)],
|
||||||
|
'params': {},
|
||||||
|
'record_id': record_id,
|
||||||
|
}
|
||||||
|
with tmp_to_org(instance.execution.org_id):
|
||||||
|
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
|
||||||
|
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
|
||||||
|
<caption></caption>
|
||||||
|
<tr style="background-color: #f2f2f2;">
|
||||||
|
<th style="border: 1px solid #ddd; padding: 10px; font-weight: bold;">{% trans 'Asset' %}</th>
|
||||||
|
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Added account' %}</th>
|
||||||
|
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Deleted account' %}</th>
|
||||||
|
</tr>
|
||||||
|
{% for name, change in change_info.items %}
|
||||||
|
<tr style="{% cycle 'background-color: #ebf5ff;' 'background-color: #fff;' %}">
|
||||||
|
<td style="border: 1px solid #ddd; padding: 10px;">{{ name }}</td>
|
||||||
|
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.add_usernames }}</td>
|
||||||
|
<td style="border: 1px solid #ddd; padding: 10px;">{{ change.remove_usernames }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
@@ -49,15 +49,15 @@ def validate_password_for_ansible(password):
|
|||||||
# validate password contains left double curly bracket
|
# validate password contains left double curly bracket
|
||||||
# check password not contains `{{`
|
# check password not contains `{{`
|
||||||
# Ansible 推送的时候不支持
|
# Ansible 推送的时候不支持
|
||||||
if '{{' in password:
|
if '{{' in password or '}}' in password:
|
||||||
raise serializers.ValidationError(_('Password can not contains `{{` '))
|
raise serializers.ValidationError(_('Password can not contains `{{` or `}}`'))
|
||||||
if '{%' in password:
|
if '{%' in password or '%}' in password:
|
||||||
raise serializers.ValidationError(_('Password can not contains `{%` '))
|
raise serializers.ValidationError(_('Password can not contains `{%` or `%}`'))
|
||||||
# Ansible Windows 推送的时候不支持
|
# Ansible Windows 推送的时候不支持
|
||||||
if "'" in password:
|
# if "'" in password:
|
||||||
raise serializers.ValidationError(_("Password can not contains `'` "))
|
# raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||||
if '"' in password:
|
# if '"' in password:
|
||||||
raise serializers.ValidationError(_('Password can not contains `"` '))
|
# raise serializers.ValidationError(_('Password can not contains `"` '))
|
||||||
|
|
||||||
|
|
||||||
def validate_ssh_key(ssh_key, passphrase=None):
|
def validate_ssh_key(ssh_key, passphrase=None):
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ from .label import *
|
|||||||
from .mixin import *
|
from .mixin import *
|
||||||
from .node import *
|
from .node import *
|
||||||
from .platform import *
|
from .platform import *
|
||||||
|
from .protocol import *
|
||||||
from .tree import *
|
from .tree import *
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ __all__ = ['CategoryViewSet']
|
|||||||
class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
|
class CategoryViewSet(ListModelMixin, JMSGenericViewSet):
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': CategorySerializer,
|
'default': CategorySerializer,
|
||||||
'types': TypeSerializer
|
'types': TypeSerializer,
|
||||||
}
|
}
|
||||||
permission_classes = (IsValidUser,)
|
permission_classes = (IsValidUser,)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class SerializeToTreeNodeMixin:
|
|||||||
return AllTypes.get_types_values(exclude_custom=True)
|
return AllTypes.get_types_values(exclude_custom=True)
|
||||||
|
|
||||||
def get_icon(self, asset):
|
def get_icon(self, asset):
|
||||||
|
if asset.category == 'device':
|
||||||
|
return 'switch'
|
||||||
if asset.type in self.support_types:
|
if asset.type in self.support_types:
|
||||||
return asset.type
|
return asset.type
|
||||||
else:
|
else:
|
||||||
|
|||||||
15
apps/assets/api/protocol.py
Normal file
15
apps/assets/api/protocol.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from rest_framework.generics import ListAPIView
|
||||||
|
|
||||||
|
from assets import serializers
|
||||||
|
from assets.const import Protocol
|
||||||
|
from common.permissions import IsValidUser
|
||||||
|
|
||||||
|
__all__ = ['ProtocolListApi']
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolListApi(ListAPIView):
|
||||||
|
serializer_class = serializers.ProtocolSerializer
|
||||||
|
permission_classes = (IsValidUser,)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return list(Protocol.protocols())
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from collections import defaultdict
|
|
||||||
from hashlib import md5
|
|
||||||
from socket import gethostname
|
from socket import gethostname
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@@ -37,8 +36,6 @@ class BasePlaybookManager:
|
|||||||
}
|
}
|
||||||
# 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式
|
# 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式
|
||||||
# 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook
|
# 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook
|
||||||
# 避免一个 playbook 中包含太多的主机
|
|
||||||
self.method_hosts_mapper = defaultdict(list)
|
|
||||||
self.playbooks = []
|
self.playbooks = []
|
||||||
self.gateway_servers = dict()
|
self.gateway_servers = dict()
|
||||||
params = self.execution.snapshot.get('params')
|
params = self.execution.snapshot.get('params')
|
||||||
@@ -146,7 +143,7 @@ class BasePlaybookManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_private_key_path(secret, path_dir):
|
def generate_private_key_path(secret, path_dir):
|
||||||
key_name = '.' + md5(secret.encode('utf-8')).hexdigest()
|
key_name = '.' + hashlib.md5(secret.encode('utf-8')).hexdigest()
|
||||||
key_path = os.path.join(path_dir, key_name)
|
key_path = os.path.join(path_dir, key_name)
|
||||||
|
|
||||||
if not os.path.exists(key_path):
|
if not os.path.exists(key_path):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
@@ -10,10 +11,10 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
register: db_info
|
register: db_info
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: postgresql
|
- hosts: postgresql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Get info
|
- name: Get info
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
ansible_shell_type: sh
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
@@ -10,7 +11,7 @@
|
|||||||
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.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
become: "{{ custom_become | default(False) }}"
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: mongodb
|
- hosts: mongodb
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MongoDB connection
|
- name: Test MongoDB connection
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
- hosts: mysql
|
- hosts: mysql
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test MySQL connection
|
- name: Test MySQL connection
|
||||||
@@ -10,8 +11,8 @@
|
|||||||
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 }}"
|
||||||
check_hostname: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||||
filter: version
|
filter: version
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: oracle
|
- hosts: oracle
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test Oracle connection
|
- name: Test Oracle connection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: postgre
|
- hosts: postgre
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test PostgreSQL connection
|
- name: Test PostgreSQL connection
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
- hosts: sqlserver
|
- hosts: sqlserver
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_python_interpreter: /usr/local/bin/python
|
ansible_python_interpreter: /opt/py3/bin/python
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test SQLServer connection
|
- name: Test SQLServer connection
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class PingManager(BasePlaybookManager):
|
class PingManager(BasePlaybookManager):
|
||||||
|
ansible_account_prefer = ''
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.host_asset_and_account_mapper = {}
|
self.host_asset_and_account_mapper = {}
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ class BaseType(TextChoices):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _parse_protocols(cls, protocol, tp):
|
def _parse_protocols(cls, protocol, tp):
|
||||||
from .protocol import Protocol
|
from .protocol import Protocol
|
||||||
settings = Protocol.settings()
|
_settings = Protocol.settings()
|
||||||
choices = protocol.get('choices', [])
|
choices = protocol.get('choices', [])
|
||||||
if choices == '__self__':
|
if choices == '__self__':
|
||||||
choices = [tp]
|
choices = [tp]
|
||||||
|
|
||||||
protocols = []
|
protocols = []
|
||||||
for name in choices:
|
for name in choices:
|
||||||
protocol = {'name': name, **settings.get(name, {})}
|
protocol = {'name': name, **_settings.get(name, {})}
|
||||||
setting = protocol.pop('setting', {})
|
setting = protocol.pop('setting', {})
|
||||||
setting_values = {k: v.get('default', None) for k, v in setting.items()}
|
setting_values = {k: v.get('default', None) for k, v in setting.items()}
|
||||||
protocol['setting'] = setting_values
|
protocol['setting'] = setting_values
|
||||||
@@ -112,7 +112,7 @@ class BaseType(TextChoices):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_choices(cls):
|
def get_choices(cls):
|
||||||
if not settings.XPACK_LICENSE_IS_VALID:
|
if not settings.XPACK_ENABLED:
|
||||||
return [
|
return [
|
||||||
(tp.value, tp.label)
|
(tp.value, tp.label)
|
||||||
for tp in cls.get_community_types()
|
for tp in cls.get_community_types()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||||||
redis = 'redis', 'Redis'
|
redis = 'redis', 'Redis'
|
||||||
mongodb = 'mongodb', 'MongoDB'
|
mongodb = 'mongodb', 'MongoDB'
|
||||||
|
|
||||||
k8s = 'k8s', 'K8S'
|
k8s = 'k8s', 'K8s'
|
||||||
http = 'http', 'HTTP(s)'
|
http = 'http', 'HTTP(s)'
|
||||||
|
|
||||||
chatgpt = 'chatgpt', 'ChatGPT'
|
chatgpt = 'chatgpt', 'ChatGPT'
|
||||||
@@ -294,12 +294,22 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||||||
**cls.gpt_protocols(),
|
**cls.gpt_protocols(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@cached_method(ttl=600)
|
||||||
|
def protocols(cls):
|
||||||
|
protocols = []
|
||||||
|
xpack_enabled = settings.XPACK_ENABLED
|
||||||
|
for protocol, config in cls.settings().items():
|
||||||
|
if not xpack_enabled and config.get('xpack', False):
|
||||||
|
continue
|
||||||
|
protocols.append(protocol)
|
||||||
|
return protocols
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@cached_method(ttl=600)
|
@cached_method(ttl=600)
|
||||||
def xpack_protocols(cls):
|
def xpack_protocols(cls):
|
||||||
return [
|
return [
|
||||||
protocol
|
protocol for protocol, config in cls.settings().items()
|
||||||
for protocol, config in cls.settings().items()
|
|
||||||
if config.get('xpack', False)
|
if config.get('xpack', False)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='systemuser',
|
model_name='systemuser',
|
||||||
name='protocol',
|
name='protocol',
|
||||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||||
),
|
),
|
||||||
migrations.RunPython(migrate_admin_user_to_system_user),
|
migrations.RunPython(migrate_admin_user_to_system_user),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='systemuser',
|
model_name='systemuser',
|
||||||
name='protocol',
|
name='protocol',
|
||||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='systemuser',
|
model_name='systemuser',
|
||||||
name='protocol',
|
name='protocol',
|
||||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AccountBackupPlanExecution',
|
name='AccountBackupPlanExecution',
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='systemuser',
|
model_name='systemuser',
|
||||||
name='protocol',
|
name='protocol',
|
||||||
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'),
|
field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'), ('mongodb', 'MongoDB'), ('k8s', 'K8s')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -107,8 +107,9 @@ def create_app_nodes(apps, org_id):
|
|||||||
'key': next_key, 'value': name, 'parent_key': parent_key,
|
'key': next_key, 'value': name, 'parent_key': parent_key,
|
||||||
'full_value': full_value, 'org_id': org_id
|
'full_value': full_value, 'org_id': org_id
|
||||||
}
|
}
|
||||||
node, created = node_model.objects.get_or_create(
|
node, __ = node_model.objects.get_or_create(
|
||||||
defaults=defaults, value=name, org_id=org_id,
|
defaults=defaults, value=name, org_id=org_id,
|
||||||
|
parent_key=parent_key
|
||||||
)
|
)
|
||||||
node.parent = parent
|
node.parent = parent
|
||||||
return node
|
return node
|
||||||
|
|||||||
@@ -6,17 +6,22 @@ from django.db import migrations
|
|||||||
def add_db2_platform(apps, schema_editor):
|
def add_db2_platform(apps, schema_editor):
|
||||||
platform_cls = apps.get_model('assets', 'Platform')
|
platform_cls = apps.get_model('assets', 'Platform')
|
||||||
automation_cls = apps.get_model('assets', 'PlatformAutomation')
|
automation_cls = apps.get_model('assets', 'PlatformAutomation')
|
||||||
platform = platform_cls.objects.create(
|
platform, _ = platform_cls.objects.update_or_create(
|
||||||
name='DB2', internal=True, category='database', type='db2',
|
name='DB2', defaults={
|
||||||
domain_enabled=True, su_enabled=False, comment='DB2',
|
'name': 'DB2', 'category': 'database',
|
||||||
created_by='System', updated_by='System',
|
'internal': True, 'type': 'db2',
|
||||||
|
'domain_enabled': True, 'su_enabled': False,
|
||||||
|
'su_method': None, 'comment': 'DB2', 'created_by': 'System',
|
||||||
|
'updated_by': 'System', 'custom_fields': []
|
||||||
|
}
|
||||||
)
|
)
|
||||||
platform.protocols.create(name='db2', port=5000, primary=True, setting={})
|
platform.protocols.update_or_create(name='db2', defaults={
|
||||||
automation_cls.objects.create(ansible_enabled=False, platform=platform)
|
'name': 'db2', 'port': 50000, 'primary': True, 'setting': {}
|
||||||
|
})
|
||||||
|
automation_cls.objects.update_or_create(platform=platform, defaults={'ansible_enabled': False})
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('assets', '0123_device_automation_ansible_enabled'),
|
('assets', '0123_device_automation_ansible_enabled'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -131,8 +131,16 @@ class JSONFilterMixin:
|
|||||||
value = [value]
|
value = [value]
|
||||||
if name == 'nodes':
|
if name == 'nodes':
|
||||||
nodes = Node.objects.filter(id__in=value)
|
nodes = Node.objects.filter(id__in=value)
|
||||||
children = Node.get_nodes_all_children(nodes, with_self=True).values_list('id', flat=True)
|
if match == 'm2m_all':
|
||||||
return Q(nodes__in=children)
|
assets = Asset.objects.all()
|
||||||
|
for n in nodes:
|
||||||
|
children_pattern = Node.get_node_all_children_key_pattern(n.key)
|
||||||
|
assets = assets.filter(nodes__key__regex=children_pattern)
|
||||||
|
q = Q(id__in=assets.values_list('id', flat=True))
|
||||||
|
return q
|
||||||
|
else:
|
||||||
|
children = Node.get_nodes_all_children(nodes, with_self=True).values_list('id', flat=True)
|
||||||
|
return Q(nodes__in=children)
|
||||||
elif name == 'category':
|
elif name == 'category':
|
||||||
return Q(platform__category__in=value)
|
return Q(platform__category__in=value)
|
||||||
elif name == 'type':
|
elif name == 'type':
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from django.db.models import QuerySet
|
||||||
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
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class DatabaseSerializer(AssetSerializer):
|
|||||||
if not platform_id and self.instance:
|
if not platform_id and self.instance:
|
||||||
platform = self.instance.platform
|
platform = self.instance.platform
|
||||||
elif getattr(self, 'instance', None):
|
elif getattr(self, 'instance', None):
|
||||||
if isinstance(self.instance, list):
|
if isinstance(self.instance, (list, QuerySet)):
|
||||||
return
|
return
|
||||||
platform = self.instance.platform
|
platform = self.instance.platform
|
||||||
elif self.context.get('request'):
|
elif self.context.get('request'):
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
from rest_framework import serializers
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'TypeSerializer', 'CategorySerializer', 'ProtocolSerializer'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TypeSerializer(serializers.Serializer):
|
class TypeSerializer(serializers.Serializer):
|
||||||
@@ -13,3 +17,8 @@ class CategorySerializer(serializers.Serializer):
|
|||||||
label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label'))
|
label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label'))
|
||||||
value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value'))
|
value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value'))
|
||||||
types = TypeSerializer(many=True, required=False, label=_('Types'), read_only=True)
|
types = TypeSerializer(many=True, required=False, label=_('Types'), read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ProtocolSerializer(serializers.Serializer):
|
||||||
|
label = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Label'))
|
||||||
|
value = serializers.CharField(max_length=64, required=False, allow_blank=True, label=_('Value'))
|
||||||
|
|||||||
@@ -94,10 +94,17 @@ class PlatformProtocolSerializer(serializers.ModelSerializer):
|
|||||||
setting_fields = protocol_settings.get(protocol, {}).get('setting')
|
setting_fields = protocol_settings.get(protocol, {}).get('setting')
|
||||||
if not setting_fields:
|
if not setting_fields:
|
||||||
return default_field
|
return default_field
|
||||||
|
|
||||||
setting_fields = [{'name': k, **v} for k, v in setting_fields.items()]
|
setting_fields = [{'name': k, **v} for k, v in setting_fields.items()]
|
||||||
name = '{}ProtocolSettingSerializer'.format(protocol.capitalize())
|
name = '{}ProtocolSettingSerializer'.format(protocol.capitalize())
|
||||||
return create_serializer_class(name, setting_fields)()
|
return create_serializer_class(name, setting_fields)()
|
||||||
|
|
||||||
|
def validate(self, cleaned_data):
|
||||||
|
name = cleaned_data.get('name')
|
||||||
|
if name in ['winrm']:
|
||||||
|
cleaned_data['public'] = False
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
def to_file_representation(self, data):
|
def to_file_representation(self, data):
|
||||||
return '{name}/{port}'.format(**data)
|
return '{name}/{port}'.format(**data)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ router.register(r'protocol-settings', api.PlatformProtocolViewSet, 'protocol-set
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
|
# path('assets/<uuid:pk>/gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'),
|
||||||
|
path('protocols/', api.ProtocolListApi.as_view(), name='asset-protocol'),
|
||||||
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
|
path('assets/<uuid:pk>/tasks/', api.AssetTaskCreateApi.as_view(), name='asset-task-create'),
|
||||||
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
|
path('assets/tasks/', api.AssetsTaskCreateApi.as_view(), name='assets-task-create'),
|
||||||
path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),
|
path('assets/<uuid:pk>/perm-users/', api.AssetPermUserListApi.as_view(), name='asset-perm-user-list'),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from terminal.models import default_storage
|
|||||||
from users.models import User
|
from users.models import User
|
||||||
from .backends import TYPE_ENGINE_MAPPING
|
from .backends import TYPE_ENGINE_MAPPING
|
||||||
from .const import ActivityChoices
|
from .const import ActivityChoices
|
||||||
|
from .filters import UserSessionFilterSet
|
||||||
from .models import (
|
from .models import (
|
||||||
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
|
FTPLog, UserLoginLog, OperateLog, PasswordChangeLog,
|
||||||
ActivityLog, JobLog, UserSession
|
ActivityLog, JobLog, UserSession
|
||||||
@@ -255,7 +256,7 @@ class PasswordChangeLogViewSet(OrgReadonlyModelViewSet):
|
|||||||
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||||
http_method_names = ('get', 'post', 'head', 'options', 'trace')
|
http_method_names = ('get', 'post', 'head', 'options', 'trace')
|
||||||
serializer_class = UserSessionSerializer
|
serializer_class = UserSessionSerializer
|
||||||
filterset_fields = ['id', 'ip', 'city', 'type']
|
filterset_class = UserSessionFilterSet
|
||||||
search_fields = ['id', 'ip', 'city']
|
search_fields = ['id', 'ip', 'city']
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'offline': ['audits.offline_usersession']
|
'offline': ['audits.offline_usersession']
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
from django.db.models import F, Value
|
from django.core.cache import cache
|
||||||
from django.db.models.functions import Concat
|
from django_filters import rest_framework as drf_filters
|
||||||
from django_filters.rest_framework import CharFilter
|
|
||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
from rest_framework.compat import coreapi, coreschema
|
from rest_framework.compat import coreapi, coreschema
|
||||||
|
|
||||||
from orgs.utils import current_org
|
|
||||||
from common.drf.filters import BaseFilterSet
|
from common.drf.filters import BaseFilterSet
|
||||||
|
from notifications.ws import WS_SESSION_KEY
|
||||||
|
from orgs.utils import current_org
|
||||||
|
from .models import UserSession
|
||||||
|
|
||||||
__all__ = ['CurrentOrgMembersFilter']
|
__all__ = ['CurrentOrgMembersFilter']
|
||||||
|
|
||||||
@@ -34,21 +35,21 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
|||||||
queryset = queryset.filter(user__in=self._get_user_list())
|
queryset = queryset.filter(user__in=self._get_user_list())
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
#
|
|
||||||
# class CommandExecutionFilter(BaseFilterSet):
|
class UserSessionFilterSet(BaseFilterSet):
|
||||||
# hostname_ip = CharFilter(method='filter_hostname_ip')
|
is_active = drf_filters.BooleanFilter(method='filter_is_active')
|
||||||
#
|
|
||||||
# class Meta:
|
@staticmethod
|
||||||
# model = CommandExecution.hosts.through
|
def filter_is_active(queryset, name, is_active):
|
||||||
# fields = (
|
redis_client = cache.client.get_client()
|
||||||
# 'id', 'asset', 'commandexecution', 'hostname_ip'
|
members = redis_client.smembers(WS_SESSION_KEY)
|
||||||
# )
|
members = [member.decode('utf-8') for member in members]
|
||||||
#
|
if is_active:
|
||||||
# def filter_hostname_ip(self, queryset, name, value):
|
queryset = queryset.filter(key__in=members)
|
||||||
# queryset = queryset.annotate(
|
else:
|
||||||
# hostname_ip=Concat(
|
queryset = queryset.exclude(key__in=members)
|
||||||
# F('asset__hostname'), Value('('),
|
return queryset
|
||||||
# F('asset__address'), Value(')')
|
|
||||||
# )
|
class Meta:
|
||||||
# ).filter(hostname_ip__icontains=value)
|
model = UserSession
|
||||||
# return queryset
|
fields = ['id', 'ip', 'city', 'type']
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from datetime import timedelta
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import caches
|
from django.core.cache import caches, cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@@ -12,6 +12,7 @@ from django.utils.translation import gettext, gettext_lazy as _
|
|||||||
|
|
||||||
from common.db.encoder import ModelJSONFieldEncoder
|
from common.db.encoder import ModelJSONFieldEncoder
|
||||||
from common.utils import lazyproperty, i18n_trans
|
from common.utils import lazyproperty, i18n_trans
|
||||||
|
from notifications.ws import WS_SESSION_KEY
|
||||||
from ops.models import JobExecution
|
from ops.models import JobExecution
|
||||||
from orgs.mixins.models import OrgModelMixin, Organization
|
from orgs.mixins.models import OrgModelMixin, Organization
|
||||||
from orgs.utils import current_org
|
from orgs.utils import current_org
|
||||||
@@ -275,6 +276,11 @@ class UserSession(models.Model):
|
|||||||
def backend_display(self):
|
def backend_display(self):
|
||||||
return gettext(self.backend)
|
return gettext(self.backend)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_active(self):
|
||||||
|
redis_client = cache.client.get_client()
|
||||||
|
return redis_client.sismember(WS_SESSION_KEY, self.key)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_expired(self):
|
def date_expired(self):
|
||||||
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
@@ -287,7 +293,7 @@ class UserSession(models.Model):
|
|||||||
def get_keys():
|
def get_keys():
|
||||||
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
cache_key_prefix = session_store_cls.cache_key_prefix
|
cache_key_prefix = session_store_cls.cache_key_prefix
|
||||||
keys = caches[settings.SESSION_CACHE_ALIAS].keys('*')
|
keys = caches[settings.SESSION_CACHE_ALIAS].iter_keys('*')
|
||||||
return [k.replace(cache_key_prefix, '') for k in keys]
|
return [k.replace(cache_key_prefix, '') for k in keys]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ class JobLogSerializer(JobExecutionSerializer):
|
|||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
"id", "material", "time_cost", 'date_start',
|
"id", "material", "time_cost", 'date_start',
|
||||||
'date_finished', 'date_created',
|
'date_finished', 'date_created',
|
||||||
'is_finished', 'is_success', 'created_by',
|
'is_finished', 'is_success',
|
||||||
'task_id'
|
'task_id', 'creator_name'
|
||||||
]
|
]
|
||||||
fields = read_only_fields + []
|
fields = read_only_fields + []
|
||||||
|
extra_kwargs = {
|
||||||
|
"creator_name": {"label": _("Creator")},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FTPLogSerializer(serializers.ModelSerializer):
|
class FTPLogSerializer(serializers.ModelSerializer):
|
||||||
@@ -177,7 +180,7 @@ class UserSessionSerializer(serializers.ModelSerializer):
|
|||||||
fields_mini = ['id']
|
fields_mini = ['id']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'type', 'ip', 'city', 'user_agent', 'user', 'is_current_user_session',
|
'type', 'ip', 'city', 'user_agent', 'user', 'is_current_user_session',
|
||||||
'backend', 'backend_display', 'date_created', 'date_expired'
|
'backend', 'backend_display', 'is_active', 'date_created', 'date_expired'
|
||||||
]
|
]
|
||||||
fields = fields_small
|
fields = fields_small
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from acls.notifications import UserLoginReminderMsg
|
|||||||
from audits.models import UserLoginLog
|
from audits.models import UserLoginLog
|
||||||
from authentication.signals import post_auth_failed, post_auth_success
|
from authentication.signals import post_auth_failed, post_auth_success
|
||||||
from authentication.utils import check_different_city_login_if_need
|
from authentication.utils import check_different_city_login_if_need
|
||||||
from common.utils import get_request_ip, get_logger
|
from common.utils import get_request_ip_or_data, get_logger
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from ..const import LoginTypeChoices
|
from ..const import LoginTypeChoices
|
||||||
from ..models import UserSession
|
from ..models import UserSession
|
||||||
@@ -60,7 +60,7 @@ def get_login_backend(request):
|
|||||||
|
|
||||||
def generate_data(username, request, login_type=None):
|
def generate_data(username, request, login_type=None):
|
||||||
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
user_agent = request.META.get('HTTP_USER_AGENT', '')
|
||||||
login_ip = get_request_ip(request) or '0.0.0.0'
|
login_ip = get_request_ip_or_data(request) or '0.0.0.0'
|
||||||
|
|
||||||
if login_type is None and isinstance(request, Request):
|
if login_type is None and isinstance(request, Request):
|
||||||
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')
|
login_type = request.META.get('HTTP_X_JMS_LOGIN_TYPE', 'U')
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
|
|||||||
log_id, before_instance = get_instance_dict_from_cache(instance_id)
|
log_id, before_instance = get_instance_dict_from_cache(instance_id)
|
||||||
|
|
||||||
field_name = str(model._meta.verbose_name)
|
field_name = str(model._meta.verbose_name)
|
||||||
|
pk_set = pk_set or {}
|
||||||
objs = model.objects.filter(pk__in=pk_set)
|
objs = model.objects.filter(pk__in=pk_set)
|
||||||
objs_display = [str(o) for o in objs]
|
objs_display = [str(o) for o in objs]
|
||||||
action = M2M_ACTION[action]
|
action = M2M_ACTION[action]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from django.core.files.storage import default_storage
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.const.crontab import CRONTAB_AT_AM_TWO
|
||||||
from common.utils import get_log_keep_day, get_logger
|
from common.utils import get_log_keep_day, get_logger
|
||||||
from common.storage.ftp_file import FTPFileStorageHandler
|
from common.storage.ftp_file import FTPFileStorageHandler
|
||||||
from ops.celery.decorator import (
|
from ops.celery.decorator import (
|
||||||
@@ -20,7 +21,6 @@ from terminal.models import Session, Command
|
|||||||
from terminal.backends import server_replay_storage
|
from terminal.backends import server_replay_storage
|
||||||
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog
|
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ def clean_expired_session_period():
|
|||||||
|
|
||||||
|
|
||||||
@shared_task(verbose_name=_('Clean audits session task log'))
|
@shared_task(verbose_name=_('Clean audits session task log'))
|
||||||
@register_as_period_task(crontab='0 2 * * *')
|
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
|
||||||
@after_app_shutdown_clean_periodic
|
@after_app_shutdown_clean_periodic
|
||||||
def clean_audits_log_period():
|
def clean_audits_log_period():
|
||||||
print("Start clean audit session task log")
|
print("Start clean audit session task log")
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from perms.models import ActionChoices
|
|||||||
from terminal.connect_methods import NativeClient, ConnectMethodUtil
|
from terminal.connect_methods import NativeClient, ConnectMethodUtil
|
||||||
from terminal.models import EndpointRule, Endpoint
|
from terminal.models import EndpointRule, Endpoint
|
||||||
from users.const import FileNameConflictResolution
|
from users.const import FileNameConflictResolution
|
||||||
|
from users.const import RDPSmartSize
|
||||||
from users.models import Preference
|
from users.models import Preference
|
||||||
from ..models import ConnectionToken, date_expired_default
|
from ..models import ConnectionToken, date_expired_default
|
||||||
from ..serializers import (
|
from ..serializers import (
|
||||||
@@ -66,18 +67,12 @@ class RDPFileClientProtocolURLMixin:
|
|||||||
'autoreconnection enabled:i': '1',
|
'autoreconnection enabled:i': '1',
|
||||||
'bookmarktype:i': '3',
|
'bookmarktype:i': '3',
|
||||||
'use redirection server name:i': '0',
|
'use redirection server name:i': '0',
|
||||||
'smart sizing:i': '1',
|
|
||||||
}
|
}
|
||||||
# 设置多屏显示
|
# 设置多屏显示
|
||||||
multi_mon = is_true(self.request.query_params.get('multi_mon'))
|
multi_mon = is_true(self.request.query_params.get('multi_mon'))
|
||||||
if multi_mon:
|
if multi_mon:
|
||||||
rdp_options['use multimon:i'] = '1'
|
rdp_options['use multimon:i'] = '1'
|
||||||
|
|
||||||
# 设置多屏显示
|
|
||||||
multi_mon = is_true(self.request.query_params.get('multi_mon'))
|
|
||||||
if multi_mon:
|
|
||||||
rdp_options['use multimon:i'] = '1'
|
|
||||||
|
|
||||||
# 设置磁盘挂载
|
# 设置磁盘挂载
|
||||||
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
drives_redirect = is_true(self.request.query_params.get('drives_redirect'))
|
||||||
if drives_redirect:
|
if drives_redirect:
|
||||||
@@ -106,6 +101,7 @@ class RDPFileClientProtocolURLMixin:
|
|||||||
rdp_options['dynamic resolution:i'] = '0'
|
rdp_options['dynamic resolution:i'] = '0'
|
||||||
|
|
||||||
# 设置其他选项
|
# 设置其他选项
|
||||||
|
rdp_options['smart sizing:i'] = self.request.query_params.get('rdp_smart_size', RDPSmartSize.DISABLE)
|
||||||
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
|
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
|
||||||
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
|
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
|
||||||
|
|
||||||
@@ -159,7 +155,7 @@ class RDPFileClientProtocolURLMixin:
|
|||||||
|
|
||||||
account = token.account or token.input_username
|
account = token.account or token.input_username
|
||||||
datetime = timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H:%M:%S')
|
datetime = timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H:%M:%S')
|
||||||
name = account + '@' + str(asset) + '[' + datetime + ']'
|
name = account + '@' + asset.name + '[' + datetime + ']'
|
||||||
data = {
|
data = {
|
||||||
'version': 2,
|
'version': 2,
|
||||||
'id': str(token.id), # 兼容老的,未来几个版本删掉
|
'id': str(token.id), # 兼容老的,未来几个版本删掉
|
||||||
@@ -351,8 +347,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
|||||||
self._insert_connect_options(data, user)
|
self._insert_connect_options(data, user)
|
||||||
asset = data.get('asset')
|
asset = data.get('asset')
|
||||||
account_name = data.get('account')
|
account_name = data.get('account')
|
||||||
|
protocol = data.get('protocol')
|
||||||
self.input_username = self.get_input_username(data)
|
self.input_username = self.get_input_username(data)
|
||||||
_data = self._validate(user, asset, account_name)
|
_data = self._validate(user, asset, account_name, protocol)
|
||||||
data.update(_data)
|
data.update(_data)
|
||||||
return serializer
|
return serializer
|
||||||
|
|
||||||
@@ -360,12 +357,12 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
|||||||
user = token.user
|
user = token.user
|
||||||
asset = token.asset
|
asset = token.asset
|
||||||
account_name = token.account
|
account_name = token.account
|
||||||
_data = self._validate(user, asset, account_name)
|
_data = self._validate(user, asset, account_name, token.protocol)
|
||||||
for k, v in _data.items():
|
for k, v in _data.items():
|
||||||
setattr(token, k, v)
|
setattr(token, k, v)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def _validate(self, user, asset, account_name):
|
def _validate(self, user, asset, account_name, protocol):
|
||||||
data = dict()
|
data = dict()
|
||||||
data['org_id'] = asset.org_id
|
data['org_id'] = asset.org_id
|
||||||
data['user'] = user
|
data['user'] = user
|
||||||
@@ -374,7 +371,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
|||||||
if account_name == AliasAccount.ANON and asset.category not in ['web', 'custom']:
|
if account_name == AliasAccount.ANON and asset.category not in ['web', 'custom']:
|
||||||
raise ValidationError(_('Anonymous account is not supported for this asset'))
|
raise ValidationError(_('Anonymous account is not supported for this asset'))
|
||||||
|
|
||||||
account = self._validate_perm(user, asset, account_name)
|
account = self._validate_perm(user, asset, account_name, protocol)
|
||||||
if account.has_secret:
|
if account.has_secret:
|
||||||
data['input_secret'] = ''
|
data['input_secret'] = ''
|
||||||
|
|
||||||
@@ -387,9 +384,9 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _validate_perm(user, asset, account_name):
|
def _validate_perm(user, asset, account_name, protocol):
|
||||||
from perms.utils.account import PermAccountUtil
|
from perms.utils.asset_perm import PermAssetDetailUtil
|
||||||
account = PermAccountUtil().validate_permission(user, asset, account_name)
|
account = PermAssetDetailUtil(user, asset).validate_permission(account_name, protocol)
|
||||||
if not account or not account.actions:
|
if not account or not account.actions:
|
||||||
msg = _('Account not found')
|
msg = _('Account not found')
|
||||||
raise JMSException(code='perm_account_invalid', detail=msg)
|
raise JMSException(code='perm_account_invalid', detail=msg)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ from django.utils.translation import gettext as _
|
|||||||
from rest_framework import authentication, exceptions
|
from rest_framework import authentication, exceptions
|
||||||
|
|
||||||
from common.auth import signature
|
from common.auth import signature
|
||||||
from common.utils import get_object_or_none
|
from common.decorators import merge_delay_run
|
||||||
|
from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip
|
||||||
from ..models import AccessKey, PrivateToken
|
from ..models import AccessKey, PrivateToken
|
||||||
|
|
||||||
|
|
||||||
@@ -16,14 +17,24 @@ def date_more_than(d, seconds):
|
|||||||
return d is None or (timezone.now() - d).seconds > seconds
|
return d is None or (timezone.now() - d).seconds > seconds
|
||||||
|
|
||||||
|
|
||||||
def after_authenticate_update_date(user, token=None):
|
@merge_delay_run(ttl=60)
|
||||||
if date_more_than(user.date_api_key_last_used, 60):
|
def update_token_last_used(tokens=()):
|
||||||
|
for token in tokens:
|
||||||
|
token.date_last_used = timezone.now()
|
||||||
|
token.save(update_fields=['date_last_used'])
|
||||||
|
|
||||||
|
|
||||||
|
@merge_delay_run(ttl=60)
|
||||||
|
def update_user_last_used(users=()):
|
||||||
|
for user in users:
|
||||||
user.date_api_key_last_used = timezone.now()
|
user.date_api_key_last_used = timezone.now()
|
||||||
user.save(update_fields=['date_api_key_last_used'])
|
user.save(update_fields=['date_api_key_last_used'])
|
||||||
|
|
||||||
if token and hasattr(token, 'date_last_used') and date_more_than(token.date_last_used, 60):
|
|
||||||
token.date_last_used = timezone.now()
|
def after_authenticate_update_date(user, token=None):
|
||||||
token.save(update_fields=['date_last_used'])
|
update_user_last_used(users=(user,))
|
||||||
|
if token:
|
||||||
|
update_token_last_used(tokens=(token,))
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenAuthentication(authentication.BaseAuthentication):
|
class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||||
@@ -122,3 +133,14 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
|||||||
return user, secret
|
return user, secret
|
||||||
except (AccessKey.DoesNotExist, exceptions.ValidationError):
|
except (AccessKey.DoesNotExist, exceptions.ValidationError):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
def is_ip_allow(self, key_id, request):
|
||||||
|
try:
|
||||||
|
ak = AccessKey.objects.get(id=key_id)
|
||||||
|
ip_group = ak.ip_group
|
||||||
|
ip = get_request_ip_or_data(request)
|
||||||
|
if not contains_ip(ip, ip_group):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except (AccessKey.DoesNotExist, exceptions.ValidationError):
|
||||||
|
return False
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class OAuth2Backend(JMSModelBackend):
|
|||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
if token_method == 'post':
|
if token_method == 'post':
|
||||||
access_token_response = requests_func(access_token_url, headers=headers, json=query_dict)
|
access_token_response = requests_func(access_token_url, headers=headers, data=query_dict)
|
||||||
else:
|
else:
|
||||||
access_token_response = requests_func(access_token_url, headers=headers)
|
access_token_response = requests_func(access_token_url, headers=headers)
|
||||||
try:
|
try:
|
||||||
|
|||||||
18
apps/authentication/migrations/0024_accesskey_ip_group.py
Normal file
18
apps/authentication/migrations/0024_accesskey_ip_group.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-10-31 05:37
|
||||||
|
|
||||||
|
import authentication.models.access_key
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
('authentication', '0023_auto_20231010_1101'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accesskey',
|
||||||
|
name='ip_group',
|
||||||
|
field=models.JSONField(default=authentication.models.access_key.default_ip_group, verbose_name='IP group'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -12,9 +12,14 @@ def default_secret():
|
|||||||
return random_string(36)
|
return random_string(36)
|
||||||
|
|
||||||
|
|
||||||
|
def default_ip_group():
|
||||||
|
return ["*"]
|
||||||
|
|
||||||
|
|
||||||
class AccessKey(models.Model):
|
class AccessKey(models.Model):
|
||||||
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(verbose_name='AccessKeyID', primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36)
|
secret = models.CharField(verbose_name='AccessKeySecret', default=default_secret, max_length=36)
|
||||||
|
ip_group = models.JSONField(default=default_ip_group, verbose_name=_('IP group'))
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name='User',
|
||||||
on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys')
|
on_delete=common.db.models.CASCADE_SIGNAL_SKIP, related_name='access_keys')
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
is_active = models.BooleanField(default=True, verbose_name=_('Active'))
|
||||||
|
|||||||
@@ -97,10 +97,9 @@ class ConnectionToken(JMSOrgBaseModel):
|
|||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def permed_account(self):
|
def permed_account(self):
|
||||||
from perms.utils import PermAccountUtil
|
from perms.utils import PermAssetDetailUtil
|
||||||
permed_account = PermAccountUtil().validate_permission(
|
permed_account = PermAssetDetailUtil(self.user, self.asset) \
|
||||||
self.user, self.asset, self.account
|
.validate_permission(self.account, self.protocol)
|
||||||
)
|
|
||||||
return permed_account
|
return permed_account
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
@@ -115,6 +114,7 @@ class ConnectionToken(JMSOrgBaseModel):
|
|||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
error = _('Connection token inactive')
|
error = _('Connection token inactive')
|
||||||
raise PermissionDenied(error)
|
raise PermissionDenied(error)
|
||||||
|
|
||||||
if self.is_expired:
|
if self.is_expired:
|
||||||
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired))
|
error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired))
|
||||||
raise PermissionDenied(error)
|
raise PermissionDenied(error)
|
||||||
|
|||||||
@@ -55,4 +55,4 @@ class IsValidUserOrConnectionToken(IsValidUser):
|
|||||||
return False
|
return False
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
token = get_object_or_none(ConnectionToken, id=token_id)
|
token = get_object_or_none(ConnectionToken, id=token_id)
|
||||||
return token and token.is_valid
|
return token and token.is_valid()
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_su_from(account):
|
def get_su_from(account):
|
||||||
|
if not hasattr(account, 'asset'):
|
||||||
|
return {}
|
||||||
su_enabled = account.asset.platform.su_enabled
|
su_enabled = account.asset.platform.su_enabled
|
||||||
su_from = account.su_from
|
su_from = account.su_from
|
||||||
if not su_from or not su_enabled:
|
if not su_from or not su_enabled:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from django.utils import timezone
|
|||||||
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 acls.serializers.rules import ip_group_child_validator, ip_group_help_text
|
||||||
from common.utils import get_object_or_none, random_string
|
from common.utils import get_object_or_none, random_string
|
||||||
from users.models import User
|
from users.models import User
|
||||||
from users.serializers import UserProfileSerializer
|
from users.serializers import UserProfileSerializer
|
||||||
@@ -17,9 +18,14 @@ __all__ = [
|
|||||||
|
|
||||||
|
|
||||||
class AccessKeySerializer(serializers.ModelSerializer):
|
class AccessKeySerializer(serializers.ModelSerializer):
|
||||||
|
ip_group = serializers.ListField(
|
||||||
|
default=['*'], label=_('Access IP'), help_text=ip_group_help_text,
|
||||||
|
child=serializers.CharField(max_length=1024, validators=[ip_group_child_validator])
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = AccessKey
|
model = AccessKey
|
||||||
fields = ['id', 'is_active', 'date_created', 'date_last_used']
|
fields = ['id', 'is_active', 'date_created', 'date_last_used'] + ['ip_group']
|
||||||
read_only_fields = ['id', 'date_created', 'date_last_used']
|
read_only_fields = ['id', 'date_created', 'date_last_used']
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -258,7 +258,13 @@
|
|||||||
|
|
||||||
.mobile-logo {
|
.mobile-logo {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 20px 30px 0 30px;
|
padding: 0 30px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-image {
|
||||||
|
height: revert;
|
||||||
|
width: revert;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Reusing failure exceptions serves several purposes:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
FAILED = exceptions.AuthenticationFailed('Invalid signature.')
|
FAILED = exceptions.AuthenticationFailed('Invalid signature.')
|
||||||
|
IP_NOT_ALLOW = exceptions.AuthenticationFailed('Ip is not in access ip list.')
|
||||||
|
|
||||||
|
|
||||||
class SignatureAuthentication(authentication.BaseAuthentication):
|
class SignatureAuthentication(authentication.BaseAuthentication):
|
||||||
@@ -43,6 +44,9 @@ class SignatureAuthentication(authentication.BaseAuthentication):
|
|||||||
"""Returns a tuple (User, secret) or (None, None)."""
|
"""Returns a tuple (User, secret) or (None, None)."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def is_ip_allow(self, key_id, request):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def authenticate_header(self, request):
|
def authenticate_header(self, request):
|
||||||
"""
|
"""
|
||||||
DRF sends this for unauthenticated responses if we're the primary
|
DRF sends this for unauthenticated responses if we're the primary
|
||||||
@@ -50,7 +54,7 @@ class SignatureAuthentication(authentication.BaseAuthentication):
|
|||||||
"""
|
"""
|
||||||
h = " ".join(self.required_headers)
|
h = " ".join(self.required_headers)
|
||||||
return 'Signature realm="%s",headers="%s"' % (
|
return 'Signature realm="%s",headers="%s"' % (
|
||||||
self.www_authenticate_realm, h)
|
self.www_authenticate_realm, h)
|
||||||
|
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
"""
|
"""
|
||||||
@@ -78,15 +82,19 @@ class SignatureAuthentication(authentication.BaseAuthentication):
|
|||||||
if len({"keyid", "algorithm", "signature"} - set(fields.keys())) > 0:
|
if len({"keyid", "algorithm", "signature"} - set(fields.keys())) > 0:
|
||||||
raise FAILED
|
raise FAILED
|
||||||
|
|
||||||
|
key_id = fields["keyid"]
|
||||||
# Fetch the secret associated with the keyid
|
# Fetch the secret associated with the keyid
|
||||||
user, secret = self.fetch_user_data(
|
user, secret = self.fetch_user_data(
|
||||||
fields["keyid"],
|
key_id,
|
||||||
algorithm=fields["algorithm"]
|
algorithm=fields["algorithm"]
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (user and secret):
|
if not (user and secret):
|
||||||
raise FAILED
|
raise FAILED
|
||||||
|
|
||||||
|
if not self.is_ip_allow(key_id, request):
|
||||||
|
raise IP_NOT_ALLOW
|
||||||
|
|
||||||
# Gather all request headers and translate them as stated in the Django docs:
|
# Gather all request headers and translate them as stated in the Django docs:
|
||||||
# https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META
|
# https://docs.djangoproject.com/en/1.6/ref/request-response/#django.http.HttpRequest.META
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user