Merge pull request #12401 from jumpserver/dev

v3.10
This commit is contained in:
Bryan 2023-12-21 15:14:19 +05:00 committed by GitHub
commit 8df720f19e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
263 changed files with 7355 additions and 3193 deletions

View File

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

View File

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

View File

@ -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('') }}"

View File

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

View File

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

View File

@ -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(),
}

View File

@ -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 后期迁移到自动化策略中

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,25 @@
- hosts: demo
gather_facts: no
tasks:
- name: "Get user home directory path"
ansible.builtin.shell:
cmd: "getent passwd {{ account.username }} | cut -d: -f6"
register: user_home_dir
ignore_errors: yes
- name: "Check if user home directory exists"
ansible.builtin.stat:
path: "{{ user_home_dir.stdout }}"
register: home_dir
when: user_home_dir.stdout != ""
- name: "Rename user home directory if it exists"
ansible.builtin.command:
cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak"
when: home_dir.stat.exists and user_home_dir.stdout != ""
- name: "Remove account"
ansible.builtin.user:
name: "{{ account.username }}"
state: absent
remove: "{{ home_dir.stat.exists }}"

View File

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

View File

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

View File

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

View File

@ -0,0 +1,67 @@
import os
from copy import deepcopy
from django.db.models import QuerySet
from accounts.const import AutomationTypes
from accounts.models import Account
from common.utils import get_logger
from ..base.manager import AccountBasePlaybookManager
logger = get_logger(__name__)
class RemoveAccountManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.host_account_mapper = {}
def prepare_runtime_dir(self):
path = super().prepare_runtime_dir()
ansible_config_path = os.path.join(path, 'ansible.cfg')
with open(ansible_config_path, 'w') as f:
f.write('[ssh_connection]\n')
f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n')
return path
@classmethod
def method_type(cls):
return AutomationTypes.remove_account
def get_gather_accounts(self, privilege_account, gather_accounts: QuerySet):
gather_account_ids = self.execution.snapshot['gather_accounts']
gather_accounts = gather_accounts.filter(id__in=gather_account_ids)
gather_accounts = gather_accounts.exclude(
username__in=[privilege_account.username, 'root', 'Administrator']
)
return gather_accounts
def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs):
if host.get('error'):
return host
gather_accounts = asset.gatheredaccount_set.all()
gather_accounts = self.get_gather_accounts(account, gather_accounts)
inventory_hosts = []
for gather_account in gather_accounts:
h = deepcopy(host)
h['name'] += '(' + gather_account.username + ')'
self.host_account_mapper[h['name']] = (asset, gather_account)
h['account'] = {'username': gather_account.username}
inventory_hosts.append(h)
return inventory_hosts
def on_host_success(self, host, result):
tuple_asset_gather_account = self.host_account_mapper.get(host)
if not tuple_asset_gather_account:
return
asset, gather_account = tuple_asset_gather_account
Account.objects.filter(
asset_id=asset.id,
username=gather_account.username
).delete()
gather_account.delete()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
- hosts: mongdb
- hosts: mongodb
gather_facts: no
vars:
ansible_python_interpreter: /opt/py3/bin/python

View File

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

View File

@ -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(),
}

View File

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

View File

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

View File

@ -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'},
),
]

View File

@ -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():
"""

View File

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

View File

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

View File

@ -0,0 +1,19 @@
from rest_framework import permissions
def check_permissions(request):
act = request.data.get('action')
if act == 'push':
code = 'accounts.push_account'
elif act == 'remove':
code = 'accounts.remove_account'
else:
code = 'accounts.verify_account'
return request.user.has_perm(code)
class AccountTaskActionPermission(permissions.IsAuthenticated):
def has_permission(self, request, view):
return super().has_permission(request, view) \
and check_permissions(request)

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
from celery import shared_task
from django.utils.translation import gettext_noop, gettext_lazy as _
from accounts.const import AutomationTypes
from accounts.tasks.common import quickstart_automation_by_snapshot
from common.utils import get_logger
logger = get_logger(__file__)
__all__ = ['remove_accounts_task']
@shared_task(
queue="ansible", verbose_name=_('Remove account'),
activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None)
)
def remove_accounts_task(gather_account_ids):
from accounts.models import GatheredAccount
gather_accounts = GatheredAccount.objects.filter(
id__in=gather_account_ids
)
task_name = gettext_noop("Remove account")
task_snapshot = {
'assets': [str(i.asset_id) for i in gather_accounts],
'gather_accounts': [str(i.id) for i in gather_accounts],
}
tp = AutomationTypes.remove_account
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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から情報を収集する'

View File

@ -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から情報を収集する'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
# Generated by Django 4.1.10 on 2023-11-22 07:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('assets', '0125_auto_20231011_1053'),
('labels', '0002_auto_20231103_1659'),
]
operations = [
migrations.RemoveField(
model_name='asset',
name='labels',
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 4.1.10 on 2023-12-05 10:03
from functools import reduce
from django.db import migrations, models
from django.db.models import F
def migrate_automation_ansible_remove_account(apps, *args):
automation_model = apps.get_model('assets', 'PlatformAutomation')
automation_map = {
('oracle',): 'remove_account_oracle',
('windows',): 'remove_account_windows',
('mongodb',): 'remove_account_mongodb',
('linux', 'unix'): 'remove_account_posix',
('sqlserver',): 'remove_account_sqlserver',
('mysql', 'mariadb'): 'remove_account_mysql',
('postgresql',): 'remove_account_postgresql',
}
update_objs = []
types = list(reduce(lambda x, y: x + y, automation_map.keys()))
qs = automation_model.objects.filter(platform__type__in=types).annotate(tp=F('platform__type'))
for automation in qs:
for types, method in automation_map.items():
if automation.tp in types:
automation.remove_account_enabled = True
automation.remove_account_method = method
break
update_objs.append(automation)
automation_model.objects.bulk_update(update_objs, ['remove_account_enabled', 'remove_account_method'])
class Migration(migrations.Migration):
dependencies = [
('assets', '0126_remove_asset_labels'),
]
operations = [
migrations.AddField(
model_name='platformautomation',
name='remove_account_enabled',
field=models.BooleanField(default=False, verbose_name='Remove account enabled'),
),
migrations.AddField(
model_name='platformautomation',
name='remove_account_method',
field=models.TextField(blank=True, max_length=32, null=True, verbose_name='Remove account method'),
),
migrations.AddField(
model_name='platformautomation',
name='remove_account_params',
field=models.JSONField(default=dict, verbose_name='Remove account params'),
),
migrations.RunPython(migrate_automation_ansible_remove_account)
]

View File

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

View File

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

View File

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

View File

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

View File

@ -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):
"""
对资产提供 约束和默认值
对资产进行抽象

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')],
},
),
]

