Compare commits

..

4 Commits

Author SHA1 Message Date
Bai 8ebcfb5b6f perf: aks encrypt 2025-09-03 11:25:59 +08:00
ibuler 000bb100cd perf: try to decrypt then origin value 2025-08-29 11:00:19 +08:00
wangruidong 36f3071eed fix: Ensure command arguments are safely quoted in safe_run_cmd 2025-08-28 14:14:36 +08:00
老广 15259fc10c
Update base.py 2025-08-21 22:05:51 +08:00
245 changed files with 4538 additions and 29309 deletions

View File

@ -1,46 +0,0 @@
name: Build and Push Python Base Image
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag to build'
required: true
default: '3.11-slim-bullseye-v1'
type: string
jobs:
build-and-push:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.ref }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:qemu-v7.0.0-28
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract repository name
id: repo
run: echo "REPO=$(basename ${{ github.repository }})" >> $GITHUB_ENV
- name: Build and push multi-arch image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
file: Dockerfile-python
tags: jumpserver/core-base:python-${{ inputs.tag }}

View File

@ -1,4 +1,4 @@
FROM jumpserver/core-base:20251014_095903 AS stage-build FROM jumpserver/core-base:20250819_064003 AS stage-build
ARG VERSION ARG VERSION
@ -19,7 +19,7 @@ RUN set -ex \
&& python manage.py compilemessages && python manage.py compilemessages
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1 FROM python:3.11-slim-bullseye
ENV LANG=en_US.UTF-8 \ ENV LANG=en_US.UTF-8 \
PATH=/opt/py3/bin:$PATH PATH=/opt/py3/bin:$PATH
@ -33,7 +33,6 @@ ARG TOOLS=" \
default-libmysqlclient-dev \ default-libmysqlclient-dev \
openssh-client \ openssh-client \
sshpass \ sshpass \
nmap \
bubblewrap" bubblewrap"
ARG APT_MIRROR=http://deb.debian.org ARG APT_MIRROR=http://deb.debian.org

View File

@ -1,4 +1,4 @@
FROM jumpserver/core-base:python-3.11-slim-bullseye-v1 FROM python:3.11-slim-bullseye
ARG TARGETARCH ARG TARGETARCH
COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/ COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /usr/local/bin/
# Install APT dependencies # Install APT dependencies
@ -28,7 +28,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \
&& echo "no" | dpkg-reconfigure dash && echo "no" | dpkg-reconfigure dash
# Install bin tools # Install bin tools
ARG CHECK_VERSION=v1.0.5 ARG CHECK_VERSION=v1.0.4
RUN set -ex \ RUN set -ex \
&& wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \ && wget https://github.com/jumpserver-dev/healthcheck/releases/download/${CHECK_VERSION}/check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \
&& tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \ && tar -xf check-${CHECK_VERSION}-linux-${TARGETARCH}.tar.gz \

View File

