mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-07-18 17:01:31 +00:00
commit
03273b2ec4
57
Dockerfile
57
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM jumpserver/python:3.9-slim-buster as stage-build
|
FROM python:3.11-slim-bullseye as stage-build
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
@ -8,9 +8,8 @@ WORKDIR /opt/jumpserver
|
|||||||
ADD . .
|
ADD . .
|
||||||
RUN cd utils && bash -ixeu build.sh
|
RUN cd utils && bash -ixeu build.sh
|
||||||
|
|
||||||
FROM jumpserver/python:3.9-slim-buster
|
FROM python:3.11-slim-bullseye
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
MAINTAINER JumpServer Team <ibuler@qq.com>
|
|
||||||
|
|
||||||
ARG BUILD_DEPENDENCIES=" \
|
ARG BUILD_DEPENDENCIES=" \
|
||||||
g++ \
|
g++ \
|
||||||
@ -22,6 +21,7 @@ ARG DEPENDENCIES=" \
|
|||||||
libpq-dev \
|
libpq-dev \
|
||||||
libffi-dev \
|
libffi-dev \
|
||||||
libjpeg-dev \
|
libjpeg-dev \
|
||||||
|
libkrb5-dev \
|
||||||
libldap2-dev \
|
libldap2-dev \
|
||||||
libsasl2-dev \
|
libsasl2-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
@ -37,13 +37,11 @@ ARG TOOLS=" \
|
|||||||
default-libmysqlclient-dev \
|
default-libmysqlclient-dev \
|
||||||
default-mysql-client \
|
default-mysql-client \
|
||||||
locales \
|
locales \
|
||||||
|
nmap \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
procps \
|
|
||||||
sshpass \
|
sshpass \
|
||||||
telnet \
|
telnet \
|
||||||
unzip \
|
|
||||||
vim \
|
vim \
|
||||||
git \
|
|
||||||
wget"
|
wget"
|
||||||
|
|
||||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||||
@ -65,46 +63,17 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
|||||||
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
&& sed -i "s@# alias @alias @g" ~/.bashrc \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
ARG DOWNLOAD_URL=https://download.jumpserver.org
|
|
||||||
|
|
||||||
RUN set -ex \
|
|
||||||
&& \
|
|
||||||
if [ "${TARGETARCH}" == "amd64" ] || [ "${TARGETARCH}" == "arm64" ]; then \
|
|
||||||
mkdir -p /opt/oracle; \
|
|
||||||
cd /opt/oracle; \
|
|
||||||
wget ${DOWNLOAD_URL}/public/instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \
|
|
||||||
unzip instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \
|
|
||||||
echo "/opt/oracle/instantclient_19_10" > /etc/ld.so.conf.d/oracle-instantclient.conf; \
|
|
||||||
ldconfig; \
|
|
||||||
rm -f instantclient-basiclite-linux.${TARGETARCH}-19.10.0.0.0.zip; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
|
||||||
COPY ./requirements ./requirements
|
|
||||||
|
|
||||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
|
||||||
ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple
|
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
set -ex \
|
|
||||||
&& pip config set global.index-url ${PIP_MIRROR} \
|
|
||||||
&& pip install --upgrade pip \
|
|
||||||
&& pip install --upgrade setuptools wheel \
|
|
||||||
&& \
|
|
||||||
if [ "${TARGETARCH}" == "loong64" ]; then \
|
|
||||||
pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-38.0.4-cp39-cp39-linux_loongarch64.whl; \
|
|
||||||
pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp39-cp39-linux_loongarch64.whl; \
|
|
||||||
pip install https://download.jumpserver.org/pypi/simple/PyNaCl/PyNaCl-1.5.0-cp39-cp39-linux_loongarch64.whl; \
|
|
||||||
pip install https://download.jumpserver.org/pypi/simple/grpcio/grpcio-1.54.2-cp39-cp39-linux_loongarch64.whl; \
|
|
||||||
fi \
|
|
||||||
&& pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \
|
|
||||||
&& pip install -r requirements/requirements.txt
|
|
||||||
|
|
||||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||||
RUN echo > /opt/jumpserver/config.yml \
|
|
||||||
&& rm -rf /tmp/build
|
|
||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
WORKDIR /opt/jumpserver
|
||||||
|
|
||||||
|
ARG PIP_MIRROR=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
|
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
|
VOLUME /opt/jumpserver/logs
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
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 jumpserver/core:${VERSION}
|
||||||
|
|
||||||
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
COPY --from=build-xpack /opt/xpack /opt/jumpserver/apps/xpack
|
||||||
|
|
||||||
WORKDIR /opt/jumpserver
|
RUN --mount=type=cache,target=/root/.cache \
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
|
||||||
set -ex \
|
set -ex \
|
||||||
&& pip install -r requirements/requirements_xpack.txt
|
&& poetry install --only=xpack
|
@ -17,6 +17,7 @@
|
|||||||
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
9 年时间,倾情投入,用心做好一款开源堡垒机。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
------------------------------
|
||||||
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
JumpServer 是广受欢迎的开源堡垒机,是符合 4A 规范的专业运维安全审计系统。
|
||||||
|
|
||||||
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型的资产,包括:
|
||||||
@ -83,9 +84,7 @@ JumpServer 堡垒机帮助企业以更安全的方式管控和登录各种类型
|
|||||||
|
|
||||||
### 参与贡献
|
### 参与贡献
|
||||||
|
|
||||||
欢迎提交 PR 参与贡献。感谢以下贡献者,他们让 JumpServer 变的越来越好。
|
欢迎提交 PR 参与贡献。 参考 [CONTRIBUTING.md](https://github.com/jumpserver/jumpserver/blob/dev/CONTRIBUTING.md)
|
||||||
|
|
||||||
<a href="https://github.com/jumpserver/jumpserver/graphs/contributors"><img src="https://opencollective.com/jumpserver/contributors.svg?width=890&button=false" /></a>
|
|
||||||
|
|
||||||
## 组件项目
|
## 组件项目
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
from .account import *
|
from .account import *
|
||||||
from .task import *
|
from .task import *
|
||||||
from .template import *
|
from .template import *
|
||||||
|
from .virtual import *
|
||||||
|
@ -22,10 +22,11 @@ __all__ = [
|
|||||||
|
|
||||||
class AccountViewSet(OrgBulkModelViewSet):
|
class AccountViewSet(OrgBulkModelViewSet):
|
||||||
model = Account
|
model = Account
|
||||||
search_fields = ('username', 'name', 'asset__name', 'asset__address')
|
search_fields = ('username', 'name', 'asset__name', 'asset__address', 'comment')
|
||||||
filterset_class = AccountFilterSet
|
filterset_class = AccountFilterSet
|
||||||
serializer_classes = {
|
serializer_classes = {
|
||||||
'default': serializers.AccountSerializer,
|
'default': serializers.AccountSerializer,
|
||||||
|
'retrieve': serializers.AccountDetailSerializer,
|
||||||
}
|
}
|
||||||
rbac_perms = {
|
rbac_perms = {
|
||||||
'partial_update': ['accounts.change_account'],
|
'partial_update': ['accounts.change_account'],
|
||||||
@ -52,20 +53,21 @@ class AccountViewSet(OrgBulkModelViewSet):
|
|||||||
return Response(data=serializer.data)
|
return Response(data=serializer.data)
|
||||||
|
|
||||||
@action(
|
@action(
|
||||||
methods=['get'], detail=False, url_path='username-suggestions',
|
methods=['post'], detail=False, url_path='username-suggestions',
|
||||||
permission_classes=[IsValidUser]
|
permission_classes=[IsValidUser]
|
||||||
)
|
)
|
||||||
def username_suggestions(self, request, *args, **kwargs):
|
def username_suggestions(self, request, *args, **kwargs):
|
||||||
asset_ids = request.query_params.get('assets')
|
asset_ids = request.data.get('assets')
|
||||||
node_keys = request.query_params.get('keys')
|
node_ids = request.data.get('nodes')
|
||||||
username = request.query_params.get('username')
|
username = request.data.get('username')
|
||||||
|
|
||||||
assets = Asset.objects.all()
|
assets = Asset.objects.all()
|
||||||
if asset_ids:
|
if asset_ids:
|
||||||
assets = assets.filter(id__in=asset_ids.split(','))
|
assets = assets.filter(id__in=asset_ids)
|
||||||
if node_keys:
|
if node_ids:
|
||||||
patten = Node.get_node_all_children_key_pattern(node_keys.split(','))
|
nodes = Node.objects.filter(id__in=node_ids)
|
||||||
assets = assets.filter(nodes__key__regex=patten)
|
node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True)
|
||||||
|
assets = assets.filter(id__in=set(list(asset_ids) + list(node_asset_ids)))
|
||||||
|
|
||||||
accounts = Account.objects.filter(asset__in=assets)
|
accounts = Account.objects.filter(asset__in=assets)
|
||||||
if username:
|
if username:
|
||||||
@ -132,11 +134,13 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, RecordViewLogMixin, List
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
account = self.get_object()
|
account = self.get_object()
|
||||||
histories = account.history.all()
|
histories = account.history.all()
|
||||||
last_history = account.history.first()
|
latest_history = account.history.first()
|
||||||
if not last_history:
|
if not latest_history:
|
||||||
|
return histories
|
||||||
|
if account.secret != latest_history.secret:
|
||||||
|
return histories
|
||||||
|
if account.secret_type != latest_history.secret_type:
|
||||||
|
return histories
|
||||||
|
histories = histories.exclude(history_id=latest_history.history_id)
|
||||||
return histories
|
return histories
|
||||||
|
|
||||||
if account.secret == last_history.secret \
|
|
||||||
and account.secret_type == last_history.secret_type:
|
|
||||||
histories = histories.exclude(history_id=last_history.history_id)
|
|
||||||
return histories
|
|
||||||
|
20
apps/accounts/api/account/virtual.py
Normal file
20
apps/accounts/api/account/virtual.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
from accounts.models import VirtualAccount
|
||||||
|
from accounts.serializers import VirtualAccountSerializer
|
||||||
|
from common.utils import is_uuid
|
||||||
|
from orgs.mixins.api import OrgBulkModelViewSet
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualAccountViewSet(OrgBulkModelViewSet):
|
||||||
|
serializer_class = VirtualAccountSerializer
|
||||||
|
search_fields = ('alias',)
|
||||||
|
filterset_fields = ('alias',)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return VirtualAccount.get_or_init_queryset()
|
||||||
|
|
||||||
|
def get_object(self, ):
|
||||||
|
pk = self.kwargs.get('pk')
|
||||||
|
kwargs = {'pk': pk} if is_uuid(pk) else {'alias': pk}
|
||||||
|
return get_object_or_404(VirtualAccount, **kwargs)
|
@ -26,8 +26,8 @@ class AccountBackupPlanViewSet(OrgBulkModelViewSet):
|
|||||||
|
|
||||||
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
serializer_class = serializers.AccountBackupPlanExecutionSerializer
|
||||||
search_fields = ('trigger',)
|
search_fields = ('trigger', 'plan__name')
|
||||||
filterset_fields = ('trigger', 'plan_id')
|
filterset_fields = ('trigger', 'plan_id', 'plan__name')
|
||||||
http_method_names = ['get', 'post', 'options']
|
http_method_names = ['get', 'post', 'options']
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import status, mixins, viewsets
|
from rest_framework import status, mixins, viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
@ -95,8 +95,8 @@ class AutomationExecutionViewSet(
|
|||||||
mixins.CreateModelMixin, mixins.ListModelMixin,
|
mixins.CreateModelMixin, mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
search_fields = ('trigger',)
|
search_fields = ('trigger', 'automation__name')
|
||||||
filterset_fields = ('trigger', 'automation_id')
|
filterset_fields = ('trigger', 'automation_id', 'automation__name')
|
||||||
serializer_class = serializers.AutomationExecutionSerializer
|
serializer_class = serializers.AutomationExecutionSerializer
|
||||||
|
|
||||||
tp: str
|
tp: str
|
||||||
|
@ -6,6 +6,5 @@ class AccountsConfig(AppConfig):
|
|||||||
name = 'accounts'
|
name = 'accounts'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signal_handlers
|
from . import signal_handlers # noqa
|
||||||
from . import tasks
|
from . import tasks # noqa
|
||||||
__all__ = signal_handlers
|
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from openpyxl import Workbook
|
|
||||||
from collections import defaultdict, OrderedDict
|
from collections import defaultdict, OrderedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import F
|
from openpyxl import Workbook
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import Account
|
|
||||||
from assets.const import AllTypes
|
|
||||||
from accounts.serializers import AccountSecretSerializer
|
|
||||||
from accounts.notifications import AccountBackupExecutionTaskMsg
|
from accounts.notifications import AccountBackupExecutionTaskMsg
|
||||||
from users.models import User
|
from accounts.serializers import AccountSecretSerializer
|
||||||
from common.utils import get_logger
|
from assets.const import AllTypes
|
||||||
from common.utils.timezone import local_now_display
|
|
||||||
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
|
||||||
logger = get_logger(__file__)
|
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')
|
||||||
|
|
||||||
@ -76,8 +71,22 @@ class AssetAccountHandler(BaseAccountHandler):
|
|||||||
)
|
)
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def handler_secret(data, section):
|
||||||
|
for account_data in data:
|
||||||
|
secret = account_data.get('secret')
|
||||||
|
if not secret:
|
||||||
|
continue
|
||||||
|
length = len(secret)
|
||||||
|
index = length // 2
|
||||||
|
if section == "front":
|
||||||
|
secret = secret[:index] + '*' * (length - index)
|
||||||
|
elif section == "back":
|
||||||
|
secret = '*' * (length - index) + secret[index:]
|
||||||
|
account_data['secret'] = secret
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_data_map(cls, accounts):
|
def create_data_map(cls, accounts, section):
|
||||||
data_map = defaultdict(list)
|
data_map = defaultdict(list)
|
||||||
|
|
||||||
if not accounts.exists():
|
if not accounts.exists():
|
||||||
@ -97,9 +106,10 @@ class AssetAccountHandler(BaseAccountHandler):
|
|||||||
for tp, _accounts in account_type_map.items():
|
for tp, _accounts in account_type_map.items():
|
||||||
sheet_name = type_dict.get(tp, tp)
|
sheet_name = type_dict.get(tp, tp)
|
||||||
data = AccountSecretSerializer(_accounts, many=True).data
|
data = AccountSecretSerializer(_accounts, many=True).data
|
||||||
|
cls.handler_secret(data, section)
|
||||||
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
data_map.update(cls.add_rows(data, header_fields, sheet_name))
|
||||||
|
|
||||||
logger.info('\n\033[33m- 共备份 {} 条账号\033[0m'.format(accounts.count()))
|
print('\n\033[33m- 共备份 {} 条账号\033[0m'.format(accounts.count()))
|
||||||
return data_map
|
return data_map
|
||||||
|
|
||||||
|
|
||||||
@ -109,8 +119,8 @@ class AccountBackupHandler:
|
|||||||
self.plan_name = self.execution.plan.name
|
self.plan_name = self.execution.plan.name
|
||||||
self.is_frozen = False # 任务状态冻结标志
|
self.is_frozen = False # 任务状态冻结标志
|
||||||
|
|
||||||
def create_excel(self):
|
def create_excel(self, section='complete'):
|
||||||
logger.info(
|
print(
|
||||||
'\n'
|
'\n'
|
||||||
'\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
|
'\033[32m>>> 正在生成资产或应用相关备份信息文件\033[0m'
|
||||||
''
|
''
|
||||||
@ -119,7 +129,7 @@ class AccountBackupHandler:
|
|||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
files = []
|
files = []
|
||||||
accounts = self.execution.backup_accounts
|
accounts = self.execution.backup_accounts
|
||||||
data_map = AssetAccountHandler.create_data_map(accounts)
|
data_map = AssetAccountHandler.create_data_map(accounts, section)
|
||||||
if not data_map:
|
if not data_map:
|
||||||
return files
|
return files
|
||||||
|
|
||||||
@ -133,14 +143,14 @@ 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)
|
||||||
logger.info('步骤完成: 用时 {}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):
|
||||||
if not files:
|
if not files:
|
||||||
return
|
return
|
||||||
recipients = User.objects.filter(id__in=list(recipients))
|
recipients = User.objects.filter(id__in=list(recipients))
|
||||||
logger.info(
|
print(
|
||||||
'\n'
|
'\n'
|
||||||
'\033[32m>>> 发送备份邮件\033[0m'
|
'\033[32m>>> 发送备份邮件\033[0m'
|
||||||
''
|
''
|
||||||
@ -155,7 +165,7 @@ class AccountBackupHandler:
|
|||||||
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)
|
||||||
logger.info('邮件已发送至{}({})'.format(user, user.email))
|
print('邮件已发送至{}({})'.format(user, user.email))
|
||||||
for file in files:
|
for file in files:
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
|
|
||||||
@ -163,33 +173,42 @@ class AccountBackupHandler:
|
|||||||
self.execution.reason = reason[:1024]
|
self.execution.reason = reason[:1024]
|
||||||
self.execution.is_success = is_success
|
self.execution.is_success = is_success
|
||||||
self.execution.save()
|
self.execution.save()
|
||||||
logger.info('已完成对任务状态的更新')
|
print('已完成对任务状态的更新')
|
||||||
|
|
||||||
def step_finished(self, is_success):
|
@staticmethod
|
||||||
|
def step_finished(is_success):
|
||||||
if is_success:
|
if is_success:
|
||||||
logger.info('任务执行成功')
|
print('任务执行成功')
|
||||||
else:
|
else:
|
||||||
logger.error('任务执行失败')
|
print('任务执行失败')
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
is_success = False
|
is_success = False
|
||||||
error = '-'
|
error = '-'
|
||||||
try:
|
try:
|
||||||
recipients = self.execution.plan_snapshot.get('recipients')
|
recipients_part_one = self.execution.snapshot.get('recipients_part_one', [])
|
||||||
if not recipients:
|
recipients_part_two = self.execution.snapshot.get('recipients_part_two', [])
|
||||||
logger.info(
|
if not recipients_part_one and not recipients_part_two:
|
||||||
|
print(
|
||||||
'\n'
|
'\n'
|
||||||
'\033[32m>>> 该备份任务未分配收件人\033[0m'
|
'\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:
|
else:
|
||||||
|
recipients = recipients_part_one or recipients_part_two
|
||||||
files = self.create_excel()
|
files = self.create_excel()
|
||||||
self.send_backup_mail(files, recipients)
|
self.send_backup_mail(files, recipients)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.is_frozen = True
|
self.is_frozen = True
|
||||||
logger.error('任务执行被异常中断')
|
print('任务执行被异常中断')
|
||||||
logger.info('下面打印发生异常的 Traceback 信息 : ')
|
print('下面打印发生异常的 Traceback 信息 : ')
|
||||||
logger.error(e, exc_info=True)
|
print(e)
|
||||||
error = str(e)
|
error = str(e)
|
||||||
else:
|
else:
|
||||||
is_success = True
|
is_success = True
|
||||||
@ -199,15 +218,15 @@ class AccountBackupHandler:
|
|||||||
self.step_finished(is_success)
|
self.step_finished(is_success)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
logger.info('任务开始: {}'.format(local_now_display()))
|
print('任务开始: {}'.format(local_now_display()))
|
||||||
time_start = time.time()
|
time_start = time.time()
|
||||||
try:
|
try:
|
||||||
self._run()
|
self._run()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('任务运行出现异常')
|
print('任务运行出现异常')
|
||||||
logger.error('下面显示异常 Traceback 信息: ')
|
print('下面显示异常 Traceback 信息: ')
|
||||||
logger.error(e, exc_info=True)
|
print(e)
|
||||||
finally:
|
finally:
|
||||||
logger.info('\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)
|
||||||
logger.info('用时: {}'.format(timedelta))
|
print('用时: {}'.format(timedelta))
|
||||||
|
@ -4,13 +4,9 @@ import time
|
|||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from common.utils import get_logger
|
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_display
|
||||||
|
|
||||||
from .handlers import AccountBackupHandler
|
from .handlers import AccountBackupHandler
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AccountBackupManager:
|
class AccountBackupManager:
|
||||||
def __init__(self, execution):
|
def __init__(self, execution):
|
||||||
@ -23,7 +19,7 @@ class AccountBackupManager:
|
|||||||
|
|
||||||
def do_run(self):
|
def do_run(self):
|
||||||
execution = self.execution
|
execution = self.execution
|
||||||
logger.info('\n\033[33m# 账号备份计划正在执行\033[0m')
|
print('\n\033[33m# 账号备份计划正在执行\033[0m')
|
||||||
handler = AccountBackupHandler(execution)
|
handler = AccountBackupHandler(execution)
|
||||||
handler.run()
|
handler.run()
|
||||||
|
|
||||||
@ -35,10 +31,10 @@ class AccountBackupManager:
|
|||||||
self.time_end = time.time()
|
self.time_end = time.time()
|
||||||
self.date_end = timezone.now()
|
self.date_end = timezone.now()
|
||||||
|
|
||||||
logger.info('\n\n' + '-' * 80)
|
print('\n\n' + '-' * 80)
|
||||||
logger.info('计划执行结束 {}\n'.format(local_now_display()))
|
print('计划执行结束 {}\n'.format(local_now_display()))
|
||||||
self.timedelta = self.time_end - self.time_start
|
self.timedelta = self.time_end - self.time_start
|
||||||
logger.info('用时: {}s'.format(self.timedelta))
|
print('用时: {}s'.format(self.timedelta))
|
||||||
self.execution.timedelta = self.timedelta
|
self.execution.timedelta = self.timedelta
|
||||||
self.execution.save()
|
self.execution.save()
|
||||||
|
|
||||||
|
@ -2,9 +2,10 @@
|
|||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
ansible_become: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- 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: "{{ jms_asset.port }}"
|
||||||
@ -12,9 +13,14 @@
|
|||||||
login_password: "{{ jms_account.secret }}"
|
login_password: "{{ jms_account.secret }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
become_method: "{{ custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
register: ping_info
|
register: ping_info
|
||||||
|
|
||||||
- name: Change asset password
|
- 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 }}"
|
||||||
@ -22,6 +28,11 @@
|
|||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
become_method: "{{ custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret }}"
|
password: "{{ account.secret }}"
|
||||||
commands: "{{ params.commands }}"
|
commands: "{{ params.commands }}"
|
||||||
@ -30,9 +41,10 @@
|
|||||||
when: ping_info is succeeded
|
when: ping_info is succeeded
|
||||||
register: change_info
|
register: change_info
|
||||||
|
|
||||||
- name: Verify password
|
- 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: "{{ jms_asset.port }}"
|
||||||
|
become: false
|
||||||
|
@ -1,10 +1,41 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Change password
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
shell: "{{ params.shell }}"
|
||||||
|
home: "{{ params.home | default('/home/' + account.username, true) }}"
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
expires: -1
|
||||||
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} group"
|
||||||
|
ansible.builtin.group:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user to group"
|
||||||
|
ansible.builtin.user:
|
||||||
|
name: "{{ account.username }}"
|
||||||
|
groups: "{{ params.groups }}"
|
||||||
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('des') }}"
|
password: "{{ account.secret | password_hash('des') }}"
|
||||||
@ -12,11 +43,6 @@
|
|||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
|
||||||
- name: create user If it already exists, no operation will be performed
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ account.username }}"
|
|
||||||
when: account.secret_type == "ssh_key"
|
|
||||||
|
|
||||||
- name: remove jumpserver ssh key
|
- name: remove jumpserver ssh key
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: "{{ ssh_params.dest }}"
|
dest: "{{ ssh_params.dest }}"
|
||||||
@ -26,30 +52,45 @@
|
|||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Change SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
dest: /etc/sudoers
|
||||||
|
state: present
|
||||||
|
regexp: "^{{ account.username }} ALL="
|
||||||
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
|
validate: visudo -cf %s
|
||||||
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Check user
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
shell: "{{ params.shell }}"
|
shell: "{{ params.shell }}"
|
||||||
@ -12,19 +19,23 @@
|
|||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: "Add {{ account.username }} group"
|
- name: "Add {{ account.username }} group"
|
||||||
ansible.builtin.group:
|
ansible.builtin.group:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: Add user groups
|
- name: "Add {{ account.username }} user to group"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
when: params.groups
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
- name: Change password
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('sha512') }}"
|
password: "{{ account.secret | password_hash('sha512') }}"
|
||||||
@ -32,11 +43,6 @@
|
|||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
|
||||||
- name: create user If it already exists, no operation will be performed
|
|
||||||
ansible.builtin.user:
|
|
||||||
name: "{{ account.username }}"
|
|
||||||
when: account.secret_type == "ssh_key"
|
|
||||||
|
|
||||||
- name: remove jumpserver ssh key
|
- name: remove jumpserver ssh key
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: "{{ ssh_params.dest }}"
|
dest: "{{ ssh_params.dest }}"
|
||||||
@ -46,14 +52,14 @@
|
|||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Change SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
- name: Set sudo setting
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: /etc/sudoers
|
dest: /etc/sudoers
|
||||||
state: present
|
state: present
|
||||||
@ -61,25 +67,30 @@
|
|||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
|
- user_info.failed
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Push user
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
shell: "{{ params.shell }}"
|
shell: "{{ params.shell }}"
|
||||||
@ -12,22 +19,26 @@
|
|||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: "Add {{ account.username }} group"
|
- name: "Add {{ account.username }} group"
|
||||||
ansible.builtin.group:
|
ansible.builtin.group:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: Add user groups
|
- name: "Add {{ account.username }} user to group"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
when: params.groups
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
- name: Push user password
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('sha512') }}"
|
password: "{{ account.secret | password_hash('des') }}"
|
||||||
update_password: always
|
update_password: always
|
||||||
ignore_errors: true
|
ignore_errors: true
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
@ -41,14 +52,14 @@
|
|||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Push SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
- name: Set sudo setting
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: /etc/sudoers
|
dest: /etc/sudoers
|
||||||
state: present
|
state: present
|
||||||
@ -56,25 +67,31 @@
|
|||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
|
- user_info.failed
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
- hosts: demo
|
- hosts: demo
|
||||||
gather_facts: no
|
gather_facts: no
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test privileged account
|
- name: "Test privileged {{ jms_account.username }} account"
|
||||||
ansible.builtin.ping:
|
ansible.builtin.ping:
|
||||||
|
|
||||||
- name: Push user
|
- name: "Check if {{ account.username }} user exists"
|
||||||
|
getent:
|
||||||
|
database: passwd
|
||||||
|
key: "{{ account.username }}"
|
||||||
|
register: user_info
|
||||||
|
ignore_errors: yes # 忽略错误,如果用户不存在时不会导致playbook失败
|
||||||
|
|
||||||
|
- name: "Add {{ account.username }} user"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
shell: "{{ params.shell }}"
|
shell: "{{ params.shell }}"
|
||||||
@ -12,19 +19,23 @@
|
|||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
expires: -1
|
expires: -1
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: "Add {{ account.username }} group"
|
- name: "Add {{ account.username }} group"
|
||||||
ansible.builtin.group:
|
ansible.builtin.group:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
state: present
|
state: present
|
||||||
|
when: user_info.failed
|
||||||
|
|
||||||
- name: Add user groups
|
- name: "Add {{ account.username }} user to group"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
groups: "{{ params.groups }}"
|
groups: "{{ params.groups }}"
|
||||||
when: params.groups
|
when:
|
||||||
|
- user_info.failed
|
||||||
|
- params.groups
|
||||||
|
|
||||||
- name: Push user password
|
- name: "Change {{ account.username }} password"
|
||||||
ansible.builtin.user:
|
ansible.builtin.user:
|
||||||
name: "{{ account.username }}"
|
name: "{{ account.username }}"
|
||||||
password: "{{ account.secret | password_hash('sha512') }}"
|
password: "{{ account.secret | password_hash('sha512') }}"
|
||||||
@ -41,14 +52,14 @@
|
|||||||
- account.secret_type == "ssh_key"
|
- account.secret_type == "ssh_key"
|
||||||
- ssh_params.strategy == "set_jms"
|
- ssh_params.strategy == "set_jms"
|
||||||
|
|
||||||
- name: Push SSH key
|
- name: "Change {{ account.username }} SSH key"
|
||||||
ansible.builtin.authorized_key:
|
ansible.builtin.authorized_key:
|
||||||
user: "{{ account.username }}"
|
user: "{{ account.username }}"
|
||||||
key: "{{ account.secret }}"
|
key: "{{ account.secret }}"
|
||||||
exclusive: "{{ ssh_params.exclusive }}"
|
exclusive: "{{ ssh_params.exclusive }}"
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
|
||||||
- name: Set sudo setting
|
- name: "Set {{ account.username }} sudo setting"
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
dest: /etc/sudoers
|
dest: /etc/sudoers
|
||||||
state: present
|
state: present
|
||||||
@ -56,25 +67,31 @@
|
|||||||
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
line: "{{ account.username + ' ALL=(ALL) NOPASSWD: ' + params.sudo }}"
|
||||||
validate: visudo -cf %s
|
validate: visudo -cf %s
|
||||||
when:
|
when:
|
||||||
|
- user_info.failed
|
||||||
- params.sudo
|
- params.sudo
|
||||||
|
|
||||||
- name: Refresh connection
|
- name: Refresh connection
|
||||||
ansible.builtin.meta: reset_connection
|
ansible.builtin.meta: reset_connection
|
||||||
|
|
||||||
- name: Verify password
|
- name: "Verify {{ account.username }} password (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_user: "{{ account.username }}"
|
||||||
vars:
|
login_password: "{{ account.secret }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_host: "{{ jms_asset.address }}"
|
||||||
ansible_password: "{{ account.secret }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "password"
|
when: account.secret_type == "password"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Verify SSH key
|
- name: "Verify {{ account.username }} SSH KEY (paramiko)"
|
||||||
ansible.builtin.ping:
|
ssh_ping:
|
||||||
become: no
|
login_host: "{{ jms_asset.address }}"
|
||||||
vars:
|
login_port: "{{ jms_asset.port }}"
|
||||||
ansible_user: "{{ account.username }}"
|
login_user: "{{ account.username }}"
|
||||||
ansible_ssh_private_key_file: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
ansible_become: no
|
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
|
||||||
|
become: false
|
||||||
when: account.secret_type == "ssh_key"
|
when: account.secret_type == "ssh_key"
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
ansible_become: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify account (paramiko)
|
- name: Verify account (paramiko)
|
||||||
@ -12,3 +13,8 @@
|
|||||||
login_password: "{{ account.secret }}"
|
login_password: "{{ account.secret }}"
|
||||||
login_secret_type: "{{ account.secret_type }}"
|
login_secret_type: "{{ account.secret_type }}"
|
||||||
login_private_key_path: "{{ account.private_key_path }}"
|
login_private_key_path: "{{ account.private_key_path }}"
|
||||||
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
become_method: "{{ custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
|
41
apps/accounts/backends/__init__.py
Normal file
41
apps/accounts/backends/__init__.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.utils.functional import LazyObject
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
from ..const import VaultTypeChoices
|
||||||
|
|
||||||
|
__all__ = ['vault_client', 'get_vault_client']
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vault_client(raise_exception=False, **kwargs):
|
||||||
|
enabled = kwargs.get('VAULT_ENABLED')
|
||||||
|
tp = 'hcp' if enabled else 'local'
|
||||||
|
try:
|
||||||
|
module_path = f'apps.accounts.backends.{tp}.main'
|
||||||
|
client = import_module(module_path).Vault(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'Init vault client failed: {e}')
|
||||||
|
if raise_exception:
|
||||||
|
raise
|
||||||
|
tp = VaultTypeChoices.local
|
||||||
|
module_path = f'apps.accounts.backends.{tp}.main'
|
||||||
|
client = import_module(module_path).Vault(**kwargs)
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
class VaultClient(LazyObject):
|
||||||
|
|
||||||
|
def _setup(self):
|
||||||
|
from jumpserver import settings as js_settings
|
||||||
|
from django.conf import settings
|
||||||
|
vault_config_names = [k for k in js_settings.__dict__.keys() if k.startswith('VAULT_')]
|
||||||
|
vault_configs = {name: getattr(settings, name, None) for name in vault_config_names}
|
||||||
|
self._wrapped = get_vault_client(**vault_configs)
|
||||||
|
|
||||||
|
|
||||||
|
""" 为了安全, 页面修改配置, 重启服务后才会重新初始化 vault_client """
|
||||||
|
vault_client = VaultClient()
|
74
apps/accounts/backends/base.py
Normal file
74
apps/accounts/backends/base.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
|
__all__ = ['BaseVault']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseVault(ABC):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.enabled = kwargs.get('VAULT_ENABLED')
|
||||||
|
|
||||||
|
def get(self, instance):
|
||||||
|
""" 返回 secret 值 """
|
||||||
|
return self._get(instance)
|
||||||
|
|
||||||
|
def create(self, instance):
|
||||||
|
if not instance.secret_has_save_to_vault:
|
||||||
|
self._create(instance)
|
||||||
|
self._clean_db_secret(instance)
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
if instance.is_sync_metadata:
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
def update(self, instance):
|
||||||
|
if not instance.secret_has_save_to_vault:
|
||||||
|
self._update(instance)
|
||||||
|
self._clean_db_secret(instance)
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
if instance.is_sync_metadata:
|
||||||
|
self.save_metadata(instance)
|
||||||
|
|
||||||
|
def delete(self, instance):
|
||||||
|
self._delete(instance)
|
||||||
|
|
||||||
|
def save_metadata(self, instance):
|
||||||
|
metadata = model_to_dict(instance, fields=[
|
||||||
|
'name', 'username', 'secret_type',
|
||||||
|
'connectivity', 'su_from', 'privileged'
|
||||||
|
])
|
||||||
|
metadata = {k: str(v)[:500] for k, v in metadata.items() if v}
|
||||||
|
return self._save_metadata(instance, metadata)
|
||||||
|
|
||||||
|
# -------- abstractmethod -------- #
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _get(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _create(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _update(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _delete(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_active(self, *args, **kwargs) -> (bool, str):
|
||||||
|
raise NotImplementedError
|
1
apps/accounts/backends/hcp/__init__.py
Normal file
1
apps/accounts/backends/hcp/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .main import *
|
84
apps/accounts/backends/hcp/entries.py
Normal file
84
apps/accounts/backends/hcp/entries.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import sys
|
||||||
|
from abc import ABC
|
||||||
|
|
||||||
|
from common.db.utils import Encryptor
|
||||||
|
from common.utils import lazyproperty
|
||||||
|
|
||||||
|
current_module = sys.modules[__name__]
|
||||||
|
|
||||||
|
__all__ = ['build_entry']
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEntry(ABC):
|
||||||
|
|
||||||
|
def __init__(self, instance):
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
@lazyproperty
|
||||||
|
def full_path(self):
|
||||||
|
path_base = self.path_base
|
||||||
|
path_spec = self.path_spec
|
||||||
|
path = f'{path_base}/{path_spec}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_base(self):
|
||||||
|
path = f'orgs/{self.instance.org_id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def to_internal_data(self):
|
||||||
|
secret = getattr(self.instance, '_secret', None)
|
||||||
|
if secret is not None:
|
||||||
|
secret = Encryptor(secret).encrypt()
|
||||||
|
data = {'secret': secret}
|
||||||
|
return data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_external_data(data):
|
||||||
|
secret = data.pop('secret', None)
|
||||||
|
if secret is not None:
|
||||||
|
secret = Encryptor(secret).decrypt()
|
||||||
|
return secret
|
||||||
|
|
||||||
|
|
||||||
|
class AccountEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'assets/{self.instance.asset_id}/accounts/{self.instance.id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class AccountTemplateEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'account-templates/{self.instance.id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class HistoricalAccountEntry(BaseEntry):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_base(self):
|
||||||
|
account = self.instance.instance
|
||||||
|
path = f'accounts/{account.id}/'
|
||||||
|
return path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_spec(self):
|
||||||
|
path = f'histories/{self.instance.history_id}'
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def build_entry(instance) -> BaseEntry:
|
||||||
|
class_name = instance.__class__.__name__
|
||||||
|
entry_class_name = f'{class_name}Entry'
|
||||||
|
entry_class = getattr(current_module, entry_class_name, None)
|
||||||
|
if not entry_class:
|
||||||
|
raise Exception(f'Entry class {entry_class_name} is not found')
|
||||||
|
return entry_class(instance)
|
53
apps/accounts/backends/hcp/main.py
Normal file
53
apps/accounts/backends/hcp/main.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
from common.db.utils import get_logger
|
||||||
|
from .entries import build_entry
|
||||||
|
from .service import VaultKVClient
|
||||||
|
from ..base import BaseVault
|
||||||
|
|
||||||
|
__all__ = ['Vault']
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Vault(BaseVault):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.client = VaultKVClient(
|
||||||
|
url=kwargs.get('VAULT_HCP_HOST'),
|
||||||
|
token=kwargs.get('VAULT_HCP_TOKEN'),
|
||||||
|
mount_point=kwargs.get('VAULT_HCP_MOUNT_POINT')
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self.client.is_active()
|
||||||
|
|
||||||
|
def _get(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
# TODO: get data 是不是层数太多了
|
||||||
|
data = self.client.get(path=entry.full_path).get('data', {})
|
||||||
|
data = entry.to_external_data(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _create(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
data = entry.to_internal_data()
|
||||||
|
self.client.create(path=entry.full_path, data=data)
|
||||||
|
|
||||||
|
def _update(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
data = entry.to_internal_data()
|
||||||
|
self.client.patch(path=entry.full_path, data=data)
|
||||||
|
|
||||||
|
def _delete(self, instance):
|
||||||
|
entry = build_entry(instance)
|
||||||
|
self.client.delete(path=entry.full_path)
|
||||||
|
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
instance.is_sync_metadata = False
|
||||||
|
instance.mark_secret_save_to_vault()
|
||||||
|
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
try:
|
||||||
|
entry = build_entry(instance)
|
||||||
|
self.client.update_metadata(path=entry.full_path, metadata=metadata)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'save metadata error: {e}')
|
102
apps/accounts/backends/hcp/service.py
Normal file
102
apps/accounts/backends/hcp/service.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
import hvac
|
||||||
|
from hvac import exceptions
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
from common.utils import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ['VaultKVClient']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultKVClient(object):
|
||||||
|
max_versions = 20
|
||||||
|
|
||||||
|
def __init__(self, url, token, mount_point):
|
||||||
|
assert isinstance(self.max_versions, int) and self.max_versions >= 3, (
|
||||||
|
'max_versions must to be an integer that is greater than or equal to 3'
|
||||||
|
)
|
||||||
|
self.client = hvac.Client(url=url, token=token)
|
||||||
|
self.mount_point = mount_point
|
||||||
|
self.enable_secrets_engine_if_need()
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
try:
|
||||||
|
if not self.client.sys.is_initialized():
|
||||||
|
return False, 'Vault is not initialized'
|
||||||
|
if self.client.sys.is_sealed():
|
||||||
|
return False, 'Vault is sealed'
|
||||||
|
if not self.client.is_authenticated():
|
||||||
|
return False, 'Vault is not authenticated'
|
||||||
|
except ConnectionError as e:
|
||||||
|
logger.error(str(e))
|
||||||
|
return False, f'Vault is not reachable: {e}'
|
||||||
|
else:
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
def enable_secrets_engine_if_need(self):
|
||||||
|
secrets_engines = self.client.sys.list_mounted_secrets_engines()
|
||||||
|
mount_points = secrets_engines.keys()
|
||||||
|
if f'{self.mount_point}/' in mount_points:
|
||||||
|
return
|
||||||
|
self.client.sys.enable_secrets_engine(
|
||||||
|
backend_type='kv',
|
||||||
|
path=self.mount_point,
|
||||||
|
options={'version': 2} # TODO: version 是否从配置中读取?
|
||||||
|
)
|
||||||
|
self.client.secrets.kv.v2.configure(
|
||||||
|
max_versions=self.max_versions,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, path, version=None):
|
||||||
|
try:
|
||||||
|
response = self.client.secrets.kv.v2.read_secret_version(
|
||||||
|
path=path,
|
||||||
|
version=version,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
except exceptions.InvalidPath as e:
|
||||||
|
return {}
|
||||||
|
data = response.get('data', {})
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create(self, path, data: dict):
|
||||||
|
self._update_or_create(path=path, data=data)
|
||||||
|
|
||||||
|
def update(self, path, data: dict):
|
||||||
|
""" 未更新的数据会被删除 """
|
||||||
|
self._update_or_create(path=path, data=data)
|
||||||
|
|
||||||
|
def patch(self, path, data: dict):
|
||||||
|
""" 未更新的数据不会被删除 """
|
||||||
|
self.client.secrets.kv.v2.patch(
|
||||||
|
path=path,
|
||||||
|
secret=data,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, path):
|
||||||
|
self.client.secrets.kv.v2.delete_metadata_and_all_versions(
|
||||||
|
path=path,
|
||||||
|
mount_point=self.mount_point,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _update_or_create(self, path, data: dict):
|
||||||
|
self.client.secrets.kv.v2.create_or_update_secret(
|
||||||
|
path=path,
|
||||||
|
secret=data,
|
||||||
|
mount_point=self.mount_point
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_metadata(self, path, metadata: dict):
|
||||||
|
try:
|
||||||
|
self.client.secrets.kv.v2.update_metadata(
|
||||||
|
path=path,
|
||||||
|
mount_point=self.mount_point,
|
||||||
|
custom_metadata=metadata
|
||||||
|
)
|
||||||
|
except exceptions.InvalidPath as e:
|
||||||
|
logger.error('Update metadata error: {}'.format(e))
|
1
apps/accounts/backends/local/__init__.py
Normal file
1
apps/accounts/backends/local/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .main import *
|
36
apps/accounts/backends/local/main.py
Normal file
36
apps/accounts/backends/local/main.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from common.utils import get_logger
|
||||||
|
from ..base import BaseVault
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
__all__ = ['Vault']
|
||||||
|
|
||||||
|
|
||||||
|
class Vault(BaseVault):
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return True, ''
|
||||||
|
|
||||||
|
def _get(self, instance):
|
||||||
|
secret = getattr(instance, '_secret', None)
|
||||||
|
return secret
|
||||||
|
|
||||||
|
def _create(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _update(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _delete(self, instance):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_metadata(self, instance, metadata):
|
||||||
|
""" Ignore """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _clean_db_secret(self, instance):
|
||||||
|
""" Ignore *重要* 不能删除本地 secret """
|
||||||
|
pass
|
@ -1,2 +1,3 @@
|
|||||||
from .account import *
|
from .account import *
|
||||||
from .automation import *
|
from .automation import *
|
||||||
|
from .vault import *
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class SecretType(TextChoices):
|
class SecretType(TextChoices):
|
||||||
@ -16,6 +16,10 @@ class AliasAccount(TextChoices):
|
|||||||
USER = '@USER', _('Dynamic user')
|
USER = '@USER', _('Dynamic user')
|
||||||
ANON = '@ANON', _('Anonymous account')
|
ANON = '@ANON', _('Anonymous account')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def virtual_choices(cls):
|
||||||
|
return [(k, v) for k, v in cls.choices if k not in (cls.ALL,)]
|
||||||
|
|
||||||
|
|
||||||
class Source(TextChoices):
|
class Source(TextChoices):
|
||||||
LOCAL = 'local', _('Local')
|
LOCAL = 'local', _('Local')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.const import Connectivity
|
from assets.const import Connectivity
|
||||||
from common.db.fields import TreeChoices
|
from common.db.fields import TreeChoices
|
||||||
|
9
apps/accounts/const/vault.py
Normal file
9
apps/accounts/const/vault.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
__all__ = ['VaultTypeChoices']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultTypeChoices(models.TextChoices):
|
||||||
|
local = 'local', _('Database')
|
||||||
|
hcp = 'hcp', _('HCP Vault')
|
@ -13,7 +13,8 @@ class AccountFilterSet(BaseFilterSet):
|
|||||||
hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact')
|
hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact')
|
||||||
username = drf_filters.CharFilter(field_name="username", lookup_expr='exact')
|
username = drf_filters.CharFilter(field_name="username", lookup_expr='exact')
|
||||||
address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact')
|
address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact')
|
||||||
asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact')
|
asset_id = drf_filters.CharFilter(field_name="asset", lookup_expr='exact')
|
||||||
|
asset = drf_filters.CharFilter(field_name='asset', lookup_expr='exact')
|
||||||
assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact')
|
assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact')
|
||||||
nodes = drf_filters.CharFilter(method='filter_nodes')
|
nodes = drf_filters.CharFilter(method='filter_nodes')
|
||||||
node_id = drf_filters.CharFilter(method='filter_nodes')
|
node_id = drf_filters.CharFilter(method='filter_nodes')
|
||||||
@ -45,7 +46,7 @@ class AccountFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Account
|
model = Account
|
||||||
fields = ['id', 'asset_id', 'source_id', 'secret_type']
|
fields = ['id', 'asset', 'source_id', 'secret_type']
|
||||||
|
|
||||||
|
|
||||||
class GatheredAccountFilterSet(BaseFilterSet):
|
class GatheredAccountFilterSet(BaseFilterSet):
|
||||||
|
28
apps/accounts/migrations/0012_auto_20230621_1456.py
Normal file
28
apps/accounts/migrations/0012_auto_20230621_1456.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-06-21 06:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0011_auto_20230506_1443'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='account',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='accounttemplate',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='historicalaccount',
|
||||||
|
old_name='secret',
|
||||||
|
new_name='_secret',
|
||||||
|
),
|
||||||
|
]
|
77
apps/accounts/migrations/0013_account_backup_recipients.py
Normal file
77
apps/accounts/migrations/0013_account_backup_recipients.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-08-03 08:28
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import common.db.encoder
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_recipients(apps, schema_editor):
|
||||||
|
account_backup_model = apps.get_model('accounts', 'AccountBackupAutomation')
|
||||||
|
execution_model = apps.get_model('accounts', 'AccountBackupExecution')
|
||||||
|
for account_backup in account_backup_model.objects.all():
|
||||||
|
recipients = list(account_backup.recipients.all())
|
||||||
|
if not recipients:
|
||||||
|
continue
|
||||||
|
account_backup.recipients_part_one.set(recipients)
|
||||||
|
|
||||||
|
objs = []
|
||||||
|
for execution in execution_model.objects.all():
|
||||||
|
snapshot = execution.snapshot
|
||||||
|
recipients = snapshot.pop('recipients', {})
|
||||||
|
snapshot.update({'recipients_part_one': recipients, 'recipients_part_two': {}})
|
||||||
|
objs.append(execution)
|
||||||
|
execution_model.objects.bulk_update(objs, ['snapshot'])
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_snapshot(apps, schema_editor):
|
||||||
|
model = apps.get_model('accounts', 'AccountBackupExecution')
|
||||||
|
objs = []
|
||||||
|
for execution in model.objects.all():
|
||||||
|
execution.snapshot = execution.plan_snapshot
|
||||||
|
objs.append(execution)
|
||||||
|
model.objects.bulk_update(objs, ['snapshot'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('accounts', '0012_auto_20230621_1456'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='recipients_part_one',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name='recipient_part_one_plans',
|
||||||
|
to=settings.AUTH_USER_MODEL, verbose_name='Recipient part one'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='recipients_part_two',
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name='recipient_part_two_plans',
|
||||||
|
to=settings.AUTH_USER_MODEL, verbose_name='Recipient part two'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='accountbackupexecution',
|
||||||
|
name='snapshot',
|
||||||
|
field=models.JSONField(
|
||||||
|
default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder,
|
||||||
|
null=True, blank=True, verbose_name='Account backup snapshot'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_snapshot),
|
||||||
|
migrations.RunPython(migrate_recipients),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='accountbackupexecution',
|
||||||
|
name='plan_snapshot',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='accountbackupautomation',
|
||||||
|
name='recipients',
|
||||||
|
),
|
||||||
|
|
||||||
|
]
|
30
apps/accounts/migrations/0014_virtualaccount.py
Normal file
30
apps/accounts/migrations/0014_virtualaccount.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-08-01 09:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0013_account_backup_recipients'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VirtualAccount',
|
||||||
|
fields=[
|
||||||
|
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
|
||||||
|
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
|
||||||
|
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||||
|
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||||
|
('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account')], max_length=128, verbose_name='Alias')),
|
||||||
|
('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('alias', 'org_id')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,5 @@
|
|||||||
from .account import *
|
from .account import *
|
||||||
from .automations import *
|
from .automations import *
|
||||||
from .base import *
|
from .base import *
|
||||||
|
from .template import *
|
||||||
|
from .virtual import *
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count, Q
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from simple_history.models import HistoricalRecords
|
from simple_history.models import HistoricalRecords
|
||||||
|
|
||||||
from assets.models.base import AbsConnectivity
|
from assets.models.base import AbsConnectivity
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from .base import BaseAccount
|
from .base import BaseAccount
|
||||||
from ..const import AliasAccount, Source
|
from .mixins import VaultModelMixin
|
||||||
|
from ..const import Source
|
||||||
|
|
||||||
__all__ = ['Account', 'AccountTemplate']
|
__all__ = ['Account', 'AccountHistoricalRecords']
|
||||||
|
|
||||||
|
|
||||||
class AccountHistoricalRecords(HistoricalRecords):
|
class AccountHistoricalRecords(HistoricalRecords):
|
||||||
@ -32,7 +31,7 @@ class AccountHistoricalRecords(HistoricalRecords):
|
|||||||
diff = attrs - history_attrs
|
diff = attrs - history_attrs
|
||||||
if not diff:
|
if not diff:
|
||||||
return
|
return
|
||||||
super().post_save(instance, created, using=using, **kwargs)
|
return super().post_save(instance, created, using=using, **kwargs)
|
||||||
|
|
||||||
def create_history_model(self, model, inherited):
|
def create_history_model(self, model, inherited):
|
||||||
if self.included_fields and not self.excluded_fields:
|
if self.included_fields and not self.excluded_fields:
|
||||||
@ -53,7 +52,7 @@ class Account(AbsConnectivity, BaseAccount):
|
|||||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||||
)
|
)
|
||||||
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
version = models.IntegerField(default=0, verbose_name=_('Version'))
|
||||||
history = AccountHistoricalRecords(included_fields=['id', 'secret', 'secret_type', 'version'])
|
history = AccountHistoricalRecords(included_fields=['id', '_secret', 'secret_type', 'version'])
|
||||||
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
source = models.CharField(max_length=30, default=Source.LOCAL, verbose_name=_('Source'))
|
||||||
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
source_id = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Source ID'))
|
||||||
|
|
||||||
@ -88,29 +87,6 @@ class Account(AbsConnectivity, BaseAccount):
|
|||||||
def has_secret(self):
|
def has_secret(self):
|
||||||
return bool(self.secret)
|
return bool(self.secret)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_special_account(cls, name):
|
|
||||||
if name == AliasAccount.INPUT.value:
|
|
||||||
return cls.get_manual_account()
|
|
||||||
elif name == AliasAccount.ANON.value:
|
|
||||||
return cls.get_anonymous_account()
|
|
||||||
else:
|
|
||||||
return cls(name=name, username=name, secret=None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_manual_account(cls):
|
|
||||||
""" @INPUT 手动登录的账号(any) """
|
|
||||||
return cls(name=AliasAccount.INPUT.label, username=AliasAccount.INPUT.value, secret=None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_anonymous_account(cls):
|
|
||||||
return cls(name=AliasAccount.ANON.label, username=AliasAccount.ANON.value, secret=None)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_user_account(cls):
|
|
||||||
""" @USER 动态用户的账号(self) """
|
|
||||||
return cls(name=AliasAccount.USER.label, username=AliasAccount.USER.value, secret=None)
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def versions(self):
|
def versions(self):
|
||||||
return self.history.count()
|
return self.history.count()
|
||||||
@ -120,81 +96,19 @@ class Account(AbsConnectivity, BaseAccount):
|
|||||||
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
||||||
|
|
||||||
|
|
||||||
class AccountTemplate(BaseAccount):
|
def replace_history_model_with_mixin():
|
||||||
su_from = models.ForeignKey(
|
"""
|
||||||
'self', related_name='su_to', null=True,
|
替换历史模型中的父类为指定的Mixin类。
|
||||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
Parameters:
|
||||||
verbose_name = _('Account template')
|
model (class): 历史模型类,例如 Account.history.model
|
||||||
unique_together = (
|
mixin_class (class): 要替换为的Mixin类
|
||||||
('name', 'org_id'),
|
|
||||||
)
|
|
||||||
permissions = [
|
|
||||||
('view_accounttemplatesecret', _('Can view asset account template secret')),
|
|
||||||
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
|
||||||
]
|
|
||||||
|
|
||||||
@classmethod
|
Returns:
|
||||||
def get_su_from_account_templates(cls, pk=None):
|
None
|
||||||
if pk is None:
|
"""
|
||||||
return cls.objects.all()
|
model = Account.history.model
|
||||||
return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk))
|
model.__bases__ = (VaultModelMixin,) + model.__bases__
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f'{self.name}({self.username})'
|
|
||||||
|
|
||||||
def get_su_from_account(self, asset):
|
replace_history_model_with_mixin()
|
||||||
su_from = self.su_from
|
|
||||||
if su_from and asset.platform.su_enabled:
|
|
||||||
account = asset.accounts.filter(
|
|
||||||
username=su_from.username,
|
|
||||||
secret_type=su_from.secret_type
|
|
||||||
).first()
|
|
||||||
return account
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.username
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bulk_update_accounts(accounts, data):
|
|
||||||
history_model = Account.history.model
|
|
||||||
account_ids = accounts.values_list('id', flat=True)
|
|
||||||
history_accounts = history_model.objects.filter(id__in=account_ids)
|
|
||||||
account_id_count_map = {
|
|
||||||
str(i['id']): i['count']
|
|
||||||
for i in history_accounts.values('id').order_by('id')
|
|
||||||
.annotate(count=Count(1)).values('id', 'count')
|
|
||||||
}
|
|
||||||
|
|
||||||
for account in accounts:
|
|
||||||
account_id = str(account.id)
|
|
||||||
account.version = account_id_count_map.get(account_id) + 1
|
|
||||||
for k, v in data.items():
|
|
||||||
setattr(account, k, v)
|
|
||||||
Account.objects.bulk_update(accounts, ['version', 'secret'])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def bulk_create_history_accounts(accounts, user_id):
|
|
||||||
history_model = Account.history.model
|
|
||||||
history_account_objs = []
|
|
||||||
for account in accounts:
|
|
||||||
history_account_objs.append(
|
|
||||||
history_model(
|
|
||||||
id=account.id,
|
|
||||||
version=account.version,
|
|
||||||
secret=account.secret,
|
|
||||||
secret_type=account.secret_type,
|
|
||||||
history_user_id=user_id,
|
|
||||||
history_date=timezone.now()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
history_model.objects.bulk_create(history_account_objs)
|
|
||||||
|
|
||||||
def bulk_sync_account_secret(self, accounts, user_id):
|
|
||||||
""" 批量同步账号密码 """
|
|
||||||
if not accounts:
|
|
||||||
return
|
|
||||||
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
|
||||||
self.bulk_create_history_accounts(accounts, user_id)
|
|
||||||
|
@ -6,7 +6,7 @@ import uuid
|
|||||||
from celery import current_task
|
from celery import current_task
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.const.choices import Trigger
|
from common.const.choices import Trigger
|
||||||
from common.db.encoder import ModelJSONFieldEncoder
|
from common.db.encoder import ModelJSONFieldEncoder
|
||||||
@ -22,9 +22,13 @@ logger = get_logger(__file__)
|
|||||||
|
|
||||||
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||||
types = models.JSONField(default=list)
|
types = models.JSONField(default=list)
|
||||||
recipients = models.ManyToManyField(
|
recipients_part_one = models.ManyToManyField(
|
||||||
'users.User', related_name='recipient_escape_route_plans', blank=True,
|
'users.User', related_name='recipient_part_one_plans', blank=True,
|
||||||
verbose_name=_("Recipient")
|
verbose_name=_("Recipient part one")
|
||||||
|
)
|
||||||
|
recipients_part_two = models.ManyToManyField(
|
||||||
|
'users.User', related_name='recipient_part_two_plans', blank=True,
|
||||||
|
verbose_name=_("Recipient part two")
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -52,9 +56,13 @@ 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,
|
||||||
'recipients': {
|
'recipients_part_one': {
|
||||||
str(recipient.id): (str(recipient), bool(recipient.secret_key))
|
str(user.id): (str(user), bool(user.secret_key))
|
||||||
for recipient in self.recipients.all()
|
for user in self.recipients_part_one.all()
|
||||||
|
},
|
||||||
|
'recipients_part_two': {
|
||||||
|
str(user.id): (str(user), bool(user.secret_key))
|
||||||
|
for user in self.recipients_part_two.all()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +76,7 @@ class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
hid = str(uuid.uuid4())
|
hid = str(uuid.uuid4())
|
||||||
execution = AccountBackupExecution.objects.create(
|
execution = AccountBackupExecution.objects.create(
|
||||||
id=hid, plan=self, plan_snapshot=self.to_attr_json(), trigger=trigger
|
id=hid, plan=self, snapshot=self.to_attr_json(), trigger=trigger
|
||||||
)
|
)
|
||||||
return execution.start()
|
return execution.start()
|
||||||
|
|
||||||
@ -85,7 +93,7 @@ class AccountBackupExecution(OrgModelMixin):
|
|||||||
timedelta = models.FloatField(
|
timedelta = models.FloatField(
|
||||||
default=0.0, verbose_name=_('Time'), null=True
|
default=0.0, verbose_name=_('Time'), null=True
|
||||||
)
|
)
|
||||||
plan_snapshot = models.JSONField(
|
snapshot = models.JSONField(
|
||||||
encoder=ModelJSONFieldEncoder, default=dict,
|
encoder=ModelJSONFieldEncoder, default=dict,
|
||||||
blank=True, null=True, verbose_name=_('Account backup snapshot')
|
blank=True, null=True, verbose_name=_('Account backup snapshot')
|
||||||
)
|
)
|
||||||
@ -108,16 +116,9 @@ class AccountBackupExecution(OrgModelMixin):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def types(self):
|
def types(self):
|
||||||
types = self.plan_snapshot.get('types')
|
types = self.snapshot.get('types')
|
||||||
return types
|
return types
|
||||||
|
|
||||||
@property
|
|
||||||
def recipients(self):
|
|
||||||
recipients = self.plan_snapshot.get('recipients')
|
|
||||||
if not recipients:
|
|
||||||
return []
|
|
||||||
return recipients.values()
|
|
||||||
|
|
||||||
@lazyproperty
|
@lazyproperty
|
||||||
def backup_accounts(self):
|
def backup_accounts(self):
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import (
|
||||||
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
|
||||||
@ -86,7 +86,7 @@ class ChangeSecretRecord(JMSBaseModel):
|
|||||||
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
|
asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True)
|
||||||
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True)
|
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True)
|
||||||
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret'))
|
||||||
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('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')
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
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.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes, Source
|
from accounts.const import AutomationTypes, Source
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.models import Account
|
from accounts.models import Account
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from .base import AccountBaseAutomation
|
from .base import AccountBaseAutomation
|
||||||
|
@ -6,36 +6,35 @@ from hashlib import md5
|
|||||||
import sshpubkeys
|
import sshpubkeys
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import SecretType
|
from accounts.const import SecretType
|
||||||
from common.db import fields
|
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
ssh_key_string_to_obj, ssh_key_gen, get_logger,
|
||||||
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
random_string, lazyproperty, parse_ssh_public_key_str, is_openssh_format_key
|
||||||
)
|
)
|
||||||
|
from accounts.models.mixins import VaultModelMixin, VaultManagerMixin, VaultQuerySetMixin
|
||||||
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
from orgs.mixins.models import JMSOrgBaseModel, OrgManager
|
||||||
|
|
||||||
logger = get_logger(__file__)
|
logger = get_logger(__file__)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountQuerySet(models.QuerySet):
|
class BaseAccountQuerySet(VaultQuerySetMixin, models.QuerySet):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.filter(is_active=True)
|
return self.filter(is_active=True)
|
||||||
|
|
||||||
|
|
||||||
class BaseAccountManager(OrgManager):
|
class BaseAccountManager(VaultManagerMixin, OrgManager):
|
||||||
def active(self):
|
def active(self):
|
||||||
return self.get_queryset().active()
|
return self.get_queryset().active()
|
||||||
|
|
||||||
|
|
||||||
class BaseAccount(JMSOrgBaseModel):
|
class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
|
||||||
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
name = models.CharField(max_length=128, verbose_name=_("Name"))
|
||||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True)
|
||||||
secret_type = models.CharField(
|
secret_type = models.CharField(
|
||||||
max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type')
|
||||||
)
|
)
|
||||||
secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
|
||||||
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
privileged = models.BooleanField(verbose_name=_("Privileged"), default=False)
|
||||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||||
|
|
||||||
|
1
apps/accounts/models/mixins/__init__.py
Normal file
1
apps/accounts/models/mixins/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .vault import *
|
94
apps/accounts/models/mixins/vault.py
Normal file
94
apps/accounts/models/mixins/vault.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from common.db import fields
|
||||||
|
|
||||||
|
__all__ = ['VaultQuerySetMixin', 'VaultManagerMixin', 'VaultModelMixin']
|
||||||
|
|
||||||
|
|
||||||
|
class VaultQuerySetMixin(models.QuerySet):
|
||||||
|
|
||||||
|
def update(self, **kwargs):
|
||||||
|
"""
|
||||||
|
1. 替换 secret 为 _secret
|
||||||
|
2. 触发 post_save 信号
|
||||||
|
"""
|
||||||
|
if 'secret' in kwargs:
|
||||||
|
kwargs.update({
|
||||||
|
'_secret': kwargs.pop('secret')
|
||||||
|
})
|
||||||
|
rows = super().update(**kwargs)
|
||||||
|
|
||||||
|
# 为了获取更新后的对象所以单独查询一次
|
||||||
|
ids = self.values_list('id', flat=True)
|
||||||
|
objs = self.model.objects.filter(id__in=ids)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=False)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
class VaultManagerMixin(models.Manager):
|
||||||
|
""" 触发 bulk_create 和 bulk_update 操作下的 post_save 信号 """
|
||||||
|
|
||||||
|
def bulk_create(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
|
objs = super().bulk_create(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=True)
|
||||||
|
return objs
|
||||||
|
|
||||||
|
def bulk_update(self, objs, batch_size=None, ignore_conflicts=False):
|
||||||
|
objs = super().bulk_update(objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts)
|
||||||
|
for obj in objs:
|
||||||
|
post_save.send(obj.__class__, instance=obj, created=False)
|
||||||
|
return objs
|
||||||
|
|
||||||
|
|
||||||
|
class VaultModelMixin(models.Model):
|
||||||
|
_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret'))
|
||||||
|
is_sync_metadata = True
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
# 缓存 secret 值, lazy-property 不能用
|
||||||
|
__secret = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret(self):
|
||||||
|
if self.__secret:
|
||||||
|
return self.__secret
|
||||||
|
from accounts.backends import vault_client
|
||||||
|
secret = vault_client.get(self)
|
||||||
|
if not secret and not self.secret_has_save_to_vault:
|
||||||
|
# vault_client 获取不到, 并且 secret 没有保存到 vault, 就从 self._secret 获取
|
||||||
|
secret = self._secret
|
||||||
|
self.__secret = secret
|
||||||
|
return self.__secret
|
||||||
|
|
||||||
|
@secret.setter
|
||||||
|
def secret(self, value):
|
||||||
|
"""
|
||||||
|
保存的时候通过 post_save 信号监听进行处理,
|
||||||
|
先保存到 db, 再保存到 vault 同时删除本地 db _secret 值
|
||||||
|
"""
|
||||||
|
self._secret = value
|
||||||
|
self.__secret = value
|
||||||
|
|
||||||
|
_secret_save_to_vault_mark = '# Secret-has-been-saved-to-vault #'
|
||||||
|
|
||||||
|
def mark_secret_save_to_vault(self):
|
||||||
|
self._secret = self._secret_save_to_vault_mark
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret_has_save_to_vault(self):
|
||||||
|
return self._secret == self._secret_save_to_vault_mark
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
""" 通过 post_save signal 处理 _secret 数据 """
|
||||||
|
update_fields = kwargs.get('update_fields')
|
||||||
|
if update_fields and 'secret' in update_fields:
|
||||||
|
update_fields.remove('secret')
|
||||||
|
update_fields.append('_secret')
|
||||||
|
return super().save(*args, **kwargs)
|
86
apps/accounts/models/template.py
Normal file
86
apps/accounts/models/template.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from .account import Account
|
||||||
|
from .base import BaseAccount
|
||||||
|
|
||||||
|
__all__ = ['AccountTemplate', ]
|
||||||
|
|
||||||
|
|
||||||
|
class AccountTemplate(BaseAccount):
|
||||||
|
su_from = models.ForeignKey(
|
||||||
|
'self', related_name='su_to', null=True,
|
||||||
|
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Account template')
|
||||||
|
unique_together = (
|
||||||
|
('name', 'org_id'),
|
||||||
|
)
|
||||||
|
permissions = [
|
||||||
|
('view_accounttemplatesecret', _('Can view asset account template secret')),
|
||||||
|
('change_accounttemplatesecret', _('Can change asset account template secret')),
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_su_from_account_templates(cls, pk=None):
|
||||||
|
if pk is None:
|
||||||
|
return cls.objects.all()
|
||||||
|
return cls.objects.exclude(Q(id=pk) | Q(su_from_id=pk))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.name}({self.username})'
|
||||||
|
|
||||||
|
def get_su_from_account(self, asset):
|
||||||
|
su_from = self.su_from
|
||||||
|
if su_from and asset.platform.su_enabled:
|
||||||
|
account = asset.accounts.filter(
|
||||||
|
username=su_from.username,
|
||||||
|
secret_type=su_from.secret_type
|
||||||
|
).first()
|
||||||
|
return account
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bulk_update_accounts(accounts, data):
|
||||||
|
history_model = Account.history.model
|
||||||
|
account_ids = accounts.values_list('id', flat=True)
|
||||||
|
history_accounts = history_model.objects.filter(id__in=account_ids)
|
||||||
|
account_id_count_map = {
|
||||||
|
str(i['id']): i['count']
|
||||||
|
for i in history_accounts.values('id').order_by('id')
|
||||||
|
.annotate(count=Count(1)).values('id', 'count')
|
||||||
|
}
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
|
account_id = str(account.id)
|
||||||
|
account.version = account_id_count_map.get(account_id) + 1
|
||||||
|
for k, v in data.items():
|
||||||
|
setattr(account, k, v)
|
||||||
|
Account.objects.bulk_update(accounts, ['version', 'secret'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def bulk_create_history_accounts(accounts, user_id):
|
||||||
|
history_model = Account.history.model
|
||||||
|
history_account_objs = []
|
||||||
|
for account in accounts:
|
||||||
|
history_account_objs.append(
|
||||||
|
history_model(
|
||||||
|
id=account.id,
|
||||||
|
version=account.version,
|
||||||
|
secret=account.secret,
|
||||||
|
secret_type=account.secret_type,
|
||||||
|
history_user_id=user_id,
|
||||||
|
history_date=timezone.now()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
history_model.objects.bulk_create(history_account_objs)
|
||||||
|
|
||||||
|
def bulk_sync_account_secret(self, accounts, user_id):
|
||||||
|
""" 批量同步账号密码 """
|
||||||
|
if not accounts:
|
||||||
|
return
|
||||||
|
self.bulk_update_accounts(accounts, {'secret': self.secret})
|
||||||
|
self.bulk_create_history_accounts(accounts, user_id)
|
103
apps/accounts/models/virtual.py
Normal file
103
apps/accounts/models/virtual.py
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.const import AliasAccount
|
||||||
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
|
||||||
|
__all__ = ['VirtualAccount']
|
||||||
|
|
||||||
|
from orgs.utils import tmp_to_org
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualAccount(JMSOrgBaseModel):
|
||||||
|
alias = models.CharField(max_length=128, choices=AliasAccount.virtual_choices(), verbose_name=_('Alias'), )
|
||||||
|
secret_from_login = models.BooleanField(default=None, null=True, verbose_name=_("Secret from login"), )
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [('alias', 'org_id')]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.get_alias_display()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
usernames_map = {
|
||||||
|
AliasAccount.INPUT: _("Manual input"),
|
||||||
|
AliasAccount.USER: _("Same with user"),
|
||||||
|
AliasAccount.ANON: ''
|
||||||
|
}
|
||||||
|
usernames_map = {str(k): v for k, v in usernames_map.items()}
|
||||||
|
return usernames_map.get(self.alias, '')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comment(self):
|
||||||
|
comments_map = {
|
||||||
|
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
|
||||||
|
AliasAccount.USER: _('The account username name same with user on connect'),
|
||||||
|
AliasAccount.ANON: _('Connect asset without using a username and password, '
|
||||||
|
'and it only supports web-based and custom-type assets'),
|
||||||
|
}
|
||||||
|
comments_map = {str(k): v for k, v in comments_map.items()}
|
||||||
|
return comments_map.get(self.alias, '')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_init_queryset(cls):
|
||||||
|
aliases = [i[0] for i in AliasAccount.virtual_choices()]
|
||||||
|
alias_created = cls.objects.all().values_list('alias', flat=True)
|
||||||
|
need_created = set(aliases) - set(alias_created)
|
||||||
|
|
||||||
|
if need_created:
|
||||||
|
accounts = [cls(alias=alias) for alias in need_created]
|
||||||
|
cls.objects.bulk_create(accounts, ignore_conflicts=True)
|
||||||
|
return cls.objects.all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_special_account(cls, alias, user, asset, input_username='', input_secret='', from_permed=True):
|
||||||
|
if alias == AliasAccount.INPUT.value:
|
||||||
|
account = cls.get_manual_account(input_username, input_secret, from_permed)
|
||||||
|
elif alias == AliasAccount.ANON.value:
|
||||||
|
account = cls.get_anonymous_account()
|
||||||
|
elif alias == AliasAccount.USER.value:
|
||||||
|
account = cls.get_same_account(user, asset, input_secret=input_secret, from_permed=from_permed)
|
||||||
|
else:
|
||||||
|
account = cls(name=alias, username=alias, secret=None)
|
||||||
|
account.alias = alias
|
||||||
|
if asset:
|
||||||
|
account.asset = asset
|
||||||
|
account.org_id = asset.org_id
|
||||||
|
return account
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_manual_account(cls, input_username='', input_secret='', from_permed=True):
|
||||||
|
""" @INPUT 手动登录的账号(any) """
|
||||||
|
from .account import Account
|
||||||
|
if from_permed:
|
||||||
|
username = AliasAccount.INPUT.value
|
||||||
|
secret = ''
|
||||||
|
else:
|
||||||
|
username = input_username
|
||||||
|
secret = input_secret
|
||||||
|
return Account(name=AliasAccount.INPUT.label, username=username, secret=secret)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_anonymous_account(cls):
|
||||||
|
from .account import Account
|
||||||
|
return Account(name=AliasAccount.ANON.label, username=AliasAccount.ANON.value, secret=None)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_same_account(cls, user, asset, input_secret='', from_permed=True):
|
||||||
|
""" @USER 动态用户的账号(self) """
|
||||||
|
from .account import Account
|
||||||
|
username = user.username
|
||||||
|
|
||||||
|
with tmp_to_org(asset.org):
|
||||||
|
same_account = cls.objects.filter(alias='@USER').first()
|
||||||
|
|
||||||
|
secret = ''
|
||||||
|
if same_account and same_account.secret_from_login:
|
||||||
|
secret = user.get_cached_password_if_has()
|
||||||
|
|
||||||
|
if not secret and not from_permed:
|
||||||
|
secret = input_secret
|
||||||
|
return Account(name=AliasAccount.USER.label, username=username, secret=secret)
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_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_async
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from .account import *
|
from .account import *
|
||||||
from .backup import *
|
from .backup import *
|
||||||
from .base import *
|
from .base import *
|
||||||
from .template import *
|
|
||||||
from .gathered_account import *
|
from .gathered_account import *
|
||||||
|
from .template import *
|
||||||
|
from .virtual import *
|
||||||
|
@ -3,7 +3,7 @@ from copy import deepcopy
|
|||||||
|
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
from rest_framework.validators import UniqueTogetherValidator
|
||||||
@ -95,6 +95,8 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
|||||||
field.name for field in template._meta.fields
|
field.name for field in template._meta.fields
|
||||||
if field.name not in ignore_fields
|
if field.name not in ignore_fields
|
||||||
]
|
]
|
||||||
|
field_names = [name if name != '_secret' else 'secret' for name in field_names]
|
||||||
|
|
||||||
attrs = {}
|
attrs = {}
|
||||||
for name in field_names:
|
for name in field_names:
|
||||||
value = getattr(template, name, None)
|
value = getattr(template, name, None)
|
||||||
@ -198,7 +200,6 @@ class AccountAssetSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerializer):
|
||||||
asset = AccountAssetSerializer(label=_('Asset'))
|
asset = AccountAssetSerializer(label=_('Asset'))
|
||||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
|
||||||
source = LabeledChoiceField(
|
source = LabeledChoiceField(
|
||||||
choices=Source.choices, label=_("Source"), required=False,
|
choices=Source.choices, label=_("Source"), required=False,
|
||||||
allow_null=True, default=Source.LOCAL
|
allow_null=True, default=Source.LOCAL
|
||||||
@ -233,6 +234,15 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDetailSerializer(AccountSerializer):
|
||||||
|
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||||
|
|
||||||
|
class Meta(AccountSerializer.Meta):
|
||||||
|
model = Account
|
||||||
|
fields = AccountSerializer.Meta.fields + ['has_secret']
|
||||||
|
read_only_fields = AccountSerializer.Meta.read_only_fields + ['has_secret']
|
||||||
|
|
||||||
|
|
||||||
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
class AssetAccountBulkSerializerResultSerializer(serializers.Serializer):
|
||||||
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
asset = serializers.CharField(read_only=True, label=_('Asset'))
|
||||||
state = serializers.CharField(read_only=True, label=_('State'))
|
state = serializers.CharField(read_only=True, label=_('State'))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
from accounts.models import AccountBackupAutomation, AccountBackupExecution
|
||||||
@ -24,7 +24,7 @@ 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', 'recipients', 'types'
|
'comment', 'types', 'recipients_part_one', 'recipients_part_two'
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'name': {'required': True},
|
'name': {'required': True},
|
||||||
@ -44,7 +44,7 @@ class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = AccountBackupExecution
|
model = AccountBackupExecution
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'id', 'date_start', 'timedelta', 'plan_snapshot',
|
'id', 'date_start', 'timedelta', 'snapshot',
|
||||||
'trigger', 'reason', 'is_success', 'org_id', 'recipients'
|
'trigger', 'reason', 'is_success', 'org_id'
|
||||||
]
|
]
|
||||||
fields = read_only_fields + ['plan']
|
fields = read_only_fields + ['plan']
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import SecretType
|
from accounts.const import SecretType
|
||||||
@ -61,20 +61,18 @@ class AuthValidateMixin(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = BaseAccount
|
model = BaseAccount
|
||||||
fields_mini = ['id', 'name', 'username']
|
fields_mini = ['id', 'name', 'username']
|
||||||
fields_small = fields_mini + [
|
fields_small = fields_mini + [
|
||||||
'secret_type', 'secret', 'has_secret', 'passphrase',
|
'secret_type', 'secret', 'passphrase',
|
||||||
'privileged', 'is_active', 'spec_info',
|
'privileged', 'is_active', 'spec_info',
|
||||||
]
|
]
|
||||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||||
fields = fields_small + fields_other
|
fields = fields_small + fields_other
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
'has_secret', 'spec_info',
|
'spec_info', 'date_verified', 'created_by', 'date_created',
|
||||||
'date_verified', 'created_by', 'date_created',
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'spec_info': {'label': _('Spec info')},
|
'spec_info': {'label': _('Spec info')},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from accounts.models import GatheredAccount
|
from accounts.models import GatheredAccount
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import AccountTemplate, Account
|
from accounts.models import AccountTemplate, Account
|
||||||
|
26
apps/accounts/serializers/account/virtual.py
Normal file
26
apps/accounts/serializers/account/virtual.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from accounts.models import VirtualAccount
|
||||||
|
|
||||||
|
__all__ = ['VirtualAccountSerializer']
|
||||||
|
|
||||||
|
|
||||||
|
class VirtualAccountSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = VirtualAccount
|
||||||
|
field_mini = ['id', 'alias', 'username', 'name']
|
||||||
|
common_fields = ['date_created', 'date_updated', 'comment']
|
||||||
|
fields = field_mini + [
|
||||||
|
'secret_from_login',
|
||||||
|
] + common_fields
|
||||||
|
read_only_fields = common_fields + common_fields
|
||||||
|
extra_kwargs = {
|
||||||
|
'comment': {'label': _('Comment')},
|
||||||
|
'name': {'label': _('Name')},
|
||||||
|
'username': {'label': _('Username')},
|
||||||
|
'secret_from_login': {'help_text': _('Current only support login from AD/LDAP. Secret priority: '
|
||||||
|
'Same account in asset secret > Login secret > Manual input')
|
||||||
|
},
|
||||||
|
'alias': {'required': False},
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.models import AutomationExecution
|
from accounts.models import AutomationExecution
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import (
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
from django.db.models.signals import pre_save
|
from django.db.models.signals import pre_save, post_save, post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
|
from accounts.backends import vault_client
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from .models import Account
|
from .models import Account, AccountTemplate
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -13,3 +14,23 @@ def on_account_pre_save(sender, instance, **kwargs):
|
|||||||
instance.version = 1
|
instance.version = 1
|
||||||
else:
|
else:
|
||||||
instance.version = instance.history.count()
|
instance.version = instance.history.count()
|
||||||
|
|
||||||
|
|
||||||
|
class VaultSignalHandler(object):
|
||||||
|
""" 处理 Vault 相关的信号 """
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_to_vault(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
vault_client.create(instance)
|
||||||
|
else:
|
||||||
|
vault_client.update(instance)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_to_vault(sender, instance, **kwargs):
|
||||||
|
vault_client.delete(instance)
|
||||||
|
|
||||||
|
|
||||||
|
for model in (Account, AccountTemplate, Account.history.model):
|
||||||
|
post_save.connect(VaultSignalHandler.save_to_vault, sender=model)
|
||||||
|
post_delete.connect(VaultSignalHandler.delete_to_vault, sender=model)
|
||||||
|
@ -23,7 +23,7 @@ def task_activity_callback(self, pid, trigger, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@shared_task(verbose_name=_('Execute account backup plan'), activity_callback=task_activity_callback)
|
@shared_task(verbose_name=_('Execute account backup plan'), activity_callback=task_activity_callback)
|
||||||
def execute_account_backup_task(pid, trigger):
|
def execute_account_backup_task(pid, trigger, **kwargs):
|
||||||
from accounts.models import AccountBackupAutomation
|
from accounts.models import AccountBackupAutomation
|
||||||
with tmp_to_root_org():
|
with tmp_to_root_org():
|
||||||
plan = get_object_or_none(AccountBackupAutomation, pk=pid)
|
plan = get_object_or_none(AccountBackupAutomation, pk=pid)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from django.utils.translation import gettext_noop, ugettext_lazy as _
|
from django.utils.translation import gettext_noop, gettext_lazy as _
|
||||||
|
|
||||||
from accounts.const import AutomationTypes
|
from accounts.const import AutomationTypes
|
||||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||||
|
68
apps/accounts/tasks/vault.py
Normal file
68
apps/accounts/tasks/vault.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from celery import shared_task
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from accounts.backends import vault_client
|
||||||
|
from accounts.models import Account, AccountTemplate
|
||||||
|
from common.utils import get_logger
|
||||||
|
from orgs.utils import tmp_to_root_org
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_instance(instance):
|
||||||
|
instance_desc = f'[{instance._meta.verbose_name}-{instance.id}-{instance}]'
|
||||||
|
if instance.secret_has_save_to_vault:
|
||||||
|
msg = f'\033[32m- 跳过同步: {instance_desc}, 原因: [已同步]'
|
||||||
|
return "skipped", msg
|
||||||
|
|
||||||
|
try:
|
||||||
|
vault_client.create(instance)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f'\033[31m- 同步失败: {instance_desc}, 原因: [{e}]'
|
||||||
|
return "failed", msg
|
||||||
|
else:
|
||||||
|
msg = f'\033[32m- 同步成功: {instance_desc}'
|
||||||
|
return "succeeded", msg
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(verbose_name=_('Sync secret to vault'))
|
||||||
|
def sync_secret_to_vault():
|
||||||
|
if not vault_client.enabled:
|
||||||
|
# 这里不能判断 settings.VAULT_ENABLED, 必须判断当前 vault_client 的类型
|
||||||
|
print('\033[35m>>> 当前 Vault 功能未开启, 不需要同步')
|
||||||
|
return
|
||||||
|
|
||||||
|
failed, skipped, succeeded = 0, 0, 0
|
||||||
|
to_sync_models = [Account, AccountTemplate, Account.history.model]
|
||||||
|
print(f'\033[33m>>> 开始同步密钥数据到 Vault ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||||
|
with tmp_to_root_org():
|
||||||
|
instances = []
|
||||||
|
for model in to_sync_models:
|
||||||
|
instances += list(model.objects.all())
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||||
|
tasks = [executor.submit(sync_instance, instance) for instance in instances]
|
||||||
|
|
||||||
|
for future in as_completed(tasks):
|
||||||
|
status, msg = future.result()
|
||||||
|
print(msg)
|
||||||
|
if status == "succeeded":
|
||||||
|
succeeded += 1
|
||||||
|
elif status == "failed":
|
||||||
|
failed += 1
|
||||||
|
elif status == "skipped":
|
||||||
|
skipped += 1
|
||||||
|
|
||||||
|
total = succeeded + failed + skipped
|
||||||
|
print(
|
||||||
|
f'\033[33m>>> 同步完成: {model.__module__}, '
|
||||||
|
f'共计: {total}, '
|
||||||
|
f'成功: {succeeded}, '
|
||||||
|
f'失败: {failed}, '
|
||||||
|
f'跳过: {skipped}'
|
||||||
|
)
|
||||||
|
print(f'\033[33m>>> 全部同步完成 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
|
||||||
|
print('\033[0m')
|
@ -9,6 +9,7 @@ app_name = 'accounts'
|
|||||||
router = BulkRouter()
|
router = BulkRouter()
|
||||||
|
|
||||||
router.register(r'accounts', api.AccountViewSet, 'account')
|
router.register(r'accounts', api.AccountViewSet, 'account')
|
||||||
|
router.register(r'virtual-accounts', api.VirtualAccountViewSet, 'virtual-account')
|
||||||
router.register(r'gathered-accounts', api.GatheredAccountViewSet, 'gathered-account')
|
router.register(r'gathered-accounts', api.GatheredAccountViewSet, 'gathered-account')
|
||||||
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret')
|
||||||
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
|
router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template')
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from accounts.const import (
|
from accounts.const import SecretType, DEFAULT_PASSWORD_RULES
|
||||||
SecretType, DEFAULT_PASSWORD_RULES
|
|
||||||
)
|
|
||||||
from common.utils import ssh_key_gen, random_string
|
from common.utils import ssh_key_gen, random_string
|
||||||
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
from common.utils import validate_ssh_private_key, parse_ssh_private_key_str
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AclsConfig(AppConfig):
|
class AclsConfig(AppConfig):
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import lazyproperty, get_logger
|
from common.utils import lazyproperty, get_logger
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_request_ip, get_ip_city
|
from common.utils import get_request_ip, get_ip_city
|
||||||
from common.utils.timezone import local_now_display
|
from common.utils.timezone import local_now_display
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .base import UserAssetAccountBaseACL
|
from .base import UserAssetAccountBaseACL
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from acls.models.base import BaseACL
|
from acls.models.base import BaseACL
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from acls.models import CommandGroup, CommandFilterACL
|
from acls.models import CommandGroup, CommandFilterACL
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from common.serializers import MethodSerializer
|
from common.serializers import MethodSerializer
|
||||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
#
|
#
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
from common.utils.ip import is_ip_address, is_ip_network, is_ip_segment
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class ApplicationsConfig(AppConfig):
|
class ApplicationsConfig(AppConfig):
|
||||||
@ -9,5 +9,4 @@ class ApplicationsConfig(AppConfig):
|
|||||||
verbose_name = _('Applications')
|
verbose_name = _('Applications')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signal_handlers
|
|
||||||
super().ready()
|
super().ready()
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django_mysql.models
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
@ -127,7 +126,7 @@ class Migration(migrations.Migration):
|
|||||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||||
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
|
('category', models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')], max_length=16, verbose_name='Category')),
|
||||||
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
|
('type', models.CharField(choices=[('mysql', 'MySQL'), ('oracle', 'Oracle'), ('postgresql', 'PostgreSQL'), ('mariadb', 'MariaDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'), ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')], max_length=16, verbose_name='Type')),
|
||||||
('attrs', django_mysql.models.JSONField(default=dict)),
|
('attrs', models.JSONField(default=dict)),
|
||||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||||
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
|
('domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='assets.Domain', verbose_name='Domain')),
|
||||||
],
|
],
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.db.models import JMSBaseModel
|
from common.db.models import JMSBaseModel
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from rest_framework.views import APIView, Response
|
from rest_framework.views import APIView, Response
|
||||||
@ -29,6 +29,7 @@ class DomainViewSet(OrgBulkModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().prefetch_related('assets')
|
return super().get_queryset().prefetch_related('assets')
|
||||||
|
|
||||||
|
|
||||||
class GatewayViewSet(HostViewSet):
|
class GatewayViewSet(HostViewSet):
|
||||||
perm_model = Gateway
|
perm_model = Gateway
|
||||||
filterset_fields = ("domain__name", "name", "domain")
|
filterset_fields = ("domain__name", "name", "domain")
|
||||||
|
@ -2,7 +2,7 @@ from typing import List
|
|||||||
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from assets.models import Node, PlatformProtocol, Protocol
|
from assets.models import Node, Protocol
|
||||||
from assets.utils import get_node_from_request, is_query_node_all_assets
|
from assets.utils import get_node_from_request, is_query_node_all_assets
|
||||||
from common.utils import lazyproperty, timeit
|
from common.utils import lazyproperty, timeit
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ class SerializeToTreeNodeMixin:
|
|||||||
'name': _name(node),
|
'name': _name(node),
|
||||||
'title': _name(node),
|
'title': _name(node),
|
||||||
'pId': node.parent_key,
|
'pId': node.parent_key,
|
||||||
'isParent': True,
|
'isParent': node.assets_amount > 0,
|
||||||
'open': _open(node),
|
'open': _open(node),
|
||||||
'meta': {
|
'meta': {
|
||||||
'data': {
|
'data': {
|
||||||
@ -70,25 +70,18 @@ class SerializeToTreeNodeMixin:
|
|||||||
|
|
||||||
@timeit
|
@timeit
|
||||||
def serialize_assets(self, assets, node_key=None, pid=None):
|
def serialize_assets(self, assets, node_key=None, pid=None):
|
||||||
sftp_enabled_platform = PlatformProtocol.objects \
|
|
||||||
.filter(name='ssh', setting__sftp_enabled=True) \
|
|
||||||
.values_list('platform', flat=True) \
|
|
||||||
.distinct()
|
|
||||||
if node_key is None:
|
if node_key is None:
|
||||||
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
get_pid = lambda asset: getattr(asset, 'parent_key', '')
|
||||||
else:
|
else:
|
||||||
get_pid = lambda asset: node_key
|
get_pid = lambda asset: node_key
|
||||||
ssh_asset_ids = [
|
sftp_asset_ids = Protocol.objects.filter(name='sftp') \
|
||||||
str(i) for i in
|
.values_list('asset_id', flat=True)
|
||||||
Protocol.objects.filter(name='ssh').values_list('asset_id', flat=True)
|
sftp_asset_ids = list(sftp_asset_ids)
|
||||||
]
|
|
||||||
data = [
|
data = [
|
||||||
{
|
{
|
||||||
'id': str(asset.id),
|
'id': str(asset.id),
|
||||||
'name': asset.name,
|
'name': asset.name,
|
||||||
'title':
|
'title': f'{asset.address}\n{asset.comment}',
|
||||||
f'{asset.address}\n{asset.comment}'
|
|
||||||
if asset.comment else asset.address,
|
|
||||||
'pId': pid or get_pid(asset),
|
'pId': pid or get_pid(asset),
|
||||||
'isParent': False,
|
'isParent': False,
|
||||||
'open': False,
|
'open': False,
|
||||||
@ -99,8 +92,7 @@ class SerializeToTreeNodeMixin:
|
|||||||
'data': {
|
'data': {
|
||||||
'platform_type': asset.platform.type,
|
'platform_type': asset.platform.type,
|
||||||
'org_name': asset.org_name,
|
'org_name': asset.org_name,
|
||||||
'sftp': (asset.platform_id in sftp_enabled_platform) \
|
'sftp': asset.id in sftp_asset_ids,
|
||||||
and (str(asset.id) in ssh_asset_ids),
|
|
||||||
'name': asset.name,
|
'name': asset.name,
|
||||||
'address': asset.address
|
'address': asset.address
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,7 @@ from collections import namedtuple, defaultdict
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from django.db.models.signals import m2m_changed
|
from django.db.models.signals import m2m_changed
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.generics import get_object_or_404
|
from rest_framework.generics import get_object_or_404
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class AssetsConfig(AppConfig):
|
class AssetsConfig(AppConfig):
|
||||||
@ -12,7 +12,6 @@ class AssetsConfig(AppConfig):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
|
from . import signal_handlers # noqa
|
||||||
|
from . import tasks # noqa
|
||||||
super().ready()
|
super().ready()
|
||||||
from . import signal_handlers
|
|
||||||
from . import tasks
|
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import yaml
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
from sshtunnel import SSHTunnelForwarder
|
||||||
|
|
||||||
from assets.automations.methods import platform_automation_methods
|
from assets.automations.methods import platform_automation_methods
|
||||||
from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen
|
from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen
|
||||||
@ -262,10 +262,10 @@ class BasePlaybookManager:
|
|||||||
info = self.file_to_json(runner.inventory)
|
info = self.file_to_json(runner.inventory)
|
||||||
servers, not_valid = [], []
|
servers, not_valid = [], []
|
||||||
for k, host in info['all']['hosts'].items():
|
for k, host in info['all']['hosts'].items():
|
||||||
jms_asset, jms_gateway = host['jms_asset'], host.get('gateway')
|
jms_asset, jms_gateway = host.get('jms_asset'), host.get('gateway')
|
||||||
if not jms_gateway:
|
if not jms_gateway:
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
server = SSHTunnelForwarder(
|
server = SSHTunnelForwarder(
|
||||||
(jms_gateway['address'], jms_gateway['port']),
|
(jms_gateway['address'], jms_gateway['port']),
|
||||||
ssh_username=jms_gateway['username'],
|
ssh_username=jms_gateway['username'],
|
||||||
@ -273,11 +273,10 @@ class BasePlaybookManager:
|
|||||||
ssh_pkey=jms_gateway['private_key_path'],
|
ssh_pkey=jms_gateway['private_key_path'],
|
||||||
remote_bind_address=(jms_asset['address'], jms_asset['port'])
|
remote_bind_address=(jms_asset['address'], jms_asset['port'])
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
server.start()
|
server.start()
|
||||||
except BaseSSHTunnelForwarderError:
|
except Exception as e:
|
||||||
err_msg = 'Gateway is not active: %s' % jms_asset.get('name', '')
|
err_msg = 'Gateway is not active: %s' % jms_asset.get('name', '')
|
||||||
print('\033[31m %s \033[0m\n' % err_msg)
|
print(f'\033[31m {err_msg} 原因: {e} \033[0m\n')
|
||||||
not_valid.append(k)
|
not_valid.append(k)
|
||||||
else:
|
else:
|
||||||
host['ansible_host'] = jms_asset['address'] = '127.0.0.1'
|
host['ansible_host'] = jms_asset['address'] = '127.0.0.1'
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
gather_facts: no
|
gather_facts: no
|
||||||
vars:
|
vars:
|
||||||
ansible_connection: local
|
ansible_connection: local
|
||||||
|
ansible_become: false
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
- name: Test asset connection (paramiko)
|
- name: Test asset connection (paramiko)
|
||||||
@ -12,3 +13,8 @@
|
|||||||
login_port: "{{ jms_asset.port }}"
|
login_port: "{{ jms_asset.port }}"
|
||||||
login_secret_type: "{{ jms_account.secret_type }}"
|
login_secret_type: "{{ jms_account.secret_type }}"
|
||||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||||
|
become: "{{ custom_become | default(False) }}"
|
||||||
|
become_method: "{{ custom_become_method | default('su') }}"
|
||||||
|
become_user: "{{ custom_become_user | default('') }}"
|
||||||
|
become_password: "{{ custom_become_password | default('') }}"
|
||||||
|
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
|
||||||
|
@ -2,7 +2,7 @@ import socket
|
|||||||
|
|
||||||
import paramiko
|
import paramiko
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.const import AutomationTypes, Connectivity
|
from assets.const import AutomationTypes, Connectivity
|
||||||
from assets.models import Gateway
|
from assets.models import Gateway
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db.models import TextChoices
|
from django.db.models import TextChoices
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Connectivity(TextChoices):
|
class Connectivity(TextChoices):
|
||||||
|
@ -33,10 +33,10 @@ class HostTypes(BaseType):
|
|||||||
def _get_protocol_constrains(cls) -> dict:
|
def _get_protocol_constrains(cls) -> dict:
|
||||||
return {
|
return {
|
||||||
'*': {
|
'*': {
|
||||||
'choices': ['ssh', 'telnet', 'vnc', 'rdp']
|
'choices': ['ssh', 'sftp', 'telnet', 'vnc', 'rdp']
|
||||||
},
|
},
|
||||||
cls.WINDOWS: {
|
cls.WINDOWS: {
|
||||||
'choices': ['rdp', 'ssh', 'vnc', 'winrm']
|
'choices': ['rdp', 'ssh', 'sftp', 'vnc', 'winrm']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ __all__ = ['Protocol']
|
|||||||
|
|
||||||
class Protocol(ChoicesMixin, models.TextChoices):
|
class Protocol(ChoicesMixin, models.TextChoices):
|
||||||
ssh = 'ssh', 'SSH'
|
ssh = 'ssh', 'SSH'
|
||||||
|
sftp = 'sftp', 'SFTP'
|
||||||
rdp = 'rdp', 'RDP'
|
rdp = 'rdp', 'RDP'
|
||||||
telnet = 'telnet', 'Telnet'
|
telnet = 'telnet', 'Telnet'
|
||||||
vnc = 'vnc', 'VNC'
|
vnc = 'vnc', 'VNC'
|
||||||
@ -36,17 +37,16 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||||||
cls.ssh: {
|
cls.ssh: {
|
||||||
'port': 22,
|
'port': 22,
|
||||||
'secret_types': ['password', 'ssh_key'],
|
'secret_types': ['password', 'ssh_key'],
|
||||||
'setting': {
|
|
||||||
'sftp_enabled': {
|
|
||||||
'type': 'bool',
|
|
||||||
'default': True,
|
|
||||||
'label': _('SFTP enabled')
|
|
||||||
},
|
},
|
||||||
|
cls.sftp: {
|
||||||
|
'port': 22,
|
||||||
|
'secret_types': ['password', 'ssh_key'],
|
||||||
|
'setting': {
|
||||||
'sftp_home': {
|
'sftp_home': {
|
||||||
'type': 'str',
|
'type': 'str',
|
||||||
'default': '/tmp',
|
'default': '/tmp',
|
||||||
'label': _('SFTP home')
|
'label': _('SFTP home')
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cls.rdp: {
|
cls.rdp: {
|
||||||
@ -81,6 +81,26 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||||||
cls.telnet: {
|
cls.telnet: {
|
||||||
'port': 23,
|
'port': 23,
|
||||||
'secret_types': ['password'],
|
'secret_types': ['password'],
|
||||||
|
'setting': {
|
||||||
|
'username_prompt': {
|
||||||
|
'type': 'str',
|
||||||
|
'default': 'username:|login:',
|
||||||
|
'label': _('Username prompt'),
|
||||||
|
'help_text': _('We will send username when we see this prompt')
|
||||||
|
},
|
||||||
|
'password_prompt': {
|
||||||
|
'type': 'str',
|
||||||
|
'default': 'password:',
|
||||||
|
'label': _('Password prompt'),
|
||||||
|
'help_text': _('We will send password when we see this prompt')
|
||||||
|
},
|
||||||
|
'success_prompt': {
|
||||||
|
'type': 'str',
|
||||||
|
'default': 'success|成功|#|>|\$',
|
||||||
|
'label': _('Success prompt'),
|
||||||
|
'help_text': _('We will consider login success when we see this prompt')
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cls.winrm: {
|
cls.winrm: {
|
||||||
'port': 5985,
|
'port': 5985,
|
||||||
@ -119,7 +139,15 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||||||
'port': 1521,
|
'port': 1521,
|
||||||
'required': True,
|
'required': True,
|
||||||
'secret_types': ['password'],
|
'secret_types': ['password'],
|
||||||
'xpack': True
|
'xpack': True,
|
||||||
|
'setting': {
|
||||||
|
'sysdba': {
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'label': _('SYSDBA'),
|
||||||
|
'help_text': _('Connect as SYSDBA')
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
cls.sqlserver: {
|
cls.sqlserver: {
|
||||||
'port': 1433,
|
'port': 1433,
|
||||||
@ -166,6 +194,15 @@ class Protocol(ChoicesMixin, models.TextChoices):
|
|||||||
'port_from_addr': True,
|
'port_from_addr': True,
|
||||||
'secret_types': ['password'],
|
'secret_types': ['password'],
|
||||||
'setting': {
|
'setting': {
|
||||||
|
'safe_mode': {
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'label': _('Safe mode'),
|
||||||
|
'help_text': _(
|
||||||
|
'When safe mode is enabled, some operations will be disabled, such as: '
|
||||||
|
'New tab, right click, visit other website, etc.'
|
||||||
|
)
|
||||||
|
},
|
||||||
'autofill': {
|
'autofill': {
|
||||||
'label': _('Autofill'),
|
'label': _('Autofill'),
|
||||||
'type': 'choice',
|
'type': 'choice',
|
||||||
|
@ -224,7 +224,7 @@ class AllTypes(ChoicesMixin):
|
|||||||
return dict(id='ROOT', name=_('All types'), title=_('All types'), open=True, isParent=True)
|
return dict(id='ROOT', name=_('All types'), title=_('All types'), open=True, isParent=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_tree_nodes(cls, resource_platforms, include_asset=False):
|
def get_tree_nodes(cls, resource_platforms, include_asset=False, get_root=True):
|
||||||
from ..models import Platform
|
from ..models import Platform
|
||||||
platform_count = defaultdict(int)
|
platform_count = defaultdict(int)
|
||||||
for platform_id in resource_platforms:
|
for platform_id in resource_platforms:
|
||||||
@ -239,10 +239,10 @@ class AllTypes(ChoicesMixin):
|
|||||||
category_type_mapper[p.category] += platform_count[p.id]
|
category_type_mapper[p.category] += platform_count[p.id]
|
||||||
tp_platforms[p.category + '_' + p.type].append(p)
|
tp_platforms[p.category + '_' + p.type].append(p)
|
||||||
|
|
||||||
nodes = [cls.get_root_nodes()]
|
nodes = [cls.get_root_nodes()] if get_root else []
|
||||||
for category, type_cls in cls.category_types():
|
for category, type_cls in cls.category_types():
|
||||||
# Category 格式化
|
# Category 格式化
|
||||||
meta = {'type': 'category', 'category': category.value}
|
meta = {'type': 'category', 'category': category.value, '_type': category.value}
|
||||||
category_node = cls.choice_to_node(category, 'ROOT', meta=meta)
|
category_node = cls.choice_to_node(category, 'ROOT', meta=meta)
|
||||||
category_count = category_type_mapper.get(category, 0)
|
category_count = category_type_mapper.get(category, 0)
|
||||||
category_node['name'] += f'({category_count})'
|
category_node['name'] += f'({category_count})'
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
from common.exceptions import JMSException
|
from common.exceptions import JMSException
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django_filters import rest_framework as drf_filters
|
|
||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
from rest_framework.compat import coreapi, coreschema
|
from rest_framework.compat import coreapi, coreschema
|
||||||
|
|
||||||
from assets.utils import get_node_from_request, is_query_node_all_assets
|
from assets.utils import get_node_from_request, is_query_node_all_assets
|
||||||
from common.drf.filters import BaseFilterSet
|
from .models import Label
|
||||||
|
|
||||||
from .models import Label, Node
|
|
||||||
|
|
||||||
|
|
||||||
class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||||
|
100
apps/assets/migrations/0121_auto_20230725_1458.py
Normal file
100
apps/assets/migrations/0121_auto_20230725_1458.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-07-25 06:58
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_platforms_sftp_protocol(apps, schema_editor):
|
||||||
|
platform_protocol_cls = apps.get_model('assets', 'PlatformProtocol')
|
||||||
|
platform_cls = apps.get_model('assets', 'Platform')
|
||||||
|
ssh_protocols = platform_protocol_cls.objects \
|
||||||
|
.filter(name='ssh', setting__sftp_enabled=True) \
|
||||||
|
.exclude(name__in=('Gateway', 'RemoteAppHost')) \
|
||||||
|
.filter(platform__type='linux')
|
||||||
|
platforms_has_sftp = platform_cls.objects.filter(protocols__name='sftp')
|
||||||
|
|
||||||
|
new_protocols = []
|
||||||
|
print("\nPlatform add sftp protocol: ")
|
||||||
|
for protocol in ssh_protocols:
|
||||||
|
protocol_setting = protocol.setting or {}
|
||||||
|
if protocol.platform in platforms_has_sftp:
|
||||||
|
continue
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'name': 'sftp',
|
||||||
|
'port': protocol.port,
|
||||||
|
'primary': False,
|
||||||
|
'required': False,
|
||||||
|
'default': True,
|
||||||
|
'public': True,
|
||||||
|
'setting': {
|
||||||
|
'sftp_home': protocol_setting.get('sftp_home', '/tmp'),
|
||||||
|
},
|
||||||
|
'platform': protocol.platform,
|
||||||
|
}
|
||||||
|
new_protocol = platform_protocol_cls(**kwargs)
|
||||||
|
new_protocols.append(new_protocol)
|
||||||
|
print(" - {}".format(protocol.platform.name))
|
||||||
|
|
||||||
|
new_protocols_dict = {(protocol.name, protocol.platform): protocol for protocol in new_protocols}
|
||||||
|
new_protocols = list(new_protocols_dict.values())
|
||||||
|
platform_protocol_cls.objects.bulk_create(new_protocols, ignore_conflicts=True)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_assets_sftp_protocol(apps, schema_editor):
|
||||||
|
asset_cls = apps.get_model('assets', 'Asset')
|
||||||
|
platform_cls = apps.get_model('assets', 'Platform')
|
||||||
|
protocol_cls = apps.get_model('assets', 'Protocol')
|
||||||
|
sftp_platforms = list(platform_cls.objects.filter(protocols__name='sftp').values_list('id'))
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
print("\nAsset add sftp protocol: ")
|
||||||
|
asset_ids = asset_cls.objects\
|
||||||
|
.filter(platform__in=sftp_platforms)\
|
||||||
|
.exclude(protocols__name='sftp')\
|
||||||
|
.distinct()\
|
||||||
|
.values_list('id', flat=True)
|
||||||
|
while True:
|
||||||
|
_asset_ids = asset_ids[count:count + 1000]
|
||||||
|
if not _asset_ids:
|
||||||
|
break
|
||||||
|
count += 1000
|
||||||
|
|
||||||
|
new_protocols = []
|
||||||
|
ssh_protocols = protocol_cls.objects.filter(name='ssh', asset_id__in=_asset_ids).distinct()
|
||||||
|
ssh_protocols_map = {protocol.asset_id: protocol for protocol in ssh_protocols}
|
||||||
|
for asset_id, protocol in ssh_protocols_map.items():
|
||||||
|
new_protocols.append(protocol_cls(name='sftp', port=protocol.port, asset_id=asset_id))
|
||||||
|
protocol_cls.objects.bulk_create(new_protocols, ignore_conflicts=True)
|
||||||
|
print(" - Add {}".format(len(new_protocols)))
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_telnet_regex(apps, schema_editor):
|
||||||
|
setting_cls = apps.get_model('settings', 'Setting')
|
||||||
|
setting = setting_cls.objects.filter(name='TERMINAL_TELNET_REGEX').first()
|
||||||
|
if not setting:
|
||||||
|
print("Not found telnet regex setting, skip")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
value = json.loads(setting.value)
|
||||||
|
except Exception:
|
||||||
|
print("Invalid telnet regex setting, skip")
|
||||||
|
return
|
||||||
|
platform_protocol_cls = apps.get_model('assets', 'PlatformProtocol')
|
||||||
|
telnets = platform_protocol_cls.objects.filter(name='telnet')
|
||||||
|
if telnets.count() > 0:
|
||||||
|
telnets.update(setting={'success_prompt': value})
|
||||||
|
print("Migrate telnet regex setting success: ", telnets.count())
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0120_auto_20230630_1613'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_platforms_sftp_protocol),
|
||||||
|
migrations.RunPython(migrate_assets_sftp_protocol),
|
||||||
|
migrations.RunPython(migrate_telnet_regex),
|
||||||
|
]
|
24
apps/assets/migrations/0122_auto_20230803_1553.py
Normal file
24
apps/assets/migrations/0122_auto_20230803_1553.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 4.1.10 on 2023-08-03 07:53
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_web_setting_safe_mode(apps, schema_editor):
|
||||||
|
platform_protocol_cls = apps.get_model('assets', 'PlatformProtocol')
|
||||||
|
protocols = platform_protocol_cls.objects.filter(name='http')
|
||||||
|
for protocol in protocols:
|
||||||
|
setting = protocol.setting or {}
|
||||||
|
setting['safe_mode'] = False
|
||||||
|
protocol.setting = setting
|
||||||
|
protocol.save(update_fields=['setting'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('assets', '0121_auto_20230725_1458'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(migrate_web_setting_safe_mode),
|
||||||
|
]
|
@ -8,7 +8,7 @@ from collections import defaultdict
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import model_to_dict
|
from django.forms import model_to_dict
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets import const
|
from assets import const
|
||||||
from common.db.fields import EncryptMixin
|
from common.db.fields import EncryptMixin
|
||||||
@ -221,8 +221,11 @@ class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseMode
|
|||||||
return self.address
|
return self.address
|
||||||
|
|
||||||
def get_target_ssh_port(self):
|
def get_target_ssh_port(self):
|
||||||
protocol = self.protocols.all().filter(name='ssh').first()
|
return self.get_protocol_port('ssh')
|
||||||
return protocol.port if protocol else 22
|
|
||||||
|
def get_protocol_port(self, protocol):
|
||||||
|
protocol = self.protocols.all().filter(name=protocol).first()
|
||||||
|
return protocol.port if protocol else 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .common import Asset
|
from .common import Asset
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import uuid
|
|||||||
|
|
||||||
from celery import current_task
|
from celery import current_task
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.models.asset import Asset
|
from assets.models.asset import Asset
|
||||||
from assets.models.node import Node
|
from assets.models.node import Node
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.const import AutomationTypes
|
from assets.const import AutomationTypes
|
||||||
from .base import AssetBaseAutomation
|
from .base import AssetBaseAutomation
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.const import AutomationTypes
|
from assets.const import AutomationTypes
|
||||||
from .base import AssetBaseAutomation
|
from .base import AssetBaseAutomation
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.const import Connectivity
|
from assets.const import Connectivity
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
|
@ -4,7 +4,7 @@ import uuid
|
|||||||
|
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from orgs.mixins.models import OrgModelMixin
|
from orgs.mixins.models import OrgModelMixin
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.db.models import JMSBaseModel
|
from common.db.models import JMSBaseModel
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
#
|
#
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from assets.const import GATEWAY_NAME
|
from assets.const import GATEWAY_NAME
|
||||||
from assets.models.platform import Platform
|
from assets.models.platform import Platform
|
||||||
|
@ -7,7 +7,7 @@ from __future__ import unicode_literals
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
__all__ = ['AssetGroup']
|
__all__ = ['AssetGroup']
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import lazyproperty
|
from common.utils import lazyproperty
|
||||||
from orgs.mixins.models import JMSOrgBaseModel
|
from orgs.mixins.models import JMSOrgBaseModel
|
||||||
|
@ -10,8 +10,7 @@ from django.core.cache import cache
|
|||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Q, Manager
|
from django.db.models import Q, Manager
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.utils.translation import ugettext
|
from django.utils.translation import gettext_lazy as _, gettext
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from common.db.models import output_as_string
|
from common.db.models import output_as_string
|
||||||
from common.utils import get_logger
|
from common.utils import get_logger
|
||||||
@ -163,7 +162,7 @@ class FamilyMixin:
|
|||||||
return key
|
return key
|
||||||
|
|
||||||
def get_next_child_preset_name(self):
|
def get_next_child_preset_name(self):
|
||||||
name = ugettext("New node")
|
name = gettext("New node")
|
||||||
values = [
|
values = [
|
||||||
child.value[child.value.rfind(' '):]
|
child.value[child.value.rfind(' '):]
|
||||||
for child in self.get_children()
|
for child in self.get_children()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user