View File

@ -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')),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,51 @@
from django.utils.translation import gettext_lazy as _
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentication import errors
from authentication.const import ConfirmType
from authentication.permissions import UserConfirmation
from common.api import RoleUserMixin, RoleAdminMixin
from common.exceptions import JMSException
from common.permissions import IsValidUser, OnlySuperUser
from common.utils import get_logger
from users.models import User
logger = get_logger(__file__)
class QRUnBindBase(APIView):
user: User
def post(self, request: Request, backend: str, **kwargs):
backend_map = {
'wecom': {'user_field': 'wecom_id', 'not_bind_err': errors.WeComNotBound},
'dingtalk': {'user_field': 'dingtalk_id', 'not_bind_err': errors.DingTalkNotBound},
'feishu': {'user_field': 'feishu_id', 'not_bind_err': errors.FeiShuNotBound},
'slack': {'user_field': 'slack_id', 'not_bind_err': errors.SlackNotBound},
}
user = self.user
backend_info = backend_map.get(backend)
if not backend_info:
raise JMSException(
_('The value in the parameter must contain %s') % ', '.join(backend_map.keys())
)
if not getattr(user, backend_info['user_field'], None):
raise backend_info['not_bind_err']
setattr(user, backend_info['user_field'], None)
user.save()
return Response()
class QRUnBindForUserApi(RoleUserMixin, QRUnBindBase):
permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),)
class QRUnBindForAdminApi(RoleAdminMixin, QRUnBindBase):
permission_classes = (OnlySuperUser,)
user_id_url_kwarg = 'user_id'

View File

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

View File

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

View File

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

View File

@ -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` 统一处理

View File

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

View File

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

View File

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

View File

@ -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):
"""
什么也不做呀😺

View File

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

View File

@ -18,7 +18,7 @@ from common.utils import lazyproperty, pretty_string, bulk_get
from common.utils.timezone import as_current_tz
from orgs.mixins.models import JMSOrgBaseModel
from orgs.utils import tmp_to_org
from terminal.models import Applet
from terminal.models import Applet, VirtualApp
def date_expired_default():
@ -177,6 +177,15 @@ class ConnectionToken(JMSOrgBaseModel):
}
return options
def get_virtual_app_option(self):
method = self.connect_method_object
if not method or method.get('type') != 'virtual_app' or method.get('disabled', False):
return None
virtual_app = VirtualApp.objects.filter(name=method.get('value')).first()
if not virtual_app:
return None
return virtual_app
def get_applet_option(self):
method = self.connect_method_object
if not method or method.get('type') != 'applet' or method.get('disabled', False):
@ -190,28 +199,23 @@ class ConnectionToken(JMSOrgBaseModel):
if not host_account:
raise JMSException({'error': 'No host account available'})
host, account, lock_key, ttl = bulk_get(host_account, ('host', 'account', 'lock_key', 'ttl'))
host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key'))
gateway = host.domain.select_gateway() if host.domain else None
data = {
'id': account.id,
'id': lock_key,
'applet': applet,
'host': host,
'gateway': gateway,
'account': account,
'remote_app_option': self.get_remote_app_option()
}
token_account_relate_key = f'token_account_relate_{account.id}'
cache.set(token_account_relate_key, lock_key, ttl)
return data
@staticmethod
def release_applet_account(account_id):
token_account_relate_key = f'token_account_relate_{account_id}'
lock_key = cache.get(token_account_relate_key)
def release_applet_account(lock_key):
if lock_key:
cache.delete(lock_key)
cache.delete(token_account_relate_key)
return True
@lazyproperty

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