mirror of
https://github.com/jumpserver/jumpserver.git
synced 2025-08-01 22:58:59 +00:00
commit
8df720f19e
@ -44,8 +44,8 @@ ARG TOOLS=" \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
@ -63,9 +63,9 @@ RUN --mount=type=cache,target=/root/.cache \
|
||||
--mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \
|
||||
set -ex \
|
||||
&& python3 -m venv /opt/py3 \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& pip install poetry -i ${PIP_MIRROR} \
|
||||
&& poetry config virtualenvs.create false \
|
||||
&& . /opt/py3/bin/activate \
|
||||
&& poetry install
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
@ -75,8 +75,9 @@ ENV LANG=zh_CN.UTF-8 \
|
||||
|
||||
ARG DEPENDENCIES=" \
|
||||
libjpeg-dev \
|
||||
libxmlsec1-openssl \
|
||||
libx11-dev"
|
||||
libx11-dev \
|
||||
freerdp2-dev \
|
||||
libxmlsec1-openssl"
|
||||
|
||||
ARG TOOLS=" \
|
||||
ca-certificates \
|
||||
@ -94,8 +95,8 @@ ARG TOOLS=" \
|
||||
wget"
|
||||
|
||||
ARG APT_MIRROR=http://mirrors.ustc.edu.cn
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \
|
||||
sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \
|
||||
&& rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
@ -118,7 +119,6 @@ ARG VERSION
|
||||
ENV VERSION=$VERSION
|
||||
|
||||
VOLUME /opt/jumpserver/data
|
||||
VOLUME /opt/jumpserver/logs
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
from rest_framework.generics import CreateAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from accounts import serializers
|
||||
from accounts.tasks import verify_accounts_connectivity_task, push_accounts_to_assets_task
|
||||
from accounts.permissions import AccountTaskActionPermission
|
||||
from accounts.tasks import (
|
||||
remove_accounts_task, verify_accounts_connectivity_task, push_accounts_to_assets_task
|
||||
)
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
from authentication.permissions import UserConfirmation, ConfirmType
|
||||
|
||||
__all__ = [
|
||||
'AccountsTaskCreateAPI',
|
||||
@ -12,16 +15,16 @@ __all__ = [
|
||||
|
||||
class AccountsTaskCreateAPI(CreateAPIView):
|
||||
serializer_class = serializers.AccountTaskSerializer
|
||||
permission_classes = (AccountTaskActionPermission,)
|
||||
|
||||
def check_permissions(self, request):
|
||||
act = request.data.get('action')
|
||||
if act == 'push':
|
||||
code = 'accounts.push_account'
|
||||
else:
|
||||
code = 'accounts.verify_account'
|
||||
has = request.user.has_perm(code)
|
||||
if not has:
|
||||
self.permission_denied(request)
|
||||
def get_permissions(self):
|
||||
act = self.request.data.get('action')
|
||||
if act == 'remove':
|
||||
self.permission_classes = [
|
||||
AccountTaskActionPermission,
|
||||
UserConfirmation.require(ConfirmType.PASSWORD)
|
||||
]
|
||||
return super().get_permissions()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
data = serializer.validated_data
|
||||
@ -31,6 +34,10 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||
|
||||
if data['action'] == 'push':
|
||||
task = push_accounts_to_assets_task.delay(account_ids, params)
|
||||
elif data['action'] == 'remove':
|
||||
gather_accounts = data.get('gather_accounts', [])
|
||||
gather_account_ids = [str(a.id) for a in gather_accounts]
|
||||
task = remove_accounts_task.delay(gather_account_ids)
|
||||
else:
|
||||
account = accounts[0]
|
||||
asset = account.asset
|
||||
@ -43,9 +50,3 @@ class AccountsTaskCreateAPI(CreateAPIView):
|
||||
data["task"] = task.id
|
||||
setattr(serializer, '_data', data)
|
||||
return task
|
||||
|
||||
def get_exception_handler(self):
|
||||
def handler(e, context):
|
||||
return Response({"error": str(e)}, status=401)
|
||||
|
||||
return handler
|
||||
|
@ -1,7 +1,6 @@
|
||||
- hosts: custom
|
||||
gather_facts: no
|
||||
vars:
|
||||
asset_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
|
||||
ansible_connection: local
|
||||
ansible_become: false
|
||||
|
||||
@ -9,7 +8,7 @@
|
||||
- name: Test privileged account (paramiko)
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ asset_port }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
@ -27,7 +26,7 @@
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ asset_port }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ custom_become | default(False) }}"
|
||||
@ -49,7 +48,7 @@
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ asset_port }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
become: "{{ account.become.ansible_become | default(False) }}"
|
||||
become_method: su
|
||||
become_user: "{{ account.become.ansible_user | default('') }}"
|
||||
|
@ -6,15 +6,26 @@ category:
|
||||
type:
|
||||
- all
|
||||
method: change_secret
|
||||
protocol: ssh
|
||||
params:
|
||||
- name: commands
|
||||
type: list
|
||||
label: '自定义命令'
|
||||
label: "{{ 'Params commands label' | trans }}"
|
||||
default: [ '' ]
|
||||
help_text: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||
help_text: "{{ 'Params commands help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
SSH account change secret:
|
||||
zh: 使用 SSH 命令行自定义改密
|
||||
ja: SSH コマンドライン方式でカスタムパスワード変更
|
||||
en: Custom password change by SSH command line
|
||||
zh: '使用 SSH 命令行自定义改密'
|
||||
ja: 'SSH コマンドライン方式でカスタムパスワード変更'
|
||||
en: 'Custom password change by SSH command line'
|
||||
|
||||
Params commands help text:
|
||||
zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,<br />请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。<br />比如针对 Cisco 主机进行改密,一般需要配置五条命令:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br />4. username {username} privilege 0 password {password} <br />5. end'
|
||||
ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、<br />{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。 <br />たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:<br />1.enable<br />2.{login_password}<br />3 .ターミナルの設定<br / >4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード} <br />5. 終了'
|
||||
en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,<br />Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task. <br />For example, to change the password of a Cisco host, you generally need to configure five commands:<br />1. enable<br />2. {login_password}<br />3. configure terminal<br / >4. username {username} privilege 0 password {password} <br />5. end'
|
||||
|
||||
Params commands label:
|
||||
zh: '自定义命令'
|
||||
ja: 'カスタムコマンド'
|
||||
en: 'Custom command'
|
||||
|
@ -3,6 +3,7 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@ -11,10 +12,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@ -28,10 +29,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@ -45,8 +46,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
|
@ -139,7 +139,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': secret_type,
|
||||
'secret': new_secret,
|
||||
'secret': account.escape_jinja2_syntax(new_secret),
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
from .push_account.manager import PushAccountManager
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .verify_account.manager import VerifyAccountManager
|
||||
from .backup_account.manager import AccountBackupManager
|
||||
from .change_secret.manager import ChangeSecretManager
|
||||
from .gather_accounts.manager import GatherAccountsManager
|
||||
from .push_account.manager import PushAccountManager
|
||||
from .remove_account.manager import RemoveAccountManager
|
||||
from .verify_account.manager import VerifyAccountManager
|
||||
from .verify_gateway_account.manager import VerifyGatewayAccountManager
|
||||
from ..const import AutomationTypes
|
||||
|
||||
@ -12,6 +13,7 @@ class ExecutionManager:
|
||||
AutomationTypes.push_account: PushAccountManager,
|
||||
AutomationTypes.change_secret: ChangeSecretManager,
|
||||
AutomationTypes.verify_account: VerifyAccountManager,
|
||||
AutomationTypes.remove_account: RemoveAccountManager,
|
||||
AutomationTypes.gather_accounts: GatherAccountsManager,
|
||||
AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager,
|
||||
# TODO 后期迁移到自动化策略中
|
||||
|
@ -2,6 +2,7 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@ -10,10 +11,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: users
|
||||
register: db_info
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
db_name: "{{ jms_asset.spec_info.db_name }}"
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@ -11,10 +12,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
@ -28,10 +29,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
password: "{{ account.secret }}"
|
||||
host: "%"
|
||||
@ -45,8 +46,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
|
@ -9,7 +9,7 @@ params:
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
@ -18,19 +18,44 @@ params:
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: '家目录'
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: '默认家目录 /home/系统用户名: /home/username'
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Aix account push:
|
||||
zh: 使用 Ansible 模块 user 执行 Aix 账号推送 (DES)
|
||||
ja: Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)
|
||||
en: Using Ansible module user to push account (DES)
|
||||
zh: '使用 Ansible 模块 user 执行 Aix 账号推送 (DES)'
|
||||
ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)'
|
||||
en: 'Using Ansible module user to push account (DES)'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
Params home help text:
|
||||
zh: '默认家目录 /home/{账号用户名}'
|
||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||
en: 'Default home directory /home/{account username}'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
en: 'Home'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
||||
|
||||
|
@ -10,7 +10,7 @@ params:
|
||||
type: str
|
||||
label: 'Sudo'
|
||||
default: '/bin/whoami'
|
||||
help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
help_text: "{{ 'Params sudo help text' | trans }}"
|
||||
|
||||
- name: shell
|
||||
type: str
|
||||
@ -20,18 +20,43 @@ params:
|
||||
|
||||
- name: home
|
||||
type: str
|
||||
label: '家目录'
|
||||
label: "{{ 'Params home label' | trans }}"
|
||||
default: ''
|
||||
help_text: '默认家目录 /home/系统用户名: /home/username'
|
||||
help_text: "{{ 'Params home help text' | trans }}"
|
||||
|
||||
- name: groups
|
||||
type: str
|
||||
label: '用户组'
|
||||
label: "{{ 'Params groups label' | trans }}"
|
||||
default: ''
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Posix account push:
|
||||
zh: 使用 Ansible 模块 user 执行账号推送 (sha512)
|
||||
ja: Ansible user モジュールを使用してアカウントをプッシュする (sha512)
|
||||
en: Using Ansible module user to push account (sha512)
|
||||
zh: '使用 Ansible 模块 user 执行账号推送 (sha512)'
|
||||
ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)'
|
||||
en: 'Using Ansible module user to push account (sha512)'
|
||||
|
||||
Params sudo help text:
|
||||
zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig'
|
||||
ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig'
|
||||
en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig'
|
||||
|
||||
Params home help text:
|
||||
zh: '默认家目录 /home/{账号用户名}'
|
||||
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
|
||||
en: 'Default home directory /home/{account username}'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
||||
Params home label:
|
||||
zh: '家目录'
|
||||
ja: 'ホームディレクトリ'
|
||||
en: 'Home'
|
||||
|
||||
Params groups label:
|
||||
zh: '用户组'
|
||||
ja: 'グループ'
|
||||
en: 'Groups'
|
@ -10,10 +10,15 @@ params:
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Windows account push:
|
||||
zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送
|
||||
ja: Ansible win_user モジュールを使用して Windows アカウントをプッシュする
|
||||
en: Using Ansible module win_user to push account
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号推送'
|
||||
ja: 'Ansible win_user モジュールを使用して Windows アカウントをプッシュする'
|
||||
en: 'Using Ansible module win_user to push account'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
@ -10,10 +10,15 @@ params:
|
||||
type: str
|
||||
label: '用户组'
|
||||
default: 'Users,Remote Desktop Users'
|
||||
help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
help_text: "{{ 'Params groups help text' | trans }}"
|
||||
|
||||
i18n:
|
||||
Windows account push rdp verify:
|
||||
zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送 RDP 协议测试最后的可连接性
|
||||
ja: Ansibleモジュールwin_userがWindowsアカウントプッシュRDPプロトコルテストを実行する最後の接続性
|
||||
en: Using the Ansible module win_user performs Windows account push RDP protocol testing for final connectivity
|
||||
zh: '使用 Ansible 模块 win_user 执行 Windows 账号推送(最后使用 Python 模块 pyfreerdp 验证账号的可连接性)'
|
||||
ja: 'Ansible モジュール win_user を使用して Windows アカウントのプッシュを実行します (最後に Python モジュール pyfreerdp を使用してアカウントの接続性を確認します)'
|
||||
en: 'Use the Ansible module win_user to perform Windows account push (finally use the Python module pyfreerdp to verify the connectability of the account)'
|
||||
|
||||
Params groups help text:
|
||||
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
|
||||
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
|
||||
en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)'
|
||||
|
@ -0,0 +1,21 @@
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
mongodb_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
ssl: "{{ jms_asset.spec_info.use_ssl }}"
|
||||
ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}"
|
||||
ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}"
|
||||
connection_options:
|
||||
- tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
@ -0,0 +1,12 @@
|
||||
id: remove_account_mongodb
|
||||
name: "{{ 'MongoDB account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mongodb
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
MongoDB account remove:
|
||||
zh: 使用 Ansible 模块 mongodb 删除账号
|
||||
ja: Ansible モジュール mongodb を使用してアカウントを削除する
|
||||
en: Delete account using Ansible module mongodb
|
@ -0,0 +1,18 @@
|
||||
- hosts: mysql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
community.mysql.mysql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
@ -0,0 +1,14 @@
|
||||
id: remove_account_mysql
|
||||
name: "{{ 'MySQL account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- mysql
|
||||
- mariadb
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
MySQL account remove:
|
||||
zh: 使用 Ansible 模块 mysql_user 删除账号
|
||||
ja: Ansible モジュール mysql_user を使用してアカウントを削除します
|
||||
en: Use the Ansible module mysql_user to delete the account
|
||||
|
@ -0,0 +1,16 @@
|
||||
- hosts: oracle
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
oracle_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_database: "{{ jms_asset.spec_info.db_name }}"
|
||||
mode: "{{ jms_account.mode }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
@ -0,0 +1,12 @@
|
||||
id: remove_account_oracle
|
||||
name: "{{ 'Oracle account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- oracle
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
Oracle account remove:
|
||||
zh: 使用 Python 模块 oracledb 删除账号
|
||||
ja: Python モジュール oracledb を使用してアカウントを検証する
|
||||
en: Using Python module oracledb to verify account
|
@ -0,0 +1,15 @@
|
||||
- hosts: postgresql
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
community.postgresql.postgresql_user:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
db: "{{ jms_asset.spec_info.db_name }}"
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
@ -0,0 +1,12 @@
|
||||
id: remove_account_postgresql
|
||||
name: "{{ 'PostgreSQL account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- postgresql
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
PostgreSQL account remove:
|
||||
zh: 使用 Ansible 模块 postgresql_user 删除账号
|
||||
ja: Ansible モジュール postgresql_user を使用してアカウントを削除します
|
||||
en: Use the Ansible module postgresql_user to delete the account
|
@ -0,0 +1,14 @@
|
||||
- hosts: sqlserver
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
community.general.mssql_script:
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
name: "{{ jms_asset.spec_info.db_name }}"
|
||||
script: "DROP USER {{ account.username }}"
|
@ -0,0 +1,12 @@
|
||||
id: remove_account_sqlserver
|
||||
name: "{{ 'SQLServer account remove' | trans }}"
|
||||
category: database
|
||||
type:
|
||||
- sqlserver
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
SQLServer account remove:
|
||||
zh: 使用 Ansible 模块 mssql 删除账号
|
||||
ja: Ansible モジュール mssql を使用してアカウントを削除する
|
||||
en: Use Ansible module mssql to delete account
|
25
apps/accounts/automations/remove_account/host/posix/main.yml
Normal file
25
apps/accounts/automations/remove_account/host/posix/main.yml
Normal file
@ -0,0 +1,25 @@
|
||||
- hosts: demo
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Get user home directory path"
|
||||
ansible.builtin.shell:
|
||||
cmd: "getent passwd {{ account.username }} | cut -d: -f6"
|
||||
register: user_home_dir
|
||||
ignore_errors: yes
|
||||
|
||||
- name: "Check if user home directory exists"
|
||||
ansible.builtin.stat:
|
||||
path: "{{ user_home_dir.stdout }}"
|
||||
register: home_dir
|
||||
when: user_home_dir.stdout != ""
|
||||
|
||||
- name: "Rename user home directory if it exists"
|
||||
ansible.builtin.command:
|
||||
cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak"
|
||||
when: home_dir.stat.exists and user_home_dir.stdout != ""
|
||||
|
||||
- name: "Remove account"
|
||||
ansible.builtin.user:
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
remove: "{{ home_dir.stat.exists }}"
|
@ -0,0 +1,13 @@
|
||||
id: remove_account_posix
|
||||
name: "{{ 'Posix account remove' | trans }}"
|
||||
category: host
|
||||
type:
|
||||
- linux
|
||||
- unix
|
||||
method: remove_account
|
||||
|
||||
i18n:
|
||||
Posix account remove:
|
||||
zh: 使用 Ansible 模块 user 删除账号
|
||||
ja: Ansible モジュール ユーザーを使用してアカウントを削除します
|
||||
en: Use the Ansible module user to delete the account
|
@ -0,0 +1,9 @@
|
||||
- hosts: windows
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: "Remove account"
|
||||
ansible.windows.win_user:
|
||||
name: "{{ account.username }}"
|
||||
state: absent
|
||||
purge: yes
|
||||
force: yes
|
@ -0,0 +1,13 @@
|
||||
id: remove_account_windows
|
||||
name: "{{ 'Windows account remove' | trans }}"
|
||||
version: 1
|
||||
method: remove_account
|
||||
category: host
|
||||
type:
|
||||
- windows
|
||||
|
||||
i18n:
|
||||
Windows account remove:
|
||||
zh: 使用 Ansible 模块 win_user 删除账号
|
||||
ja: Ansible モジュール win_user を使用してアカウントを削除する
|
||||
en: Use the Ansible module win_user to delete an account
|
67
apps/accounts/automations/remove_account/manager.py
Normal file
67
apps/accounts/automations/remove_account/manager.py
Normal file
@ -0,0 +1,67 @@
|
||||
import os
|
||||
from copy import deepcopy
|
||||
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.models import Account
|
||||
from common.utils import get_logger
|
||||
from ..base.manager import AccountBasePlaybookManager
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class RemoveAccountManager(AccountBasePlaybookManager):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_account_mapper = {}
|
||||
|
||||
def prepare_runtime_dir(self):
|
||||
path = super().prepare_runtime_dir()
|
||||
ansible_config_path = os.path.join(path, 'ansible.cfg')
|
||||
|
||||
with open(ansible_config_path, 'w') as f:
|
||||
f.write('[ssh_connection]\n')
|
||||
f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n')
|
||||
return path
|
||||
|
||||
@classmethod
|
||||
def method_type(cls):
|
||||
return AutomationTypes.remove_account
|
||||
|
||||
def get_gather_accounts(self, privilege_account, gather_accounts: QuerySet):
|
||||
gather_account_ids = self.execution.snapshot['gather_accounts']
|
||||
gather_accounts = gather_accounts.filter(id__in=gather_account_ids)
|
||||
gather_accounts = gather_accounts.exclude(
|
||||
username__in=[privilege_account.username, 'root', 'Administrator']
|
||||
)
|
||||
return gather_accounts
|
||||
|
||||
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
|
||||
if host.get('error'):
|
||||
return host
|
||||
|
||||
gather_accounts = asset.gatheredaccount_set.all()
|
||||
gather_accounts = self.get_gather_accounts(account, gather_accounts)
|
||||
|
||||
inventory_hosts = []
|
||||
|
||||
for gather_account in gather_accounts:
|
||||
h = deepcopy(host)
|
||||
h['name'] += '(' + gather_account.username + ')'
|
||||
self.host_account_mapper[h['name']] = (asset, gather_account)
|
||||
h['account'] = {'username': gather_account.username}
|
||||
inventory_hosts.append(h)
|
||||
return inventory_hosts
|
||||
|
||||
def on_host_success(self, host, result):
|
||||
tuple_asset_gather_account = self.host_account_mapper.get(host)
|
||||
if not tuple_asset_gather_account:
|
||||
return
|
||||
asset, gather_account = tuple_asset_gather_account
|
||||
Account.objects.filter(
|
||||
asset_id=asset.id,
|
||||
username=gather_account.username
|
||||
).delete()
|
||||
gather_account.delete()
|
@ -8,7 +8,7 @@
|
||||
- name: Verify account (pyfreerdp)
|
||||
rdp_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
|
@ -5,9 +5,10 @@ category:
|
||||
type:
|
||||
- windows
|
||||
method: verify_account
|
||||
protocol: rdp
|
||||
|
||||
i18n:
|
||||
Windows rdp account verify:
|
||||
zh: 使用 Python 模块 pyfreerdp 验证账号
|
||||
ja: Python モジュール pyfreerdp を使用してアカウントを検証する
|
||||
en: Using Python module pyfreerdp to verify account
|
||||
zh: '使用 Python 模块 pyfreerdp 验证账号'
|
||||
ja: 'Python モジュール pyfreerdp を使用してアカウントを検証する'
|
||||
en: 'Using Python module pyfreerdp to verify account'
|
||||
|
@ -9,7 +9,7 @@
|
||||
- name: Verify account (paramiko)
|
||||
ssh_ping:
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_user: "{{ account.username }}"
|
||||
login_password: "{{ account.secret }}"
|
||||
login_secret_type: "{{ account.secret_type }}"
|
||||
|
@ -6,9 +6,10 @@ category:
|
||||
type:
|
||||
- all
|
||||
method: verify_account
|
||||
protocol: ssh
|
||||
|
||||
i18n:
|
||||
SSH account verify:
|
||||
zh: 使用 Python 模块 paramiko 验证账号
|
||||
ja: Python モジュール paramiko を使用してアカウントを検証する
|
||||
en: Using Python module paramiko to verify account
|
||||
zh: '使用 Python 模块 paramiko 验证账号'
|
||||
ja: 'Python モジュール paramiko を使用してアカウントを検証する'
|
||||
en: 'Using Python module paramiko to verify account'
|
||||
|
@ -1,4 +1,4 @@
|
||||
- hosts: mongdb
|
||||
- hosts: mongodb
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
|
@ -2,6 +2,7 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Verify account
|
||||
@ -10,8 +11,8 @@
|
||||
login_password: "{{ account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
|
@ -62,7 +62,7 @@ class VerifyAccountManager(AccountBasePlaybookManager):
|
||||
'name': account.name,
|
||||
'username': account.username,
|
||||
'secret_type': account.secret_type,
|
||||
'secret': secret,
|
||||
'secret': account.escape_jinja2_syntax(secret),
|
||||
'private_key_path': private_key_path,
|
||||
'become': account.get_ansible_become_auth(),
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ class AutomationTypes(models.TextChoices):
|
||||
push_account = 'push_account', _('Push account')
|
||||
change_secret = 'change_secret', _('Change secret')
|
||||
verify_account = 'verify_account', _('Verify account')
|
||||
remove_account = 'remove_account', _('Remove account')
|
||||
gather_accounts = 'gather_accounts', _('Gather accounts')
|
||||
verify_gateway_account = 'verify_gateway_account', _('Verify gateway account')
|
||||
|
||||
|
@ -51,6 +51,7 @@ class AccountFilterSet(BaseFilterSet):
|
||||
|
||||
class GatheredAccountFilterSet(BaseFilterSet):
|
||||
node_id = drf_filters.CharFilter(method='filter_nodes')
|
||||
asset_id = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact')
|
||||
|
||||
@staticmethod
|
||||
def filter_nodes(queryset, name, value):
|
||||
@ -58,4 +59,4 @@ class GatheredAccountFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = GatheredAccount
|
||||
fields = ['id', 'asset_id', 'username']
|
||||
fields = ['id', 'username']
|
||||
|
@ -4,7 +4,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0006_gatheredaccount'),
|
||||
]
|
||||
@ -12,6 +11,13 @@ class Migration(migrations.Migration):
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='account',
|
||||
options={'permissions': [('view_accountsecret', 'Can view asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret'), ('verify_account', 'Can verify account'), ('push_account', 'Can push account')], 'verbose_name': 'Account'},
|
||||
options={'permissions': [
|
||||
('view_accountsecret', 'Can view asset account secret'),
|
||||
('view_historyaccount', 'Can view asset history account'),
|
||||
('view_historyaccountsecret', 'Can view asset history account secret'),
|
||||
('verify_account', 'Can verify account'),
|
||||
('push_account', 'Can push account'),
|
||||
('remove_account', 'Can remove account'),
|
||||
], 'verbose_name': 'Account'},
|
||||
),
|
||||
]
|
||||
|
@ -4,6 +4,7 @@ from simple_history.models import HistoricalRecords
|
||||
|
||||
from assets.models.base import AbsConnectivity
|
||||
from common.utils import lazyproperty
|
||||
from labels.mixins import LabeledMixin
|
||||
from .base import BaseAccount
|
||||
from .mixins import VaultModelMixin
|
||||
from ..const import Source
|
||||
@ -42,7 +43,7 @@ class AccountHistoricalRecords(HistoricalRecords):
|
||||
return super().create_history_model(model, inherited)
|
||||
|
||||
|
||||
class Account(AbsConnectivity, BaseAccount):
|
||||
class Account(AbsConnectivity, LabeledMixin, BaseAccount):
|
||||
asset = models.ForeignKey(
|
||||
'assets.Asset', related_name='accounts',
|
||||
on_delete=models.CASCADE, verbose_name=_('Asset')
|
||||
@ -68,10 +69,15 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
('view_historyaccountsecret', _('Can view asset history account secret')),
|
||||
('verify_account', _('Can verify account')),
|
||||
('push_account', _('Can push account')),
|
||||
('remove_account', _('Can remove account')),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return '{}'.format(self.username)
|
||||
if self.asset_id:
|
||||
host = self.asset.name
|
||||
else:
|
||||
host = 'Dynamic'
|
||||
return '{}({})'.format(self.name, host)
|
||||
|
||||
@lazyproperty
|
||||
def platform(self):
|
||||
@ -95,14 +101,13 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
""" 排除自己和以自己为 su-from 的账号 """
|
||||
return self.asset.accounts.exclude(id=self.id).exclude(su_from=self)
|
||||
|
||||
@staticmethod
|
||||
def make_account_ansible_vars(su_from):
|
||||
def make_account_ansible_vars(self, su_from):
|
||||
var = {
|
||||
'ansible_user': su_from.username,
|
||||
}
|
||||
if not su_from.secret:
|
||||
return var
|
||||
var['ansible_password'] = su_from.secret
|
||||
var['ansible_password'] = self.escape_jinja2_syntax(su_from.secret)
|
||||
var['ansible_ssh_private_key_file'] = su_from.private_key_path
|
||||
return var
|
||||
|
||||
@ -119,9 +124,25 @@ class Account(AbsConnectivity, BaseAccount):
|
||||
auth['ansible_become'] = True
|
||||
auth['ansible_become_method'] = become_method
|
||||
auth['ansible_become_user'] = self.username
|
||||
auth['ansible_become_password'] = password
|
||||
auth['ansible_become_password'] = self.escape_jinja2_syntax(password)
|
||||
return auth
|
||||
|
||||
@staticmethod
|
||||
def escape_jinja2_syntax(value):
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
def escape(v):
|
||||
v = v.replace('{{', '__TEMP_OPEN_BRACES__') \
|
||||
.replace('}}', '__TEMP_CLOSE_BRACES__')
|
||||
|
||||
v = v.replace('__TEMP_OPEN_BRACES__', '{{ "{{" }}') \
|
||||
.replace('__TEMP_CLOSE_BRACES__', '{{ "}}" }}')
|
||||
|
||||
return v.replace('{%', '{{ "{%" }}').replace('%}', '{{ "%}" }}')
|
||||
|
||||
return escape(value)
|
||||
|
||||
|
||||
def replace_history_model_with_mixin():
|
||||
"""
|
||||
|
@ -3,13 +3,14 @@ from django.db.models import Count, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from labels.mixins import LabeledMixin
|
||||
from .account import Account
|
||||
from .base import BaseAccount, SecretWithRandomMixin
|
||||
|
||||
__all__ = ['AccountTemplate', ]
|
||||
|
||||
|
||||
class AccountTemplate(BaseAccount, SecretWithRandomMixin):
|
||||
class AccountTemplate(LabeledMixin, BaseAccount, SecretWithRandomMixin):
|
||||
su_from = models.ForeignKey(
|
||||
'self', related_name='su_to', null=True,
|
||||
on_delete=models.SET_NULL, verbose_name=_("Su from")
|
||||
|
@ -3,8 +3,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storage
|
||||
from notifications.notifications import UserMessage
|
||||
from users.models import User
|
||||
from terminal.models.component.storage import ReplayStorage
|
||||
from users.models import User
|
||||
|
||||
|
||||
class AccountBackupExecutionTaskMsg(object):
|
||||
@ -23,8 +23,8 @@ class AccountBackupExecutionTaskMsg(object):
|
||||
else:
|
||||
return _("{} - The account backup passage task has been completed: "
|
||||
"the encryption password has not been set - "
|
||||
"please go to personal information -> file encryption password "
|
||||
"to set the encryption password").format(name)
|
||||
"please go to personal information -> Basic file encryption password for preference settings"
|
||||
).format(name)
|
||||
|
||||
def publish(self, attachment_list=None):
|
||||
send_mail_attachment_async(
|
||||
|
19
apps/accounts/permissions.py
Normal file
19
apps/accounts/permissions.py
Normal file
@ -0,0 +1,19 @@
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
def check_permissions(request):
|
||||
act = request.data.get('action')
|
||||
if act == 'push':
|
||||
code = 'accounts.push_account'
|
||||
elif act == 'remove':
|
||||
code = 'accounts.remove_account'
|
||||
else:
|
||||
code = 'accounts.verify_account'
|
||||
return request.user.has_perm(code)
|
||||
|
||||
|
||||
class AccountTaskActionPermission(permissions.IsAuthenticated):
|
||||
|
||||
def has_permission(self, request, view):
|
||||
return super().has_permission(request, view) \
|
||||
and check_permissions(request)
|
@ -10,7 +10,7 @@ from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from accounts.const import SecretType, Source, AccountInvalidPolicy
|
||||
from accounts.models import Account, AccountTemplate
|
||||
from accounts.models import Account, AccountTemplate, GatheredAccount
|
||||
from accounts.tasks import push_accounts_to_assets_task
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Asset
|
||||
@ -66,6 +66,9 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
name = initial_data.get('name')
|
||||
if name is not None:
|
||||
return
|
||||
request = self.context.get('request')
|
||||
if request and request.method == 'PATCH':
|
||||
return
|
||||
if not name:
|
||||
name = initial_data.get('username')
|
||||
if self.instance and self.instance.name == name:
|
||||
@ -238,7 +241,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
queryset = queryset.prefetch_related(
|
||||
'asset', 'asset__platform',
|
||||
'asset__platform__automation'
|
||||
)
|
||||
).prefetch_related('labels', 'labels__label')
|
||||
return queryset
|
||||
|
||||
|
||||
@ -455,11 +458,15 @@ class AccountTaskSerializer(serializers.Serializer):
|
||||
('test', 'test'),
|
||||
('verify', 'verify'),
|
||||
('push', 'push'),
|
||||
('remove', 'remove'),
|
||||
)
|
||||
action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True)
|
||||
accounts = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Account.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
gather_accounts = serializers.PrimaryKeyRelatedField(
|
||||
queryset=GatheredAccount.objects, required=False, allow_empty=True, many=True
|
||||
)
|
||||
task = serializers.CharField(read_only=True)
|
||||
params = serializers.JSONField(
|
||||
decoder=None, encoder=None, required=False,
|
||||
|
@ -5,6 +5,7 @@ from rest_framework import serializers
|
||||
from accounts.const import SecretType
|
||||
from accounts.models import BaseAccount
|
||||
from accounts.utils import validate_password_for_ansible, validate_ssh_key
|
||||
from common.serializers import ResourceLabelsMixin
|
||||
from common.serializers.fields import EncryptedField, LabeledChoiceField
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
|
||||
@ -60,8 +61,7 @@ class AuthValidateMixin(serializers.Serializer):
|
||||
return super().update(instance, validated_data)
|
||||
|
||||
|
||||
class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
|
||||
class BaseAccountSerializer(AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = BaseAccount
|
||||
fields_mini = ['id', 'name', 'username']
|
||||
@ -70,7 +70,7 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer):
|
||||
'privileged', 'is_active', 'spec_info',
|
||||
]
|
||||
fields_other = ['created_by', 'date_created', 'date_updated', 'comment']
|
||||
fields = fields_small + fields_other
|
||||
fields = fields_small + fields_other + ['labels']
|
||||
read_only_fields = [
|
||||
'spec_info', 'date_verified', 'created_by', 'date_created',
|
||||
]
|
||||
|
@ -15,6 +15,9 @@ class PasswordRulesSerializer(serializers.Serializer):
|
||||
uppercase = serializers.BooleanField(default=True, label=_('Uppercase'))
|
||||
digit = serializers.BooleanField(default=True, label=_('Digit'))
|
||||
symbol = serializers.BooleanField(default=True, label=_('Special symbol'))
|
||||
exclude_symbols = serializers.CharField(
|
||||
default='', allow_blank=True, max_length=16, label=_('Exclude symbol')
|
||||
)
|
||||
|
||||
|
||||
class AccountTemplateSerializer(BaseAccountSerializer):
|
||||
|
@ -2,5 +2,6 @@ from .automation import *
|
||||
from .backup_account import *
|
||||
from .gather_accounts import *
|
||||
from .push_account import *
|
||||
from .remove_account import *
|
||||
from .template import *
|
||||
from .verify_account import *
|
||||
|
31
apps/accounts/tasks/remove_account.py
Normal file
31
apps/accounts/tasks/remove_account.py
Normal file
@ -0,0 +1,31 @@
|
||||
from celery import shared_task
|
||||
from django.utils.translation import gettext_noop, gettext_lazy as _
|
||||
|
||||
from accounts.const import AutomationTypes
|
||||
from accounts.tasks.common import quickstart_automation_by_snapshot
|
||||
from common.utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
__all__ = ['remove_accounts_task']
|
||||
|
||||
|
||||
@shared_task(
|
||||
queue="ansible", verbose_name=_('Remove account'),
|
||||
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None)
|
||||
)
|
||||
def remove_accounts_task(gather_account_ids):
|
||||
from accounts.models import GatheredAccount
|
||||
|
||||
gather_accounts = GatheredAccount.objects.filter(
|
||||
id__in=gather_account_ids
|
||||
)
|
||||
task_name = gettext_noop("Remove account")
|
||||
|
||||
task_snapshot = {
|
||||
'assets': [str(i.asset_id) for i in gather_accounts],
|
||||
'gather_accounts': [str(i.id) for i in gather_accounts],
|
||||
}
|
||||
|
||||
tp = AutomationTypes.remove_account
|
||||
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
|
@ -30,7 +30,8 @@ class SecretGenerator:
|
||||
'lower': rules['lowercase'],
|
||||
'upper': rules['uppercase'],
|
||||
'digit': rules['digit'],
|
||||
'special_char': rules['symbol']
|
||||
'special_char': rules['symbol'],
|
||||
'exclude_chars': rules.get('exclude_symbols', ''),
|
||||
}
|
||||
return random_string(**rules)
|
||||
|
||||
@ -46,18 +47,10 @@ class SecretGenerator:
|
||||
|
||||
def validate_password_for_ansible(password):
|
||||
""" 校验 Ansible 不支持的特殊字符 """
|
||||
# validate password contains left double curly bracket
|
||||
# check password not contains `{{`
|
||||
# Ansible 推送的时候不支持
|
||||
if '{{' in password or '}}' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{{` or `}}`'))
|
||||
if '{%' in password or '%}' in password:
|
||||
raise serializers.ValidationError(_('Password can not contains `{%` or `%}`'))
|
||||
# Ansible Windows 推送的时候不支持
|
||||
# if "'" in password:
|
||||
# raise serializers.ValidationError(_("Password can not contains `'` "))
|
||||
# if '"' in password:
|
||||
# raise serializers.ValidationError(_('Password can not contains `"` '))
|
||||
if password.startswith('{{') and password.endswith('}}'):
|
||||
raise serializers.ValidationError(
|
||||
_('If the password starts with {{` and ends with }} `, then the password is not allowed.')
|
||||
)
|
||||
|
||||
|
||||
def validate_ssh_key(ssh_key, passphrase=None):
|
||||
|
@ -11,7 +11,7 @@ __all__ = ['CommandFilterACLViewSet', 'CommandGroupViewSet']
|
||||
class CommandGroupViewSet(OrgBulkModelViewSet):
|
||||
model = models.CommandGroup
|
||||
filterset_fields = ('name', 'command_filters')
|
||||
search_fields = filterset_fields
|
||||
search_fields = ('name',)
|
||||
serializer_class = serializers.CommandGroupSerializer
|
||||
|
||||
|
||||
|
@ -2,7 +2,6 @@ from .asset import *
|
||||
from .category import *
|
||||
from .domain import *
|
||||
from .favorite_asset import *
|
||||
from .label import *
|
||||
from .mixin import *
|
||||
from .node import *
|
||||
from .platform import *
|
||||
|
@ -3,7 +3,6 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework import status
|
||||
@ -14,7 +13,7 @@ from rest_framework.status import HTTP_200_OK
|
||||
from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task
|
||||
from assets import serializers
|
||||
from assets.exceptions import NotSupportedTemporarilyError
|
||||
from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend
|
||||
from assets.filters import IpInFilterBackend, NodeFilterBackend
|
||||
from assets.models import Asset, Gateway, Platform, Protocol
|
||||
from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual
|
||||
from common.api import SuggestionMixin
|
||||
@ -33,7 +32,6 @@ __all__ = [
|
||||
|
||||
|
||||
class AssetFilterSet(BaseFilterSet):
|
||||
labels = django_filters.CharFilter(method='filter_labels')
|
||||
platform = django_filters.CharFilter(method='filter_platform')
|
||||
domain = django_filters.CharFilter(method='filter_domain')
|
||||
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
|
||||
@ -64,7 +62,7 @@ class AssetFilterSet(BaseFilterSet):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
"id", "name", "address", "is_active", "labels",
|
||||
"id", "name", "address", "is_active",
|
||||
"type", "category", "platform",
|
||||
]
|
||||
|
||||
@ -87,16 +85,6 @@ class AssetFilterSet(BaseFilterSet):
|
||||
value = value.split(',')
|
||||
return queryset.filter(protocols__name__in=value).distinct()
|
||||
|
||||
@staticmethod
|
||||
def filter_labels(queryset, name, value):
|
||||
if ':' in value:
|
||||
n, v = value.split(':', 1)
|
||||
queryset = queryset.filter(labels__name=n, labels__value=v)
|
||||
else:
|
||||
q = Q(labels__name__contains=value) | Q(labels__value__contains=value)
|
||||
queryset = queryset.filter(q).distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
"""
|
||||
@ -105,7 +93,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
model = Asset
|
||||
filterset_class = AssetFilterSet
|
||||
search_fields = ("name", "address", "comment")
|
||||
ordering_fields = ('name', 'connectivity', 'platform', 'date_updated')
|
||||
ordering_fields = ('name', 'connectivity', 'platform', 'date_updated', 'date_created')
|
||||
serializer_classes = (
|
||||
("default", serializers.AssetSerializer),
|
||||
("platform", serializers.PlatformSerializer),
|
||||
@ -121,7 +109,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet):
|
||||
("sync_platform_protocols", "assets.change_asset"),
|
||||
)
|
||||
extra_filter_backends = [
|
||||
LabelFilterBackend, IpInFilterBackend,
|
||||
IpInFilterBackend,
|
||||
NodeFilterBackend, AttrRulesFilterBackend
|
||||
]
|
||||
|
||||
|
@ -1,43 +0,0 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the GNU General Public License v2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.gnu.org/licenses/gpl-2.0.html
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.db.models import Count
|
||||
|
||||
from common.utils import get_logger
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from ..models import Label
|
||||
from .. import serializers
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
__all__ = ['LabelViewSet']
|
||||
|
||||
|
||||
class LabelViewSet(OrgBulkModelViewSet):
|
||||
model = Label
|
||||
filterset_fields = ("name", "value")
|
||||
search_fields = filterset_fields
|
||||
serializer_class = serializers.LabelSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if request.query_params.get("distinct"):
|
||||
self.serializer_class = serializers.LabelDistinctSerializer
|
||||
self.queryset = self.queryset.values("name").distinct()
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = Label.objects.prefetch_related(
|
||||
'assets').annotate(asset_count=Count("assets"))
|
||||
return self.queryset
|
@ -17,6 +17,62 @@ from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SSHTunnelManager:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.gateway_servers = dict()
|
||||
|
||||
@staticmethod
|
||||
def file_to_json(path):
|
||||
with open(path, 'r') as f:
|
||||
d = json.load(f)
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def json_to_file(path, data):
|
||||
with open(path, 'w') as f:
|
||||
json.dump(data, f, indent=4, sort_keys=True)
|
||||
|
||||
def local_gateway_prepare(self, runner):
|
||||
info = self.file_to_json(runner.inventory)
|
||||
servers, not_valid = [], []
|
||||
for k, host in info['all']['hosts'].items():
|
||||
jms_asset, jms_gateway = host.get('jms_asset'), host.get('gateway')
|
||||
if not jms_gateway:
|
||||
continue
|
||||
try:
|
||||
server = SSHTunnelForwarder(
|
||||
(jms_gateway['address'], jms_gateway['port']),
|
||||
ssh_username=jms_gateway['username'],
|
||||
ssh_password=jms_gateway['secret'],
|
||||
ssh_pkey=jms_gateway['private_key_path'],
|
||||
remote_bind_address=(jms_asset['address'], jms_asset['port'])
|
||||
)
|
||||
server.start()
|
||||
except Exception as e:
|
||||
err_msg = 'Gateway is not active: %s' % jms_asset.get('name', '')
|
||||
print(f'\033[31m {err_msg} 原因: {e} \033[0m\n')
|
||||
not_valid.append(k)
|
||||
else:
|
||||
local_bind_port = server.local_bind_port
|
||||
host['ansible_host'] = jms_asset['address'] = host['login_host'] = '127.0.0.1'
|
||||
host['ansible_port'] = jms_asset['port'] = host['login_port'] = local_bind_port
|
||||
servers.append(server)
|
||||
|
||||
# 网域不可连接的,就不继续执行此资源的后续任务了
|
||||
for a in set(not_valid):
|
||||
info['all']['hosts'].pop(a)
|
||||
self.json_to_file(runner.inventory, info)
|
||||
self.gateway_servers[runner.id] = servers
|
||||
|
||||
def local_gateway_clean(self, runner):
|
||||
servers = self.gateway_servers.get(runner.id, [])
|
||||
for s in servers:
|
||||
try:
|
||||
s.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class PlaybookCallback(DefaultCallback):
|
||||
def playbook_on_stats(self, event_data, **kwargs):
|
||||
super().playbook_on_stats(event_data, **kwargs)
|
||||
@ -37,7 +93,6 @@ class BasePlaybookManager:
|
||||
# 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式
|
||||
# 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook
|
||||
self.playbooks = []
|
||||
self.gateway_servers = dict()
|
||||
params = self.execution.snapshot.get('params')
|
||||
self.params = params or {}
|
||||
|
||||
@ -157,22 +212,19 @@ class BasePlaybookManager:
|
||||
os.chmod(key_path, 0o400)
|
||||
return key_path
|
||||
|
||||
def generate_inventory(self, platformed_assets, inventory_path):
|
||||
def generate_inventory(self, platformed_assets, inventory_path, protocol):
|
||||
inventory = JMSInventory(
|
||||
assets=platformed_assets,
|
||||
account_prefer=self.ansible_account_prefer,
|
||||
account_policy=self.ansible_account_policy,
|
||||
host_callback=self.host_callback,
|
||||
task_type=self.__class__.method_type(),
|
||||
protocol=protocol,
|
||||
)
|
||||
inventory.write_to_file(inventory_path)
|
||||
|
||||
def generate_playbook(self, platformed_assets, platform, sub_playbook_dir):
|
||||
method_id = getattr(platform.automation, '{}_method'.format(self.__class__.method_type()))
|
||||
method = self.method_id_meta_mapper.get(method_id)
|
||||
if not method:
|
||||
logger.error("Method not found: {}".format(method_id))
|
||||
return
|
||||
@staticmethod
|
||||
def generate_playbook(method, sub_playbook_dir):
|
||||
method_playbook_dir_path = method['dir']
|
||||
sub_playbook_path = os.path.join(sub_playbook_dir, 'project', 'main.yml')
|
||||
shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path))
|
||||
@ -204,8 +256,16 @@ class BasePlaybookManager:
|
||||
sub_dir = '{}_{}'.format(platform.name, i)
|
||||
playbook_dir = os.path.join(self.runtime_dir, sub_dir)
|
||||
inventory_path = os.path.join(self.runtime_dir, sub_dir, 'hosts.json')
|
||||
self.generate_inventory(_assets, inventory_path)
|
||||
playbook_path = self.generate_playbook(_assets, platform, playbook_dir)
|
||||
|
||||
method_id = getattr(platform.automation, '{}_method'.format(self.__class__.method_type()))
|
||||
method = self.method_id_meta_mapper.get(method_id)
|
||||
|
||||
if not method:
|
||||
logger.error("Method not found: {}".format(method_id))
|
||||
continue
|
||||
protocol = method.get('protocol')
|
||||
self.generate_inventory(_assets, inventory_path, protocol)
|
||||
playbook_path = self.generate_playbook(method, playbook_dir)
|
||||
if not playbook_path:
|
||||
continue
|
||||
|
||||
@ -247,66 +307,10 @@ class BasePlaybookManager:
|
||||
def on_runner_failed(self, runner, e):
|
||||
print("Runner failed: {} {}".format(e, self))
|
||||
|
||||
@staticmethod
|
||||
def file_to_json(path):
|
||||
with open(path, 'r') as f:
|
||||
d = json.load(f)
|
||||
return d
|
||||
|
||||
@staticmethod
|
||||
def json_dumps(data):
|
||||
return json.dumps(data, indent=4, sort_keys=True)
|
||||
|
||||
@staticmethod
|
||||
def json_to_file(path, data):
|
||||
with open(path, 'w') as f:
|
||||
json.dump(data, f, indent=4, sort_keys=True)
|
||||
|
||||
def local_gateway_prepare(self, runner):
|
||||
info = self.file_to_json(runner.inventory)
|
||||
servers, not_valid = [], []
|
||||
for k, host in info['all']['hosts'].items():
|
||||
jms_asset, jms_gateway = host.get('jms_asset'), host.get('gateway')
|
||||
if not jms_gateway:
|
||||
continue
|
||||
try:
|
||||
server = SSHTunnelForwarder(
|
||||
(jms_gateway['address'], jms_gateway['port']),
|
||||
ssh_username=jms_gateway['username'],
|
||||
ssh_password=jms_gateway['secret'],
|
||||
ssh_pkey=jms_gateway['private_key_path'],
|
||||
remote_bind_address=(jms_asset['address'], jms_asset['port'])
|
||||
)
|
||||
server.start()
|
||||
except Exception as e:
|
||||
err_msg = 'Gateway is not active: %s' % jms_asset.get('name', '')
|
||||
print(f'\033[31m {err_msg} 原因: {e} \033[0m\n')
|
||||
not_valid.append(k)
|
||||
else:
|
||||
host['ansible_host'] = jms_asset['address'] = '127.0.0.1'
|
||||
host['ansible_port'] = jms_asset['port'] = server.local_bind_port
|
||||
servers.append(server)
|
||||
|
||||
# 网域不可连接的,就不继续执行此资源的后续任务了
|
||||
for a in set(not_valid):
|
||||
info['all']['hosts'].pop(a)
|
||||
self.json_to_file(runner.inventory, info)
|
||||
self.gateway_servers[runner.id] = servers
|
||||
|
||||
def local_gateway_clean(self, runner):
|
||||
servers = self.gateway_servers.get(runner.id, [])
|
||||
for s in servers:
|
||||
try:
|
||||
s.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def before_runner_start(self, runner):
|
||||
self.local_gateway_prepare(runner)
|
||||
|
||||
def after_runner_end(self, runner):
|
||||
self.local_gateway_clean(runner)
|
||||
|
||||
def delete_runtime_dir(self):
|
||||
if settings.DEBUG_DEV:
|
||||
return
|
||||
@ -326,14 +330,15 @@ class BasePlaybookManager:
|
||||
for i, runner in enumerate(runners, start=1):
|
||||
if len(runners) > 1:
|
||||
print(">>> 开始执行第 {} 批任务".format(i))
|
||||
self.before_runner_start(runner)
|
||||
ssh_tunnel = SSHTunnelManager()
|
||||
ssh_tunnel.local_gateway_prepare(runner)
|
||||
try:
|
||||
cb = runner.run(**kwargs)
|
||||
self.on_runner_success(runner, cb)
|
||||
except Exception as e:
|
||||
self.on_runner_failed(runner, e)
|
||||
finally:
|
||||
self.after_runner_end(runner)
|
||||
ssh_tunnel.local_gateway_clean(runner)
|
||||
print('\n')
|
||||
self.execution.status = 'success'
|
||||
self.execution.date_finished = timezone.now()
|
||||
|
@ -2,6 +2,7 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Get info
|
||||
@ -10,10 +11,10 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
register: db_info
|
||||
|
||||
|
@ -7,6 +7,6 @@ type:
|
||||
method: gather_facts
|
||||
i18n:
|
||||
Gather posix facts:
|
||||
zh: 使用 Ansible 指令 gather_facts 从主机获取设备信息
|
||||
en: Gather facts from asset using gather_facts
|
||||
ja: gather_factsを使用してPosixから情報を収集する
|
||||
zh: '使用 Ansible 指令 gather_facts 从主机获取设备信息'
|
||||
en: 'Gather facts from asset using gather_facts'
|
||||
ja: 'gather_factsを使用してPosixから情報を収集する'
|
||||
|
@ -7,6 +7,6 @@ type:
|
||||
- windows
|
||||
i18n:
|
||||
Gather facts windows:
|
||||
zh: 使用 Ansible 指令 gather_facts 从 Windows 获取设备信息
|
||||
en: Gather facts from Windows using gather_facts
|
||||
ja: gather_factsを使用してWindowsから情報を収集する
|
||||
zh: '使用 Ansible 指令 gather_facts 从 Windows 获取设备信息'
|
||||
en: 'Gather facts from Windows using gather_facts'
|
||||
ja: 'gather_factsを使用してWindowsから情報を収集する'
|
||||
|
@ -31,7 +31,7 @@ def generate_serializer(data):
|
||||
return create_serializer_class(serializer_name, params)
|
||||
|
||||
|
||||
def get_platform_automation_methods(path):
|
||||
def get_platform_automation_methods(path, lang=None):
|
||||
methods = []
|
||||
for root, dirs, files in os.walk(path, topdown=False):
|
||||
for name in files:
|
||||
@ -40,7 +40,7 @@ def get_platform_automation_methods(path):
|
||||
continue
|
||||
|
||||
with open(path, 'r', encoding='utf8') as f:
|
||||
manifest = yaml_load_with_i18n(f)
|
||||
manifest = yaml_load_with_i18n(f, lang=lang)
|
||||
check_platform_method(manifest, path)
|
||||
manifest['dir'] = os.path.dirname(path)
|
||||
manifest['params_serializer'] = generate_serializer(manifest)
|
||||
|
@ -10,6 +10,6 @@
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
|
@ -6,8 +6,10 @@ category:
|
||||
type:
|
||||
- windows
|
||||
method: ping
|
||||
protocol: rdp
|
||||
|
||||
i18n:
|
||||
Ping by pyfreerdp:
|
||||
zh: 使用 Python 模块 pyfreerdp 测试主机可连接性
|
||||
en: Ping by pyfreerdp module
|
||||
ja: Pyfreerdpモジュールを使用してホストにPingする
|
||||
zh: '使用 Python 模块 pyfreerdp 测试主机可连接性'
|
||||
en: 'Ping by pyfreerdp module'
|
||||
ja: 'Pyfreerdpモジュールを使用してホストにPingする'
|
||||
|
@ -11,7 +11,7 @@
|
||||
login_user: "{{ jms_account.username }}"
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
login_secret_type: "{{ jms_account.secret_type }}"
|
||||
login_private_key_path: "{{ jms_account.private_key_path }}"
|
||||
become: "{{ custom_become | default(False) }}"
|
||||
|
@ -6,8 +6,10 @@ category:
|
||||
type:
|
||||
- all
|
||||
method: ping
|
||||
protocol: ssh
|
||||
|
||||
i18n:
|
||||
Ping by paramiko:
|
||||
zh: 使用 Python 模块 paramiko 测试主机可连接性
|
||||
en: Ping by paramiko module
|
||||
ja: Paramikoモジュールを使用してホストにPingする
|
||||
zh: '使用 Python 模块 paramiko 测试主机可连接性'
|
||||
en: 'Ping by paramiko module'
|
||||
ja: 'Paramikoモジュールを使用してホストにPingする'
|
||||
|
@ -2,6 +2,7 @@
|
||||
gather_facts: no
|
||||
vars:
|
||||
ansible_python_interpreter: /opt/py3/bin/python
|
||||
check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}"
|
||||
|
||||
tasks:
|
||||
- name: Test MySQL connection
|
||||
@ -10,8 +11,8 @@
|
||||
login_password: "{{ jms_account.secret }}"
|
||||
login_host: "{{ jms_asset.address }}"
|
||||
login_port: "{{ jms_asset.port }}"
|
||||
check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}"
|
||||
check_hostname: "{{ check_ssl if check_ssl else omit }}"
|
||||
ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}"
|
||||
client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}"
|
||||
client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}"
|
||||
filter: version
|
||||
|
@ -2,9 +2,11 @@ import json
|
||||
from collections import defaultdict
|
||||
from copy import deepcopy
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from common.db.models import ChoicesMixin
|
||||
from jumpserver.utils import get_current_request
|
||||
from .category import Category
|
||||
from .cloud import CloudTypes
|
||||
from .custom import CustomTypes
|
||||
@ -22,6 +24,8 @@ class AllTypes(ChoicesMixin):
|
||||
CloudTypes, WebTypes, CustomTypes, GPTTypes
|
||||
]
|
||||
_category_constrains = {}
|
||||
_automation_methods = None
|
||||
_current_language = settings.LANGUAGE_CODE
|
||||
|
||||
@classmethod
|
||||
def choices(cls):
|
||||
@ -61,9 +65,28 @@ class AllTypes(ChoicesMixin):
|
||||
|
||||
@classmethod
|
||||
def get_automation_methods(cls):
|
||||
from assets.automations import platform_automation_methods as asset_methods
|
||||
from accounts.automations import platform_automation_methods as account_methods
|
||||
return asset_methods + account_methods
|
||||
from assets.automations import methods as asset
|
||||
from accounts.automations import methods as account
|
||||
|
||||
automation_methods = \
|
||||
asset.platform_automation_methods + \
|
||||
account.platform_automation_methods
|
||||
|
||||
request = get_current_request()
|
||||
if request is None:
|
||||
return automation_methods
|
||||
|
||||
language = request.LANGUAGE_CODE
|
||||
if cls._automation_methods is not None and language == cls._current_language:
|
||||
automation_methods = cls._automation_methods
|
||||
else:
|
||||
automation_methods = \
|
||||
asset.get_platform_automation_methods(asset.BASE_DIR, language) + \
|
||||
account.get_platform_automation_methods(account.BASE_DIR, language)
|
||||
|
||||
cls._current_language = language
|
||||
cls._automation_methods = automation_methods
|
||||
return cls._automation_methods
|
||||
|
||||
@classmethod
|
||||
def set_automation_methods(cls, category, tp_name, constraints):
|
||||
|
@ -5,7 +5,6 @@ from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
from assets.utils import get_node_from_request, is_query_node_all_assets
|
||||
from .models import Label
|
||||
|
||||
|
||||
class AssetByNodeFilterBackend(filters.BaseFilterBackend):
|
||||
@ -72,57 +71,6 @@ class NodeFilterBackend(filters.BaseFilterBackend):
|
||||
return queryset.filter(nodes__key=node.key).distinct()
|
||||
|
||||
|
||||
class LabelFilterBackend(filters.BaseFilterBackend):
|
||||
sep = ':'
|
||||
query_arg = 'label'
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
example = self.sep.join(['os', 'linux'])
|
||||
return [
|
||||
coreapi.Field(
|
||||
name=self.query_arg, location='query', required=False,
|
||||
type='string', example=example, description=''
|
||||
)
|
||||
]
|
||||
|
||||
def get_query_labels(self, request):
|
||||
labels_query = request.query_params.getlist(self.query_arg)
|
||||
if not labels_query:
|
||||
return None
|
||||
|
||||
q = None
|
||||
for kv in labels_query:
|
||||
if '#' in kv:
|
||||
self.sep = '#'
|
||||
break
|
||||
|
||||
for kv in labels_query:
|
||||
if self.sep not in kv:
|
||||
continue
|
||||
key, value = kv.strip().split(self.sep)[:2]
|
||||
if not all([key, value]):
|
||||
continue
|
||||
if q:
|
||||
q |= Q(name=key, value=value)
|
||||
else:
|
||||
q = Q(name=key, value=value)
|
||||
if not q:
|
||||
return []
|
||||
labels = Label.objects.filter(q, is_active=True) \
|
||||
.values_list('id', flat=True)
|
||||
return labels
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
labels = self.get_query_labels(request)
|
||||
if labels is None:
|
||||
return queryset
|
||||
if len(labels) == 0:
|
||||
return queryset.none()
|
||||
for label in labels:
|
||||
queryset = queryset.filter(labels=label)
|
||||
return queryset
|
||||
|
||||
|
||||
class IpInFilterBackend(filters.BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
ips = request.query_params.get('ips')
|
||||
|
@ -123,7 +123,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
|
||||
field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Node'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
|
@ -50,7 +50,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='asset',
|
||||
name='nodes',
|
||||
field=models.ManyToManyField(default=assets.models.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'),
|
||||
field=models.ManyToManyField(default=assets.models.default_node, related_name='assets', to='assets.Node', verbose_name='Node'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
|
@ -31,7 +31,7 @@ class Migration(migrations.Migration):
|
||||
('type', models.CharField(max_length=16, verbose_name='Type')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Is active')),
|
||||
('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')),
|
||||
('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')),
|
||||
('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Node')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Automation task',
|
||||
|
18
apps/assets/migrations/0126_remove_asset_labels.py
Normal file
18
apps/assets/migrations/0126_remove_asset_labels.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.10 on 2023-11-22 07:33
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0125_auto_20231011_1053'),
|
||||
('labels', '0002_auto_20231103_1659'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='asset',
|
||||
name='labels',
|
||||
),
|
||||
]
|
55
apps/assets/migrations/0127_automation_remove_account.py
Normal file
55
apps/assets/migrations/0127_automation_remove_account.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Generated by Django 4.1.10 on 2023-12-05 10:03
|
||||
from functools import reduce
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import F
|
||||
|
||||
|
||||
def migrate_automation_ansible_remove_account(apps, *args):
|
||||
automation_model = apps.get_model('assets', 'PlatformAutomation')
|
||||
automation_map = {
|
||||
('oracle',): 'remove_account_oracle',
|
||||
('windows',): 'remove_account_windows',
|
||||
('mongodb',): 'remove_account_mongodb',
|
||||
('linux', 'unix'): 'remove_account_posix',
|
||||
('sqlserver',): 'remove_account_sqlserver',
|
||||
('mysql', 'mariadb'): 'remove_account_mysql',
|
||||
('postgresql',): 'remove_account_postgresql',
|
||||
}
|
||||
|
||||
update_objs = []
|
||||
types = list(reduce(lambda x, y: x + y, automation_map.keys()))
|
||||
qs = automation_model.objects.filter(platform__type__in=types).annotate(tp=F('platform__type'))
|
||||
for automation in qs:
|
||||
for types, method in automation_map.items():
|
||||
if automation.tp in types:
|
||||
automation.remove_account_enabled = True
|
||||
automation.remove_account_method = method
|
||||
break
|
||||
update_objs.append(automation)
|
||||
automation_model.objects.bulk_update(update_objs, ['remove_account_enabled', 'remove_account_method'])
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('assets', '0126_remove_asset_labels'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='platformautomation',
|
||||
name='remove_account_enabled',
|
||||
field=models.BooleanField(default=False, verbose_name='Remove account enabled'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platformautomation',
|
||||
name='remove_account_method',
|
||||
field=models.TextField(blank=True, max_length=32, null=True, verbose_name='Remove account method'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='platformautomation',
|
||||
name='remove_account_params',
|
||||
field=models.JSONField(default=dict, verbose_name='Remove account params'),
|
||||
),
|
||||
migrations.RunPython(migrate_automation_ansible_remove_account)
|
||||
]
|
@ -13,7 +13,9 @@ from django.utils.translation import gettext_lazy as _
|
||||
from assets import const
|
||||
from common.db.fields import EncryptMixin
|
||||
from common.utils import lazyproperty
|
||||
from labels.mixins import LabeledMixin
|
||||
from orgs.mixins.models import OrgManager, JMSOrgBaseModel
|
||||
from rbac.models import ContentType
|
||||
from ..base import AbsConnectivity
|
||||
from ..platform import Platform
|
||||
|
||||
@ -150,7 +152,7 @@ class JSONFilterMixin:
|
||||
return None
|
||||
|
||||
|
||||
class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseModel):
|
||||
class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseModel):
|
||||
Category = const.Category
|
||||
Type = const.AllTypes
|
||||
|
||||
@ -160,9 +162,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseMode
|
||||
domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets',
|
||||
verbose_name=_("Domain"), on_delete=models.SET_NULL)
|
||||
nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets',
|
||||
verbose_name=_("Nodes"))
|
||||
verbose_name=_("Node"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_('Is active'))
|
||||
labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels"))
|
||||
gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息
|
||||
custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict)
|
||||
|
||||
@ -171,6 +172,13 @@ class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseMode
|
||||
def __str__(self):
|
||||
return '{0.name}({0.address})'.format(self)
|
||||
|
||||
def get_labels(self):
|
||||
from labels.models import Label, LabeledResource
|
||||
res_type = ContentType.objects.get_for_model(self.__class__)
|
||||
label_ids = LabeledResource.objects.filter(res_type=res_type, res_id=self.id) \
|
||||
.values_list('label_id', flat=True)
|
||||
return Label.objects.filter(id__in=label_ids)
|
||||
|
||||
@staticmethod
|
||||
def get_spec_values(instance, fields):
|
||||
info = {}
|
||||
|
@ -15,7 +15,7 @@ from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel
|
||||
|
||||
class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel):
|
||||
accounts = models.JSONField(default=list, verbose_name=_("Accounts"))
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes"))
|
||||
nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Node"))
|
||||
assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets"))
|
||||
type = models.CharField(max_length=16, verbose_name=_('Type'))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("Is active"))
|
||||
|
@ -29,7 +29,7 @@ class CommandFilter(OrgModelMixin):
|
||||
)
|
||||
nodes = models.ManyToManyField(
|
||||
'assets.Node', related_name='cmd_filters', blank=True,
|
||||
verbose_name=_("Nodes")
|
||||
verbose_name=_("Node")
|
||||
)
|
||||
assets = models.ManyToManyField(
|
||||
'assets.Asset', related_name='cmd_filters', blank=True,
|
||||
|
@ -6,6 +6,7 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils import get_logger
|
||||
from labels.mixins import LabeledMixin
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from .gateway import Gateway
|
||||
|
||||
@ -14,7 +15,7 @@ logger = get_logger(__file__)
|
||||
__all__ = ['Domain']
|
||||
|
||||
|
||||
class Domain(JMSOrgBaseModel):
|
||||
class Domain(LabeledMixin, JMSOrgBaseModel):
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
|
||||
class Meta:
|
||||
|
@ -9,6 +9,7 @@ from common.db.models import JMSBaseModel
|
||||
__all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation']
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from labels.mixins import LabeledMixin
|
||||
|
||||
|
||||
class PlatformProtocol(models.Model):
|
||||
@ -71,10 +72,16 @@ class PlatformAutomation(models.Model):
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")
|
||||
)
|
||||
gather_accounts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params"))
|
||||
|
||||
remove_account_enabled = models.BooleanField(default=False, verbose_name=_("Remove account enabled"))
|
||||
remove_account_method = models.TextField(
|
||||
max_length=32, blank=True, null=True, verbose_name=_("Remove account method")
|
||||
)
|
||||
remove_account_params = models.JSONField(default=dict, verbose_name=_("Remove account params"))
|
||||
platform = models.OneToOneField('Platform', on_delete=models.CASCADE, related_name='automation', null=True)
|
||||
|
||||
|
||||
class Platform(JMSBaseModel):
|
||||
class Platform(LabeledMixin, JMSBaseModel):
|
||||
"""
|
||||
对资产提供 约束和默认值
|
||||
对资产进行抽象
|
||||
|
@ -2,11 +2,10 @@
|
||||
#
|
||||
|
||||
from .asset import *
|
||||
from .label import *
|
||||
from .node import *
|
||||
from .gateway import *
|
||||
from .automations import *
|
||||
from .cagegory import *
|
||||
from .domain import *
|
||||
from .favorite_asset import *
|
||||
from .gateway import *
|
||||
from .node import *
|
||||
from .platform import *
|
||||
from .cagegory import *
|
||||
from .automations import *
|
||||
|
@ -11,13 +11,14 @@ from accounts.serializers import AccountSerializer
|
||||
from common.const import UUID_PATTERN
|
||||
from common.serializers import (
|
||||
WritableNestedModelSerializer, SecretReadableMixin,
|
||||
CommonModelSerializer, MethodSerializer
|
||||
CommonModelSerializer, MethodSerializer, ResourceLabelsMixin
|
||||
)
|
||||
from common.serializers.common import DictSerializer
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from labels.models import Label
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ...const import Category, AllTypes
|
||||
from ...models import Asset, Node, Platform, Label, Protocol
|
||||
from ...models import Asset, Node, Platform, Protocol
|
||||
|
||||
__all__ = [
|
||||
'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer',
|
||||
@ -117,10 +118,9 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer):
|
||||
class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category'))
|
||||
type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type'))
|
||||
labels = AssetLabelSerializer(many=True, required=False, label=_('Label'))
|
||||
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
|
||||
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account'))
|
||||
nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path"))
|
||||
@ -201,8 +201,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.prefetch_related('domain', 'nodes', 'labels', 'protocols') \
|
||||
queryset = queryset.prefetch_related('domain', 'nodes', 'protocols', ) \
|
||||
.prefetch_related('platform', 'platform__automation') \
|
||||
.prefetch_related('labels', 'labels__label') \
|
||||
.annotate(category=F("platform__category")) \
|
||||
.annotate(type=F("platform__type"))
|
||||
return queryset
|
||||
|
@ -3,6 +3,7 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers import ResourceLabelsMixin
|
||||
from common.serializers.fields import ObjectRelatedField
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .gateway import GatewayWithAccountSecretSerializer
|
||||
@ -11,7 +12,7 @@ from ..models import Domain, Asset
|
||||
__all__ = ['DomainSerializer', 'DomainWithGatewaySerializer']
|
||||
|
||||
|
||||
class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
|
||||
gateways = ObjectRelatedField(
|
||||
many=True, required=False, label=_('Gateway'), read_only=True,
|
||||
)
|
||||
@ -41,6 +42,12 @@ class DomainSerializer(BulkOrgResourceModelSerializer):
|
||||
instance = super().update(instance, validated_data)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset \
|
||||
.prefetch_related('labels', 'labels__label')
|
||||
return queryset
|
||||
|
||||
|
||||
class DomainWithGatewaySerializer(serializers.ModelSerializer):
|
||||
gateways = GatewayWithAccountSecretSerializer(many=True, read_only=True)
|
||||
|
@ -1,47 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Count
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from ..models import Label
|
||||
|
||||
|
||||
class LabelSerializer(BulkOrgResourceModelSerializer):
|
||||
asset_count = serializers.ReadOnlyField(label=_("Assets amount"))
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields_mini = ['id', 'name']
|
||||
fields_small = fields_mini + [
|
||||
'value', 'category', 'is_active',
|
||||
'date_created', 'comment',
|
||||
]
|
||||
fields_m2m = ['asset_count', 'assets']
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = (
|
||||
'category', 'date_created', 'asset_count',
|
||||
)
|
||||
extra_kwargs = {
|
||||
'assets': {'required': False, 'label': _('Asset')}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.prefetch_related('assets') \
|
||||
.annotate(asset_count=Count('assets'))
|
||||
return queryset
|
||||
|
||||
|
||||
class LabelDistinctSerializer(BulkOrgResourceModelSerializer):
|
||||
value = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Label
|
||||
fields = ("name", "value")
|
||||
|
||||
@staticmethod
|
||||
def get_value(obj):
|
||||
labels = Label.objects.filter(name=obj["name"])
|
||||
return ', '.join([label.value for label in labels])
|
@ -5,7 +5,7 @@ from rest_framework.validators import UniqueValidator
|
||||
|
||||
from common.serializers import (
|
||||
WritableNestedModelSerializer, type_field_map, MethodSerializer,
|
||||
DictSerializer, create_serializer_class
|
||||
DictSerializer, create_serializer_class, ResourceLabelsMixin
|
||||
)
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.utils import lazyproperty
|
||||
@ -123,7 +123,7 @@ class PlatformCustomField(serializers.Serializer):
|
||||
choices = serializers.ListField(default=list, label=_("Choices"), required=False)
|
||||
|
||||
|
||||
class PlatformSerializer(WritableNestedModelSerializer):
|
||||
class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer):
|
||||
SU_METHOD_CHOICES = [
|
||||
("sudo", "sudo su -"),
|
||||
("su", "su - "),
|
||||
@ -160,6 +160,7 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
||||
fields = fields_small + [
|
||||
"protocols", "domain_enabled", "su_enabled",
|
||||
"su_method", "automation", "comment", "custom_fields",
|
||||
"labels"
|
||||
] + read_only_fields
|
||||
extra_kwargs = {
|
||||
"su_enabled": {"label": _('Su enabled')},
|
||||
@ -201,9 +202,8 @@ class PlatformSerializer(WritableNestedModelSerializer):
|
||||
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
queryset = queryset.prefetch_related(
|
||||
'protocols', 'automation'
|
||||
)
|
||||
queryset = queryset.prefetch_related('protocols', 'automation') \
|
||||
.prefetch_related('labels', 'labels__label')
|
||||
return queryset
|
||||
|
||||
def validate_protocols(self, protocols):
|
||||
|
@ -17,7 +17,6 @@ router.register(r'clouds', api.CloudViewSet, 'cloud')
|
||||
router.register(r'gpts', api.GPTViewSet, 'gpt')
|
||||
router.register(r'customs', api.CustomViewSet, 'custom')
|
||||
router.register(r'platforms', api.AssetPlatformViewSet, 'platform')
|
||||
router.register(r'labels', api.LabelViewSet, 'label')
|
||||
router.register(r'nodes', api.NodeViewSet, 'node')
|
||||
router.register(r'domains', api.DomainViewSet, 'domain')
|
||||
router.register(r'gateways', api.GatewayViewSet, 'gateway')
|
||||
|
@ -2,6 +2,7 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from audits.models import OperateLog
|
||||
from perms.const import ActionChoices
|
||||
|
||||
|
||||
class OperateLogStore(object):
|
||||
@ -45,20 +46,29 @@ class OperateLogStore(object):
|
||||
before[k], after[k] = before_value, after_value
|
||||
return before, after
|
||||
|
||||
@staticmethod
|
||||
def _get_special_handler(resource_type):
|
||||
# 根据资源类型,处理特殊字段
|
||||
resource_map = {
|
||||
'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v
|
||||
}
|
||||
return resource_map.get(resource_type, lambda k, v: v)
|
||||
|
||||
@classmethod
|
||||
def convert_diff_friendly(cls, raw_diff):
|
||||
def convert_diff_friendly(cls, op_log):
|
||||
diff_list = list()
|
||||
for k, v in raw_diff.items():
|
||||
handler = cls._get_special_handler(op_log.resource_type)
|
||||
for k, v in op_log.diff.items():
|
||||
before, after = v.split(cls.SEP, 1)
|
||||
diff_list.append({
|
||||
'field': _(k),
|
||||
'before': before if before else _('empty'),
|
||||
'after': after if after else _('empty'),
|
||||
'before': handler(k, before) if before else _('empty'),
|
||||
'after': handler(k, after) if after else _('empty'),
|
||||
})
|
||||
return diff_list
|
||||
|
||||
def save(self, **kwargs):
|
||||
log_id = kwargs.pop('id', None)
|
||||
log_id = kwargs.get('id', None)
|
||||
before = kwargs.pop('before') or {}
|
||||
after = kwargs.pop('after') or {}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
# Generated by Django 4.1.10 on 2023-09-15 08:58
|
||||
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -31,7 +32,7 @@ class Migration(migrations.Migration):
|
||||
options={
|
||||
'verbose_name': 'User session',
|
||||
'ordering': ['-date_created'],
|
||||
'permissions': [('offline_usersession', 'Offline ussr session')],
|
||||
'permissions': [('offline_usersession', 'Offline user session')],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -305,5 +305,5 @@ class UserSession(models.Model):
|
||||
ordering = ['-date_created']
|
||||
verbose_name = _('User session')
|
||||
permissions = [
|
||||
('offline_usersession', _('Offline ussr session')),
|
||||
('offline_usersession', _('Offline user session')),
|
||||
]
|
||||
|
@ -77,10 +77,7 @@ class OperateLogActionDetailSerializer(serializers.ModelSerializer):
|
||||
fields = ('diff',)
|
||||
|
||||
def to_representation(self, instance):
|
||||
data = super().to_representation(instance)
|
||||
diff = OperateLogStore.convert_diff_friendly(data['diff'])
|
||||
data['diff'] = diff
|
||||
return data
|
||||
return {'diff': OperateLogStore.convert_diff_friendly(instance)}
|
||||
|
||||
|
||||
class OperateLogSerializer(BulkOrgResourceModelSerializer):
|
||||
|
@ -36,6 +36,7 @@ class AuthBackendLabelMapping(LazyObject):
|
||||
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token")
|
||||
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom")
|
||||
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu")
|
||||
backend_label_mapping[settings.AUTH_BACKEND_SLACK] = _("Slack")
|
||||
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk")
|
||||
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token")
|
||||
backend_label_mapping[settings.AUTH_BACKEND_PASSKEY] = _("Passkey")
|
||||
|
@ -33,7 +33,9 @@ def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs):
|
||||
|
||||
with translation.override('en'):
|
||||
resource_type = instance._meta.verbose_name
|
||||
current_instance = model_to_dict(instance, include_model_fields=False)
|
||||
current_instance = model_to_dict(
|
||||
instance, include_model_fields=False, include_related_fields=[model]
|
||||
)
|
||||
|
||||
instance_id = current_instance.get('id')
|
||||
log_id, before_instance = get_instance_dict_from_cache(instance_id)
|
||||
@ -176,7 +178,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
|
||||
'PermedAsset', 'PermedAccount', 'MenuPermission',
|
||||
'Permission', 'TicketSession', 'ApplyLoginTicket',
|
||||
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
|
||||
'FavoriteAsset',
|
||||
'FavoriteAsset', 'Asset'
|
||||
}
|
||||
for i, app in enumerate(apps.get_models(), 1):
|
||||
app_name = app._meta.app_label
|
||||
|
@ -2,6 +2,7 @@ import copy
|
||||
from datetime import datetime
|
||||
from itertools import chain
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.db import models
|
||||
|
||||
from common.db.fields import RelatedManager
|
||||
@ -36,6 +37,9 @@ def _get_instance_field_value(
|
||||
if not include_model_fields and not getattr(f, 'primary_key', False):
|
||||
continue
|
||||
|
||||
if isinstance(f, GenericForeignKey):
|
||||
continue
|
||||
|
||||
if isinstance(f, (models.FileField, models.ImageField)):
|
||||
continue
|
||||
|
||||
@ -65,37 +69,49 @@ def _get_instance_field_value(
|
||||
continue
|
||||
data.setdefault(k, v)
|
||||
continue
|
||||
data.setdefault(str(f.verbose_name), value)
|
||||
elif isinstance(f, GenericRelation):
|
||||
value = [str(v) for v in value.all()]
|
||||
elif isinstance(f, GenericForeignKey):
|
||||
continue
|
||||
try:
|
||||
data.setdefault(str(f.verbose_name), value)
|
||||
except Exception as e:
|
||||
print(f.__dict__)
|
||||
raise e
|
||||
return data
|
||||
|
||||
|
||||
def model_to_dict_for_operate_log(
|
||||
instance, include_model_fields=True, include_related_fields=False
|
||||
instance, include_model_fields=True, include_related_fields=None
|
||||
):
|
||||
model_need_continue_fields = ['date_updated']
|
||||
m2m_need_continue_fields = ['history_passwords']
|
||||
def get_related_values(f):
|
||||
value = []
|
||||
if instance.pk is not None:
|
||||
related_name = getattr(f, 'attname', '') or getattr(f, 'related_name', '')
|
||||
if not related_name or related_name in ['history_passwords']:
|
||||
return
|
||||
try:
|
||||
value = [str(i) for i in getattr(instance, related_name).all()]
|
||||
except:
|
||||
pass
|
||||
if not value:
|
||||
return
|
||||
try:
|
||||
field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name
|
||||
data.setdefault(str(field_key), value)
|
||||
except:
|
||||
pass
|
||||
|
||||
data = _get_instance_field_value(
|
||||
instance, include_model_fields, model_need_continue_fields
|
||||
instance, include_model_fields, ['date_updated']
|
||||
)
|
||||
|
||||
if include_related_fields:
|
||||
opts = instance._meta
|
||||
for f in opts.many_to_many:
|
||||
value = []
|
||||
if instance.pk is not None:
|
||||
related_name = getattr(f, 'attname', '') or getattr(f, 'related_name', '')
|
||||
if not related_name or related_name in m2m_need_continue_fields:
|
||||
continue
|
||||
try:
|
||||
value = [str(i) for i in getattr(instance, related_name).all()]
|
||||
except:
|
||||
pass
|
||||
if not value:
|
||||
for f in chain(opts.many_to_many, opts.related_objects):
|
||||
related_model = getattr(f, 'related_model', None)
|
||||
if related_model not in include_related_fields:
|
||||
continue
|
||||
try:
|
||||
field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name
|
||||
data.setdefault(str(field_key), value)
|
||||
except:
|
||||
pass
|
||||
get_related_values(f)
|
||||
|
||||
return data
|
||||
|
@ -4,7 +4,6 @@
|
||||
from .access_key import *
|
||||
from .confirm import *
|
||||
from .connection_token import *
|
||||
from .dingtalk import *
|
||||
from .feishu import *
|
||||
from .login_confirm import *
|
||||
from .mfa import *
|
||||
@ -12,4 +11,4 @@ from .password import *
|
||||
from .sso import *
|
||||
from .temp_token import *
|
||||
from .token import *
|
||||
from .wecom import *
|
||||
from .common import *
|
||||
|
51
apps/authentication/api/common.py
Normal file
51
apps/authentication/api/common.py
Normal file
@ -0,0 +1,51 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentication import errors
|
||||
from authentication.const import ConfirmType
|
||||
from authentication.permissions import UserConfirmation
|
||||
from common.api import RoleUserMixin, RoleAdminMixin
|
||||
from common.exceptions import JMSException
|
||||
from common.permissions import IsValidUser, OnlySuperUser
|
||||
from common.utils import get_logger
|
||||
from users.models import User
|
||||
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class QRUnBindBase(APIView):
|
||||
user: User
|
||||
|
||||
def post(self, request: Request, backend: str, **kwargs):
|
||||
backend_map = {
|
||||
'wecom': {'user_field': 'wecom_id', 'not_bind_err': errors.WeComNotBound},
|
||||
'dingtalk': {'user_field': 'dingtalk_id', 'not_bind_err': errors.DingTalkNotBound},
|
||||
'feishu': {'user_field': 'feishu_id', 'not_bind_err': errors.FeiShuNotBound},
|
||||
'slack': {'user_field': 'slack_id', 'not_bind_err': errors.SlackNotBound},
|
||||
}
|
||||
user = self.user
|
||||
|
||||
backend_info = backend_map.get(backend)
|
||||
if not backend_info:
|
||||
raise JMSException(
|
||||
_('The value in the parameter must contain %s') % ', '.join(backend_map.keys())
|
||||
)
|
||||
|
||||
if not getattr(user, backend_info['user_field'], None):
|
||||
raise backend_info['not_bind_err']
|
||||
|
||||
setattr(user, backend_info['user_field'], None)
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class QRUnBindForUserApi(RoleUserMixin, QRUnBindBase):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
|
||||
class QRUnBindForAdminApi(RoleAdminMixin, QRUnBindBase):
|
||||
permission_classes = (OnlySuperUser,)
|
||||
user_id_url_kwarg = 'user_id'
|
@ -27,13 +27,13 @@ from perms.models import ActionChoices
|
||||
from terminal.connect_methods import NativeClient, ConnectMethodUtil
|
||||
from terminal.models import EndpointRule, Endpoint
|
||||
from users.const import FileNameConflictResolution
|
||||
from users.const import RDPSmartSize
|
||||
from users.const import RDPSmartSize, RDPColorQuality
|
||||
from users.models import Preference
|
||||
from ..models import ConnectionToken, date_expired_default
|
||||
from ..serializers import (
|
||||
ConnectionTokenSerializer, ConnectionTokenSecretSerializer,
|
||||
SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer,
|
||||
ConnectionTokenReusableSerializer,
|
||||
ConnectionTokenReusableSerializer, ConnectTokenVirtualAppOptionSerializer
|
||||
)
|
||||
|
||||
__all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet']
|
||||
@ -49,7 +49,6 @@ class RDPFileClientProtocolURLMixin:
|
||||
'full address:s': '',
|
||||
'username:s': '',
|
||||
'use multimon:i': '0',
|
||||
'session bpp:i': '32',
|
||||
'audiomode:i': '0',
|
||||
'disable wallpaper:i': '0',
|
||||
'disable full window drag:i': '0',
|
||||
@ -100,10 +99,13 @@ class RDPFileClientProtocolURLMixin:
|
||||
rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}'
|
||||
rdp_options['dynamic resolution:i'] = '0'
|
||||
|
||||
color_quality = self.request.query_params.get('rdp_color_quality')
|
||||
color_quality = color_quality if color_quality else os.getenv('JUMPSERVER_COLOR_DEPTH', RDPColorQuality.HIGH)
|
||||
|
||||
# 设置其他选项
|
||||
rdp_options['smart sizing:i'] = self.request.query_params.get('rdp_smart_size', RDPSmartSize.DISABLE)
|
||||
rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32')
|
||||
rdp_options['session bpp:i'] = color_quality
|
||||
rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0')
|
||||
rdp_options['smart sizing:i'] = self.request.query_params.get('rdp_smart_size', RDPSmartSize.DISABLE)
|
||||
|
||||
# 设置远程应用, 不是 Mstsc
|
||||
if token.connect_method != NativeClient.mstsc:
|
||||
@ -464,6 +466,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
'get_secret_detail': 'authentication.view_superconnectiontokensecret',
|
||||
'get_applet_info': 'authentication.view_superconnectiontoken',
|
||||
'release_applet_account': 'authentication.view_superconnectiontoken',
|
||||
'get_virtual_app_info': 'authentication.view_superconnectiontoken',
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
@ -529,14 +532,24 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
|
||||
serializer = ConnectTokenAppletOptionSerializer(data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['POST'], detail=False, url_path='virtual-app-option')
|
||||
def get_virtual_app_info(self, *args, **kwargs):
|
||||
token_id = self.request.data.get('id')
|
||||
token = get_object_or_404(ConnectionToken, pk=token_id)
|
||||
if token.is_expired:
|
||||
return Response({'error': 'Token expired'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
data = token.get_virtual_app_option()
|
||||
serializer = ConnectTokenVirtualAppOptionSerializer(data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(methods=['DELETE', 'POST'], detail=False, url_path='applet-account/release')
|
||||
def release_applet_account(self, *args, **kwargs):
|
||||
account_id = self.request.data.get('id')
|
||||
released = ConnectionToken.release_applet_account(account_id)
|
||||
lock_key = self.request.data.get('id')
|
||||
released = ConnectionToken.release_applet_account(lock_key)
|
||||
|
||||
if released:
|
||||
logger.debug('Release applet account success: {}'.format(account_id))
|
||||
logger.debug('Release applet account success: {}'.format(lock_key))
|
||||
return Response({'msg': 'released'})
|
||||
else:
|
||||
logger.error('Release applet account error: {}'.format(account_id))
|
||||
logger.error('Release applet account error: {}'.format(lock_key))
|
||||
return Response({'error': 'not found or expired'}, status=400)
|
||||
|
@ -1,35 +0,0 @@
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentication import errors
|
||||
from authentication.const import ConfirmType
|
||||
from authentication.permissions import UserConfirmation
|
||||
from common.api import RoleUserMixin, RoleAdminMixin
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import get_logger
|
||||
from users.models import User
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class DingTalkQRUnBindBase(APIView):
|
||||
user: User
|
||||
|
||||
def post(self, request: Request, **kwargs):
|
||||
user = self.user
|
||||
|
||||
if not user.dingtalk_id:
|
||||
raise errors.DingTalkNotBound
|
||||
|
||||
user.dingtalk_id = None
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
|
||||
class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
@ -2,39 +2,13 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentication import errors
|
||||
from authentication.const import ConfirmType
|
||||
from authentication.permissions import UserConfirmation
|
||||
from common.api import RoleUserMixin, RoleAdminMixin
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import get_logger
|
||||
from users.models import User
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FeiShuQRUnBindBase(APIView):
|
||||
user: User
|
||||
|
||||
def post(self, request: Request, **kwargs):
|
||||
user = self.user
|
||||
|
||||
if not user.feishu_id:
|
||||
raise errors.FeiShuNotBound
|
||||
|
||||
user.feishu_id = None
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
|
||||
class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
||||
|
||||
|
||||
class FeiShuEventSubscriptionCallback(APIView):
|
||||
"""
|
||||
# https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM
|
||||
|
@ -14,7 +14,7 @@ from common.api import JMSGenericViewSet
|
||||
from common.const.http import POST, GET
|
||||
from common.permissions import OnlySuperUser
|
||||
from common.serializers import EmptySerializer
|
||||
from common.utils import reverse
|
||||
from common.utils import reverse, safe_next_url
|
||||
from common.utils.timezone import utc_now
|
||||
from users.models import User
|
||||
from ..errors import SSOAuthClosed
|
||||
@ -45,6 +45,7 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
|
||||
username = serializer.validated_data['username']
|
||||
user = User.objects.get(username=username)
|
||||
next_url = serializer.validated_data.get(NEXT_URL)
|
||||
next_url = safe_next_url(next_url, request=request)
|
||||
|
||||
operator = request.user.username
|
||||
# TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理
|
||||
|
@ -1,35 +0,0 @@
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentication import errors
|
||||
from authentication.const import ConfirmType
|
||||
from authentication.permissions import UserConfirmation
|
||||
from common.api import RoleUserMixin, RoleAdminMixin
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import get_logger
|
||||
from users.models import User
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
class WeComQRUnBindBase(APIView):
|
||||
user: User
|
||||
|
||||
def post(self, request: Request, **kwargs):
|
||||
user = self.user
|
||||
|
||||
if not user.wecom_id:
|
||||
raise errors.WeComNotBound
|
||||
|
||||
user.wecom_id = None
|
||||
user.save()
|
||||
return Response()
|
||||
|
||||
|
||||
class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase):
|
||||
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
|
||||
|
||||
|
||||
class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase):
|
||||
user_id_url_kwarg = 'user_id'
|
@ -8,7 +8,7 @@ from django.utils.translation import gettext as _
|
||||
from rest_framework import authentication, exceptions
|
||||
|
||||
from common.auth import signature
|
||||
from common.decorators import delay_run
|
||||
from common.decorators import merge_delay_run
|
||||
from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip
|
||||
from ..models import AccessKey, PrivateToken
|
||||
|
||||
@ -17,22 +17,24 @@ def date_more_than(d, seconds):
|
||||
return d is None or (timezone.now() - d).seconds > seconds
|
||||
|
||||
|
||||
@delay_run(ttl=60)
|
||||
def update_token_last_used(token):
|
||||
token.date_last_used = timezone.now()
|
||||
token.save(update_fields=['date_last_used'])
|
||||
@merge_delay_run(ttl=60)
|
||||
def update_token_last_used(tokens=()):
|
||||
for token in tokens:
|
||||
token.date_last_used = timezone.now()
|
||||
token.save(update_fields=['date_last_used'])
|
||||
|
||||
|
||||
@delay_run(ttl=60)
|
||||
def update_user_last_used(user):
|
||||
user.date_api_key_last_used = timezone.now()
|
||||
user.save(update_fields=['date_api_key_last_used'])
|
||||
@merge_delay_run(ttl=60)
|
||||
def update_user_last_used(users=()):
|
||||
for user in users:
|
||||
user.date_api_key_last_used = timezone.now()
|
||||
user.save(update_fields=['date_api_key_last_used'])
|
||||
|
||||
|
||||
def after_authenticate_update_date(user, token=None):
|
||||
update_user_last_used(user)
|
||||
update_user_last_used(users=(user,))
|
||||
if token:
|
||||
update_token_last_used(token)
|
||||
update_token_last_used(tokens=(token,))
|
||||
|
||||
|
||||
class AccessTokenAuthentication(authentication.BaseAuthentication):
|
||||
|
@ -20,10 +20,11 @@ from django.core.exceptions import SuspiciousOperation
|
||||
from django.http import HttpResponseRedirect, QueryDict
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
|
||||
from django.utils.http import urlencode
|
||||
from django.views.generic import View
|
||||
|
||||
from authentication.utils import build_absolute_uri_for_oidc
|
||||
from common.utils import safe_next_url
|
||||
from .utils import get_logger
|
||||
|
||||
logger = get_logger(__file__)
|
||||
@ -100,8 +101,7 @@ class OIDCAuthRequestView(View):
|
||||
# Stores the "next" URL in the session if applicable.
|
||||
logger.debug(log_prompt.format('Stores next url in the session'))
|
||||
next_url = request.GET.get('next')
|
||||
request.session['oidc_auth_next_url'] = next_url \
|
||||
if url_has_allowed_host_and_scheme(url=next_url, allowed_hosts=(request.get_host(),)) else None
|
||||
request.session['oidc_auth_next_url'] = safe_next_url(next_url, request=request)
|
||||
|
||||
# Redirects the user to authorization endpoint.
|
||||
logger.debug(log_prompt.format('Construct redirect url'))
|
||||
|
@ -55,6 +55,19 @@ class FeiShuAuthentication(JMSModelBackend):
|
||||
pass
|
||||
|
||||
|
||||
class SlackAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return settings.AUTH_SLACK
|
||||
|
||||
def authenticate(self, request, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationTokenAuthentication(JMSModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
|
@ -33,6 +33,11 @@ class FeiShuNotBound(JMSException):
|
||||
default_detail = _('FeiShu is not bound')
|
||||
|
||||
|
||||
class SlackNotBound(JMSException):
|
||||
default_code = 'slack_not_bound'
|
||||
default_detail = _('Slack is not bound')
|
||||
|
||||
|
||||
class PasswordInvalid(JMSException):
|
||||
default_code = 'passwd_invalid'
|
||||
default_detail = _('Your password is invalid')
|
||||
|
@ -18,7 +18,7 @@ from common.utils import lazyproperty, pretty_string, bulk_get
|
||||
from common.utils.timezone import as_current_tz
|
||||
from orgs.mixins.models import JMSOrgBaseModel
|
||||
from orgs.utils import tmp_to_org
|
||||
from terminal.models import Applet
|
||||
from terminal.models import Applet, VirtualApp
|
||||
|
||||
|
||||
def date_expired_default():
|
||||
@ -177,6 +177,15 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||
}
|
||||
return options
|
||||
|
||||
def get_virtual_app_option(self):
|
||||
method = self.connect_method_object
|
||||
if not method or method.get('type') != 'virtual_app' or method.get('disabled', False):
|
||||
return None
|
||||
virtual_app = VirtualApp.objects.filter(name=method.get('value')).first()
|
||||
if not virtual_app:
|
||||
return None
|
||||
return virtual_app
|
||||
|
||||
def get_applet_option(self):
|
||||
method = self.connect_method_object
|
||||
if not method or method.get('type') != 'applet' or method.get('disabled', False):
|
||||
@ -190,28 +199,23 @@ class ConnectionToken(JMSOrgBaseModel):
|
||||
if not host_account:
|
||||
raise JMSException({'error': 'No host account available'})
|
||||
|
||||
host, account, lock_key, ttl = bulk_get(host_account, ('host', 'account', 'lock_key', 'ttl'))
|
||||
host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key'))
|
||||
gateway = host.domain.select_gateway() if host.domain else None
|
||||
|
||||
data = {
|
||||
'id': account.id,
|
||||
'id': lock_key,
|
||||
'applet': applet,
|
||||
'host': host,
|
||||
'gateway': gateway,
|
||||
'account': account,
|
||||
'remote_app_option': self.get_remote_app_option()
|
||||
}
|
||||
token_account_relate_key = f'token_account_relate_{account.id}'
|
||||
cache.set(token_account_relate_key, lock_key, ttl)
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def release_applet_account(account_id):
|
||||
token_account_relate_key = f'token_account_relate_{account_id}'
|
||||
lock_key = cache.get(token_account_relate_key)
|
||||
def release_applet_account(lock_key):
|
||||
if lock_key:
|
||||
cache.delete(lock_key)
|
||||
cache.delete(token_account_relate_key)
|
||||
return True
|
||||
|
||||
@lazyproperty
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user