From 8e703d306cc6797338e4eb5927f8738eedfade16 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Thu, 11 Dec 2025 16:42:10 +0800 Subject: [PATCH] feat: Add permission check for reading account secrets based on system settings (#16337) --- apps/accounts/api/account/account.py | 3 ++- apps/accounts/api/account/application.py | 5 ++++- apps/accounts/api/account/template.py | 2 ++ apps/accounts/serializers/account/account.py | 8 +++++--- apps/accounts/serializers/account/template.py | 5 +++-- apps/common/serializers/mixin.py | 18 ++++++++++++++++++ apps/i18n/lina/en.json | 3 ++- apps/i18n/lina/zh.json | 5 +++-- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings/custom.py | 3 ++- apps/settings/serializers/public.py | 1 + 11 files changed, 43 insertions(+), 11 deletions(-) diff --git a/apps/accounts/api/account/account.py b/apps/accounts/api/account/account.py index abdab7e90..189f9eb50 100644 --- a/apps/accounts/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.db import transaction from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ @@ -166,7 +167,7 @@ class AccountViewSet(OrgBulkModelViewSet): class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet): """ - 因为可能要导出所有账号,所以单独建立了一个 viewset + 因为可能要导出所有账号,所以单独建立了一个 viewset """ serializer_classes = { 'default': serializers.AccountSecretSerializer, diff --git a/apps/accounts/api/account/application.py b/apps/accounts/api/account/application.py index 7ff9e20ed..b1a7bd873 100644 --- a/apps/accounts/api/account/application.py +++ b/apps/accounts/api/account/application.py @@ -81,4 +81,7 @@ class IntegrationApplicationViewSet(OrgBulkModelViewSet): remote_addr=get_request_ip(request), service=service.name, service_id=service.id, account=f'{account.name}({account.username})', asset=f'{asset.name}({asset.address})', ) - return Response(data={'id': request.user.id, 'secret': account.secret}) + + # 根据配置决定是否返回密码 + secret = account.secret if settings.SECURITY_ACCOUNT_SECRET_READ else None + return Response(data={'id': request.user.id, 'secret': secret}) diff --git a/apps/accounts/api/account/template.py b/apps/accounts/api/account/template.py index 3aa0dd5de..655762e32 100644 --- a/apps/accounts/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -1,3 +1,5 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ from django_filters import rest_framework as drf_filters from rest_framework import status from rest_framework.decorators import action diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py index fd9facc40..d5c8daa82 100644 --- a/apps/accounts/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -14,7 +14,7 @@ from accounts.models import Account, AccountTemplate, GatheredAccount from accounts.tasks import push_accounts_to_assets_task from assets.const import Category, AllTypes from assets.models import Asset -from common.serializers import SecretReadableMixin, CommonBulkModelSerializer +from common.serializers import SecretReadableMixin, SecretReadableCheckMixin, CommonBulkModelSerializer from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from common.utils import get_logger from .base import BaseAccountSerializer, AuthValidateMixin @@ -478,7 +478,7 @@ class AssetAccountBulkSerializer( return results -class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): +class AccountSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountSerializer): spec_info = serializers.DictField(label=_('Spec info'), read_only=True) class Meta(AccountSerializer.Meta): @@ -491,9 +491,10 @@ class AccountSecretSerializer(SecretReadableMixin, AccountSerializer): exclude_backup_fields = [ 'passphrase', 'push_now', 'params', 'spec_info' ] + secret_fields = ['secret'] -class AccountHistorySerializer(serializers.ModelSerializer): +class AccountHistorySerializer(SecretReadableCheckMixin, serializers.ModelSerializer): 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) @@ -509,6 +510,7 @@ class AccountHistorySerializer(serializers.ModelSerializer): 'history_user': {'label': _('User')}, 'history_date': {'label': _('Date')}, } + secret_fields = ['secret'] class AccountTaskSerializer(serializers.Serializer): diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py index 2910eb0f8..1c6b4368f 100644 --- a/apps/accounts/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -2,7 +2,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from accounts.models import AccountTemplate -from common.serializers import SecretReadableMixin +from common.serializers import SecretReadableMixin, SecretReadableCheckMixin from common.serializers.fields import ObjectRelatedField from .base import BaseAccountSerializer @@ -62,10 +62,11 @@ class AccountDetailTemplateSerializer(AccountTemplateSerializer): fields = AccountTemplateSerializer.Meta.fields + ['spec_info'] -class AccountTemplateSecretSerializer(SecretReadableMixin, AccountDetailTemplateSerializer): +class AccountTemplateSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountDetailTemplateSerializer): class Meta(AccountDetailTemplateSerializer.Meta): fields = AccountDetailTemplateSerializer.Meta.fields extra_kwargs = { **AccountDetailTemplateSerializer.Meta.extra_kwargs, 'secret': {'write_only': False}, } + secret_fields = ['secret'] diff --git a/apps/common/serializers/mixin.py b/apps/common/serializers/mixin.py index 01eef8182..09a95fffc 100644 --- a/apps/common/serializers/mixin.py +++ b/apps/common/serializers/mixin.py @@ -30,12 +30,30 @@ __all__ = [ "CommonSerializerMixin", "CommonBulkSerializerMixin", "SecretReadableMixin", + "SecretReadableCheckMixin", "CommonModelSerializer", "CommonBulkModelSerializer", "ResourceLabelsMixin", ] +class SecretReadableCheckMixin(serializers.Serializer): + """ + 根据 SECURITY_ACCOUNT_SECRET_READ 配置控制密码字段的可读性 + 当配置为 False 时,密码字段返回 None + """ + + def to_representation(self, instance): + ret = super().to_representation(instance) + + if not settings.SECURITY_ACCOUNT_SECRET_READ: + secret_fields = getattr(self.Meta, 'secret_fields', ['secret']) + for field_name in secret_fields: + if field_name in ret: + ret[field_name] = '' + return ret + + class SecretReadableMixin(serializers.Serializer): """加密字段 (EncryptedField) 可读性""" diff --git a/apps/i18n/lina/en.json b/apps/i18n/lina/en.json index 01c1a783e..a7834956e 100644 --- a/apps/i18n/lina/en.json +++ b/apps/i18n/lina/en.json @@ -1636,7 +1636,8 @@ "setVariable": "Set variable", "userId": "User ID", "userName": "User name", + "AccountSecretReadDisabled": "Account secret reading has been disabled by administrator", "AccessToken": "Access tokens", "AccessTokenTip": "Access Token is a temporary credential generated through the OAuth2 (Authorization Code Grant) flow using the JumpServer client, which is used to access protected resources.", "Revoke": "Revoke" -} +} \ No newline at end of file diff --git a/apps/i18n/lina/zh.json b/apps/i18n/lina/zh.json index 7275c373a..df69c7087 100644 --- a/apps/i18n/lina/zh.json +++ b/apps/i18n/lina/zh.json @@ -1648,5 +1648,6 @@ "selectFiles": "已选择选择{number}文件", "AccessToken": "访问令牌", "AccessTokenTip": "访问令牌是通过 JumpServer 客户端使用 OAuth2(授权码授权)流程生成的临时凭证,用于访问受保护的资源。", - "Revoke": "撤销" -} + "Revoke": "撤销", + "AccountSecretReadDisabled": "账号密码读取功能已被管理员禁用" +} \ No newline at end of file diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index ed751eb56..887cc2d41 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -575,6 +575,7 @@ class Config(dict): ], 'SECURITY_SERVICE_ACCOUNT_REGISTRATION': 'auto', 'SECURITY_VIEW_AUTH_NEED_MFA': True, + 'SECURITY_ACCOUNT_SECRET_READ': True, 'SECURITY_MAX_IDLE_TIME': 30, 'SECURITY_MAX_SESSION_TIME': 24, 'SECURITY_PASSWORD_EXPIRATION_TIME': 9999, diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index b47fc7c31..94f96b464 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -60,6 +60,7 @@ VERIFY_CODE_TTL = CONFIG.VERIFY_CODE_TTL SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_UNCOMMON_USERS_TTL = CONFIG.SECURITY_UNCOMMON_USERS_TTL SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA +SECURITY_ACCOUNT_SECRET_READ = CONFIG.SECURITY_ACCOUNT_SECRET_READ SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED SECURITY_MFA_IN_LOGIN_PAGE = CONFIG.SECURITY_MFA_IN_LOGIN_PAGE @@ -268,4 +269,4 @@ LOKI_BASE_URL = CONFIG.LOKI_BASE_URL TOOL_USER_ENABLED = CONFIG.TOOL_USER_ENABLED SUGGESTION_LIMIT = CONFIG.SUGGESTION_LIMIT -MCP_ENABLED = CONFIG.MCP_ENABLED \ No newline at end of file +MCP_ENABLED = CONFIG.MCP_ENABLED diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index 2b85d766c..a6ae916f2 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -21,6 +21,7 @@ class PrivateSettingSerializer(PublicSettingSerializer): TICKET_AUTHORIZE_DEFAULT_TIME_UNIT = serializers.CharField() AUTH_LDAP_SYNC_ORG_IDS = serializers.ListField() SECURITY_MAX_IDLE_TIME = serializers.IntegerField() + SECURITY_ACCOUNT_SECRET_READ = serializers.BooleanField() SECURITY_VIEW_AUTH_NEED_MFA = serializers.BooleanField() SECURITY_MFA_AUTH = serializers.IntegerField() SECURITY_MFA_VERIFY_TTL = serializers.IntegerField()