diff --git a/Dockerfile-ce b/Dockerfile-ce
index 55ac03003..915494216 100644
--- a/Dockerfile-ce
+++ b/Dockerfile-ce
@@ -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
diff --git a/apps/accounts/api/account/task.py b/apps/accounts/api/account/task.py
index 16ea84eae..c4f6ebd9f 100644
--- a/apps/accounts/api/account/task.py
+++ b/apps/accounts/api/account/task.py
@@ -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
diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml
index c4381b730..8ff38475f 100644
--- a/apps/accounts/automations/change_secret/custom/ssh/main.yml
+++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml
@@ -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('') }}"
diff --git a/apps/accounts/automations/change_secret/custom/ssh/manifest.yml b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml
index c46511344..7d3d0edde 100644
--- a/apps/accounts/automations/change_secret/custom/ssh/manifest.yml
+++ b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml
@@ -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 连接的用户密码 字段,
请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。
比如针对 Cisco 主机进行改密,一般需要配置五条命令:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
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 连接的用户密码 字段,
请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。
比如针对 Cisco 主机进行改密,一般需要配置五条命令:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
5. end'
+ ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、
{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。
たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:
1.enable
2.{login_password}
3 .ターミナルの設定
4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード}
5. 終了'
+ en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,
Please use {username}, {password}, {login_password 125; format, which will be replaced when executing the task.
For example, to change the password of a Cisco host, you generally need to configure five commands:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
5. end'
+
+ Params commands label:
+ zh: '自定义命令'
+ ja: 'カスタムコマンド'
+ en: 'Custom command'
diff --git a/apps/accounts/automations/change_secret/database/mysql/main.yml b/apps/accounts/automations/change_secret/database/mysql/main.yml
index 91b7d6f2c..f36eff171 100644
--- a/apps/accounts/automations/change_secret/database/mysql/main.yml
+++ b/apps/accounts/automations/change_secret/database/mysql/main.yml
@@ -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
diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py
index adc840c3f..46d3ef2b6 100644
--- a/apps/accounts/automations/change_secret/manager.py
+++ b/apps/accounts/automations/change_secret/manager.py
@@ -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(),
}
diff --git a/apps/accounts/automations/endpoint.py b/apps/accounts/automations/endpoint.py
index c1a19c968..f045858a7 100644
--- a/apps/accounts/automations/endpoint.py
+++ b/apps/accounts/automations/endpoint.py
@@ -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 后期迁移到自动化策略中
diff --git a/apps/accounts/automations/gather_accounts/database/mysql/main.yml b/apps/accounts/automations/gather_accounts/database/mysql/main.yml
index e8cf3cac4..e36925209 100644
--- a/apps/accounts/automations/gather_accounts/database/mysql/main.yml
+++ b/apps/accounts/automations/gather_accounts/database/mysql/main.yml
@@ -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
diff --git a/apps/accounts/automations/push_account/database/mysql/main.yml b/apps/accounts/automations/push_account/database/mysql/main.yml
index 91b7d6f2c..f36eff171 100644
--- a/apps/accounts/automations/push_account/database/mysql/main.yml
+++ b/apps/accounts/automations/push_account/database/mysql/main.yml
@@ -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
diff --git a/apps/accounts/automations/push_account/host/aix/manifest.yml b/apps/accounts/automations/push_account/host/aix/manifest.yml
index 949d49758..ee62d7020 100644
--- a/apps/accounts/automations/push_account/host/aix/manifest.yml
+++ b/apps/accounts/automations/push_account/host/aix/manifest.yml
@@ -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'
diff --git a/apps/accounts/automations/push_account/host/posix/manifest.yml b/apps/accounts/automations/push_account/host/posix/manifest.yml
index 0c1d31845..32964f1d6 100644
--- a/apps/accounts/automations/push_account/host/posix/manifest.yml
+++ b/apps/accounts/automations/push_account/host/posix/manifest.yml
@@ -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'
\ No newline at end of file
diff --git a/apps/accounts/automations/push_account/host/windows/manifest.yml b/apps/accounts/automations/push_account/host/windows/manifest.yml
index 7866e3c13..dcbdfe7f8 100644
--- a/apps/accounts/automations/push_account/host/windows/manifest.yml
+++ b/apps/accounts/automations/push_account/host/windows/manifest.yml
@@ -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)'
diff --git a/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml b/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml
index 449cf726f..d08a29ebc 100644
--- a/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml
+++ b/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml
@@ -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)'
diff --git a/apps/accounts/automations/remove_account/__init__.py b/apps/accounts/automations/remove_account/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/accounts/automations/remove_account/database/mongodb/main.yml b/apps/accounts/automations/remove_account/database/mongodb/main.yml
new file mode 100644
index 000000000..3ec800981
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/mongodb/main.yml
@@ -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
\ No newline at end of file
diff --git a/apps/accounts/automations/remove_account/database/mongodb/manifest.yml b/apps/accounts/automations/remove_account/database/mongodb/manifest.yml
new file mode 100644
index 000000000..681a6a521
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/mongodb/manifest.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/database/mysql/main.yml b/apps/accounts/automations/remove_account/database/mysql/main.yml
new file mode 100644
index 000000000..563a7d74b
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/mysql/main.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/database/mysql/manifest.yml b/apps/accounts/automations/remove_account/database/mysql/manifest.yml
new file mode 100644
index 000000000..ff8845a6d
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/mysql/manifest.yml
@@ -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
+
diff --git a/apps/accounts/automations/remove_account/database/oracle/main.yml b/apps/accounts/automations/remove_account/database/oracle/main.yml
new file mode 100644
index 000000000..ffd846d47
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/oracle/main.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/database/oracle/manifest.yml b/apps/accounts/automations/remove_account/database/oracle/manifest.yml
new file mode 100644
index 000000000..173053673
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/oracle/manifest.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/database/postgresql/main.yml b/apps/accounts/automations/remove_account/database/postgresql/main.yml
new file mode 100644
index 000000000..7004dc945
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/postgresql/main.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/database/postgresql/manifest.yml b/apps/accounts/automations/remove_account/database/postgresql/manifest.yml
new file mode 100644
index 000000000..c6c143e94
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/postgresql/manifest.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/database/sqlserver/main.yml b/apps/accounts/automations/remove_account/database/sqlserver/main.yml
new file mode 100644
index 000000000..597e12906
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/sqlserver/main.yml
@@ -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 }}"
diff --git a/apps/accounts/automations/remove_account/database/sqlserver/manifest.yml b/apps/accounts/automations/remove_account/database/sqlserver/manifest.yml
new file mode 100644
index 000000000..33c4895e2
--- /dev/null
+++ b/apps/accounts/automations/remove_account/database/sqlserver/manifest.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/host/posix/main.yml b/apps/accounts/automations/remove_account/host/posix/main.yml
new file mode 100644
index 000000000..de91b8552
--- /dev/null
+++ b/apps/accounts/automations/remove_account/host/posix/main.yml
@@ -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 }}"
diff --git a/apps/accounts/automations/remove_account/host/posix/manifest.yml b/apps/accounts/automations/remove_account/host/posix/manifest.yml
new file mode 100644
index 000000000..753c63574
--- /dev/null
+++ b/apps/accounts/automations/remove_account/host/posix/manifest.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/host/windows/main.yml b/apps/accounts/automations/remove_account/host/windows/main.yml
new file mode 100644
index 000000000..7be9940b3
--- /dev/null
+++ b/apps/accounts/automations/remove_account/host/windows/main.yml
@@ -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
\ No newline at end of file
diff --git a/apps/accounts/automations/remove_account/host/windows/manifest.yml b/apps/accounts/automations/remove_account/host/windows/manifest.yml
new file mode 100644
index 000000000..588d7efea
--- /dev/null
+++ b/apps/accounts/automations/remove_account/host/windows/manifest.yml
@@ -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
diff --git a/apps/accounts/automations/remove_account/manager.py b/apps/accounts/automations/remove_account/manager.py
new file mode 100644
index 000000000..37dd28f2d
--- /dev/null
+++ b/apps/accounts/automations/remove_account/manager.py
@@ -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()
diff --git a/apps/accounts/automations/verify_account/custom/rdp/main.yml b/apps/accounts/automations/verify_account/custom/rdp/main.yml
index 017f4bab3..b0c7cbe4f 100644
--- a/apps/accounts/automations/verify_account/custom/rdp/main.yml
+++ b/apps/accounts/automations/verify_account/custom/rdp/main.yml
@@ -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 }}"
diff --git a/apps/accounts/automations/verify_account/custom/rdp/manifest.yml b/apps/accounts/automations/verify_account/custom/rdp/manifest.yml
index 79fcce96b..e4b034366 100644
--- a/apps/accounts/automations/verify_account/custom/rdp/manifest.yml
+++ b/apps/accounts/automations/verify_account/custom/rdp/manifest.yml
@@ -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'
diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml
index 4519fc3ad..05be21f0c 100644
--- a/apps/accounts/automations/verify_account/custom/ssh/main.yml
+++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml
@@ -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 }}"
diff --git a/apps/accounts/automations/verify_account/custom/ssh/manifest.yml b/apps/accounts/automations/verify_account/custom/ssh/manifest.yml
index 666266416..bebc02c7f 100644
--- a/apps/accounts/automations/verify_account/custom/ssh/manifest.yml
+++ b/apps/accounts/automations/verify_account/custom/ssh/manifest.yml
@@ -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'
diff --git a/apps/accounts/automations/verify_account/database/mongodb/main.yml b/apps/accounts/automations/verify_account/database/mongodb/main.yml
index 2770e6eb4..13ecccb61 100644
--- a/apps/accounts/automations/verify_account/database/mongodb/main.yml
+++ b/apps/accounts/automations/verify_account/database/mongodb/main.yml
@@ -1,4 +1,4 @@
-- hosts: mongdb
+- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python
diff --git a/apps/accounts/automations/verify_account/database/mysql/main.yml b/apps/accounts/automations/verify_account/database/mysql/main.yml
index 2ae3a4abd..e2768d2c2 100644
--- a/apps/accounts/automations/verify_account/database/mysql/main.yml
+++ b/apps/accounts/automations/verify_account/database/mysql/main.yml
@@ -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
diff --git a/apps/accounts/automations/verify_account/manager.py b/apps/accounts/automations/verify_account/manager.py
index 18478fb21..794cf4ff6 100644
--- a/apps/accounts/automations/verify_account/manager.py
+++ b/apps/accounts/automations/verify_account/manager.py
@@ -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(),
}
diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py
index 0a67de5c8..cde7fe982 100644
--- a/apps/accounts/const/automation.py
+++ b/apps/accounts/const/automation.py
@@ -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')
diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py
index e71a5b9fb..5e4e0c257 100644
--- a/apps/accounts/filters.py
+++ b/apps/accounts/filters.py
@@ -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']
diff --git a/apps/accounts/migrations/0007_alter_account_options.py b/apps/accounts/migrations/0007_alter_account_options.py
index 4ec798a21..6def6de6e 100644
--- a/apps/accounts/migrations/0007_alter_account_options.py
+++ b/apps/accounts/migrations/0007_alter_account_options.py
@@ -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'},
),
]
diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py
index 8a9187190..4a175609f 100644
--- a/apps/accounts/models/account.py
+++ b/apps/accounts/models/account.py
@@ -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():
"""
diff --git a/apps/accounts/models/template.py b/apps/accounts/models/template.py
index c56be1464..63ed1b20d 100644
--- a/apps/accounts/models/template.py
+++ b/apps/accounts/models/template.py
@@ -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")
diff --git a/apps/accounts/notifications.py b/apps/accounts/notifications.py
index b650410d1..404125fd3 100644
--- a/apps/accounts/notifications.py
+++ b/apps/accounts/notifications.py
@@ -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(
diff --git a/apps/accounts/permissions.py b/apps/accounts/permissions.py
new file mode 100644
index 000000000..6d3b94260
--- /dev/null
+++ b/apps/accounts/permissions.py
@@ -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)
diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py
index 8b8d9be36..197c64c7d 100644
--- a/apps/accounts/serializers/account/account.py
+++ b/apps/accounts/serializers/account/account.py
@@ -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,
diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py
index 5289ea25b..23dec0d3e 100644
--- a/apps/accounts/serializers/account/base.py
+++ b/apps/accounts/serializers/account/base.py
@@ -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',
]
diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py
index 635c221bf..ea302c3fd 100644
--- a/apps/accounts/serializers/account/template.py
+++ b/apps/accounts/serializers/account/template.py
@@ -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):
diff --git a/apps/accounts/tasks/__init__.py b/apps/accounts/tasks/__init__.py
index 8a79c1aa4..a20eba291 100644
--- a/apps/accounts/tasks/__init__.py
+++ b/apps/accounts/tasks/__init__.py
@@ -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 *
diff --git a/apps/accounts/tasks/remove_account.py b/apps/accounts/tasks/remove_account.py
new file mode 100644
index 000000000..269a7f349
--- /dev/null
+++ b/apps/accounts/tasks/remove_account.py
@@ -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)
diff --git a/apps/accounts/utils.py b/apps/accounts/utils.py
index 229cb0111..9df67daf6 100644
--- a/apps/accounts/utils.py
+++ b/apps/accounts/utils.py
@@ -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):
diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py
index 2043f274d..8f7cc531a 100644
--- a/apps/acls/api/command_acl.py
+++ b/apps/acls/api/command_acl.py
@@ -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
diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py
index 8ae0cd9bd..9eccd4977 100644
--- a/apps/assets/api/__init__.py
+++ b/apps/assets/api/__init__.py
@@ -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 *
diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py
index 4b36b4bbe..e0b1aa3bf 100644
--- a/apps/assets/api/asset/asset.py
+++ b/apps/assets/api/asset/asset.py
@@ -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
]
diff --git a/apps/assets/api/label.py b/apps/assets/api/label.py
deleted file mode 100644
index d970d2180..000000000
--- a/apps/assets/api/label.py
+++ /dev/null
@@ -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
diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py
index 817952d46..2a74243d0 100644
--- a/apps/assets/automations/base/manager.py
+++ b/apps/assets/automations/base/manager.py
@@ -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()
diff --git a/apps/assets/automations/gather_facts/database/mysql/main.yml b/apps/assets/automations/gather_facts/database/mysql/main.yml
index b9d1fce8c..348a2150d 100644
--- a/apps/assets/automations/gather_facts/database/mysql/main.yml
+++ b/apps/assets/automations/gather_facts/database/mysql/main.yml
@@ -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
diff --git a/apps/assets/automations/gather_facts/host/posix/manifest.yml b/apps/assets/automations/gather_facts/host/posix/manifest.yml
index a92c496e9..d1a833311 100644
--- a/apps/assets/automations/gather_facts/host/posix/manifest.yml
+++ b/apps/assets/automations/gather_facts/host/posix/manifest.yml
@@ -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から情報を収集する'
diff --git a/apps/assets/automations/gather_facts/host/windows/manifest.yml b/apps/assets/automations/gather_facts/host/windows/manifest.yml
index 809208e10..7068f4d6e 100644
--- a/apps/assets/automations/gather_facts/host/windows/manifest.yml
+++ b/apps/assets/automations/gather_facts/host/windows/manifest.yml
@@ -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から情報を収集する'
diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py
index 8db474d7b..1453cc7a1 100644
--- a/apps/assets/automations/methods.py
+++ b/apps/assets/automations/methods.py
@@ -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)
diff --git a/apps/assets/automations/ping/custom/rdp/main.yml b/apps/assets/automations/ping/custom/rdp/main.yml
index a68670998..75e40c027 100644
--- a/apps/assets/automations/ping/custom/rdp/main.yml
+++ b/apps/assets/automations/ping/custom/rdp/main.yml
@@ -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 }}"
diff --git a/apps/assets/automations/ping/custom/rdp/manifest.yml b/apps/assets/automations/ping/custom/rdp/manifest.yml
index 77b8a855e..b8346c3f2 100644
--- a/apps/assets/automations/ping/custom/rdp/manifest.yml
+++ b/apps/assets/automations/ping/custom/rdp/manifest.yml
@@ -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する'
diff --git a/apps/assets/automations/ping/custom/ssh/main.yml b/apps/assets/automations/ping/custom/ssh/main.yml
index 925d3f2e1..b974425be 100644
--- a/apps/assets/automations/ping/custom/ssh/main.yml
+++ b/apps/assets/automations/ping/custom/ssh/main.yml
@@ -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) }}"
diff --git a/apps/assets/automations/ping/custom/ssh/manifest.yml b/apps/assets/automations/ping/custom/ssh/manifest.yml
index d57a50a2e..7a7068108 100644
--- a/apps/assets/automations/ping/custom/ssh/manifest.yml
+++ b/apps/assets/automations/ping/custom/ssh/manifest.yml
@@ -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する'
diff --git a/apps/assets/automations/ping/database/mysql/main.yml b/apps/assets/automations/ping/database/mysql/main.yml
index 2180610d4..f99333bdb 100644
--- a/apps/assets/automations/ping/database/mysql/main.yml
+++ b/apps/assets/automations/ping/database/mysql/main.yml
@@ -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
diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py
index 8654002b9..c33052c64 100644
--- a/apps/assets/const/types.py
+++ b/apps/assets/const/types.py
@@ -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):
diff --git a/apps/assets/filters.py b/apps/assets/filters.py
index 17e8414bf..f1fe6d666 100644
--- a/apps/assets/filters.py
+++ b/apps/assets/filters.py
@@ -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')
diff --git a/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py b/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py
index 7cbf78a43..6392158fd 100644
--- a/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py
+++ b/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py
@@ -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',
diff --git a/apps/assets/migrations/0007_auto_20180225_1815.py b/apps/assets/migrations/0007_auto_20180225_1815.py
index 4ce2b1e05..1097dd182 100644
--- a/apps/assets/migrations/0007_auto_20180225_1815.py
+++ b/apps/assets/migrations/0007_auto_20180225_1815.py
@@ -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',
diff --git a/apps/assets/migrations/0107_automation.py b/apps/assets/migrations/0107_automation.py
index 56c2cf4eb..6841cb912 100644
--- a/apps/assets/migrations/0107_automation.py
+++ b/apps/assets/migrations/0107_automation.py
@@ -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',
diff --git a/apps/assets/migrations/0126_remove_asset_labels.py b/apps/assets/migrations/0126_remove_asset_labels.py
new file mode 100644
index 000000000..44590dc4c
--- /dev/null
+++ b/apps/assets/migrations/0126_remove_asset_labels.py
@@ -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',
+ ),
+ ]
diff --git a/apps/assets/migrations/0127_automation_remove_account.py b/apps/assets/migrations/0127_automation_remove_account.py
new file mode 100644
index 000000000..e0746326f
--- /dev/null
+++ b/apps/assets/migrations/0127_automation_remove_account.py
@@ -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)
+ ]
diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py
index a22e552e9..558164df6 100644
--- a/apps/assets/models/asset/common.py
+++ b/apps/assets/models/asset/common.py
@@ -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 = {}
diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py
index 27e5ed74a..c3e1fa639 100644
--- a/apps/assets/models/automations/base.py
+++ b/apps/assets/models/automations/base.py
@@ -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"))
diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index ec6f1be3e..63fd7ca34 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -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,
diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py
index 587f41d7c..e424a2d46 100644
--- a/apps/assets/models/domain.py
+++ b/apps/assets/models/domain.py
@@ -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:
diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py
index 8fed01acf..a93a96863 100644
--- a/apps/assets/models/platform.py
+++ b/apps/assets/models/platform.py
@@ -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):
"""
对资产提供 约束和默认值
对资产进行抽象
diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py
index cbf21454d..e071e24c0 100644
--- a/apps/assets/serializers/__init__.py
+++ b/apps/assets/serializers/__init__.py
@@ -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 *
diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py
index 75d8c4c19..6c45944de 100644
--- a/apps/assets/serializers/asset/common.py
+++ b/apps/assets/serializers/asset/common.py
@@ -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
diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py
index a22d1a9ad..d2b3e3550 100644
--- a/apps/assets/serializers/domain.py
+++ b/apps/assets/serializers/domain.py
@@ -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)
diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py
deleted file mode 100644
index 3d913aeea..000000000
--- a/apps/assets/serializers/label.py
+++ /dev/null
@@ -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])
diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py
index 2a0634a43..f67df9906 100644
--- a/apps/assets/serializers/platform.py
+++ b/apps/assets/serializers/platform.py
@@ -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):
diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py
index 5644d7dc0..408550ece 100644
--- a/apps/assets/urls/api_urls.py
+++ b/apps/assets/urls/api_urls.py
@@ -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')
diff --git a/apps/audits/backends/db.py b/apps/audits/backends/db.py
index 870c25c9c..e8fe43c22 100644
--- a/apps/audits/backends/db.py
+++ b/apps/audits/backends/db.py
@@ -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 {}
diff --git a/apps/audits/migrations/0024_usersession.py b/apps/audits/migrations/0024_usersession.py
index 3cca28f75..c5a6c9331 100644
--- a/apps/audits/migrations/0024_usersession.py
+++ b/apps/audits/migrations/0024_usersession.py
@@ -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')],
},
),
]
diff --git a/apps/audits/models.py b/apps/audits/models.py
index 97496102b..64a0ebc5b 100644
--- a/apps/audits/models.py
+++ b/apps/audits/models.py
@@ -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')),
]
diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py
index b68d1a516..78c42fdd0 100644
--- a/apps/audits/serializers.py
+++ b/apps/audits/serializers.py
@@ -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):
diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py
index 7765e470b..5829e4f5e 100644
--- a/apps/audits/signal_handlers/login_log.py
+++ b/apps/audits/signal_handlers/login_log.py
@@ -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")
diff --git a/apps/audits/signal_handlers/operate_log.py b/apps/audits/signal_handlers/operate_log.py
index 7d89f77bd..d095a7ad6 100644
--- a/apps/audits/signal_handlers/operate_log.py
+++ b/apps/audits/signal_handlers/operate_log.py
@@ -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
diff --git a/apps/audits/utils.py b/apps/audits/utils.py
index 44e858098..4312962e6 100644
--- a/apps/audits/utils.py
+++ b/apps/audits/utils.py
@@ -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
diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py
index 17e83813c..7a63c007c 100644
--- a/apps/authentication/api/__init__.py
+++ b/apps/authentication/api/__init__.py
@@ -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 *
diff --git a/apps/authentication/api/common.py b/apps/authentication/api/common.py
new file mode 100644
index 000000000..6624078a7
--- /dev/null
+++ b/apps/authentication/api/common.py
@@ -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'
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py
index 954fb6074..076767946 100644
--- a/apps/authentication/api/connection_token.py
+++ b/apps/authentication/api/connection_token.py
@@ -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)
diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py
deleted file mode 100644
index ad3bd26b1..000000000
--- a/apps/authentication/api/dingtalk.py
+++ /dev/null
@@ -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'
diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py
index cf95bb6ea..ca90be807 100644
--- a/apps/authentication/api/feishu.py
+++ b/apps/authentication/api/feishu.py
@@ -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
diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py
index 0756bad4e..6e48bda41 100644
--- a/apps/authentication/api/sso.py
+++ b/apps/authentication/api/sso.py
@@ -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` 统一处理
diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py
deleted file mode 100644
index 6dcfe539c..000000000
--- a/apps/authentication/api/wecom.py
+++ /dev/null
@@ -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'
diff --git a/apps/authentication/backends/drf.py b/apps/authentication/backends/drf.py
index f6818e4a4..86999037d 100644
--- a/apps/authentication/backends/drf.py
+++ b/apps/authentication/backends/drf.py
@@ -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):
diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py
index 98bd2ef2a..56c1e4fb0 100644
--- a/apps/authentication/backends/oidc/views.py
+++ b/apps/authentication/backends/oidc/views.py
@@ -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'))
diff --git a/apps/authentication/backends/sso.py b/apps/authentication/backends/sso.py
index 7bee484dc..66f15a3a4 100644
--- a/apps/authentication/backends/sso.py
+++ b/apps/authentication/backends/sso.py
@@ -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):
"""
什么也不做呀😺
diff --git a/apps/authentication/errors/mfa.py b/apps/authentication/errors/mfa.py
index 8a0844145..b1ace594d 100644
--- a/apps/authentication/errors/mfa.py
+++ b/apps/authentication/errors/mfa.py
@@ -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')
diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py
index 48c8e8e16..2ef28eab1 100644
--- a/apps/authentication/models/connection_token.py
+++ b/apps/authentication/models/connection_token.py
@@ -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
diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py
index 6685316bd..3eea79e4e 100644
--- a/apps/authentication/serializers/connect_token_secret.py
+++ b/apps/authentication/serializers/connect_token_secret.py
@@ -15,7 +15,8 @@ from users.models import User
from ..models import ConnectionToken
__all__ = [
- 'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer'
+ 'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer',
+ 'ConnectTokenVirtualAppOptionSerializer',
]
@@ -161,3 +162,10 @@ class ConnectTokenAppletOptionSerializer(serializers.Serializer):
account = _ConnectionTokenAccountSerializer(read_only=True)
gateway = _ConnectionTokenGatewaySerializer(read_only=True)
remote_app_option = serializers.JSONField(read_only=True)
+
+
+class ConnectTokenVirtualAppOptionSerializer(serializers.Serializer):
+ name = serializers.CharField(label=_('Name'))
+ image_name = serializers.CharField(label=_('Image name'))
+ image_port = serializers.IntegerField(label=_('Image port'))
+ image_protocol = serializers.CharField(label=_('Image protocol'))
diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html
index 7e644c047..4d7489034 100644
--- a/apps/authentication/templates/authentication/login.html
+++ b/apps/authentication/templates/authentication/login.html
@@ -286,9 +286,9 @@
{% blocktranslate %} - The following {{ item_type }} will expire in {{ count }} days + The following {{ item_type }} will expire in {{ count }} {% endblocktranslate %}
diff --git a/apps/perms/templates/perms/_msg_permed_items_expire.html b/apps/perms/templates/perms/_msg_permed_items_expire.html index f4229b7eb..eb2a3cc47 100644 --- a/apps/perms/templates/perms/_msg_permed_items_expire.html +++ b/apps/perms/templates/perms/_msg_permed_items_expire.html @@ -5,7 +5,7 @@{% blocktranslate %} - The following {{ item_type }} will expire in {{ count }} days + The following {{ item_type }} will expire in {{ count }} {% endblocktranslate %}
@@ -15,7 +15,7 @@ {% endfor %} -
{% trans 'If you have any question, please contact the administrator' %}
diff --git a/apps/rbac/api/__init__.py b/apps/rbac/api/__init__.py
index 894655045..28436525a 100644
--- a/apps/rbac/api/__init__.py
+++ b/apps/rbac/api/__init__.py
@@ -1,4 +1,4 @@
+from .content_type import *
from .permission import *
from .role import *
from .rolebinding import *
-
diff --git a/apps/rbac/api/content_type.py b/apps/rbac/api/content_type.py
new file mode 100644
index 000000000..2afe021ca
--- /dev/null
+++ b/apps/rbac/api/content_type.py
@@ -0,0 +1,11 @@
+from rest_framework import viewsets
+
+from .. import serializers
+from ..models import ContentType
+
+
+class ContentTypeViewSet(viewsets.ModelViewSet):
+ serializer_class = serializers.ContentTypeSerializer
+ filterset_fields = ("app_label", "model",)
+ search_fields = filterset_fields
+ queryset = ContentType.objects.all()
diff --git a/apps/rbac/api/role.py b/apps/rbac/api/role.py
index 387d0bc9c..b9133c5c5 100644
--- a/apps/rbac/api/role.py
+++ b/apps/rbac/api/role.py
@@ -56,9 +56,22 @@ class RoleViewSet(JMSModelViewSet):
return
instance.permissions.set(clone.get_permissions())
+ def filter_builtins(self, queryset):
+ keyword = self.request.query_params.get('search')
+ if not keyword:
+ return queryset
+
+ builtins = list(self.get_queryset().filter(builtin=True))
+ matched = [role.id for role in builtins if keyword in role.display_name]
+ if not matched:
+ return queryset
+ queryset = list(queryset.exclude(id__in=matched))
+ return queryset + builtins
+
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = queryset.order_by(*self.ordering)
+ queryset = self.filter_builtins(queryset)
return queryset
def set_users_amount(self, queryset):
diff --git a/apps/rbac/const.py b/apps/rbac/const.py
index 01c8bd915..71eb47fbc 100644
--- a/apps/rbac/const.py
+++ b/apps/rbac/const.py
@@ -28,6 +28,7 @@ exclude_permissions = (
('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'),
('authentication', 'temptoken', 'delete', 'temptoken'),
('users', 'userpasswordhistory', '*', '*'),
+ ('users', 'usersession', '*', '*'),
('assets', 'adminuser', '*', '*'),
('assets', 'assetgroup', '*', '*'),
('assets', 'cluster', '*', '*'),
@@ -73,7 +74,7 @@ exclude_permissions = (
('perms', 'rebuildusertreetask', '*', '*'),
('perms', 'permedasset', '*', 'permedasset'),
('perms', 'permedapplication', 'add,change,delete', 'permedapplication'),
- ('rbac', 'contenttype', '*', '*'),
+ ('rbac', 'contenttype', 'add,change,delete', '*'),
('rbac', 'permission', 'add,delete,change', 'permission'),
('rbac', 'rolebinding', '*', '*'),
('rbac', 'systemrolebinding', 'change', 'systemrolebinding'),
diff --git a/apps/rbac/migrations/0014_auto_20231208_1548.py b/apps/rbac/migrations/0014_auto_20231208_1548.py
new file mode 100644
index 000000000..5fb8cefb7
--- /dev/null
+++ b/apps/rbac/migrations/0014_auto_20231208_1548.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.10 on 2023-12-08 07:48
+
+from django.db import migrations
+
+
+def migrate_update_offline_usersession_permission_name(apps, *args):
+ perm_model = apps.get_model('auth', 'Permission')
+ perm_model.objects.filter(codename='offline_usersession').update(name='Offline user session')
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('rbac', '0013_alter_menupermission_options'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_update_offline_usersession_permission_name)
+ ]
diff --git a/apps/rbac/models/permission.py b/apps/rbac/models/permission.py
index 17653f151..2b597c3c9 100644
--- a/apps/rbac/models/permission.py
+++ b/apps/rbac/models/permission.py
@@ -1,8 +1,10 @@
+from django.apps import apps
from django.contrib.auth.models import ContentType as DjangoContentType
from django.contrib.auth.models import Permission as DjangoPermission
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
+from common.utils import lazyproperty
from .. import const
Scope = const.Scope
@@ -14,10 +16,59 @@ class ContentType(DjangoContentType):
class Meta:
proxy = True
+ _apps_map = {}
+
@property
def app_model(self):
return '%s.%s' % (self.app_label, self.model)
+ @classmethod
+ def apps_map(cls):
+ from ..tree import app_nodes_data
+ if cls._apps_map:
+ return cls._apps_map
+ mapper = {}
+ for d in app_nodes_data:
+ i = d['id']
+ name = d.get('name')
+
+ if not name:
+ config = apps.get_app_config(d['id'])
+ if config:
+ name = config.verbose_name
+ if name:
+ mapper[i] = name
+ cls._apps_map = mapper
+ return mapper
+
+ @property
+ def app_display(self):
+ return self.apps_map().get(self.app_label)
+
+ @lazyproperty
+ def fields(self):
+ model = self.model_class()
+ return model._meta.fields
+
+ @lazyproperty
+ def field_names(self):
+ return [f.name for f in self.fields]
+
+ @lazyproperty
+ def filter_field_names(self):
+ names = []
+ if 'name' in self.field_names:
+ names.append('name')
+ if 'address' in self.field_names:
+ names.append('address')
+ return names
+
+ def filter_queryset(self, queryset, keyword):
+ q = Q()
+ for name in self.filter_field_names:
+ q |= Q(**{name + '__icontains': keyword})
+ return queryset.filter(q)
+
class Permission(DjangoPermission):
""" 权限类 """
diff --git a/apps/rbac/serializers/__init__.py b/apps/rbac/serializers/__init__.py
index 894655045..28436525a 100644
--- a/apps/rbac/serializers/__init__.py
+++ b/apps/rbac/serializers/__init__.py
@@ -1,4 +1,4 @@
+from .content_type import *
from .permission import *
from .role import *
from .rolebinding import *
-
diff --git a/apps/rbac/serializers/content_type.py b/apps/rbac/serializers/content_type.py
new file mode 100644
index 000000000..8e811230c
--- /dev/null
+++ b/apps/rbac/serializers/content_type.py
@@ -0,0 +1,13 @@
+from rest_framework import serializers
+
+from ..models import ContentType
+
+__all__ = ['ContentTypeSerializer']
+
+
+class ContentTypeSerializer(serializers.ModelSerializer):
+ app_display = serializers.CharField()
+
+ class Meta:
+ model = ContentType
+ fields = ('id', 'app_label', 'app_display', 'model', 'name')
diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py
index 7a7a989f7..8e9a56831 100644
--- a/apps/rbac/tree.py
+++ b/apps/rbac/tree.py
@@ -39,7 +39,9 @@ app_nodes_data = [
{'id': 'rbac', 'view': 'view_console'},
{'id': 'settings', 'view': 'view_setting'},
{'id': 'tickets', 'view': 'view_other'},
+ {'id': 'labels', 'view': 'view_console'},
{'id': 'authentication', 'view': 'view_other'},
+ {'id': 'ops', 'view': 'view_workbench'},
]
# 额外其他节点,可以在不同的层次,需要指定父节点,可以将一些 model 归类到这个节点下面
diff --git a/apps/rbac/urls/api_urls.py b/apps/rbac/urls/api_urls.py
index 5dc080930..f68aab11d 100644
--- a/apps/rbac/urls/api_urls.py
+++ b/apps/rbac/urls/api_urls.py
@@ -6,7 +6,6 @@ from .. import api
app_name = 'rbac'
-
router = BulkRouter()
router.register(r'roles', api.RoleViewSet, 'role')
router.register(r'role-bindings', api.RoleBindingViewSet, 'role-binding')
@@ -18,6 +17,7 @@ router.register(r'org-roles', api.OrgRoleViewSet, 'org-role')
router.register(r'org-role-bindings', api.OrgRoleBindingViewSet, 'org-role-binding')
router.register(r'permissions', api.PermissionViewSet, 'permission')
+router.register(r'content-types', api.ContentTypeViewSet, 'content-type')
system_role_router = routers.NestedDefaultRouter(router, r'system-roles', lookup='system_role')
system_role_router.register(r'permissions', api.SystemRolePermissionsViewSet, 'system-role-permission')
diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py
index 510925590..686a52f3b 100644
--- a/apps/settings/api/__init__.py
+++ b/apps/settings/api/__init__.py
@@ -1,3 +1,4 @@
+from .chat import *
from .dingtalk import *
from .email import *
from .feishu import *
@@ -5,6 +6,7 @@ from .ldap import *
from .public import *
from .security import *
from .settings import *
+from .slack import *
from .sms import *
from .vault import *
from .wecom import *
diff --git a/apps/settings/api/chat.py b/apps/settings/api/chat.py
new file mode 100644
index 000000000..68a80f013
--- /dev/null
+++ b/apps/settings/api/chat.py
@@ -0,0 +1,119 @@
+import httpx
+import openai
+from django.conf import settings
+from django.utils.translation import gettext_lazy as _
+from rest_framework import status
+from rest_framework.generics import GenericAPIView
+from rest_framework.response import Response
+
+from common.api import JMSModelViewSet
+from common.permissions import IsValidUser, OnlySuperUser
+from .. import serializers
+from ..models import ChatPrompt
+from ..prompt import DefaultChatPrompt
+
+
+class ChatAITestingAPI(GenericAPIView):
+ serializer_class = serializers.ChatAISettingSerializer
+ rbac_perms = {
+ 'POST': 'settings.change_chatai'
+ }
+
+ def get_config(self, request):
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ data = self.serializer_class().data
+ data.update(serializer.validated_data)
+ for k, v in data.items():
+ if v:
+ continue
+ # 页面没有传递值, 从 settings 中获取
+ data[k] = getattr(settings, k, None)
+ return data
+
+ def post(self, request):
+ config = self.get_config(request)
+ chat_ai_enabled = config['CHAT_AI_ENABLED']
+ if not chat_ai_enabled:
+ return Response(
+ status=status.HTTP_400_BAD_REQUEST,
+ data={'msg': _('Chat AI is not enabled')}
+ )
+
+ proxy = config['GPT_PROXY']
+ model = config['GPT_MODEL']
+
+ kwargs = {
+ 'base_url': config['GPT_BASE_URL'] or None,
+ 'api_key': config['GPT_API_KEY'],
+ }
+ try:
+ if proxy:
+ kwargs['http_client'] = httpx.Client(
+ proxies=proxy,
+ transport=httpx.HTTPTransport(local_address='0.0.0.0')
+ )
+ client = openai.OpenAI(**kwargs)
+
+ ok = False
+ error = ''
+
+ client.chat.completions.create(
+ messages=[
+ {
+ "role": "user",
+ "content": "Say this is a test",
+ }
+ ],
+ model=model,
+ )
+ ok = True
+ except openai.APIConnectionError as e:
+ error = str(e.__cause__) # an underlying Exception, likely raised within httpx.
+ except openai.APIStatusError as e:
+ error = str(e.message)
+ except Exception as e:
+ ok, error = False, str(e)
+
+ if ok:
+ _status, msg = status.HTTP_200_OK, _('Test success')
+ else:
+ _status, msg = status.HTTP_400_BAD_REQUEST, error
+
+ return Response(status=_status, data={'msg': msg})
+
+
+class ChatPromptViewSet(JMSModelViewSet):
+ serializer_classes = {
+ 'default': serializers.ChatPromptSerializer,
+ }
+ permission_classes = [IsValidUser]
+ queryset = ChatPrompt.objects.all()
+ http_method_names = ['get', 'options']
+ filterset_fields = ['name']
+ search_fields = filterset_fields
+
+ def get_permissions(self):
+ if self.action in ['create', 'update', 'partial_update', 'destroy']:
+ self.permission_classes = [OnlySuperUser]
+ return super().get_permissions()
+
+ def filter_default_prompts(self):
+ lang = self.request.LANGUAGE_CODE
+ default_prompts = DefaultChatPrompt.get_prompts(lang)
+ search_query = self.request.query_params.get('search')
+ search_query = search_query or self.request.query_params.get('name')
+ if not search_query:
+ return default_prompts
+
+ search_query = search_query.lower()
+ filtered_prompts = [
+ prompt for prompt in default_prompts
+ if search_query in prompt['name'].lower()
+ ]
+ return filtered_prompts
+
+ def filter_queryset(self, queryset):
+ queryset = super().filter_queryset(queryset)
+ default_prompts = self.filter_default_prompts()
+ return list(queryset) + default_prompts
diff --git a/apps/settings/api/ldap.py b/apps/settings/api/ldap.py
index 486da3562..9c619e24d 100644
--- a/apps/settings/api/ldap.py
+++ b/apps/settings/api/ldap.py
@@ -28,71 +28,6 @@ from ..utils import (
logger = get_logger(__file__)
-class LDAPTestingConfigAPI(AsyncApiMixin, CreateAPIView):
- serializer_class = LDAPTestConfigSerializer
- perm_model = Setting
- rbac_perms = {
- 'POST': 'settings.change_auth',
- 'create': 'settings.change_auth',
- }
-
- def is_need_async(self):
- return True
-
- def create(self, request, *args, **kwargs):
- serializer = self.serializer_class(data=request.data)
- if not serializer.is_valid():
- return Response({"error": str(serializer.errors)}, status=400)
- config = self.get_ldap_config(serializer)
- ok, msg = LDAPTestUtil(config).test_config()
- status = 200 if ok else 400
- return Response(msg, status=status)
-
- @staticmethod
- def get_ldap_config(serializer):
- server_uri = serializer.validated_data["AUTH_LDAP_SERVER_URI"]
- bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"]
- password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"]
- use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False)
- search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"]
- search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"]
- attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"]
- auth_ldap = serializer.validated_data.get('AUTH_LDAP', False)
-
- if not password:
- password = settings.AUTH_LDAP_BIND_PASSWORD
-
- config = {
- 'server_uri': server_uri,
- 'bind_dn': bind_dn,
- 'password': password,
- 'use_ssl': use_ssl,
- 'search_ou': search_ou,
- 'search_filter': search_filter,
- 'attr_map': attr_map,
- 'auth_ldap': auth_ldap
- }
- return config
-
-
-class LDAPTestingLoginAPI(APIView):
- serializer_class = LDAPTestLoginSerializer
- perm_model = Setting
- rbac_perms = {
- 'POST': 'settings.change_auth'
- }
-
- def post(self, request):
- serializer = self.serializer_class(data=request.data)
- if not serializer.is_valid():
- return Response({"error": str(serializer.errors)}, status=400)
- username = serializer.validated_data['username']
- password = serializer.validated_data['password']
- ok, msg = LDAPTestUtil().test_login(username, password)
- status = 200 if ok else 400
- return Response(msg, status=status)
-
-
class LDAPUserListApi(generics.ListAPIView):
serializer_class = LDAPUserSerializer
perm_model = Setting
@@ -162,31 +97,10 @@ class LDAPUserListApi(generics.ListAPIView):
# 缓存有数据
if queryset is not None:
return super().list(request, *args, **kwargs)
-
- sync_util = LDAPSyncUtil()
- # 还没有同步任务
- if sync_util.task_no_start:
- ok, msg = LDAPTestUtil().test_config()
- if not ok:
- return Response(data={'msg': msg}, status=400)
- # 任务外部设置 task running 状态
- sync_util.set_task_status(sync_util.TASK_STATUS_IS_RUNNING)
- t = threading.Thread(target=sync_ldap_user)
- t.start()
- data = {'msg': _('Synchronization start, please wait.')}
- return Response(data=data, status=409)
- # 同步任务正在执行
- if sync_util.task_is_running:
- data = {'msg': _('Synchronization is running, please wait.')}
- return Response(data=data, status=409)
- # 同步任务执行结束
- if sync_util.task_is_over:
- msg = sync_util.get_task_error_msg()
- data = {'error': _('Synchronization error: {}'.format(msg))}
+ else:
+ data = {'msg': _('Users are not synchronized, please click the user synchronization button')}
return Response(data=data, status=400)
- return super().list(request, *args, **kwargs)
-
class LDAPUserImportAPI(APIView):
perm_model = Setting
@@ -232,18 +146,3 @@ class LDAPUserImportAPI(APIView):
return Response({
'msg': _('Imported {} users successfully (Organization: {})').format(count, orgs_name)
})
-
-
-class LDAPCacheRefreshAPI(generics.RetrieveAPIView):
- perm_model = Setting
- rbac_perms = {
- 'retrieve': 'settings.change_auth'
- }
-
- def retrieve(self, request, *args, **kwargs):
- try:
- LDAPSyncUtil().clear_cache()
- except Exception as e:
- logger.error(str(e))
- return Response(data={'msg': str(e)}, status=400)
- return Response(data={'msg': 'success'})
diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py
index 6b5f096cf..c03e99b6c 100644
--- a/apps/settings/api/settings.py
+++ b/apps/settings/api/settings.py
@@ -39,6 +39,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'wecom': serializers.WeComSettingSerializer,
'dingtalk': serializers.DingTalkSettingSerializer,
'feishu': serializers.FeiShuSettingSerializer,
+ 'slack': serializers.SlackSettingSerializer,
'auth': serializers.AuthSettingSerializer,
'oidc': serializers.OIDCSettingSerializer,
'keycloak': serializers.KeycloakSettingSerializer,
@@ -56,9 +57,11 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'cmpp2': serializers.CMPP2SMSSettingSerializer,
'custom': serializers.CustomSMSSettingSerializer,
'vault': serializers.VaultSettingSerializer,
+ 'chat': serializers.ChatAISettingSerializer,
'announcement': serializers.AnnouncementSettingSerializer,
'ticket': serializers.TicketSettingSerializer,
'ops': serializers.OpsSettingSerializer,
+ 'virtualapp': serializers.VirtualAppSerializer,
}
rbac_category_permissions = {
@@ -66,6 +69,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'terminal': 'settings.change_terminal',
'ops': 'settings.change_ops',
'ticket': 'settings.change_ticket',
+ 'virtualapp': 'settings.change_virtualapp',
'announcement': 'settings.change_announcement',
'security': 'settings.change_security',
'security_basic': 'settings.change_security',
diff --git a/apps/settings/api/slack.py b/apps/settings/api/slack.py
new file mode 100644
index 000000000..3a4ad289d
--- /dev/null
+++ b/apps/settings/api/slack.py
@@ -0,0 +1,40 @@
+from rest_framework.views import Response
+from rest_framework.generics import GenericAPIView
+from rest_framework.exceptions import APIException
+from rest_framework import status
+from django.utils.translation import gettext_lazy as _
+
+from settings.models import Setting
+from common.sdk.im.slack import Slack
+
+from .. import serializers
+
+
+class SlackTestingAPI(GenericAPIView):
+ serializer_class = serializers.SlackSettingSerializer
+ rbac_perms = {
+ 'POST': 'settings.change_auth'
+ }
+
+ def post(self, request):
+ serializer = self.serializer_class(data=request.data)
+ serializer.is_valid(raise_exception=True)
+
+ bot_token = serializer.validated_data.get('SLACK_BOT_TOKEN')
+ if not bot_token:
+ secret = Setting.objects.filter(name='SLACK_BOT_TOKEN').first()
+ if secret:
+ bot_token = secret.cleaned_value
+
+ bot_token = bot_token or ''
+
+ try:
+ slack = Slack(bot_token=bot_token)
+ slack.is_valid()
+ return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')})
+ except APIException as e:
+ try:
+ error = e.detail['errmsg']
+ except:
+ error = e.detail
+ return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
diff --git a/apps/settings/migrations/0010_alter_setting_options.py b/apps/settings/migrations/0010_alter_setting_options.py
index 3d52a64da..0ce372e76 100644
--- a/apps/settings/migrations/0010_alter_setting_options.py
+++ b/apps/settings/migrations/0010_alter_setting_options.py
@@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration):
-
dependencies = [
('settings', '0009_alter_cas_username_attribute'),
]
@@ -12,6 +11,22 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='setting',
- options={'permissions': [('change_email', 'Can change email setting'), ('change_auth', 'Can change auth setting'), ('change_ops', 'Can change auth ops'), ('change_ticket', 'Can change auth ticket'), ('change_announcement', 'Can change auth announcement'), ('change_vault', 'Can change vault setting'), ('change_systemmsgsubscription', 'Can change system msg sub setting'), ('change_sms', 'Can change sms setting'), ('change_security', 'Can change security setting'), ('change_clean', 'Can change clean setting'), ('change_interface', 'Can change interface setting'), ('change_license', 'Can change license setting'), ('change_terminal', 'Can change terminal setting'), ('change_other', 'Can change other setting')], 'verbose_name': 'System setting'},
+ options={'permissions': [
+ ('change_email', 'Can change email setting'),
+ ('change_auth', 'Can change auth setting'),
+ ('change_ops', 'Can change auth ops'),
+ ('change_ticket', 'Can change auth ticket'),
+ ('change_announcement', 'Can change auth announcement'),
+ ('change_vault', 'Can change vault setting'),
+ ('change_chatai', 'Can change chat ai setting'),
+ ('change_systemmsgsubscription', 'Can change system msg sub setting'),
+ ('change_sms', 'Can change sms setting'),
+ ('change_security', 'Can change security setting'),
+ ('change_clean', 'Can change clean setting'),
+ ('change_interface', 'Can change interface setting'),
+ ('change_license', 'Can change license setting'),
+ ('change_terminal', 'Can change terminal setting'),
+ ('change_other', 'Can change other setting')
+ ], 'verbose_name': 'System setting'},
),
]
diff --git a/apps/settings/migrations/0011_chatprompt.py b/apps/settings/migrations/0011_chatprompt.py
new file mode 100644
index 000000000..1e2b05801
--- /dev/null
+++ b/apps/settings/migrations/0011_chatprompt.py
@@ -0,0 +1,31 @@
+# Generated by Django 4.1.10 on 2023-12-13 11:07
+
+import uuid
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('settings', '0010_alter_setting_options'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ChatPrompt',
+ fields=[
+ ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
+ ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
+ ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
+ ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
+ ('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
+ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
+ ('content', models.TextField(verbose_name='Content')),
+ ('builtin', models.BooleanField(default=False, verbose_name='Builtin')),
+ ],
+ options={
+ 'verbose_name': 'Chat prompt',
+ },
+ ),
+ ]
diff --git a/apps/settings/migrations/0012_alter_setting_options.py b/apps/settings/migrations/0012_alter_setting_options.py
new file mode 100644
index 000000000..f35da135f
--- /dev/null
+++ b/apps/settings/migrations/0012_alter_setting_options.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.1.10 on 2023-12-20 07:51
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('settings', '0011_chatprompt'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='setting',
+ options={'permissions': [('change_email', 'Can change email setting'), ('change_auth', 'Can change auth setting'), ('change_ops', 'Can change auth ops'), ('change_ticket', 'Can change auth ticket'), ('change_virtualapp', 'Can change virtual app setting'), ('change_announcement', 'Can change auth announcement'), ('change_vault', 'Can change vault setting'), ('change_chatai', 'Can change chat ai setting'), ('change_systemmsgsubscription', 'Can change system msg sub setting'), ('change_sms', 'Can change sms setting'), ('change_security', 'Can change security setting'), ('change_clean', 'Can change clean setting'), ('change_interface', 'Can change interface setting'), ('change_license', 'Can change license setting'), ('change_terminal', 'Can change terminal setting'), ('change_other', 'Can change other setting')], 'verbose_name': 'System setting'},
+ ),
+ ]
diff --git a/apps/settings/models.py b/apps/settings/models.py
index dc599bbe4..d6cda11df 100644
--- a/apps/settings/models.py
+++ b/apps/settings/models.py
@@ -8,6 +8,7 @@ from django.db import models
from django.db.utils import ProgrammingError, OperationalError
from django.utils.translation import gettext_lazy as _
+from common.db.models import JMSBaseModel
from common.utils import signer, get_logger
logger = get_logger(__name__)
@@ -161,8 +162,10 @@ class Setting(models.Model):
('change_auth', _('Can change auth setting')),
('change_ops', _('Can change auth ops')),
('change_ticket', _('Can change auth ticket')),
+ ('change_virtualapp', _('Can change virtual app setting')),
('change_announcement', _('Can change auth announcement')),
('change_vault', _('Can change vault setting')),
+ ('change_chatai', _('Can change chat ai setting')),
('change_systemmsgsubscription', _('Can change system msg sub setting')),
('change_sms', _('Can change sms setting')),
('change_security', _('Can change security setting')),
@@ -172,3 +175,15 @@ class Setting(models.Model):
('change_terminal', _('Can change terminal setting')),
('change_other', _('Can change other setting')),
]
+
+
+class ChatPrompt(JMSBaseModel):
+ name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True)
+ content = models.TextField(blank=False, null=False, verbose_name=_('Content'))
+ builtin = models.BooleanField(default=False, verbose_name=_('Builtin'))
+
+ class Meta:
+ verbose_name = _("Chat prompt")
+
+ def __str__(self):
+ return self.name
diff --git a/apps/settings/prompt.py b/apps/settings/prompt.py
new file mode 100644
index 000000000..5e2b72c96
--- /dev/null
+++ b/apps/settings/prompt.py
@@ -0,0 +1,197 @@
+class DefaultChatPrompt:
+ DEFAULT = {
+ 'zh': [
+ {
+ 'name': '周报生成器',
+ 'content': '使用下面提供的文本作为中文周报的基础,生成一个简洁的摘要,突出最重要的内容。该报告应以 markdown 格式编写,'
+ '并应易于阅读和理解,以满足一般受众的需要。特别是要注重提供对利益相关者和决策者有用的见解和分析。'
+ '你也可以根据需要使用任何额外的信息或来源。',
+ },
+ {
+ 'name': '数据库专家',
+ 'content': '我希望你充当一个数据库专家的角色,当我问你 sql 相关的问题时,'
+ '我需要你转换为标准的 sql 语句,当我的描述不够精准时,请给出合适的反馈',
+ },
+ {
+ 'name': '全栈程序员',
+ 'content': '我希望你能扮演一个软件开发者的角色。我将提供一些关于网络应用需求的具体信息,'
+ '而你的工作是提出一个架构和代码,用 Golang 和 Angular 开发安全的应用。',
+ },
+ {
+ 'name': '前端开发',
+ 'content': '我希望你能担任高级前端开发员。我将描述一个项目的细节,你将用这些工具来编码项目。'
+ 'Create React App, yarn, Ant Design, List, Redux Toolkit, createSlice, thunk, axios. '
+ '你应该将文件合并到单一的 index.js 文件中,而不是其他。不要写解释。',
+ },
+ {
+ 'name': '架构师 IT',
+ 'content': '我希望你能扮演一个 IT 架构师的角色。我将提供一些关于应用程序或其他数字产品功能的细节,'
+ '而你的工作是想出将其整合到 IT 环境中的方法。这可能涉及到分析业务需求,进行差距分析,'
+ '并将新系统的功能映射到现有的 IT 环境中。接下来的步骤是创建一个解决方案设计,'
+ '一个物理网络蓝图,定义系统集成的接口和部署环境的蓝图。',
+ },
+ {
+ 'name': '代码释义器',
+ 'content': '我希望你能充当代码解释者,阐明代码的语法和语义。',
+ },
+ {
+ 'name': 'IT 编程问题',
+ 'content': '我想让你充当 Stackoverflow 的帖子。我将提出与编程有关的问题,你将回答答案是什么。'
+ '我希望你只回答给定的答案,在没有足够的细节时写出解释。当我需要用英语告诉你一些事情时,我会把文字放在大括号里{像这样}。'
+ },
+ {
+ 'name': '小红书风格',
+ 'content': '请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、'
+ '每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。',
+ },
+ {
+ 'name': '写作助理',
+ 'content': '作为一名中文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,'
+ '同时分解长句,减少重复,并提供改进建议。请只提供文本的更正版本,避免包括解释。',
+ },
+ {
+ 'name': 'Nature 风格润色',
+ 'content': '我希望你能充当专业的拼写和语法校对者,并改进我的文章。'
+ '我想让你用更美丽、优雅、高级的英语单词和句子替换我的简化 A0 级别的单词和句子,'
+ '保持意思不变,但使它们更具文学性,在《自然》杂志风格中提高我的表达水平。',
+ },
+ ],
+ 'en': [
+ {
+ "name": "Weekly Report Generator",
+ "content": "Using the text provided below as a basis for a Chinese weekly report, "
+ "generate a concise summary that highlights the most important content. "
+ "The report should be written in markdown format and should be easy to read "
+ "and understand for a general audience, especially focusing on providing insights "
+ "and analysis useful to stakeholders and decision-makers. You may also use any additional "
+ "information or sources as needed."
+ },
+ {
+ "name": "Database Expert",
+ "content": "I want you to act as a database expert. When I ask you questions related to SQL, "
+ "I need you to convert them into standard SQL statements. "
+ "Please provide appropriate feedback when my descriptions are not precise enough."
+ },
+ {
+ "name": "Full-Stack Programmer",
+ "content": "I want you to play the role of a software developer. "
+ "I will provide specific information about web application requirements, "
+ "and your job is to propose an architecture and code for developing a secure application "
+ "using Golang and Angular."
+ },
+ {
+ "name": "Front-End Developer",
+ "content": "I want you to act as a senior front-end developer. "
+ "I will describe the details of a project, and you will code the project using these tools:"
+ " Create React App, yarn, Ant Design, List, Redux Toolkit, createSlice, thunk, axios. "
+ "You should consolidate files into a single index.js file, and not write explanations."
+ },
+ {
+ "name": "IT Architect",
+ "content": "I want you to play the role of an IT architect. "
+ "I will provide details about the functionality of applications or other digital products, "
+ "and your job is to figure out how to integrate them into the IT environment. "
+ "This may involve analyzing business requirements, conducting gap analysis, "
+ "and mapping the new system's features to the existing IT environment. "
+ "The next steps are to create a solution design, a physical network blueprint, "
+ "define interfaces for system integration, and a blueprint for the deployment environment."
+ },
+ {
+ "name": "Code Interpreter",
+ "content": "I want you to act as a code interpreter, clarifying the syntax and semantics of code."
+ },
+ {
+ "name": "IT Programming Questions",
+ "content": "I want you to act as a Stackoverflow post. I will ask questions related to programming, "
+ "and you will answer what the answer is. Write out explanations "
+ "when there are not enough details. When I need to tell you something in English, "
+ "I will enclose the text in braces {like this}."
+ },
+ {
+ "name": "Xiaohongshu Style",
+ "content": "Please edit the following paragraphs in Emoji style. "
+ "This style is characterized by engaging titles, the inclusion of emojis in each paragraph, "
+ "and adding related tags at the end. Ensure the original meaning is maintained."
+ },
+ {
+ "name": "Writing Assistant",
+ "content": "As a Chinese writing improvement assistant, "
+ "your task is to improve the provided text in terms of spelling, grammar, clarity, "
+ "conciseness, and overall readability. Also, break down long sentences, reduce repetition, "
+ "and provide suggestions for improvement. Please only provide the corrected version of "
+ "the text, avoiding including explanations."
+ },
+ {
+ "name": "Nature Style Editing",
+ "content": "I want you to act as a professional spelling and grammar proofreader and improve "
+ "my article. I want you to replace my simplified A0 level words and sentences with "
+ "more beautiful, elegant, and advanced English words and sentences. Keep the meaning "
+ "the same but make them more literary, enhancing my expression in the style of 'Nature' "
+ "magazine."
+ },
+ ],
+ 'ja': [
+ {
+ "name": "週報ジェネレータ",
+ "content": "以下のテキストを基にして中国語の週報の簡潔な要約を作成し、最も重要な内容を強調してください。"
+ "このレポートはマークダウン形式で書かれ、一般の聴衆にとって読みやすく理解しやすいものでなければなりません。"
+ "特に、利害関係者や意思決定者に有用な洞察と分析を提供することに重点を置いてください。"
+ "必要に応じて、追加の情報やソースを使用しても構いません。"
+ },
+ {
+ "name": "データベースの専門家",
+ "content": "データベース専門家として機能し、私がsqlに関連する質問をするとき、"
+ "それを標準のsqlステートメントに変換してください。私の説明が不正確な場合は、適切なフィードバックを提供してください。"
+ },
+ {
+ "name": "フルスタックプログラマー",
+ "content": "ソフトウェア開発者の役割を果たしてください。私はウェブアプリケーションの要件に関する具体的な情報を提供します。"
+ "あなたの仕事は、GolangとAngularを使用して安全なアプリケーションを開発するためのアーキテクチャとコードを提案することです。"
+ },
+ {
+ "name": "フロントエンド開発",
+ "content": "上級フロントエンド開発者として機能してください。私はプロジェクトの詳細を説明し、"
+ "これらのツールを使用してプロジェクトをコーディングします。"
+ "Create React App、yarn、Ant Design、List、Redux Toolkit、createSlice、thunk、axiosを使用してください。"
+ "ファイルをindex.jsファイルに統合し、他のファイルではなく、説明は書かないでください。"
+ },
+ {
+ "name": "ITアーキテクト",
+ "content": "ITアーキテクトの役割を果たしてください。私はアプリケーションや他のデジタル製品の機能に関する詳細を提供します。"
+ "あなたの仕事は、それをIT環境に統合する方法を考えることです。これには、ビジネス要件の分析、ギャップ分析の実施、"
+ "新システムの機能を既存のIT環境にマッピングすることが含まれる場合があります。次のステップは、"
+ "ソリューションデザインの作成、物理的なネットワークのブループリント、システム統合のインターフェース、およびデプロイメント環境のブループリントを定義することです。"
+ },
+ {
+ "name": "コードインタープリター",
+ "content": "コードの解釈者として機能し、コードの文法と意味を明確に説明してください。"
+ },
+ {
+ "name": "ITプログラミングの問題",
+ "content": "Stackoverflowの投稿として機能してください。私はプログラミングに関連する質問をします。"
+ "あなたは答えを何であるか答えます。十分な詳細がない場合は説明を書いてください。英語で何かを伝える必要があるときは、"
+ "大括弧でテキストを囲みます{このように}。"
+ },
+ {
+ "name": "小红书風格",
+ "content": "Emojiスタイルで以下の段落を編集してください。このスタイルは、魅力的なタイトル、"
+ "各段落に絵文字を含め、関連するタグを末尾に追加することが特徴です。原文の意味を保持してください。"
+ },
+ {
+ "name": "ライティングアシスタント",
+ "content": "中国語のライティング改善アシスタントとして、提供されたテキストのスペル、"
+ "文法、明瞭さ、簡潔さ、全体的な可読性を改善し、長い文を分解し、重複を減らし、"
+ "改善提案を提供します。テキストの修正版のみを提供し、説明は含めないでください。"
+ },
+ {
+ "name": "Nature スタイルの編集",
+ "content": "プロのスペルと文法の校正者として機能し、私の記事を改善してください。"
+ "私の簡素化されたA0レベルの単語や文章を、より美しく、優雅で、"
+ "高度な英語の単語や文章に置き換えて、文学的な要素を加え、「自然」誌スタイルで表現レベルを高めてください。"
+ },
+ ]
+ }
+
+ @classmethod
+ def get_prompts(cls, lang: str) -> list:
+ return cls.DEFAULT.get(lang[:2], 'zh')
diff --git a/apps/settings/serializers/__init__.py b/apps/settings/serializers/__init__.py
index fe94eb1da..600684387 100644
--- a/apps/settings/serializers/__init__.py
+++ b/apps/settings/serializers/__init__.py
@@ -8,6 +8,7 @@ from .feature import *
from .msg import *
from .msg import *
from .other import *
+from .prompt import *
from .public import *
from .security import *
from .settings import *
diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py
index aeca390ac..2b641114c 100644
--- a/apps/settings/serializers/auth/__init__.py
+++ b/apps/settings/serializers/auth/__init__.py
@@ -11,3 +11,4 @@ from .saml2 import *
from .sms import *
from .sso import *
from .wecom import *
+from .slack import *
diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py
index 57034e730..fbc833124 100644
--- a/apps/settings/serializers/auth/base.py
+++ b/apps/settings/serializers/auth/base.py
@@ -17,7 +17,8 @@ class AuthSettingSerializer(serializers.Serializer):
AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth'))
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('DingTalk Auth'))
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('FeiShu Auth'))
- AUTH_WECOM = serializers.BooleanField(default=False, label=_('WeCom Auth'))
+ AUTH_WECOM = serializers.BooleanField(default=False, label=_('Slack Auth'))
+ AUTH_SLACK = serializers.BooleanField(default=False, label=_('WeCom Auth'))
AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth"))
AUTH_PASSKEY = serializers.BooleanField(default=False, label=_("Passkey Auth"))
FORGOT_PASSWORD_URL = serializers.CharField(
diff --git a/apps/settings/serializers/auth/slack.py b/apps/settings/serializers/auth/slack.py
new file mode 100644
index 000000000..019137f4c
--- /dev/null
+++ b/apps/settings/serializers/auth/slack.py
@@ -0,0 +1,15 @@
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from common.serializers.fields import EncryptedField
+
+__all__ = ['SlackSettingSerializer']
+
+
+class SlackSettingSerializer(serializers.Serializer):
+ PREFIX_TITLE = _('Slack')
+
+ AUTH_SLACK = serializers.BooleanField(default=False, label=_('Enable Slack Auth'))
+ SLACK_CLIENT_ID = serializers.CharField(max_length=256, required=True, label='Client ID')
+ SLACK_CLIENT_SECRET = EncryptedField(max_length=256, required=False, label='Client Secret')
+ SLACK_BOT_TOKEN = EncryptedField(max_length=256, required=False, label='Client bot Token')
diff --git a/apps/settings/serializers/basic.py b/apps/settings/serializers/basic.py
index ad1c2f8e3..4e253c602 100644
--- a/apps/settings/serializers/basic.py
+++ b/apps/settings/serializers/basic.py
@@ -1,6 +1,8 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
+from orgs.models import Organization
+
class BasicSettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('Basic')
@@ -34,3 +36,10 @@ class BasicSettingSerializer(serializers.Serializer):
if not s:
return 'http://127.0.0.1'
return s.strip('/')
+
+ @staticmethod
+ def validate_GLOBAL_ORG_DISPLAY_NAME(s):
+ org_names = Organization.objects.values_list('name', flat=True)
+ if s in org_names:
+ raise serializers.ValidationError(_('Organization name already exists'))
+ return s
diff --git a/apps/settings/serializers/feature.py b/apps/settings/serializers/feature.py
index f2efe5b03..8dc0de959 100644
--- a/apps/settings/serializers/feature.py
+++ b/apps/settings/serializers/feature.py
@@ -3,11 +3,13 @@ import uuid
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
+from assets.const import Protocol
from common.serializers.fields import EncryptedField
__all__ = [
'AnnouncementSettingSerializer', 'OpsSettingSerializer',
- 'VaultSettingSerializer', 'TicketSettingSerializer'
+ 'VaultSettingSerializer', 'TicketSettingSerializer',
+ 'ChatAISettingSerializer', 'VirtualAppSerializer',
]
@@ -54,6 +56,44 @@ class VaultSettingSerializer(serializers.Serializer):
)
+class ChatAISettingSerializer(serializers.Serializer):
+ PREFIX_TITLE = _('Chat AI')
+ GPT_MODEL_CHOICES = []
+
+ CHAT_AI_ENABLED = serializers.BooleanField(
+ required=False, label=_('Enable Chat AI')
+ )
+ GPT_BASE_URL = serializers.CharField(
+ max_length=256, allow_blank=True, required=False, label=_('Base Url')
+ )
+ GPT_API_KEY = EncryptedField(
+ max_length=256, allow_blank=True, required=False, label=_('API Key'),
+ )
+ GPT_PROXY = serializers.CharField(
+ max_length=256, allow_blank=True, required=False, label=_('Proxy')
+ )
+ GPT_MODEL = serializers.ChoiceField(
+ default='', choices=GPT_MODEL_CHOICES, label=_("GPT Model"), required=False,
+ )
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.set_GPT_MODEL_choices()
+
+ def set_GPT_MODEL_choices(self):
+ field_gpt_model = self.fields.get("GPT_MODEL")
+ if not field_gpt_model:
+ return
+ gpt_api_model = Protocol.gpt_protocols()[Protocol.chatgpt]['setting']['api_mode']
+ choices = gpt_api_model['choices']
+ field_gpt_model._choices = choices
+ field_gpt_model.default = gpt_api_model['default']
+ cls = self.__class__
+ if cls.GPT_MODEL_CHOICES:
+ return
+ cls.GPT_MODEL_CHOICES.extend(choices)
+
+
class TicketSettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('Ticket')
@@ -80,3 +120,11 @@ class OpsSettingSerializer(serializers.Serializer):
label=_('Operation center command blacklist'),
help_text=_("Commands that are not allowed execute.")
)
+
+
+class VirtualAppSerializer(serializers.Serializer):
+ PREFIX_TITLE = _('Virtual app')
+
+ VIRTUAL_APP_ENABLED = serializers.BooleanField(
+ required=False, label=_('Enable virtual app'),
+ )
diff --git a/apps/settings/serializers/prompt.py b/apps/settings/serializers/prompt.py
new file mode 100644
index 000000000..6b7804516
--- /dev/null
+++ b/apps/settings/serializers/prompt.py
@@ -0,0 +1,11 @@
+from rest_framework import serializers
+
+from settings.models import ChatPrompt
+
+
+class ChatPromptSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ChatPrompt
+ fields = [
+ 'id', 'name', 'content', 'builtin'
+ ]
diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py
index 1b5c78609..6e5a74c8e 100644
--- a/apps/settings/serializers/public.py
+++ b/apps/settings/serializers/public.py
@@ -53,6 +53,10 @@ class PrivateSettingSerializer(PublicSettingSerializer):
CONNECTION_TOKEN_REUSABLE = serializers.BooleanField()
CACHE_LOGIN_PASSWORD_ENABLED = serializers.BooleanField()
VAULT_ENABLED = serializers.BooleanField()
+ VIRTUAL_APP_ENABLED = serializers.BooleanField()
+ CHAT_AI_ENABLED = serializers.BooleanField()
+ GPT_MODEL = serializers.CharField()
+ FILE_UPLOAD_SIZE_LIMIT_MB = serializers.IntegerField()
class ServerInfoSerializer(serializers.Serializer):
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index 97055bc83..fdfae5146 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -1,24 +1,26 @@
from __future__ import absolute_import
from django.urls import path
+from rest_framework_bulk.routes import BulkRouter
from .. import api
app_name = 'common'
+router = BulkRouter()
+router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt')
urlpatterns = [
path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'),
- path('ldap/testing/config/', api.LDAPTestingConfigAPI.as_view(), name='ldap-testing-config'),
- path('ldap/testing/login/', api.LDAPTestingLoginAPI.as_view(), name='ldap-testing-login'),
path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'),
path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'),
- path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'),
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
+ path('slack/testing/', api.SlackTestingAPI.as_view(), name='slack-testing'),
path('sms/
-
+