@ -1,11 +0,0 @@
FROM python:3.11-slim-bullseye
ARG TARGETARCH
# Install APT dependencies
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
apt-get update; \
apt-get -y --no-install-recommends upgrade; \
rm -rf /var/lib/apt/lists/*
# upgrade pip and setuptools
RUN pip install --no-cache-dir --upgrade pip setuptools wheel

View File

@ -2,7 +2,7 @@
<a name="readme-top"></a> <a name="readme-top"></a>
<a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a> <a href="https://jumpserver.com" target="_blank"><img src="https://download.jumpserver.org/images/jumpserver-logo.svg" alt="JumpServer" width="300" /></a>
## An open-source PAM platform (Bastion Host) ## An open-source PAM tool (Bastion Host)
[![][license-shield]][license-link] [![][license-shield]][license-link]
[![][docs-shield]][docs-link] [![][docs-shield]][docs-link]
@ -19,7 +19,7 @@
## What is JumpServer? ## What is JumpServer?
JumpServer is an open-source Privileged Access Management (PAM) platform that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser. JumpServer is an open-source Privileged Access Management (PAM) tool that provides DevOps and IT teams with on-demand and secure access to SSH, RDP, Kubernetes, Database and RemoteApp endpoints through a web browser.
<picture> <picture>

View File

@ -11,7 +11,6 @@ from accounts.const import ChangeSecretRecordStatusChoice
from accounts.filters import AccountFilterSet, NodeFilterBackend from accounts.filters import AccountFilterSet, NodeFilterBackend
from accounts.mixins import AccountRecordViewLogMixin from accounts.mixins import AccountRecordViewLogMixin
from accounts.models import Account, ChangeSecretRecord from accounts.models import Account, ChangeSecretRecord
from assets.const.gpt import create_or_update_chatx_resources
from assets.models import Asset, Node from assets.models import Asset, Node
from authentication.permissions import UserConfirmation, ConfirmType from authentication.permissions import UserConfirmation, ConfirmType
from common.api.mixin import ExtraFilterFieldsMixin from common.api.mixin import ExtraFilterFieldsMixin
@ -19,7 +18,6 @@ from common.drf.filters import AttrRulesFilterBackend
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.utils import lazyproperty, get_logger from common.utils import lazyproperty, get_logger
from orgs.mixins.api import OrgBulkModelViewSet from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
logger = get_logger(__file__) logger = get_logger(__file__)
@ -45,7 +43,6 @@ class AccountViewSet(OrgBulkModelViewSet):
'clear_secret': 'accounts.change_account', 'clear_secret': 'accounts.change_account',
'move_to_assets': 'accounts.delete_account', 'move_to_assets': 'accounts.delete_account',
'copy_to_assets': 'accounts.add_account', 'copy_to_assets': 'accounts.add_account',
'chat': 'accounts.view_account',
} }
export_as_zip = True export_as_zip = True
@ -155,13 +152,6 @@ class AccountViewSet(OrgBulkModelViewSet):
def copy_to_assets(self, request, *args, **kwargs): def copy_to_assets(self, request, *args, **kwargs):
return self._copy_or_move_to_assets(request, move=False) return self._copy_or_move_to_assets(request, move=False)
@action(methods=['get'], detail=False, url_path='chat')
def chat(self, request, *args, **kwargs):
with tmp_to_root_org():
__, account = create_or_update_chatx_resources()
serializer = self.get_serializer(account)
return Response(serializer.data)
class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet): class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
""" """
@ -200,7 +190,6 @@ class AccountHistoriesSecretAPI(ExtraFilterFieldsMixin, AccountRecordViewLogMixi
rbac_perms = { rbac_perms = {
'GET': 'accounts.view_accountsecret', 'GET': 'accounts.view_accountsecret',
} }
queryset = Account.history.model.objects.none()
@lazyproperty @lazyproperty
def account(self) -> Account: def account(self) -> Account:

View File

@ -12,8 +12,6 @@ class VirtualAccountViewSet(OrgBulkModelViewSet):
filterset_fields = ('alias',) filterset_fields = ('alias',)
def get_queryset(self): def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return VirtualAccount.objects.none()
return VirtualAccount.get_or_init_queryset() return VirtualAccount.get_or_init_queryset()
def get_object(self, ): def get_object(self, ):

View File

@ -41,7 +41,6 @@ class AutomationAssetsListApi(generics.ListAPIView):
class AutomationRemoveAssetApi(generics.UpdateAPIView): class AutomationRemoveAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch'] http_method_names = ['patch']
@ -60,7 +59,6 @@ class AutomationRemoveAssetApi(generics.UpdateAPIView):
class AutomationAddAssetApi(generics.UpdateAPIView): class AutomationAddAssetApi(generics.UpdateAPIView):
model = BaseAutomation model = BaseAutomation
queryset = BaseAutomation.objects.all()
serializer_class = serializers.UpdateAssetSerializer serializer_class = serializers.UpdateAssetSerializer
http_method_names = ['patch'] http_method_names = ['patch']

View File

@ -154,10 +154,12 @@ class ChangSecretAddAssetApi(AutomationAddAssetApi):
model = ChangeSecretAutomation model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateAssetSerializer serializer_class = serializers.ChangeSecretUpdateAssetSerializer
class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi): class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = ChangeSecretAutomation model = ChangeSecretAutomation
serializer_class = serializers.ChangeSecretUpdateNodeSerializer serializer_class = serializers.ChangeSecretUpdateNodeSerializer
class ChangeSecretStatusViewSet(OrgBulkModelViewSet): class ChangeSecretStatusViewSet(OrgBulkModelViewSet):
perm_model = ChangeSecretAutomation perm_model = ChangeSecretAutomation
filterset_class = ChangeSecretStatusFilterSet filterset_class = ChangeSecretStatusFilterSet

View File

@ -62,8 +62,7 @@ class ChangeSecretDashboardApi(APIView):
status_counts = defaultdict(lambda: defaultdict(int)) status_counts = defaultdict(lambda: defaultdict(int))
for date_finished, status in results: for date_finished, status in results:
dt_local = timezone.localtime(date_finished) date_str = str(date_finished.date())
date_str = str(dt_local.date())
if status == ChangeSecretRecordStatusChoice.failed: if status == ChangeSecretRecordStatusChoice.failed:
status_counts[date_str]['failed'] += 1 status_counts[date_str]['failed'] += 1
elif status == ChangeSecretRecordStatusChoice.success: elif status == ChangeSecretRecordStatusChoice.success:

View File

@ -150,9 +150,6 @@ class CheckAccountEngineViewSet(JMSModelViewSet):
http_method_names = ['get', 'options'] http_method_names = ['get', 'options']
def get_queryset(self): def get_queryset(self):
if getattr(self, "swagger_fake_view", False):
return CheckAccountEngine.objects.none()
return CheckAccountEngine.get_default_engines() return CheckAccountEngine.get_default_engines()
def filter_queryset(self, queryset: list): def filter_queryset(self, queryset: list):

View File

@ -63,10 +63,12 @@ class PushAccountRemoveAssetApi(AutomationRemoveAssetApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountAddAssetApi(AutomationAddAssetApi): class PushAccountAddAssetApi(AutomationAddAssetApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateAssetSerializer serializer_class = serializers.PushAccountUpdateAssetSerializer
class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi): class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi):
model = PushAccountAutomation model = PushAccountAutomation
serializer_class = serializers.PushAccountUpdateNodeSerializer serializer_class = serializers.PushAccountUpdateNodeSerializer

View File

@ -235,8 +235,8 @@ class AccountBackupHandler:
except Exception as e: except Exception as e:
error = str(e) error = str(e)
print(f'\033[31m>>> {error}\033[0m') print(f'\033[31m>>> {error}\033[0m')
self.manager.status = Status.error self.execution.status = Status.error
self.manager.summary['error'] = error self.execution.summary['error'] = error
def backup_by_obj_storage(self): def backup_by_obj_storage(self):
object_id = self.execution.snapshot.get('id') object_id = self.execution.snapshot.get('id')

View File

@ -113,16 +113,6 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if host.get('error'): if host.get('error'):
return host return host
inventory_hosts = []
if asset.type == HostTypes.WINDOWS:
if self.secret_type == SecretType.SSH_KEY:
host['error'] = _("Windows does not support SSH key authentication")
return host
new_secret = self.get_secret(account)
if '>' in new_secret or '^' in new_secret:
host['error'] = _("Windows password cannot contain special characters like > ^")
return host
host['ssh_params'] = {} host['ssh_params'] = {}
accounts = self.get_accounts(account) accounts = self.get_accounts(account)
@ -140,6 +130,11 @@ class BaseChangeSecretPushManager(AccountBasePlaybookManager):
if asset.type == HostTypes.WINDOWS: if asset.type == HostTypes.WINDOWS:
accounts = accounts.filter(secret_type=SecretType.PASSWORD) accounts = accounts.filter(secret_type=SecretType.PASSWORD)
inventory_hosts = []
if asset.type == HostTypes.WINDOWS and self.secret_type == SecretType.SSH_KEY:
print(f'Windows {asset} does not support ssh key push')
return inventory_hosts
for account in accounts: for account in accounts:
h = deepcopy(host) h = deepcopy(host)
h['name'] += '(' + account.username + ')' # To distinguish different accounts h['name'] += '(' + account.username + ')' # To distinguish different accounts

View File

@ -5,14 +5,12 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
register: db_info register: db_info
@ -25,53 +23,45 @@
var: info var: info
- name: Check whether SQLServer User exist - name: Check whether SQLServer User exist
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';" script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded when: db_info is succeeded
register: user_exist register: user_exist
- name: Change SQLServer password - name: Change SQLServer password
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version" script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length != 0 when: user_exist.query_results[0] | length != 0
- name: Add SQLServer user - name: Add SQLServer user
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version" script: "CREATE LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; CREATE USER {{ account.username }} FOR LOGIN {{ account.username }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length == 0 when: user_exist.query_results[0] | length == 0
- name: Verify password - name: Verify password
mssql_script: community.general.mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
when: check_conn_after_change when: check_conn_after_change

View File

@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@ -28,12 +28,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@ -67,11 +61,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@ -97,11 +86,6 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@ -30,12 +30,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@ -69,11 +63,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@ -99,11 +88,6 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@ -9,7 +9,7 @@ from accounts.const import (
AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice AutomationTypes, SecretStrategy, ChangeSecretRecordStatusChoice
) )
from accounts.models import ChangeSecretRecord from accounts.models import ChangeSecretRecord
from accounts.notifications import ChangeSecretExecutionTaskMsg from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretReportMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer from accounts.serializers import ChangeSecretRecordBackUpSerializer
from common.utils import get_logger from common.utils import get_logger
from common.utils.file import encrypt_and_compress_zip_file from common.utils.file import encrypt_and_compress_zip_file
@ -94,6 +94,10 @@ class ChangeSecretManager(BaseChangeSecretPushManager):
if not recipients: if not recipients:
return return
context = self.get_report_context()
for user in recipients:
ChangeSecretReportMsg(user, context).publish()
if not records: if not records:
return return

View File

@ -5,14 +5,12 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT SELECT
l.name, l.name,

View File

@ -5,14 +5,12 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
register: db_info register: db_info
@ -25,55 +23,47 @@
var: info var: info
- name: Check whether SQLServer User exist - name: Check whether SQLServer User exist
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';" script: "SELECT 1 from sys.sql_logins WHERE name='{{ account.username }}';"
when: db_info is succeeded when: db_info is succeeded
register: user_exist register: user_exist
- name: Change SQLServer password - name: Change SQLServer password
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version" script: "ALTER LOGIN {{ account.username }} WITH PASSWORD = '{{ account.secret }}', DEFAULT_DATABASE = {{ jms_asset.spec_info.db_name }}; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length != 0 when: user_exist.query_results[0] | length != 0
register: change_info register: change_info
- name: Add SQLServer user - name: Add SQLServer user
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version" script: "CREATE LOGIN [{{ account.username }}] WITH PASSWORD = '{{ account.secret }}'; CREATE USER [{{ account.username }}] FOR LOGIN [{{ account.username }}]; select @@version"
ignore_errors: true ignore_errors: true
when: user_exist.query_results[0] | length == 0 when: user_exist.query_results[0] | length == 0
register: change_info register: change_info
- name: Verify password - name: Verify password
mssql_script: community.general.mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version
when: check_conn_after_change when: check_conn_after_change

View File

@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@ -28,12 +28,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@ -67,11 +61,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@ -97,11 +86,6 @@ i18n:
ja: 'グループ' ja: 'グループ'
en: 'Groups' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'
ja: 'ユーザーID' ja: 'ユーザーID'

View File

@ -18,7 +18,6 @@
uid: "{{ params.uid | int if params.uid | length > 0 else omit }}" uid: "{{ params.uid | int if params.uid | length > 0 else omit }}"
shell: "{{ params.shell if params.shell | length > 0 else omit }}" shell: "{{ params.shell if params.shell | length > 0 else omit }}"
home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}" home: "{{ params.home if params.home | length > 0 else '/home/' + account.username }}"
group: "{{ params.group if params.group | length > 0 else omit }}"
groups: "{{ params.groups if params.groups | length > 0 else omit }}" groups: "{{ params.groups if params.groups | length > 0 else omit }}"
append: "{{ true if params.groups | length > 0 else false }}" append: "{{ true if params.groups | length > 0 else false }}"
expires: -1 expires: -1

View File

@ -30,12 +30,6 @@ params:
default: '' default: ''
help_text: "{{ 'Params home help text' | trans }}" help_text: "{{ 'Params home help text' | trans }}"
- name: group
type: str
label: "{{ 'Params group label' | trans }}"
default: ''
help_text: "{{ 'Params group help text' | trans }}"
- name: groups - name: groups
type: str type: str
label: "{{ 'Params groups label' | trans }}" label: "{{ 'Params groups label' | trans }}"
@ -69,11 +63,6 @@ i18n:
ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}'
en: 'Default home directory /home/{account username}' en: 'Default home directory /home/{account username}'
Params group help text:
zh: '请输入用户组(名字或数字),只能输入一个(需填写已存在的用户组)'
ja: 'ユーザー グループ (名前または番号) を入力してください。入力できるのは 1 つだけです (既存のユーザー グループを入力する必要があります)'
en: 'Please enter a user group (name or number), only one can be entered (must fill in an existing user group)'
Params groups help text: Params groups help text:
zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)'
ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)'
@ -95,14 +84,9 @@ i18n:
en: 'Home' en: 'Home'
Params groups label: Params groups label:
zh: '附加组' zh: '用户组'
ja: '追加グループ' ja: 'グループ'
en: 'Additional Group' en: 'Groups'
Params group label:
zh: '主组'
ja: '主组'
en: 'Main group'
Params uid label: Params uid label:
zh: '用户ID' zh: '用户ID'

View File

@ -5,13 +5,11 @@
tasks: tasks:
- name: "Remove account" - name: "Remove account"
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: "{{ jms_asset.spec_info.db_name }}" name: "{{ jms_asset.spec_info.db_name }}"
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: "DROP LOGIN {{ account.username }}; select @@version" script: "DROP LOGIN {{ account.username }}; select @@version"

View File

@ -5,13 +5,11 @@
tasks: tasks:
- name: Verify account - name: Verify account
mssql_script: community.general.mssql_script:
login_user: "{{ account.username }}" login_user: "{{ account.username }}"
login_password: "{{ account.secret }}" login_password: "{{ account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version

View File

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
from common.utils import get_logger from common.utils import get_logger
@ -11,9 +14,6 @@ __all__ = ['AZUREVaultClient']
class AZUREVaultClient(object): class AZUREVaultClient(object):
def __init__(self, vault_url, tenant_id, client_id, client_secret): def __init__(self, vault_url, tenant_id, client_id, client_secret):
from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
authentication_endpoint = 'https://login.microsoftonline.com/' \ authentication_endpoint = 'https://login.microsoftonline.com/' \
if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/' if ('azure.net' in vault_url) else 'https://login.chinacloudapi.cn/'
@ -23,8 +23,6 @@ class AZUREVaultClient(object):
self.client = SecretClient(vault_url=vault_url, credential=credentials) self.client = SecretClient(vault_url=vault_url, credential=credentials)
def is_active(self): def is_active(self):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try: try:
self.client.set_secret('jumpserver', '666') self.client.set_secret('jumpserver', '666')
except (ResourceNotFoundError, ClientAuthenticationError) as e: except (ResourceNotFoundError, ClientAuthenticationError) as e:
@ -34,8 +32,6 @@ class AZUREVaultClient(object):
return True, '' return True, ''
def get(self, name, version=None): def get(self, name, version=None):
from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError
try: try:
secret = self.client.get_secret(name, version) secret = self.client.get_secret(name, version)
return secret.value return secret.value

View File

@ -132,7 +132,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.asset.platform return self.asset.platform
@lazyproperty @lazyproperty
def alias(self) -> str: def alias(self):
""" """
别称因为有虚拟账号@INPUT @MANUAL @USER, 否则为 id 别称因为有虚拟账号@INPUT @MANUAL @USER, 否则为 id
""" """
@ -140,13 +140,13 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.username return self.username
return str(self.id) return str(self.id)
def is_virtual(self) -> bool: def is_virtual(self):
""" """
不要用 username 去判断因为可能是构造的 account 对象设置了同名账号的用户名, 不要用 username 去判断因为可能是构造的 account 对象设置了同名账号的用户名,
""" """
return self.alias.startswith('@') return self.alias.startswith('@')
def is_ds_account(self) -> bool: def is_ds_account(self):
if self.is_virtual(): if self.is_virtual():
return '' return ''
if not self.asset.is_directory_service: if not self.asset.is_directory_service:
@ -160,7 +160,7 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return self.asset.ds return self.asset.ds
@lazyproperty @lazyproperty
def ds_domain(self) -> str: def ds_domain(self):
"""这个不能去掉perm_account 会动态设置这个值,以更改 full_username""" """这个不能去掉perm_account 会动态设置这个值,以更改 full_username"""
if self.is_virtual(): if self.is_virtual():
return '' return ''
@ -172,17 +172,17 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
return '@' in self.username or '\\' in self.username return '@' in self.username or '\\' in self.username
@property @property
def full_username(self) -> str: def full_username(self):
if not self.username_has_domain() and self.ds_domain: if not self.username_has_domain() and self.ds_domain:
return '{}@{}'.format(self.username, self.ds_domain) return '{}@{}'.format(self.username, self.ds_domain)
return self.username return self.username
@lazyproperty @lazyproperty
def has_secret(self) -> bool: def has_secret(self):
return bool(self.secret) return bool(self.secret)
@lazyproperty @lazyproperty
def versions(self) -> int: def versions(self):
return self.history.count() return self.history.count()
def get_su_from_accounts(self): def get_su_from_accounts(self):

View File

@ -33,7 +33,7 @@ class IntegrationApplication(JMSOrgBaseModel):
return qs.filter(*query) return qs.filter(*query)
@property @property
def accounts_amount(self) -> int: def accounts_amount(self):
return self.get_accounts().count() return self.get_accounts().count()
@property @property

View File

@ -75,11 +75,11 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return bool(self.secret) return bool(self.secret)
@property @property
def has_username(self) -> bool: def has_username(self):
return bool(self.username) return bool(self.username)
@property @property
def spec_info(self) -> dict: def spec_info(self):
data = {} data = {}
if self.secret_type != SecretType.SSH_KEY: if self.secret_type != SecretType.SSH_KEY:
return data return data
@ -87,13 +87,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return data return data
@property @property
def password(self) -> str: def password(self):
if self.secret_type == SecretType.PASSWORD: if self.secret_type == SecretType.PASSWORD:
return self.secret return self.secret
return None return None
@property @property
def private_key(self) -> str: def private_key(self):
if self.secret_type == SecretType.SSH_KEY: if self.secret_type == SecretType.SSH_KEY:
return self.secret return self.secret
return None return None
@ -110,7 +110,7 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
return None return None
@property @property
def ssh_key_fingerprint(self) -> str: def ssh_key_fingerprint(self):
if self.public_key: if self.public_key:
public_key = self.public_key public_key = self.public_key
elif self.private_key: elif self.private_key:

View File

@ -56,7 +56,7 @@ class VaultModelMixin(models.Model):
__secret = None __secret = None
@property @property
def secret(self) -> str: def secret(self):
if self.__secret: if self.__secret:
return self.__secret return self.__secret
from accounts.backends import vault_client from accounts.backends import vault_client

View File

@ -18,11 +18,11 @@ class VirtualAccount(JMSOrgBaseModel):
verbose_name = _('Virtual account') verbose_name = _('Virtual account')
@property @property
def name(self) -> str: def name(self):
return self.get_alias_display() return self.get_alias_display()
@property @property
def username(self) -> str: def username(self):
usernames_map = { usernames_map = {
AliasAccount.INPUT: _("Manual input"), AliasAccount.INPUT: _("Manual input"),
AliasAccount.USER: _("Same with user"), AliasAccount.USER: _("Same with user"),
@ -32,7 +32,7 @@ class VirtualAccount(JMSOrgBaseModel):
return usernames_map.get(self.alias, '') return usernames_map.get(self.alias, '')
@property @property
def comment(self) -> str: def comment(self):
comments_map = { comments_map = {
AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'), AliasAccount.INPUT: _('Non-asset account, Input username/password on connect'),
AliasAccount.USER: _('The account username name same with user on connect'), AliasAccount.USER: _('The account username name same with user on connect'),

View File

@ -253,8 +253,6 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
'source_id': {'required': False, 'allow_null': True}, 'source_id': {'required': False, 'allow_null': True},
} }
fields_unimport_template = ['params'] fields_unimport_template = ['params']
# 手动判断唯一性校验
validators = []
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
@ -265,21 +263,6 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
) )
return queryset return queryset
def validate(self, attrs):
instance = getattr(self, "instance", None)
if instance:
return super().validate(attrs)
field_errors = {}
for _fields in Account._meta.unique_together:
lookup = {field: attrs.get(field) for field in _fields}
if Account.objects.filter(**lookup).exists():
verbose_names = ', '.join([str(Account._meta.get_field(f).verbose_name) for f in _fields])
msg_template = _('Account already exists. Field(s): {fields} must be unique.')
field_errors[_fields[0]] = msg_template.format(fields=verbose_names)
raise serializers.ValidationError(field_errors)
return attrs
class AccountDetailSerializer(AccountSerializer): class AccountDetailSerializer(AccountSerializer):
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
@ -473,8 +456,6 @@ class AssetAccountBulkSerializer(
class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta(AccountSerializer.Meta): class Meta(AccountSerializer.Meta):
fields = AccountSerializer.Meta.fields + ['spec_info'] fields = AccountSerializer.Meta.fields + ['spec_info']
extra_kwargs = { extra_kwargs = {
@ -489,7 +470,6 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer):
class AccountHistorySerializer(serializers.ModelSerializer): class AccountHistorySerializer(serializers.ModelSerializer):
secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type')) secret_type = LabeledChoiceField(choices=SecretType.choices, label=_('Secret type'))
secret = serializers.CharField(label=_('Secret'), read_only=True)
id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True) id = serializers.IntegerField(label=_('ID'), source='history_id', read_only=True)
class Meta: class Meta:

View File

@ -70,8 +70,6 @@ class AuthValidateMixin(serializers.Serializer):
class BaseAccountSerializer( class BaseAccountSerializer(
AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer
): ):
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
class Meta: class Meta:
model = BaseAccount model = BaseAccount
fields_mini = ["id", "name", "username"] fields_mini = ["id", "name", "username"]

View File

@ -130,7 +130,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
@staticmethod @staticmethod
def get_is_success(obj) -> bool: def get_is_success(obj):
return obj.status == ChangeSecretRecordStatusChoice.success return obj.status == ChangeSecretRecordStatusChoice.success
@ -157,7 +157,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
@staticmethod @staticmethod
def get_asset(instance) -> str: def get_asset(instance):
return str(instance.asset) return str(instance.asset)
@staticmethod @staticmethod
@ -165,7 +165,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
return str(instance.account) return str(instance.account)
@staticmethod @staticmethod
def get_is_success(obj) -> str: def get_is_success(obj):
if obj.status == ChangeSecretRecordStatusChoice.success.value: if obj.status == ChangeSecretRecordStatusChoice.success.value:
return _("Success") return _("Success")
return _("Failed") return _("Failed")
@ -196,9 +196,9 @@ class ChangeSecretAccountSerializer(serializers.ModelSerializer):
read_only_fields = fields read_only_fields = fields
@staticmethod @staticmethod
def get_meta(obj) -> dict: def get_meta(obj):
return account_secret_task_status.get(str(obj.id)) return account_secret_task_status.get(str(obj.id))
@staticmethod @staticmethod
def get_ttl(obj) -> int: def get_ttl(obj):
return account_secret_task_status.get_ttl(str(obj.id)) return account_secret_task_status.get_ttl(str(obj.id))

View File

@ -69,7 +69,7 @@ class AssetRiskSerializer(serializers.Serializer):
risk_summary = serializers.SerializerMethodField() risk_summary = serializers.SerializerMethodField()
@staticmethod @staticmethod
def get_risk_summary(obj) -> dict: def get_risk_summary(obj):
summary = {} summary = {}
for risk in RiskChoice.choices: for risk in RiskChoice.choices:
summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0) summary[f"{risk[0]}_count"] = obj.get(f"{risk[0]}_count", 0)

View File

@ -0,0 +1,36 @@
{% load i18n %}
<h3>{% trans 'Task name' %}: {{ name }}</h3>
<h3>{% trans 'Task execution id' %}: {{ execution_id }}</h3>
<p>{% trans 'Respectful' %} {{ recipient }}</p>
<p>{% trans 'Hello! The following is the failure of changing the password of your assets or pushing the account. Please check and handle it in time.' %}</p>
<table style="width: 100%; border-collapse: collapse; max-width: 100%; text-align: left; margin-top: 20px;">
<caption></caption>
<thead>
<tr style="background-color: #f2f2f2;">
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Asset' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Account' %}</th>
<th style="border: 1px solid #ddd; padding: 10px;">{% trans 'Error' %}</th>
</tr>
</thead>
<tbody>
{% for asset_name, account_username, error in asset_account_errors %}
<tr>
<td style="border: 1px solid #ddd; padding: 10px;">{{ asset_name }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">{{ account_username }}</td>
<td style="border: 1px solid #ddd; padding: 10px;">
<div style="
max-width: 90%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;"
title="{{ error }}"
>
{{ error }}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -3,4 +3,3 @@ from .connect_method import *
from .login_acl import * from .login_acl import *
from .login_asset_acl import * from .login_asset_acl import *
from .login_asset_check import * from .login_asset_check import *
from .data_masking import *

View File

@ -1,20 +0,0 @@
from orgs.mixins.api import OrgBulkModelViewSet
from .common import ACLUserFilterMixin
from ..models import DataMaskingRule
from .. import serializers
__all__ = ['DataMaskingRuleViewSet']
class DataMaskingRuleFilter(ACLUserFilterMixin):
class Meta:
model = DataMaskingRule
fields = ('name',)
class DataMaskingRuleViewSet(OrgBulkModelViewSet):
model = DataMaskingRule
filterset_class = DataMaskingRuleFilter
search_fields = ('name',)
serializer_class = serializers.DataMaskingRuleSerializer

View File

@ -8,7 +8,7 @@ __all__ = ['LoginAssetACLViewSet']
class LoginAssetACLFilter(ACLUserAssetFilterMixin): class LoginAssetACLFilter(ACLUserAssetFilterMixin):
class Meta: class Meta:
model = models.LoginAssetACL model = models.LoginAssetACL
fields = ['name', 'action'] fields = ['name', ]
class LoginAssetACLViewSet(OrgBulkModelViewSet): class LoginAssetACLViewSet(OrgBulkModelViewSet):

View File

@ -1,45 +0,0 @@
# Generated by Django 4.1.13 on 2025-10-07 16:16
import common.db.fields
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('acls', '0002_auto_20210926_1047'),
]
operations = [
migrations.CreateModel(
name='DataMaskingRule',
fields=[
('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')),
('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('comment', models.TextField(blank=True, default='', verbose_name='Comment')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('priority', models.IntegerField(default=50, help_text='1-100, the lower the value will be match first', validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)], verbose_name='Priority')),
('action', models.CharField(default='reject', max_length=64, verbose_name='Action')),
('is_active', models.BooleanField(default=True, verbose_name='Active')),
('users', common.db.fields.JSONManyToManyField(default=dict, to='users.User', verbose_name='Users')),
('assets', common.db.fields.JSONManyToManyField(default=dict, to='assets.Asset', verbose_name='Assets')),
('accounts', models.JSONField(default=list, verbose_name='Accounts')),
('name', models.CharField(max_length=128, verbose_name='Name')),
('fields_pattern', models.CharField(default='password', max_length=128, verbose_name='Fields pattern')),
('masking_method', models.CharField(choices=[('fixed_char', 'Fixed Character Replacement'), ('hide_middle', 'Hide Middle Characters'), ('keep_prefix', 'Keep Prefix Only'), ('keep_suffix', 'Keep Suffix Only')], default='fixed_char', max_length=32, verbose_name='Masking Method')),
('mask_pattern', models.CharField(blank=True, default='######', max_length=128, null=True, verbose_name='Mask Pattern')),
('reviewers', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Reviewers')),
],
options={
'verbose_name': 'Data Masking Rule',
'unique_together': {('org_id', 'name')},
},
),
]

View File

@ -2,4 +2,3 @@ from .command_acl import *
from .connect_method import * from .connect_method import *
from .login_acl import * from .login_acl import *
from .login_asset_acl import * from .login_asset_acl import *
from .data_masking import *

View File

@ -1,42 +0,0 @@
from django.db import models
from acls.models import UserAssetAccountBaseACL
from common.utils import get_logger
from django.utils.translation import gettext_lazy as _
logger = get_logger(__file__)
__all__ = ['MaskingMethod', 'DataMaskingRule']
class MaskingMethod(models.TextChoices):
fixed_char = "fixed_char", _("Fixed Character Replacement") # 固定字符替换
hide_middle = "hide_middle", _("Hide Middle Characters") # 隐藏中间几位
keep_prefix = "keep_prefix", _("Keep Prefix Only") # 只保留前缀
keep_suffix = "keep_suffix", _("Keep Suffix Only") # 只保留后缀
class DataMaskingRule(UserAssetAccountBaseACL):
name = models.CharField(max_length=128, verbose_name=_("Name"))
fields_pattern = models.CharField(max_length=128, default='password', verbose_name=_("Fields pattern"))
masking_method = models.CharField(
max_length=32,
choices=MaskingMethod.choices,
default=MaskingMethod.fixed_char,
verbose_name=_("Masking Method"),
)
mask_pattern = models.CharField(
max_length=128,
verbose_name=_("Mask Pattern"),
default="######",
blank=True,
null=True,
)
def __str__(self):
return self.name
class Meta:
unique_together = [('org_id', 'name')]
verbose_name = _("Data Masking Rule")

View File

@ -1,52 +1,30 @@
from django.utils import timezone from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from accounts.models import Account from accounts.models import Account
from acls.models import LoginACL, LoginAssetACL
from assets.models import Asset from assets.models import Asset
from audits.models import UserLoginLog from audits.models import UserLoginLog
from common.views.template import custom_render_to_string
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
from users.models import User from users.models import User
class UserLoginReminderMsg(UserMessage): class UserLoginReminderMsg(UserMessage):
subject = _('User login reminder') subject = _('User login reminder')
template_name = 'acls/user_login_reminder.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "recipient_name", "label": _("Recipient name"), "default": "John"},
{"name": "recipient_username", "label": _("Recipient username"), "default": "john"},
{"name": "user_agent", "label": _('User agent'), "default": "Mozilla/5.0"},
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
{"name": "login_from", "label": _('Login from'), "default": "web"},
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"},
]
def __init__(self, user, user_log: UserLoginLog, acl: LoginACL): def __init__(self, user, user_log: UserLoginLog):
self.user_log = user_log self.user_log = user_log
self.acl_name = str(acl)
self.login_from = user_log.get_type_display()
now = timezone.localtime(user_log.datetime)
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
super().__init__(user) super().__init__(user)
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
user_log = self.user_log user_log = self.user_log
context = { context = {
'ip': user_log.ip, 'ip': user_log.ip,
'time': self.time,
'city': user_log.city, 'city': user_log.city,
'acl_name': self.acl_name,
'login_from': self.login_from,
'username': user_log.username, 'username': user_log.username,
'recipient_name': self.user.name, 'recipient': self.user,
'recipient_username': self.user.username,
'user_agent': user_log.user_agent, 'user_agent': user_log.user_agent,
} }
message = custom_render_to_string(self.template_name, context) message = render_to_string('acls/user_login_reminder.html', context)
return { return {
'subject': str(self.subject), 'subject': str(self.subject),
@ -62,55 +40,24 @@ class UserLoginReminderMsg(UserMessage):
class AssetLoginReminderMsg(UserMessage): class AssetLoginReminderMsg(UserMessage):
subject = _('User login alert for asset') subject = _('User login alert for asset')
template_name = 'acls/asset_login_reminder.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "name", "label": _('Name'), "default": "John"},
{"name": "asset", "label": _('Asset'), "default": "dev server"},
{"name": "recipient_name", "label": _('Recipient name'), "default": "John"},
{"name": "recipient_username", "label": _('Recipient username'), "default": "john"},
{"name": "account", "label": _('Account Input username'), "default": "root"},
{"name": "account_name", "label": _('Account name'), "default": "root"},
{"name": "acl_name", "label": _('ACL name'), "default": "login acl"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "login_from", "label": _('Login from'), "default": "web"},
{"name": "time", "label": _('Login time'), "default": "2025-01-01 12:00:00"}
]
def __init__( def __init__(self, user, asset: Asset, login_user: User, account: Account, input_username):
self, user, asset: Asset, login_user: User,
account: Account, acl: LoginAssetACL,
ip, input_username, login_from
):
self.ip = ip
self.asset = asset self.asset = asset
self.login_user = login_user self.login_user = login_user
self.account = account self.account = account
self.acl_name = str(acl)
self.login_from = login_from
self.login_user = login_user
self.input_username = input_username self.input_username = input_username
now = timezone.localtime(timezone.now())
self.time = now.strftime('%Y-%m-%d %H:%M:%S')
super().__init__(user) super().__init__(user)
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
context = { context = {
'ip': self.ip, 'recipient': self.user,
'time': self.time,
'login_from': self.login_from,
'recipient_name': self.user.name,
'recipient_username': self.user.username,
'username': self.login_user.username, 'username': self.login_user.username,
'name': self.login_user.name, 'name': self.login_user.name,
'asset': str(self.asset), 'asset': str(self.asset),
'account': self.input_username, 'account': self.input_username,
'account_name': self.account.name, 'account_name': self.account.name,
'acl_name': self.acl_name,
} }
message = custom_render_to_string(self.template_name, context) message = render_to_string('acls/asset_login_reminder.html', context)
return { return {
'subject': str(self.subject), 'subject': str(self.subject),

View File

@ -3,4 +3,3 @@ from .connect_method import *
from .login_acl import * from .login_acl import *
from .login_asset_acl import * from .login_asset_acl import *
from .login_asset_check import * from .login_asset_check import *
from .data_masking import *

View File

@ -90,7 +90,7 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
fields_small = fields_mini + [ fields_small = fields_mini + [
"is_active", "priority", "action", "is_active", "priority", "action",
"date_created", "date_updated", "date_created", "date_updated",
"comment", "created_by" "comment", "created_by", "org_id",
] ]
fields_m2m = ["reviewers", ] fields_m2m = ["reviewers", ]
fields = fields_small + fields_m2m fields = fields_small + fields_m2m
@ -100,20 +100,6 @@ class BaseACLSerializer(ActionAclSerializer, serializers.Serializer):
'reviewers': {'label': _('Recipients')}, 'reviewers': {'label': _('Recipients')},
} }
class BaseUserACLSerializer(BaseACLSerializer):
users = JSONManyToManyField(label=_('User'))
class Meta(BaseACLSerializer.Meta):
fields = BaseACLSerializer.Meta.fields + ['users']
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
assets = JSONManyToManyField(label=_('Asset'))
accounts = serializers.ListField(label=_('Account'))
class Meta(BaseUserACLSerializer.Meta):
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts', 'org_id']
def validate_reviewers(self, reviewers): def validate_reviewers(self, reviewers):
action = self.initial_data.get('action') action = self.initial_data.get('action')
if not action and self.instance: if not action and self.instance:
@ -132,4 +118,19 @@ class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
"None of the reviewers belong to Organization `{}`".format(org.name) "None of the reviewers belong to Organization `{}`".format(org.name)
) )
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return valid_reviewers return valid_reviewers
class BaseUserACLSerializer(BaseACLSerializer):
users = JSONManyToManyField(label=_('User'))
class Meta(BaseACLSerializer.Meta):
fields = BaseACLSerializer.Meta.fields + ['users']
class BaseUserAssetAccountACLSerializer(BaseUserACLSerializer):
assets = JSONManyToManyField(label=_('Asset'))
accounts = serializers.ListField(label=_('Account'))
class Meta(BaseUserACLSerializer.Meta):
fields = BaseUserACLSerializer.Meta.fields + ['assets', 'accounts']

View File

@ -1,4 +1,4 @@
from common.serializers.mixin import CommonBulkModelSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
from ..const import ActionChoices from ..const import ActionChoices
from ..models import ConnectMethodACL from ..models import ConnectMethodACL
@ -6,15 +6,16 @@ from ..models import ConnectMethodACL
__all__ = ["ConnectMethodACLSerializer"] __all__ = ["ConnectMethodACLSerializer"]
class ConnectMethodACLSerializer(BaseSerializer, CommonBulkModelSerializer): class ConnectMethodACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
class Meta(BaseSerializer.Meta): class Meta(BaseSerializer.Meta):
model = ConnectMethodACL model = ConnectMethodACL
fields = [ fields = [
i for i in BaseSerializer.Meta.fields + ['connect_methods'] i for i in BaseSerializer.Meta.fields + ['connect_methods']
if i not in ['assets', 'accounts', 'org_id'] if i not in ['assets', 'accounts']
] ]
action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [ action_choices_exclude = BaseSerializer.Meta.action_choices_exclude + [
ActionChoices.review, ActionChoices.review,
ActionChoices.accept,
ActionChoices.notice, ActionChoices.notice,
ActionChoices.face_verify, ActionChoices.face_verify,
ActionChoices.face_online, ActionChoices.face_online,

View File

@ -1,19 +0,0 @@
from django.utils.translation import gettext_lazy as _
from acls.models import MaskingMethod, DataMaskingRule
from common.serializers.fields import LabeledChoiceField
from common.serializers.mixin import CommonBulkModelSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserAssetAccountACLSerializer as BaseSerializer
__all__ = ['DataMaskingRuleSerializer']
class DataMaskingRuleSerializer(BaseSerializer, BulkOrgResourceModelSerializer):
masking_method = LabeledChoiceField(
choices=MaskingMethod.choices, default=MaskingMethod.fixed_char, label=_('Masking Method')
)
class Meta(BaseSerializer.Meta):
model = DataMaskingRule
fields = BaseSerializer.Meta.fields + ['fields_pattern', 'masking_method', 'mask_pattern']

View File

@ -1,7 +1,7 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from common.serializers import CommonBulkModelSerializer
from common.serializers import MethodSerializer from common.serializers import MethodSerializer
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from .base import BaseUserACLSerializer from .base import BaseUserACLSerializer
from .rules import RuleSerializer from .rules import RuleSerializer
from ..const import ActionChoices from ..const import ActionChoices
@ -12,12 +12,12 @@ __all__ = ["LoginACLSerializer"]
common_help_text = _("With * indicating a match all. ") common_help_text = _("With * indicating a match all. ")
class LoginACLSerializer(BaseUserACLSerializer, CommonBulkModelSerializer): class LoginACLSerializer(BaseUserACLSerializer, BulkOrgResourceModelSerializer):
rules = MethodSerializer(label=_('Rule')) rules = MethodSerializer(label=_('Rule'))
class Meta(BaseUserACLSerializer.Meta): class Meta(BaseUserACLSerializer.Meta):
model = LoginACL model = LoginACL
fields = list((set(BaseUserACLSerializer.Meta.fields) | {'rules'})) fields = BaseUserACLSerializer.Meta.fields + ['rules', ]
action_choices_exclude = [ action_choices_exclude = [
ActionChoices.warning, ActionChoices.warning,
ActionChoices.notify_and_warn, ActionChoices.notify_and_warn,

View File

@ -1,17 +1,13 @@
{% load i18n %} {% load i18n %}
<h3>{% trans 'Dear' %}: {{ recipient_name }}[{{ recipient_username }}]</h3> <h3>{% trans 'Dear' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
<hr> <hr>
<p>{% trans 'We would like to inform you that a user has recently logged into the following asset:' %}<p> <p>{% trans 'We would like to inform you that a user has recently logged into the following asset:' %}<p>
<p><strong>{% trans 'Asset details' %}:</strong></p> <p><strong>{% trans 'Asset details' %}:</strong></p>
<ul> <ul>
<li><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</li> <li><strong>{% trans 'User' %}:</strong> [{{ name }}({{ username }})]</li>
<li><strong>IP:</strong> [{{ ip }}]</li>
<li><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</li> <li><strong>{% trans 'Assets' %}:</strong> [{{ asset }}]</li>
<li><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</li> <li><strong>{% trans 'Account' %}:</strong> [{{ account_name }}({{ account }})]</li>
<li><strong>{% trans 'Login asset acl' %}:</strong> [{{ acl_name }}]</li>
<li><strong>{% trans 'Login from' %}:</strong> [{{ login_from }}]</li>
<li><strong>{% trans 'Time' %}:</strong> [{{ time }}]</li>
</ul> </ul>
<hr> <hr>

View File

@ -1,6 +1,6 @@
{% load i18n %} {% load i18n %}
<h3>{% trans 'Dear' %}: {{ recipient_name }}[{{ recipient_username }}]</h3> <h3>{% trans 'Dear' %}: {{ recipient.name }}[{{ recipient.username }}]</h3>
<hr> <hr>
<p>{% trans 'We would like to inform you that a user has recently logged:' %}<p> <p>{% trans 'We would like to inform you that a user has recently logged:' %}<p>
<p><strong>{% trans 'User details' %}:</strong></p> <p><strong>{% trans 'User details' %}:</strong></p>
@ -8,10 +8,7 @@
<li><strong>{% trans 'User' %}:</strong> [{{ username }}]</li> <li><strong>{% trans 'User' %}:</strong> [{{ username }}]</li>
<li><strong>IP:</strong> [{{ ip }}]</li> <li><strong>IP:</strong> [{{ ip }}]</li>
<li><strong>{% trans 'Login city' %}:</strong> [{{ city }}]</li> <li><strong>{% trans 'Login city' %}:</strong> [{{ city }}]</li>
<li><strong>{% trans 'Login from' %}:</strong> [{{ login_from }}]</li>
<li><strong>{% trans 'User agent' %}:</strong> [{{ user_agent }}]</li> <li><strong>{% trans 'User agent' %}:</strong> [{{ user_agent }}]</li>
<li><strong>{% trans 'Login acl' %}:</strong> [{{ acl_name }}]</li>
<li><strong>{% trans 'Time' %}:</strong> [{{ time }}]</li>
</ul> </ul>
<hr> <hr>

View File

@ -11,7 +11,6 @@ router.register(r'login-asset-acls', api.LoginAssetACLViewSet, 'login-asset-acl'
router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl') router.register(r'command-filter-acls', api.CommandFilterACLViewSet, 'command-filter-acl')
router.register(r'command-groups', api.CommandGroupViewSet, 'command-group') router.register(r'command-groups', api.CommandGroupViewSet, 'command-group')
router.register(r'connect-method-acls', api.ConnectMethodACLViewSet, 'connect-method-acl') router.register(r'connect-method-acls', api.ConnectMethodACLViewSet, 'connect-method-acl')
router.register(r'data-masking-rules', api.DataMaskingRuleViewSet, 'data-masking-rule')
urlpatterns = [ urlpatterns = [
path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'), path('login-asset/check/', api.LoginAssetCheckAPI.as_view(), name='login-asset-check'),

View File

@ -14,7 +14,6 @@ class FavoriteAssetViewSet(BulkModelViewSet):
serializer_class = FavoriteAssetSerializer serializer_class = FavoriteAssetSerializer
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
filterset_fields = ['asset'] filterset_fields = ['asset']
page_no_limit = True
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
with tmp_to_root_org(): with tmp_to_root_org():

View File

@ -7,7 +7,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from assets.const import AllTypes from assets.const import AllTypes
from assets.models import Platform, Node, Asset, PlatformProtocol, PlatformAutomation from assets.models import Platform, Node, Asset, PlatformProtocol
from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer from assets.serializers import PlatformSerializer, PlatformProtocolSerializer, PlatformListSerializer
from common.api import JMSModelViewSet from common.api import JMSModelViewSet
from common.permissions import IsValidUser from common.permissions import IsValidUser
@ -43,7 +43,6 @@ class AssetPlatformViewSet(JMSModelViewSet):
'ops_methods': 'assets.view_platform', 'ops_methods': 'assets.view_platform',
'filter_nodes_assets': 'assets.view_platform', 'filter_nodes_assets': 'assets.view_platform',
} }
page_no_limit = True
def get_queryset(self): def get_queryset(self):
# 因为没有走分页逻辑,所以需要这里 prefetch # 因为没有走分页逻辑,所以需要这里 prefetch
@ -113,7 +112,6 @@ class PlatformProtocolViewSet(JMSModelViewSet):
class PlatformAutomationMethodsApi(generics.ListAPIView): class PlatformAutomationMethodsApi(generics.ListAPIView):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
queryset = PlatformAutomation.objects.none()
@staticmethod @staticmethod
def automation_methods(): def automation_methods():

View File

@ -161,7 +161,6 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView):
'GET': 'assets.view_asset', 'GET': 'assets.view_asset',
'list': 'assets.view_asset', 'list': 'assets.view_asset',
} }
queryset = Node.objects.none()
def get_assets(self): def get_assets(self):
key = self.request.query_params.get('key') key = self.request.query_params.get('key')

View File

@ -6,13 +6,11 @@
tasks: tasks:
- name: Test SQLServer connection - name: Test SQLServer connection
mssql_script: community.general.mssql_script:
login_user: "{{ jms_account.username }}" login_user: "{{ jms_account.username }}"
login_password: "{{ jms_account.secret }}" login_password: "{{ jms_account.secret }}"
login_host: "{{ jms_asset.address }}" login_host: "{{ jms_asset.address }}"
login_port: "{{ jms_asset.port }}" login_port: "{{ jms_asset.port }}"
name: '{{ jms_asset.spec_info.db_name }}' name: '{{ jms_asset.spec_info.db_name }}'
encryption: "{{ jms_asset.encryption | default(None) }}"
tds_version: "{{ jms_asset.tds_version | default(None) }}"
script: | script: |
SELECT @@version SELECT @@version

View File

@ -1,6 +1,5 @@
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from orgs.models import Organization
from .base import BaseType from .base import BaseType
@ -53,41 +52,3 @@ class GPTTypes(BaseType):
return [ return [
cls.CHATGPT, cls.CHATGPT,
] ]
CHATX_NAME = 'ChatX'
def create_or_update_chatx_resources(chatx_name=CHATX_NAME, org_id=Organization.SYSTEM_ID):
from django.apps import apps
platform_model = apps.get_model('assets', 'Platform')
asset_model = apps.get_model('assets', 'Asset')
account_model = apps.get_model('accounts', 'Account')
platform, __ = platform_model.objects.get_or_create(
name=chatx_name,
defaults={
'internal': True,
'type': chatx_name,
'category': 'ai',
}
)
asset, __ = asset_model.objects.get_or_create(
address=chatx_name,
defaults={
'name': chatx_name,
'platform': platform,
'org_id': org_id
}
)
account, __ = account_model.objects.get_or_create(
username=chatx_name,
defaults={
'name': chatx_name,
'asset': asset,
'org_id': org_id
}
)
return asset, account

View File

@ -250,12 +250,6 @@ class Protocol(ChoicesMixin, models.TextChoices):
'default': False, 'default': False,
'label': _('Auth username') 'label': _('Auth username')
}, },
'enable_cluster_mode': {
'type': 'bool',
'default': False,
'label': _('Enable cluster mode'),
'help_text': _('Enable if this Redis instance is part of a cluster')
},
} }
}, },
} }

View File

@ -112,7 +112,7 @@ class Protocol(models.Model):
return protocols[0] if len(protocols) > 0 else {} return protocols[0] if len(protocols) > 0 else {}
@property @property
def setting(self) -> dict: def setting(self):
if self._setting is not None: if self._setting is not None:
return self._setting return self._setting
return self.asset_platform_protocol.get('setting', {}) return self.asset_platform_protocol.get('setting', {})
@ -122,7 +122,7 @@ class Protocol(models.Model):
self._setting = value self._setting = value
@property @property
def public(self) -> bool: def public(self):
return self.asset_platform_protocol.get('public', True) return self.asset_platform_protocol.get('public', True)
@ -210,7 +210,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
return self.category == const.Category.DS and hasattr(self, 'ds') return self.category == const.Category.DS and hasattr(self, 'ds')
@lazyproperty @lazyproperty
def spec_info(self) -> dict: def spec_info(self):
instance = getattr(self, self.category, None) instance = getattr(self, self.category, None)
if not instance: if not instance:
return {} return {}
@ -240,7 +240,7 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
return info return info
@lazyproperty @lazyproperty
def auto_config(self) -> dict: def auto_config(self):
platform = self.platform platform = self.platform
auto_config = { auto_config = {
'su_enabled': platform.su_enabled, 'su_enabled': platform.su_enabled,
@ -343,11 +343,11 @@ class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin,
return names return names
@lazyproperty @lazyproperty
def type(self) -> str: def type(self):
return self.platform.type return self.platform.type
@lazyproperty @lazyproperty
def category(self) -> str: def category(self):
return self.platform.category return self.platform.category
def is_category(self, category): def is_category(self, category):

View File

@ -573,7 +573,7 @@ class Node(JMSOrgBaseModel, SomeNodesMixin, FamilyMixin, NodeAssetsMixin):
return not self.__gt__(other) return not self.__gt__(other)
@property @property
def name(self) -> str: def name(self):
return self.value return self.value
def computed_full_value(self): def computed_full_value(self):

View File

@ -25,7 +25,7 @@ class PlatformProtocol(models.Model):
return '{}/{}'.format(self.name, self.port) return '{}/{}'.format(self.name, self.port)
@property @property
def secret_types(self) -> list: def secret_types(self):
return Protocol.settings().get(self.name, {}).get('secret_types', ['password']) return Protocol.settings().get(self.name, {}).get('secret_types', ['password'])
@lazyproperty @lazyproperty

View File

@ -147,7 +147,6 @@ class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, Writa
protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=()) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=())
accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts')) accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Accounts'))
nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path")) nodes_display = NodeDisplaySerializer(read_only=False, required=False, label=_("Node path"))
auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'), platform = ObjectRelatedField(queryset=Platform.objects, required=True, label=_('Platform'),
attrs=('id', 'name', 'type')) attrs=('id', 'name', 'type'))
accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount')) accounts_amount = serializers.IntegerField(read_only=True, label=_('Accounts amount'))
@ -426,18 +425,6 @@ class DetailMixin(serializers.Serializer):
gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True) gathered_info = MethodSerializer(label=_('Gathered info'), read_only=True)
auto_config = serializers.DictField(read_only=True, label=_('Auto info')) auto_config = serializers.DictField(read_only=True, label=_('Auto info'))
@staticmethod
def get_auto_config(obj) -> dict:
return obj.auto_config
@staticmethod
def get_gathered_info(obj) -> dict:
return obj.gathered_info
@staticmethod
def get_spec_info(obj) -> dict:
return obj.spec_info
def get_instance(self): def get_instance(self):
request = self.context.get('request') request = self.context.get('request')
if not self.instance and UUID_PATTERN.findall(request.path): if not self.instance and UUID_PATTERN.findall(request.path):

View File

@ -59,10 +59,7 @@ class DatabaseSerializer(AssetSerializer):
if not platform: if not platform:
return return
if platform.type in [ if platform.type in ['mysql', 'mariadb']:
'mysql', 'mariadb', 'oracle', 'sqlserver',
'db2', 'dameng', 'clickhouse', 'redis'
]:
db_field.required = False db_field.required = False
db_field.allow_blank = True db_field.allow_blank = True
db_field.allow_null = True db_field.allow_null = True

View File

@ -26,13 +26,4 @@ class WebSerializer(AssetSerializer):
'submit_selector': { 'submit_selector': {
'default': 'id=login_button', 'default': 'id=login_button',
}, },
'script': {
'default': [],
}
} }
def to_internal_value(self, data):
data = data.copy()
if data.get('script') in ("", None):
data.pop('script', None)
return super().to_internal_value(data)

View File

@ -19,13 +19,11 @@ __all__ = [
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets')) assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes')) nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
executed_amount = serializers.IntegerField(read_only=True, label=_('Executed amount'))
class Meta: class Meta:
read_only_fields = [ read_only_fields = [
'date_created', 'date_updated', 'created_by', 'date_created', 'date_updated', 'created_by',
'periodic_display', 'executed_amount', 'type', 'periodic_display', 'executed_amount', 'type', 'last_execution_date'
'last_execution_date',
] ]
mini_fields = [ mini_fields = [
'id', 'name', 'type', 'is_periodic', 'interval', 'id', 'name', 'type', 'is_periodic', 'interval',

View File

@ -1,6 +1,6 @@
import os import os
import uuid import uuid
from datetime import timedelta, datetime from datetime import timedelta
from importlib import import_module from importlib import import_module
from django.conf import settings from django.conf import settings
@ -40,7 +40,7 @@ __all__ = [
class JobLog(JobExecution): class JobLog(JobExecution):
@property @property
def creator_name(self) -> str: def creator_name(self):
return self.creator.name return self.creator.name
class Meta: class Meta:
@ -232,7 +232,7 @@ class UserLoginLog(models.Model):
return '%s(%s)' % (self.username, self.city) return '%s(%s)' % (self.username, self.city)
@property @property
def backend_display(self) -> str: def backend_display(self):
return gettext(self.backend) return gettext(self.backend)
@classmethod @classmethod
@ -258,7 +258,7 @@ class UserLoginLog(models.Model):
return login_logs return login_logs
@property @property
def reason_display(self) -> str: def reason_display(self):
from authentication.errors import reason_choices, old_reason_choices from authentication.errors import reason_choices, old_reason_choices
reason = reason_choices.get(self.reason) reason = reason_choices.get(self.reason)
@ -300,15 +300,15 @@ class UserSession(models.Model):
return '%s(%s)' % (self.user, self.ip) return '%s(%s)' % (self.user, self.ip)
@property @property
def backend_display(self) -> str: def backend_display(self):
return gettext(self.backend) return gettext(self.backend)
@property @property
def is_active(self) -> bool: def is_active(self):
return user_session_manager.check_active(self.key) return user_session_manager.check_active(self.key)
@property @property
def date_expired(self) -> datetime: def date_expired(self):
session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
session_store = session_store_cls(session_key=self.key) session_store = session_store_cls(session_key=self.key)
cache_key = session_store.cache_key cache_key = session_store.cache_key

View File

@ -119,11 +119,11 @@ class OperateLogSerializer(BulkOrgResourceModelSerializer):
fields = fields_small fields = fields_small
@staticmethod @staticmethod
def get_resource_type(instance) -> str: def get_resource_type(instance):
return _(instance.resource_type) return _(instance.resource_type)
@staticmethod @staticmethod
def get_resource(instance) -> str: def get_resource(instance):
return i18n_trans(instance.resource) return i18n_trans(instance.resource)
@ -147,11 +147,11 @@ class ActivityUnionLogSerializer(serializers.Serializer):
r_type = serializers.CharField(read_only=True) r_type = serializers.CharField(read_only=True)
@staticmethod @staticmethod
def get_timestamp(obj) -> str: def get_timestamp(obj):
return as_current_tz(obj['datetime']).strftime('%Y-%m-%d %H:%M:%S') return as_current_tz(obj['datetime']).strftime('%Y-%m-%d %H:%M:%S')
@staticmethod @staticmethod
def get_content(obj) -> str: def get_content(obj):
if not obj['r_detail']: if not obj['r_detail']:
action = obj['r_action'].replace('_', ' ').capitalize() action = obj['r_action'].replace('_', ' ').capitalize()
ctn = _('%s %s this resource') % (obj['r_user'], _(action).lower()) ctn = _('%s %s this resource') % (obj['r_user'], _(action).lower())
@ -160,7 +160,7 @@ class ActivityUnionLogSerializer(serializers.Serializer):
return ctn return ctn
@staticmethod @staticmethod
def get_detail_url(obj) -> str: def get_detail_url(obj):
detail_url = '' detail_url = ''
detail_id, obj_type = obj['r_detail_id'], obj['r_type'] detail_id, obj_type = obj['r_detail_id'], obj['r_type']
if not detail_id: if not detail_id:
@ -210,7 +210,7 @@ class UserSessionSerializer(serializers.ModelSerializer):
"backend_display": {"label": _("Auth backend display")}, "backend_display": {"label": _("Auth backend display")},
} }
def get_is_current_user_session(self, obj) -> bool: def get_is_current_user_session(self, obj):
request = self.context.get('request') request = self.context.get('request')
if not request: if not request:
return False return False

View File

@ -116,7 +116,7 @@ def send_login_info_to_reviewers(instance: UserLoginLog | str, auth_acl_id):
reviewers = acl.reviewers.all() reviewers = acl.reviewers.all()
for reviewer in reviewers: for reviewer in reviewers:
UserLoginReminderMsg(reviewer, instance, acl).publish_async() UserLoginReminderMsg(reviewer, instance).publish_async()
@receiver(post_auth_success) @receiver(post_auth_success)

View File

@ -69,8 +69,6 @@ class RDPFileClientProtocolURLMixin:
'autoreconnection enabled:i': '1', 'autoreconnection enabled:i': '1',
'bookmarktype:i': '3', 'bookmarktype:i': '3',
'use redirection server name:i': '0', 'use redirection server name:i': '0',
'bitmapcachepersistenable:i': '0',
'bitmapcachesize:i': '1500',
} }
# copy from # copy from
@ -78,6 +76,7 @@ class RDPFileClientProtocolURLMixin:
rdp_low_speed_broadband_option = { rdp_low_speed_broadband_option = {
"connection type:i": 2, "connection type:i": 2,
"disable wallpaper:i": 1, "disable wallpaper:i": 1,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1, "disable full window drag:i": 1,
"disable menu anims:i": 1, "disable menu anims:i": 1,
"allow font smoothing:i": 0, "allow font smoothing:i": 0,
@ -88,6 +87,7 @@ class RDPFileClientProtocolURLMixin:
rdp_high_speed_broadband_option = { rdp_high_speed_broadband_option = {
"connection type:i": 4, "connection type:i": 4,
"disable wallpaper:i": 0, "disable wallpaper:i": 0,
"bitmapcachepersistenable:i": 1,
"disable full window drag:i": 1, "disable full window drag:i": 1,
"disable menu anims:i": 0, "disable menu anims:i": 0,
"allow font smoothing:i": 0, "allow font smoothing:i": 0,
@ -375,7 +375,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
for name in default_name_opts.keys(): for name in default_name_opts.keys():
value = preferences.get(name, default_name_opts[name]) value = preferences.get(name, default_name_opts[name])
connect_options[name] = value connect_options[name] = value
connect_options['lang'] = getattr(user, 'lang') or settings.LANGUAGE_CODE connect_options['lang'] = getattr(user, 'lang', settings.LANGUAGE_CODE)
data['connect_options'] = connect_options data['connect_options'] = connect_options
@staticmethod @staticmethod
@ -431,7 +431,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
if account.username != AliasAccount.INPUT: if account.username != AliasAccount.INPUT:
data['input_username'] = '' data['input_username'] = ''
ticket = self._validate_acl(user, asset, account, connect_method, protocol) ticket = self._validate_acl(user, asset, account, connect_method)
if ticket: if ticket:
data['from_ticket'] = ticket data['from_ticket'] = ticket
@ -470,7 +470,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
after=after, object_name=object_name after=after, object_name=object_name
) )
def _validate_acl(self, user, asset, account, connect_method, protocol): def _validate_acl(self, user, asset, account, connect_method):
from acls.models import LoginAssetACL from acls.models import LoginAssetACL
kwargs = {'user': user, 'asset': asset, 'account': account} kwargs = {'user': user, 'asset': asset, 'account': account}
if account.username == AliasAccount.INPUT: if account.username == AliasAccount.INPUT:
@ -523,15 +523,9 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
return return
self._record_operate_log(acl, asset) self._record_operate_log(acl, asset)
os = get_request_os(self.request) if self.request else 'windows'
method = ConnectMethodUtil.get_connect_method(
connect_method, protocol=protocol, os=os
)
login_from = method['label'] if method else connect_method
for reviewer in reviewers: for reviewer in reviewers:
AssetLoginReminderMsg( AssetLoginReminderMsg(
reviewer, asset, user, account, acl, reviewer, asset, user, account, self.input_username
ip, self.input_username, login_from
).publish_async() ).publish_async()
def create_face_verify(self, response): def create_face_verify(self, response):
@ -624,8 +618,6 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
token_id = request.data.get('id') or '' token_id = request.data.get('id') or ''
token = ConnectionToken.get_typed_connection_token(token_id) token = ConnectionToken.get_typed_connection_token(token_id)
if not token:
raise PermissionDenied('Token {} is not valid'.format(token))
token.is_valid() token.is_valid()
serializer = self.get_serializer(instance=token) serializer = self.get_serializer(instance=token)

View File

@ -67,9 +67,8 @@ class UserResetPasswordSendCodeApi(CreateAPIView):
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False) code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('Forgot password')) subject = '%s: %s' % (get_login_title(), _('Forgot password'))
tip = _('The validity period of the verification code is {} minute').format(settings.VERIFY_CODE_TTL // 60)
context = { context = {
'user': user, 'title': subject, 'code': code, 'tip': tip, 'user': user, 'title': subject, 'code': code,
} }
message = render_to_string('authentication/_msg_reset_password_code.html', context) message = render_to_string('authentication/_msg_reset_password_code.html', context)
content = {'subject': subject, 'message': message} content = {'subject': subject, 'message': message}

View File

@ -25,10 +25,7 @@ class JMSBaseAuthBackend:
""" """
# 三方用户认证完成后,在后续的 get_user 获取逻辑中,也应该需要检查用户是否有效 # 三方用户认证完成后,在后续的 get_user 获取逻辑中,也应该需要检查用户是否有效
is_valid = getattr(user, 'is_valid', None) is_valid = getattr(user, 'is_valid', None)
if not is_valid: return is_valid or is_valid is None
logger.info("User %s is not valid", getattr(user, "username", "<unknown>"))
return False
return True
# allow user to authenticate # allow user to authenticate
def username_allow_authenticate(self, username): def username_allow_authenticate(self, username):

View File

@ -136,7 +136,7 @@ class SignatureAuthentication(signature.SignatureAuthentication):
# example implementation: # example implementation:
try: try:
key = AccessKey.objects.get(id=key_id) key = AccessKey.objects.get(id=key_id)
if not key.is_valid: if not key.is_active:
return None, None return None, None
user, secret = key.user, str(key.secret) user, secret = key.user, str(key.secret)
after_authenticate_update_date(user, key) after_authenticate_update_date(user, key)

View File

@ -14,9 +14,7 @@ class TempTokenAuthBackend(JMSBaseAuthBackend):
return settings.AUTH_TEMP_TOKEN return settings.AUTH_TEMP_TOKEN
def authenticate(self, request, username='', password=''): def authenticate(self, request, username='', password=''):
tokens = self.model.objects.filter(username=username).order_by('-date_created')[:500] token = self.model.objects.filter(username=username, secret=password).first()
token = next((t for t in tokens if t.secret == password), None)
if not token: if not token:
return None return None
if not token.is_valid: if not token.is_valid:

View File

@ -38,7 +38,7 @@ class BaseMFA(abc.ABC):
if not ok: if not ok:
return False, msg return False, msg
cache.set(cache_key, code, settings.VERIFY_CODE_TTL) cache.set(cache_key, code, 60)
return True, msg return True, msg
def is_authenticated(self): def is_authenticated(self):

View File

@ -39,14 +39,13 @@ class MFAEmail(BaseMFA):
def send_challenge(self): def send_challenge(self):
code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False) code = random_string(settings.SMS_CODE_LENGTH, lower=False, upper=False)
subject = '%s: %s' % (get_login_title(), _('MFA code')) subject = '%s: %s' % (get_login_title(), _('MFA code'))
tip = _('The validity period of the verification code is {} minute').format(settings.VERIFY_CODE_TTL // 60)
context = { context = {
'user': self.user, 'title': subject, 'code': code, 'tip': tip, 'user': self.user, 'title': subject, 'code': code,
} }
message = render_to_string('authentication/_msg_mfa_email_code.html', context) message = render_to_string('authentication/_msg_mfa_email_code.html', context)
content = {'subject': subject, 'message': message} content = {'subject': subject, 'message': message}
sender_util = SendAndVerifyCodeUtil( sender_util = SendAndVerifyCodeUtil(
self.user.email, code=code, backend=self.name, **content self.user.email, code=code, backend=self.name, timeout=60, **content
) )
sender_util.gen_and_send_async() sender_util.gen_and_send_async()

View File

@ -23,6 +23,7 @@ def save_access_key_secrets(apps, schema_editor):
ak.secret = old_value ak.secret = old_value
ak.save(update_fields=["secret"]) ak.save(update_fields=["secret"])
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [

View File

@ -25,10 +25,6 @@ class AccessKey(models.Model):
date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used')) date_last_used = models.DateTimeField(null=True, blank=True, verbose_name=_('Date last used'))
date_created = models.DateTimeField(auto_now_add=True) date_created = models.DateTimeField(auto_now_add=True)
@property
def is_valid(self):
return self.is_active and self.user.is_valid
def get_id(self): def get_id(self):
return str(self.id) return str(self.id)

View File

@ -4,7 +4,6 @@ from datetime import timedelta
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
@ -77,10 +76,7 @@ class ConnectionToken(JMSOrgBaseModel):
@classmethod @classmethod
def get_typed_connection_token(cls, token_id): def get_typed_connection_token(cls, token_id):
try: token = get_object_or_404(cls, id=token_id)
token = get_object_or_404(cls, id=token_id)
except ValidationError:
return None
if token.type == ConnectionTokenType.ADMIN.value: if token.type == ConnectionTokenType.ADMIN.value:
token = AdminConnectionToken.objects.get(id=token_id) token = AdminConnectionToken.objects.get(id=token_id)
@ -89,11 +85,11 @@ class ConnectionToken(JMSOrgBaseModel):
return token return token
@property @property
def is_expired(self) -> bool: def is_expired(self):
return self.date_expired < timezone.now() return self.date_expired < timezone.now()
@property @property
def expire_time(self) -> int: def expire_time(self):
interval = self.date_expired - timezone.now() interval = self.date_expired - timezone.now()
seconds = interval.total_seconds() seconds = interval.total_seconds()
if seconds < 0: if seconds < 0:
@ -165,7 +161,7 @@ class ConnectionToken(JMSOrgBaseModel):
def expire_at(self): def expire_at(self):
return self.permed_account.date_expired.timestamp() return self.permed_account.date_expired.timestamp()
def is_valid(self) -> bool: def is_valid(self):
if not self.is_active: if not self.is_active:
error = _('Connection token inactive') error = _('Connection token inactive')
raise PermissionDenied(error) raise PermissionDenied(error)
@ -338,18 +334,6 @@ class ConnectionToken(JMSOrgBaseModel):
acls = CommandFilterACL.filter_queryset(**kwargs).valid() acls = CommandFilterACL.filter_queryset(**kwargs).valid()
return acls return acls
@lazyproperty
def data_masking_rules(self):
from acls.models import DataMaskingRule
kwargs = {
'user': self.user,
'asset': self.asset,
'account': self.account_object,
}
with tmp_to_org(self.asset.org_id):
rules = DataMaskingRule.filter_queryset(**kwargs).valid()
return rules
class SuperConnectionToken(ConnectionToken): class SuperConnectionToken(ConnectionToken):
_type = ConnectionTokenType.SUPER _type = ConnectionTokenType.SUPER

View File

@ -1,24 +1,14 @@
from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string
from django.utils.translation import gettext as _
from common.utils import get_logger from common.utils import get_logger
from common.utils.timezone import local_now_display from common.utils.timezone import local_now_display
from common.views.template import custom_render_to_string
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
logger = get_logger(__file__) logger = get_logger(__file__)
class DifferentCityLoginMessage(UserMessage): class DifferentCityLoginMessage(UserMessage):
subject = _('Different city login reminder')
template_name = 'authentication/_msg_different_city.html'
contexts = [
{"name": "city", "label": _('Login city'), "default": "Shanghai"},
{"name": "username", "label": _('User'), "default": "john"},
{"name": "name", "label": _('Name'), "default": "John"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "time", "label": _('Login Date'), "default": "2025-01-01 12:00:00"},
]
def __init__(self, user, ip, city): def __init__(self, user, ip, city):
self.ip = ip self.ip = ip
self.city = city self.city = city
@ -26,16 +16,18 @@ class DifferentCityLoginMessage(UserMessage):
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
now = local_now_display() now = local_now_display()
subject = _('Different city login reminder')
context = dict( context = dict(
subject=subject,
name=self.user.name, name=self.user.name,
username=self.user.username, username=self.user.username,
ip=self.ip, ip=self.ip,
time=now, time=now,
city=self.city, city=self.city,
) )
message = custom_render_to_string(self.template_name, context) message = render_to_string('authentication/_msg_different_city.html', context)
return { return {
'subject': str(self.subject), 'subject': subject,
'message': message 'message': message
} }
@ -49,16 +41,6 @@ class DifferentCityLoginMessage(UserMessage):
class OAuthBindMessage(UserMessage): class OAuthBindMessage(UserMessage):
subject = _('OAuth binding reminder')
template_name = 'authentication/_msg_oauth_bind.html'
contexts = [
{"name": "username", "label": _('User'), "default": "john"},
{"name": "name", "label": _('Name'), "default": "John"},
{"name": "ip", "label": "IP", "default": "192.168.1.1"},
{"name": "oauth_name", "label": _('OAuth name'), "default": "WeCom"},
{"name": "oauth_id", "label": _('OAuth ID'), "default": "000001"},
]
def __init__(self, user, ip, oauth_name, oauth_id): def __init__(self, user, ip, oauth_name, oauth_id):
super().__init__(user) super().__init__(user)
self.ip = ip self.ip = ip
@ -69,6 +51,7 @@ class OAuthBindMessage(UserMessage):
now = local_now_display() now = local_now_display()
subject = self.oauth_name + ' ' + _('binding reminder') subject = self.oauth_name + ' ' + _('binding reminder')
context = dict( context = dict(
subject=subject,
name=self.user.name, name=self.user.name,
username=self.user.username, username=self.user.username,
ip=self.ip, ip=self.ip,
@ -76,9 +59,9 @@ class OAuthBindMessage(UserMessage):
oauth_name=self.oauth_name, oauth_name=self.oauth_name,
oauth_id=self.oauth_id oauth_id=self.oauth_id
) )
message = custom_render_to_string(self.template_name, context) message = render_to_string('authentication/_msg_oauth_bind.html', context)
return { return {
'subject': str(subject), 'subject': subject,
'message': message 'message': message
} }

View File

@ -20,10 +20,9 @@ class UserConfirmation(permissions.BasePermission):
if not settings.SECURITY_VIEW_AUTH_NEED_MFA: if not settings.SECURITY_VIEW_AUTH_NEED_MFA:
return True return True
session = getattr(request, 'session', {}) confirm_level = request.session.get('CONFIRM_LEVEL')
confirm_level = session.get('CONFIRM_LEVEL') confirm_type = request.session.get('CONFIRM_TYPE')
confirm_type = session.get('CONFIRM_TYPE') confirm_time = request.session.get('CONFIRM_TIME')
confirm_time = session.get('CONFIRM_TIME')
ttl = self.get_ttl(confirm_type) ttl = self.get_ttl(confirm_type)
now = int(time.time()) now = int(time.time())

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
from accounts.const import SecretType from accounts.const import SecretType
from accounts.models import Account from accounts.models import Account
from acls.models import CommandGroup, CommandFilterACL, DataMaskingRule from acls.models import CommandGroup, CommandFilterACL
from assets.models import Asset, Platform, Gateway, Zone from assets.models import Asset, Platform, Gateway, Zone
from assets.serializers.asset import AssetProtocolsSerializer from assets.serializers.asset import AssetProtocolsSerializer
from assets.serializers.platform import PlatformSerializer from assets.serializers.platform import PlatformSerializer
@ -60,7 +60,7 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer):
] ]
@staticmethod @staticmethod
def get_su_from(account) -> dict: def get_su_from(account):
if not hasattr(account, 'asset'): if not hasattr(account, 'asset'):
return {} return {}
su_enabled = account.asset.platform.su_enabled su_enabled = account.asset.platform.su_enabled
@ -83,14 +83,6 @@ class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
] ]
class _ConnectionTokenDataMaskingRuleSerializer(serializers.ModelSerializer):
class Meta:
model = DataMaskingRule
fields = ['id', 'name', 'fields_pattern',
'masking_method', 'mask_pattern',
'is_active', 'priority']
class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer): class _ConnectionTokenCommandFilterACLSerializer(serializers.ModelSerializer):
command_groups = ObjectRelatedField( command_groups = ObjectRelatedField(
many=True, required=False, queryset=CommandGroup.objects, many=True, required=False, queryset=CommandGroup.objects,
@ -113,7 +105,7 @@ class _ConnectionTokenPlatformSerializer(PlatformSerializer):
class Meta(PlatformSerializer.Meta): class Meta(PlatformSerializer.Meta):
model = Platform model = Platform
fields = [field for field in PlatformSerializer.Meta.fields fields = [field for field in PlatformSerializer.Meta.fields
if field not in PlatformSerializer.Meta.fields_m2m] if field not in PlatformSerializer.Meta.fields_m2m]
def get_field_names(self, declared_fields, info): def get_field_names(self, declared_fields, info):
names = super().get_field_names(declared_fields, info) names = super().get_field_names(declared_fields, info)
@ -147,7 +139,6 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
platform = _ConnectionTokenPlatformSerializer(read_only=True) platform = _ConnectionTokenPlatformSerializer(read_only=True)
zone = ObjectRelatedField(queryset=Zone.objects, required=False, label=_('Domain')) zone = ObjectRelatedField(queryset=Zone.objects, required=False, label=_('Domain'))
command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True) command_filter_acls = _ConnectionTokenCommandFilterACLSerializer(read_only=True, many=True)
data_masking_rules = _ConnectionTokenDataMaskingRuleSerializer(read_only=True, many=True)
expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True) expire_now = serializers.BooleanField(label=_('Expired now'), write_only=True, default=True)
connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object') connect_method = _ConnectTokenConnectMethodSerializer(read_only=True, source='connect_method_object')
connect_options = serializers.JSONField(read_only=True) connect_options = serializers.JSONField(read_only=True)
@ -158,7 +149,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin):
model = ConnectionToken model = ConnectionToken
fields = [ fields = [
'id', 'value', 'user', 'asset', 'account', 'id', 'value', 'user', 'asset', 'account',
'platform', 'command_filter_acls', 'data_masking_rules', 'protocol', 'platform', 'command_filter_acls', 'protocol',
'zone', 'gateway', 'actions', 'expire_at', 'zone', 'gateway', 'actions', 'expire_at',
'from_ticket', 'expire_now', 'connect_method', 'from_ticket', 'expire_now', 'connect_method',
'connect_options', 'face_monitor_token' 'connect_options', 'face_monitor_token'

View File

@ -6,7 +6,6 @@ from common.serializers import CommonModelSerializer
from common.serializers.fields import EncryptedField from common.serializers.fields import EncryptedField
from perms.serializers.permission import ActionChoicesField from perms.serializers.permission import ActionChoicesField
from ..models import ConnectionToken, AdminConnectionToken from ..models import ConnectionToken, AdminConnectionToken
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
__all__ = [ __all__ = [
'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer', 'ConnectionTokenSerializer', 'SuperConnectionTokenSerializer',
@ -14,7 +13,7 @@ __all__ = [
] ]
class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): class ConnectionTokenSerializer(CommonModelSerializer):
expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) expire_time = serializers.IntegerField(read_only=True, label=_('Expired time'))
input_secret = EncryptedField( input_secret = EncryptedField(
label=_("Input secret"), max_length=40960, required=False, allow_blank=True label=_("Input secret"), max_length=40960, required=False, allow_blank=True
@ -61,7 +60,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
validated_data['remote_addr'] = get_request_ip(request) validated_data['remote_addr'] = get_request_ip(request)
return super().create(validated_data) return super().create(validated_data)
def get_from_ticket_info(self, instance) -> dict: def get_from_ticket_info(self, instance):
if not instance.from_ticket: if not instance.from_ticket:
return {} return {}
user = self.get_request_user() user = self.get_request_user()

View File

@ -46,7 +46,7 @@ class BearerTokenSerializer(serializers.Serializer):
user = UserProfileSerializer(read_only=True) user = UserProfileSerializer(read_only=True)
@staticmethod @staticmethod
def get_keyword(obj) -> str: def get_keyword(obj):
return 'Bearer' return 'Bearer'
@staticmethod @staticmethod

View File

@ -12,7 +12,7 @@
<td style="height: 50px;">{% trans 'MFA code' %}: <span style="font-weight: bold;">{{ code }}</span></td> <td style="height: 50px;">{% trans 'MFA code' %}: <span style="font-weight: bold;">{{ code }}</span></td>
</tr> </tr>
<tr style="border: 1px solid #eee"> <tr style="border: 1px solid #eee">
<td style="height: 30px;">{{ tip }}</td> <td style="height: 30px;">{% trans 'The validity period of the verification code is one minute' %}</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -11,6 +11,8 @@
<b>{% trans 'Time' %}:</b> {{ time }}<br> <b>{% trans 'Time' %}:</b> {{ time }}<br>
<b>{% trans 'IP' %}:</b> {{ ip }} <b>{% trans 'IP' %}:</b> {{ ip }}
</p> </p>
-
<p> <p>
{% trans 'If the operation is not your own, unbind and change the password.' %} {% trans 'If the operation is not your own, unbind and change the password.' %}
</p> </p>

View File

@ -6,12 +6,12 @@
{% trans 'Please click the link below to reset your password, if not your request, concern your account security' %} {% trans 'Please click the link below to reset your password, if not your request, concern your account security' %}
<br> <br>
<br> <br>
<a href="{{ rest_password_url }}?token={{ rest_password_token }}" class='showLink' target="_blank"> <a href="{{ rest_password_url }}?token={{ rest_password_token}}" class='showLink' target="_blank">
{% trans 'Click here reset password' %} {% trans 'Click here reset password' %}
</a> </a>
</p> </p>
<br> <br>
<p> <p>
{% trans 'This link is valid for 1 hour. After it expires' %} {% trans 'This link is valid for 1 hour. After it expires' %}
<a href="{{ forget_password_url }}?email={{ email }}">{% trans 'request new one' %}</a> <a href="{{ forget_password_url }}?email={{ user.email }}">{% trans 'request new one' %}</a>
</p> </p>

View File

@ -15,7 +15,7 @@
<td style="height: 30px;"> {% trans 'Copy the verification code to the Reset Password page to reset the password.' %} </td> <td style="height: 30px;"> {% trans 'Copy the verification code to the Reset Password page to reset the password.' %} </td>
</tr> </tr>
<tr style="border: 1px solid #eee"> <tr style="border: 1px solid #eee">
<td style="height: 30px;">{{ tip }}</td> <td style="height: 30px;">{% trans 'The validity period of the verification code is one minute' %}</td>
</tr> </tr>
</table> </table>
</div> </div>

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import datetime import datetime
import os import os
from typing import Callable from typing import Callable
from urllib.parse import urlparse, urlsplit, urlunsplit, urlencode from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.contrib.auth import BACKEND_SESSION_KEY from django.contrib.auth import BACKEND_SESSION_KEY
@ -155,18 +155,9 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
auth_name, redirect_url = auth_method['name'], auth_method['url'] auth_name, redirect_url = auth_method['name'], auth_method['url']
next_url = request.GET.get('next') or '/' next_url = request.GET.get('next') or '/'
next_url = safe_next_url(next_url, request=request) next_url = safe_next_url(next_url, request=request)
query_string = request.GET.urlencode()
redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string)
merged_qs_items = dict(request.GET.lists())
merged_qs_items.pop('next', None)
merged = {}
for k, v_list in merged_qs_items.items():
merged[k] = v_list if len(v_list) > 1 else (v_list[0] if v_list else '')
merged['next'] = next_url
query = urlencode(merged, doseq=True)
u = urlsplit(redirect_url)
redirect_url = urlunsplit((u.scheme, u.netloc, u.path, query, u.fragment))
if settings.LOGIN_REDIRECT_MSG_ENABLED: if settings.LOGIN_REDIRECT_MSG_ENABLED:
message_data = { message_data = {
'title': _('Redirecting'), 'title': _('Redirecting'),
@ -174,7 +165,7 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView):
'redirect_url': redirect_url, 'redirect_url': redirect_url,
'interval': 3, 'interval': 3,
'has_cancel': True, 'has_cancel': True,
'cancel_url': reverse('authentication:login') + f'?admin=1&{query}' 'cancel_url': reverse('authentication:login') + f'?admin=1&{query_string}'
} }
redirect_url = FlashMessageUtil.gen_message_url(message_data) redirect_url = FlashMessageUtil.gen_message_url(message_data)
return redirect_url return redirect_url

View File

@ -5,7 +5,6 @@ from contextlib import nullcontext
from itertools import chain from itertools import chain
from typing import Callable from typing import Callable
from django.conf import settings
from django.db import models from django.db import models
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from rest_framework.request import Request from rest_framework.request import Request
@ -17,7 +16,6 @@ from common.drf.filters import (
IDNotFilterBackend, NotOrRelFilterBackend, LabelFilterBackend IDNotFilterBackend, NotOrRelFilterBackend, LabelFilterBackend
) )
from common.utils import get_logger, lazyproperty from common.utils import get_logger, lazyproperty
from common.utils import is_uuid
from orgs.utils import tmp_to_org, tmp_to_root_org from orgs.utils import tmp_to_org, tmp_to_root_org
from .action import RenderToJsonMixin from .action import RenderToJsonMixin
from .serializer import SerializerMixin from .serializer import SerializerMixin
@ -97,33 +95,9 @@ class QuerySetMixin:
request: Request request: Request
get_serializer_class: Callable get_serializer_class: Callable
get_queryset: Callable get_queryset: Callable
slug_field = 'name'
def get_object(self): def get_queryset(self):
pk = self.kwargs.get(self.lookup_field) return super().get_queryset()
if not pk or is_uuid(pk) or pk.isdigit():
return super().get_object()
return self.get_queryset().get(**{self.slug_field: pk})
def limit_queryset_if_no_page(self, queryset):
if self.request.query_params.get('format') in ['csv', 'xlsx']:
return queryset
action = getattr(self, 'action', None)
if action != 'list':
return queryset
# 如果分页器有设置 limit则不限制
if self.paginator and self.paginator.get_limit(self.request):
return queryset
# 如果分页器没有设置 limit则不限制
if getattr(self, 'page_no_limit', False):
return queryset
if not settings.DEFAULT_PAGE_SIZE:
return queryset
return queryset[:settings.DEFAULT_PAGE_SIZE]
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset) queryset = super().filter_queryset(queryset)
@ -132,7 +106,6 @@ class QuerySetMixin:
if self.action == 'metadata': if self.action == 'metadata':
queryset = queryset.none() queryset = queryset.none()
queryset = self.setup_eager_loading(queryset) queryset = self.setup_eager_loading(queryset)
queryset = self.limit_queryset_if_no_page(queryset)
return queryset return queryset
def setup_eager_loading(self, queryset, is_paginated=False): def setup_eager_loading(self, queryset, is_paginated=False):

View File

@ -77,7 +77,6 @@ class Language(models.TextChoices):
es = 'es', 'Español' es = 'es', 'Español'
ru = 'ru', 'Русский' ru = 'ru', 'Русский'
ko = 'ko', '한국어' ko = 'ko', '한국어'
vi = 'vi', 'Tiếng Việt'
@classmethod @classmethod
def get_code_mapper(cls): def get_code_mapper(cls):

View File

@ -143,8 +143,7 @@ class EncryptMixin:
if value is None: if value is None:
return value return value
encryptor = Encryptor(value) plain_value = Encryptor(value).decrypt()
plain_value = encryptor.decrypt()
# 可能和Json mix所以要先解密再json # 可能和Json mix所以要先解密再json
sp = super() sp = super()

View File

@ -1,5 +1,4 @@
from contextlib import contextmanager from contextlib import contextmanager
import base64
from django.db import connections, transaction, connection from django.db import connections, transaction, connection
from django.utils.encoding import force_str from django.utils.encoding import force_str
@ -103,54 +102,6 @@ class Encryptor:
def __init__(self, value): def __init__(self, value):
self.value = force_str(value) self.value = force_str(value)
def is_encrypted_data(self):
"""
检测数据是否为加密格式
返回 True 表示是加密数据False 表示是原始数据
"""
if not self.value:
return False
# 检测 base64 编码格式 (crypto.encrypt 的输出)
try:
# 尝试不同的 base64 解码方式
# 1. 标准 base64
try:
base64.b64decode(self.value)
return True
except Exception:
pass
# 2. URL-safe base64
try:
# 添加必要的填充
missing_padding = len(self.value) % 4
if missing_padding:
padded_value = self.value + '=' * (4 - missing_padding)
else:
padded_value = self.value
base64.urlsafe_b64decode(padded_value)
return True
except Exception:
pass
except Exception:
pass
# 检测 AES GCM 格式 (固定72字符metadata)
if len(self.value) > 72:
try:
# 前72字符应该是3个24字符的base64编码
metadata = self.value[:72]
for i in range(0, 72, 24):
part = metadata[i:i+24]
base64.b64decode(part)
return True
except Exception:
pass
return False
def decrypt(self): def decrypt(self):
plain_value = crypto.decrypt(self.value) plain_value = crypto.decrypt(self.value)

View File

@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model from django.db.models import Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import empty from rest_framework.fields import ChoiceField, empty
from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField from common.db.fields import TreeChoices, JSONManyToManyField as ModelJSONManyToManyField
from common.utils import decrypt_password, is_uuid from common.utils import decrypt_password, is_uuid
@ -53,7 +53,7 @@ class EncryptedField(serializers.CharField):
return decrypt_password(value) return decrypt_password(value)
class LabeledChoiceField(serializers.ChoiceField): class LabeledChoiceField(ChoiceField):
def to_representation(self, key): def to_representation(self, key):
if key is None: if key is None:
return key return key
@ -180,122 +180,6 @@ class ObjectRelatedField(serializers.RelatedField):
except (TypeError, ValueError): except (TypeError, ValueError):
self.fail("incorrect_type", data_type=type(pk).__name__) self.fail("incorrect_type", data_type=type(pk).__name__)
def get_schema(self):
"""
drf-spectacular 提供 OpenAPI schema
"""
# 获取字段的基本信息
field_type = 'array' if self.many else 'object'
if field_type == 'array':
# 如果是多对多关系
return {
'type': 'array',
'items': self._get_openapi_item_schema(),
'description': getattr(self, 'help_text', ''),
'title': getattr(self, 'label', ''),
}
else:
# 如果是一对一关系
return {
'type': 'object',
'properties': self._get_openapi_properties_schema(),
'description': getattr(self, 'help_text', ''),
'title': getattr(self, 'label', ''),
}
def _get_openapi_item_schema(self):
"""
获取数组项的 OpenAPI schema
"""
return self._get_openapi_object_schema()
def _get_openapi_object_schema(self):
"""
获取对象的 OpenAPI schema
"""
properties = {}
# 动态分析 attrs 中的属性类型
for attr in self.attrs:
# 尝试从 queryset 的 model 中获取字段信息
field_type = self._infer_field_type(attr)
properties[attr] = {
'type': field_type,
'description': f'{attr} field'
}
return {
'type': 'object',
'properties': properties,
'required': ['id'] if 'id' in self.attrs else []
}
def _infer_field_type(self, attr_name):
"""
智能推断字段类型
"""
try:
# 如果有 queryset尝试从 model 中获取字段信息
if hasattr(self, 'queryset') and self.queryset is not None:
model = self.queryset.model
if hasattr(model, '_meta') and hasattr(model._meta, 'fields'):
field = model._meta.get_field(attr_name)
if field:
return self._map_django_field_type(field)
except Exception:
pass
# 如果没有 queryset 或无法获取字段信息,使用启发式规则
return self._heuristic_field_type(attr_name)
def _map_django_field_type(self, field):
"""
Django 字段类型映射到 OpenAPI 类型
"""
field_type = type(field).__name__
# 整数类型
if 'Integer' in field_type or 'BigInteger' in field_type or 'SmallInteger' in field_type:
return 'integer'
# 浮点数类型
elif 'Float' in field_type or 'Decimal' in field_type:
return 'number'
# 布尔类型
elif 'Boolean' in field_type:
return 'boolean'
# 日期时间类型
elif 'DateTime' in field_type or 'Date' in field_type or 'Time' in field_type:
return 'string'
# 文件类型
elif 'File' in field_type or 'Image' in field_type:
return 'string'
# 其他类型默认为字符串
else:
return 'string'
def _heuristic_field_type(self, attr_name):
"""
启发式推断字段类型
"""
# 基于属性名的启发式规则
if attr_name in ['is_active', 'enabled', 'visible'] or attr_name.startswith('is_'):
return 'boolean'
elif attr_name in ['count', 'number', 'size', 'amount']:
return 'integer'
elif attr_name in ['price', 'rate', 'percentage']:
return 'number'
else:
# 默认返回字符串类型
return 'string'
def _get_openapi_properties_schema(self):
"""
获取对象属性的 OpenAPI schema
"""
return self._get_openapi_object_schema()['properties']
class TreeChoicesField(serializers.MultipleChoiceField): class TreeChoicesField(serializers.MultipleChoiceField):
def __init__(self, choice_cls, **kwargs): def __init__(self, choice_cls, **kwargs):
@ -354,23 +238,6 @@ class BitChoicesField(TreeChoicesField):
value |= name_value_map[name] value |= name_value_map[name]
return value return value
def get_schema(self):
"""
drf-spectacular 提供 OpenAPI schema
"""
return {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'value': {'type': 'string'},
'label': {'type': 'string'}
}
},
'description': getattr(self, 'help_text', ''),
'title': getattr(self, 'label', ''),
}
def run_validation(self, data=empty): def run_validation(self, data=empty):
""" """
备注: 备注:

View File

@ -185,9 +185,3 @@ def check_migrations_file_prefix_conflict(*args, **kwargs):
print(f'{msg_left}{msg_right1}\n{msg_right2}\n') print(f'{msg_left}{msg_right1}\n{msg_right2}\n')
print('=' * 80) print('=' * 80)
@receiver(django_ready)
def clear_response_cache(sender, **kwargs):
from django.core.cache import cache
cache.delete_pattern('views.decorators.cache.cache_page:*')

View File

@ -3,14 +3,14 @@
import os import os
from azure.storage.blob import BlobServiceClient
from .base import ObjectStorage from .base import ObjectStorage
class AzureStorage(ObjectStorage): class AzureStorage(ObjectStorage):
def __init__(self, config): def __init__(self, config):
from azure.storage.blob import BlobServiceClient
self.account_name = config.get("ACCOUNT_NAME", None) self.account_name = config.get("ACCOUNT_NAME", None)
self.account_key = config.get("ACCOUNT_KEY", None) self.account_key = config.get("ACCOUNT_KEY", None)
self.container_name = config.get("CONTAINER_NAME", None) self.container_name = config.get("CONTAINER_NAME", None)

View File

@ -97,7 +97,10 @@ def send_mail_attachment_async(subject, message, recipient_list, attachment_list
for attachment in attachment_list: for attachment in attachment_list:
email.attach_file(attachment) email.attach_file(attachment)
os.remove(attachment) os.remove(attachment)
return email.send() try:
return email.send()
except Exception as e:
logger.error("Sending mail attachment error: {}".format(e))
@shared_task( @shared_task(

View File

@ -10,7 +10,7 @@ import socket
import time import time
import uuid import uuid
from collections import OrderedDict from collections import OrderedDict
from functools import wraps, cached_property from functools import wraps
from itertools import chain from itertools import chain
import html2text import html2text
@ -246,19 +246,17 @@ def dict_get_any(d, keys):
return None return None
# class lazyproperty: class lazyproperty:
# def __init__(self, func): def __init__(self, func):
# self.func = func self.func = func
# def __get__(self, instance, cls): def __get__(self, instance, cls):
# if instance is None: if instance is None:
# return self return self
# else: else:
# value = self.func(instance) value = self.func(instance)
# setattr(instance, self.func.__name__, value) setattr(instance, self.func.__name__, value)
# return value return value
lazyproperty = cached_property
def get_disk_usage(path): def get_disk_usage(path):

View File

@ -47,6 +47,7 @@ class Subscription:
self.ch = pb.ch self.ch = pb.ch
self.sub = sub self.sub = sub
self.unsubscribed = False self.unsubscribed = False
logger.info(f"Subscribed to channel: {sub}")
def _handle_msg(self, _next, error, complete): def _handle_msg(self, _next, error, complete):
""" """

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