29 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-admin-rest 01 execute 1
qy_lty/aiapp/serializers.py
qy_lty/aiapp/views.py
qy_lty/userapp/admin_urls.py
true
CRED-03
CRED-04
truths artifacts key_links
携 admin token 调用 GET /api/v1/admin/credential-slot/ 返回 200 + 标准壳层 + access_token 末 4 位脱敏
携 admin token 调用 PUT /api/v1/admin/credential-slot/ 写入后 DB 被全字段覆写、updated_at 自动刷新、响应中 access_token 同样脱敏
DB 不存在 pk=1 记录时人为删除场景PUT 仍能 get_or_create 成功创建并写入
无 Authorization 头返回 401DRF NotAuthenticated → middleware 兜底 success:false 标准壳层)
携普通 user token非 staff返回 403 + 标准壳层 + message='需要管理员权限'
/swagger.json 包含 /api/v1/admin/credential-slot/ 路径条目,含 GET 与 PUT 两个 method 及 access_token 脱敏掩码描述
path provides contains
qy_lty/aiapp/serializers.py CredentialSlotSerializer ModelSerializer 类 class CredentialSlotSerializer
path provides contains
qy_lty/aiapp/views.py CredentialSlotAdminView APIView 类(含 GET/PUT 两个方法 + swagger 装饰器) class CredentialSlotAdminView
path provides contains
qy_lty/userapp/admin_urls.py /api/v1/admin/credential-slot/ URL 注册 admin_credential_slot
from to via pattern
qy_lty/userapp/admin_urls.py qy_lty/aiapp/views.py:CredentialSlotAdminView from aiapp.views import CredentialSlotAdminView credential-slot
from to via pattern
qy_lty/aiapp/views.py:CredentialSlotAdminView qy_lty/aiapp/models.py:CredentialSlot.get_solo() instance = CredentialSlot.get_solo() CredentialSlot.get_solo
from to via pattern
qy_lty/aiapp/views.py:CredentialSlotAdminView qy_lty/common/utils.py:mask_token data['access_token'] = mask_token(instance.access_token) mask_token(instance.access_token
from to via pattern
qy_lty/aiapp/views.py:CredentialSlotAdminView (is_staff 校验) PUT/GET 早返回 403 if not request.user.is_staff: return error_response(... code=403 ...) is_staff
本 plan 在 `/api/v1/admin/credential-slot/` 暴露 GET脱敏+ PUT全字段覆写两个端点覆盖 CRED-03 + CRED-04。

Purpose

  • CRED-03管理后台读取脱敏后的凭据槽位仅末 4 位明文,避免运营在 admin UI 看到完整明文)
  • CRED-04管理后台以全字段覆写方式更新凭据槽位PUT 响应同样走脱敏避免明文回显

Output

  • 新增 CredentialSlotSerializerModelSerializer3 字段 + read_only_fields + extra_kwargs allow_blank
  • 新增 CredentialSlotAdminView(自定义 APIView + 手写 GET/PUT1:1 复刻 RTCChatHistoryAPIView 风格)
  • userapp/admin_urls.py 追加 path('credential-slot/', ...) 注册
  • View 方法挂 @swagger_auto_schema 装饰器,响应 schema 显式标注 access_token 末 4 位脱敏掩码语义

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/REQUIREMENTS.md @.planning/phases/02-admin-rest/02-CONTEXT.md @.planning/phases/02-admin-rest/02-RESEARCH.md

Phase 1 落地决策(为何模型走 pk=1 + get_solo + mask_token

@.planning/phases/01-credential-data-layer/01-01-SUMMARY.md @.planning/phases/01-credential-data-layer/01-02-SUMMARY.md

1:1 复刻样板(必读)

@qy_lty/aiapp/views.py @qy_lty/aiapp/serializers.py @qy_lty/aiapp/models.py @qy_lty/aiapp/urls.py @qy_lty/userapp/views.py @qy_lty/userapp/admin_urls.py @qy_lty/userapp/authentication.py @qy_lty/userapp/utils.py @qy_lty/common/responses.py @qy_lty/common/middleware.py @qy_lty/common/swagger_utils.py @qy_lty/common/utils.py @qy_lty/qy_lty/urls.py

【已存在 — 复用】Model: qy_lty/aiapp/models.py

class CredentialSlot(models.Model):
    app_id = models.CharField(max_length=128, blank=True, default='')
    access_token = models.CharField(max_length=512, blank=True, default='')
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        # pk=1 单例钩子:已有记录时把 pk 重定向DB 永远只有一行
        ...

    @classmethod
    def get_solo(cls):
        instance, _created = cls.objects.get_or_create(pk=1)
        return instance

【已存在 — 复用】Auth: qy_lty/userapp/authentication.py

class RedisTokenAuthentication(BaseAuthentication):
    # 读取 Authorization: Bearer <token>,调 get_user_id_from_token
    # 优先查 admin_token:{token},否则 token:{token};都查不到 → AuthenticationFailed (401)
    # 命中 → 返回 (ParadiseUser, None)
    def authenticate(self, request): ...

注意:本类不区分 admin / user token。区分点是 request.user.is_staff(只有走 AdminEmailLoginView 的用户才会是 staff

【已存在 — 复用】Helpers: qy_lty/common/responses.py

def success_response(data=None, message="操作成功", code=200, **kwargs) -> Response  # 200
def error_response(message="操作失败", code=400, status_code=400, **kwargs) -> Response

两者已构造壳层四字段success / code / message / data与 StandardResponseMiddleware 协同middleware.py:53-55 检查 success+code 二者皆在则不二次包装)。

【已存在 — 复用】Tool: qy_lty/common/utils.py

def mask_token(token: str, visible_tail: int = 4, mask_char: str = '*') -> str:
    # 'sk-abcdef1234' -> '*********1234'
    # ''  -> ''
    # 'abc' -> '***'  # 短于 visible_tail 时全脱敏

【已存在 — 复用】Swagger: qy_lty/common/swagger_utils.py

def get_standardized_response_schema(data_schema=None) -> openapi.Schema
# 返回 OpenAPI Schematype=OBJECTproperties=success/code/message + 可选 data

【新增 — 本 plan 落地】Serializer: qy_lty/aiapp/serializers.py

class CredentialSlotSerializer(serializers.ModelSerializer):
    class Meta:
        model = CredentialSlot
        fields = ['app_id', 'access_token', 'updated_at']
        read_only_fields = ['updated_at']
        extra_kwargs = {
            'app_id':       {'allow_blank': True, 'allow_null': False, 'required': False},
            'access_token': {'allow_blank': True, 'allow_null': False, 'required': False},
        }

【新增 — 本 plan 落地】View: qy_lty/aiapp/views.py 末尾

class CredentialSlotAdminView(APIView):
    authentication_classes = [RedisTokenAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request): ...   # 返回脱敏 data
    def put(self, request): ...   # 全字段覆写 + 脱敏响应
Task 1新增 CredentialSlotSerializeraiapp/serializers.py qy_lty/aiapp/serializers.py - 必读 qy_lty/aiapp/serializers.py 全文(仅 9 行;现有 ChatMessageSerializer 是同款 ModelSerializer 模板) - 必读 qy_lty/aiapp/models.py 中 CredentialSlot 模型定义(验证字段名 app_id / access_token / updated_at - 必读 02-RESEARCH.md `Pattern 2DRF ModelSerializer 写法` 段落(含完整骨架) 在 `qy_lty/aiapp/serializers.py` 顶部 import 行追加 `CredentialSlot`,文件末尾追加 `CredentialSlotSerializer` 类。

完整修改后文件内容(直接覆写)

from rest_framework import serializers
from .models import ChatMessage, CredentialSlot

class ChatMessageSerializer(serializers.ModelSerializer):
    class Meta:
        model = ChatMessage
        fields = ['id', 'user', 'bot', 'message', 'timestamp', 'sender', 'message_type', 'message_audio_url', 'message_video_url']
        read_only_fields = ['id', 'timestamp', 'sender']


class CredentialSlotSerializer(serializers.ModelSerializer):
    """通用凭据槽位序列化器(明文存储,脱敏由 view 层完成)。

    设计动机per CONTEXT.md D-Serializer
    - 脱敏放 view 层不放 serializerPUT 路径需要明文走 is_valid + saveserializer
      不应承担"既要明文又要脱敏"的双重责任。
    - app_id / access_token 在模型层 blank=True, default='',对应 serializer 配
      allow_blank=True, allow_null=False, required=False既允许空字符串覆写、又
      拒绝 None缺字段时由 ModelSerializer 默认行为(用现有值兜底)。
    - updated_at 由模型层 auto_now=True 自动维护read_only 双重保险。
    """

    class Meta:
        model = CredentialSlot
        fields = ['app_id', 'access_token', 'updated_at']
        read_only_fields = ['updated_at']
        extra_kwargs = {
            'app_id':       {'allow_blank': True, 'allow_null': False, 'required': False},
            'access_token': {'allow_blank': True, 'allow_null': False, 'required': False},
        }

注意

  • 不要发明 CredentialSlotReadSerializer / CredentialSlotWriteSerializer 拆分两个类CONTEXT.md / RESEARCH.md "Alternatives Considered" 已锁定单一 serializer + view 层脱敏)
  • 不要在 serializer 内写 to_representation 覆写做脱敏(同上锁定)
  • 不要给 app_id / access_tokenallow_null=True(与模型层 blank=True/default='' 不一致) cd qy_lty && python -c "from aiapp.serializers import CredentialSlotSerializer; from aiapp.models import CredentialSlot; s = CredentialSlotSerializer(CredentialSlot.get_solo()); assert set(s.data.keys()) == {'app_id', 'access_token', 'updated_at'}, s.data.keys(); print('OK fields=', list(s.data.keys()))" <acceptance_criteria>
    • python -c "from aiapp.serializers import CredentialSlotSerializer" 无 ImportError
    • serializer 实例化已有的 pk=1 记录后 .data 三键齐全:app_id / access_token / updated_at
    • serializer.fields['updated_at'].read_only == True
    • serializer.fields['app_id'].allow_blank == True 且 allow_null == False
    • serializer.fields['access_token'].allow_blank == True 且 allow_null == False
    • 文件不再含其它 serializer 类(仅追加 CredentialSlotSerializer </acceptance_criteria> aiapp/serializers.py 含 CredentialSlotSerializer 类,三字段对齐模型,updated_at read-onlyGET 用它返回 .data 含明文view 层稍后脱敏PUT 用它做 is_valid + save。
Task 2新增 CredentialSlotAdminViewaiapp/views.py 末尾追加) qy_lty/aiapp/views.py - 必读 qy_lty/aiapp/views.py:1-18顶部 import 区,确认现有 import 结构) - 必读 qy_lty/aiapp/views.py:434-555RTCChatHistoryAPIView 完整段;本 view 1:1 复刻其单 URL 多方法骨架) - 必读 qy_lty/userapp/views.py:705-823AdminEmailLoginView + AdminLogoutViewadmin-only 二次校验 `is_staff` 模板line 748-754 - 必读 qy_lty/userapp/views.py:722-730method-level @swagger_auto_schema 简洁样板) - 必读 02-RESEARCH.md `Pattern 1` + `Pattern 4` + `Pitfall 1/2/3/5` - 必读 qy_lty/common/swagger_utils.py 全文(确认 `get_standardized_response_schema` 签名) - 必读 qy_lty/common/responses.py 全文(确认 `success_response` / `error_response` 签名) **Step 1在 `qy_lty/aiapp/views.py` 顶部 import 区追加 / 修改三处**

修改第 5 行(追加 CredentialSlot

# 修改前:
from .models import ChatMessage, Bot
# 修改后:
from .models import ChatMessage, Bot, CredentialSlot

修改第 7 行(追加 CredentialSlotSerializer

# 修改前:
from .serializers import ChatMessageSerializer
# 修改后:
from .serializers import ChatMessageSerializer, CredentialSlotSerializer

在第 13 行(from drf_yasg.utils import swagger_auto_schema 已存在)之后追加两行(如果文件中已存在则跳过该行;只追加缺失的):

from common.utils import mask_token
from common.swagger_utils import get_standardized_response_schema

Step 2qy_lty/aiapp/views.py 文件末尾(紧跟 RTCChatHistoryAPIView.delete 之后)追加完整代码块



# ======================================================================
# Phase 2 — 通用凭据槽位管理端读写接口CRED-03 + CRED-04
# 1:1 复刻 RTCChatHistoryAPIView 的单 URL 多方法 APIView 风格
# ======================================================================

class CredentialSlotPutRequestSchema(serializers.Serializer):
    """drf-yasg 专用 — PUT 请求体 schema不参与实际写入校验仅给 swagger 看)。

    实际写入校验由 CredentialSlotSerializer.is_valid 执行。
    """
    app_id = serializers.CharField(
        required=False, allow_blank=True,
        help_text="第三方服务商分配的 APP ID明文写入缺省时保留原值"
    )
    access_token = serializers.CharField(
        required=False, allow_blank=True,
        help_text="第三方服务商访问令牌(明文写入;响应阶段会脱敏返回末 4 位)"
    )


# 响应 data 子 schemaaccess_token 字段 description 显式标注脱敏掩码语义
_credential_slot_data_schema = openapi.Schema(
    type=openapi.TYPE_OBJECT,
    properties={
        'app_id': openapi.Schema(
            type=openapi.TYPE_STRING,
            description='第三方服务商分配的 APP ID明文',
        ),
        'access_token': openapi.Schema(
            type=openapi.TYPE_STRING,
            description='Access Token 末 4 位脱敏掩码(如 "*********1234",前缀字符数 = 原长 - 4',
        ),
        'updated_at': openapi.Schema(
            type=openapi.TYPE_STRING,
            format='date-time',
            description='最近一次更新时间ISO 8601',
        ),
    },
)


class CredentialSlotAdminView(APIView):
    """通用凭据槽位管理端读写接口admin token 鉴权)。

    GET: 返回 access_token 末 4 位脱敏后的凭据槽位
    PUT: 全字段覆写凭据槽位(空记录场景自动 get_or_create响应同样脱敏
    """
    authentication_classes = [RedisTokenAuthentication]
    permission_classes = [IsAuthenticated]
    tags = ['通用凭据槽位(管理端)']

    def _ensure_admin(self, request):
        """admin-only 二次校验:拒绝非 staff 用户(含普通 user token 持有者)。

        per RESEARCH.md仓库零处 IsAdminTokenAuthenticated permission 类;
        现有 AdminEmailLoginView (userapp/views.py:748-754) / AdminLogoutView 一律走
        视图内 is_staff 检查。统一沿用此模式,不发明新 permission 类。
        """
        if not request.user.is_staff:
            logger.warning(
                f"Non-admin user attempted CredentialSlot admin endpoint: user_id={request.user.id}"
            )
            return error_response(
                message="需要管理员权限",
                code=403,
                status_code=status.HTTP_403_FORBIDDEN,
            )
        return None

    def _build_response_data(self, instance):
        """构造脱敏后的响应 data 字典。

        per CONTEXT.mdGET 与 PUT 响应都必须脱敏 access_token避免运营在
        admin UI 看到自己刚提交的明文回显CONTEXT.md 决策"PUT 响应也走脱敏")。
        """
        serializer = CredentialSlotSerializer(instance)
        data = dict(serializer.data)
        # 关键脱敏点:用 instance.access_token明文走 mask_token覆盖 serializer.data 里的明文
        data['access_token'] = mask_token(instance.access_token)
        return data

    @swagger_auto_schema(
        operation_description="读取通用凭据槽位access_token 末 4 位脱敏返回admin token 鉴权)",
        responses={
            200: openapi.Response('读取成功', get_standardized_response_schema(_credential_slot_data_schema)),
            401: openapi.Response('未提供有效 token', get_standardized_response_schema()),
            403: openapi.Response('需要管理员权限', get_standardized_response_schema()),
        },
        security=[{'Bearer': []}],
        tags=['通用凭据槽位(管理端)'],
    )
    def get(self, request):
        forbidden = self._ensure_admin(request)
        if forbidden:
            return forbidden
        instance = CredentialSlot.get_solo()
        data = self._build_response_data(instance)
        return success_response(data=data, message="读取成功")

    @swagger_auto_schema(
        request_body=CredentialSlotPutRequestSchema,
        operation_description="全字段覆写通用凭据槽位admin token 鉴权;写入后响应脱敏返回)",
        responses={
            200: openapi.Response('更新成功', get_standardized_response_schema(_credential_slot_data_schema)),
            400: openapi.Response('参数无效', get_standardized_response_schema()),
            401: openapi.Response('未提供有效 token', get_standardized_response_schema()),
            403: openapi.Response('需要管理员权限', get_standardized_response_schema()),
        },
        security=[{'Bearer': []}],
        tags=['通用凭据槽位(管理端)'],
    )
    def put(self, request):
        forbidden = self._ensure_admin(request)
        if forbidden:
            return forbidden
        instance = CredentialSlot.get_solo()  # 空记录场景自动 get_or_create
        serializer = CredentialSlotSerializer(instance, data=request.data)
        if not serializer.is_valid():
            return error_response(
                message="参数无效",
                code=400,
                status_code=status.HTTP_400_BAD_REQUEST,
                data=serializer.errors,
            )
        serializer.save()  # auto_now 自动刷 updated_at
        # 重新读取 instance.access_tokenserializer.save() 后 instance 已被同步刷新;
        # _build_response_data 内部会再次 dict(serializer.data) 拿最新 OrderedDict。
        data = self._build_response_data(instance)
        return success_response(data=data, message="凭据已更新")

关键约束(不要违反)

  • 不要把 view 写成 RetrieveUpdateAPIView 子类仓库零先例per RESEARCH.md "Alternatives Considered"
  • 不要直接 return Response({...}, status=200);一律走 success_response / error_response(避免 middleware 二次包装的不确定行为per Pitfall 2
  • 不要在 PUT 路径忘记脱敏:return success_response(data=serializer.data) 直接返回是 BUG —— serializer.data 含明文 access_tokenper Pitfall 3
  • 不要新增 IsAdminTokenAuthenticated permission 类仓库零先例与现有约定相悖per RESEARCH.md "Anti-Patterns"
  • 不要把 _ensure_admin 改成 permission_classes = [IsAdminUser]DRF 自带 IsAdminUser 也是 is_staff但与本仓库"视图内手写 is_staff"统一约定不一致) cd qy_lty && python -c "from aiapp.views import CredentialSlotAdminView; v = CredentialSlotAdminView(); assert hasattr(v, 'get') and hasattr(v, 'put'); from userapp.authentication import RedisTokenAuthentication; assert RedisTokenAuthentication in CredentialSlotAdminView.authentication_classes; print('OK view loaded with auth')" <acceptance_criteria>
    • python -c "from aiapp.views import CredentialSlotAdminView" 无 ImportError
    • View 类拥有 get / put / _ensure_admin / _build_response_data 方法
    • CredentialSlotAdminView.authentication_classesRedisTokenAuthentication
    • CredentialSlotAdminView.permission_classesIsAuthenticated(不是 IsAdminUser、不是自定义 admin permission
    • grep qy_lty/aiapp/views.pymask_token(instance.access_token) 至少一次(脱敏调用点)
    • grep qy_lty/aiapp/views.pyif not request.user.is_staff 至少一次admin 二次校验)
    • grep qy_lty/aiapp/views.py 不含 RetrieveUpdateAPIView(不准走 DRF 通用 view
    • grep qy_lty/aiapp/views.py 不含 IsAdminTokenAuthenticated(不准发明新 permission 类)
    • swagger 装饰器在 GET 与 PUT 各挂一份grep @swagger_auto_schema 出现次数比改动前多 2 </acceptance_criteria> aiapp/views.py 末尾追加 CredentialSlotAdminView 类(含 GET/PUT 两方法 + admin 二次校验 + 脱敏 helper顶部 import 已对齐view 可被 import 不报错。
Task 3注册 URLuserapp/admin_urls.py 追加 path qy_lty/userapp/admin_urls.py - 必读 qy_lty/userapp/admin_urls.py 全文11 行;现有 login/logout 两条 path - 必读 qy_lty/qy_lty/urls.py:59`path('v1/admin/', include('userapp.admin_urls'))` — 确认 prefix 拼接:`/api/v1/admin/credential-slot/` - 必读 02-RESEARCH.md `Pitfall 4`admin_urls.py 漏注册导致 404 **完整修改后 `qy_lty/userapp/admin_urls.py` 内容(直接覆写)**
from django.urls import path
from .views import AdminEmailLoginView, AdminLogoutView
# Phase 2 — 通用凭据槽位管理端读写接口CRED-03 + CRED-04
from aiapp.views import CredentialSlotAdminView

# 管理员专用API路径
urlpatterns = [
    # 管理员登录
    path('login/', AdminEmailLoginView.as_view(), name='admin_login'),
    # 管理员登出
    path('logout/', AdminLogoutView.as_view(), name='admin_logout'),
    # 通用凭据槽位GET 脱敏读取 / PUT 全字段覆写admin token 鉴权)
    path('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot'),
    # 后续可以添加更多管理员专用接口
]

关键约束

  • import 必须从 aiapp.views 引入(凭据槽位 view 落地于 aiapp不是 userapp
  • 路径必须是 'credential-slot/'trailing slash + 中划线,对齐 CONTEXT.md "trailing slash 沿用 Django 默认风格"
  • name 必须是 'admin_credential_slot'reverse 时使用)
  • 不要在 aiapp/urls.py 重复注册CONTEXT.md 锁定路由汇总点是 userapp/admin_urls.py cd qy_lty && python -c "from django.urls import reverse; from django.conf import settings; import django; django.setup() if not settings.configured else None; from django.urls import get_resolver; r = get_resolver(); url = reverse('admin_credential_slot'); assert url == '/api/v1/admin/credential-slot/', f'got {url}'; print('OK url=', url)" 2>&1 || (cd qy_lty && DJANGO_SETTINGS_MODULE=qy_lty.settings python -c "import django; django.setup(); from django.urls import reverse; print('url=', reverse('admin_credential_slot'))") <acceptance_criteria>
    • python manage.py check 无 URL 注册类报错
    • reverse('admin_credential_slot') 返回 /api/v1/admin/credential-slot/
    • qy_lty/userapp/admin_urls.pyfrom aiapp.views import CredentialSlotAdminView
    • qy_lty/userapp/admin_urls.py 不含将凭据槽位重复注册到 aiapp/urls.py 的逻辑
    • qy_lty/aiapp/urls.py新增 credential-slot 注册(汇总点单一) </acceptance_criteria> /api/v1/admin/credential-slot/ URL 可被 reverse 解析到 CredentialSlotAdminViewDjango check 通过;与 login/logout 在同一 admin namespace 注册块内。
本 plan 三个 task 完成后,做一轮 plan 内自验(不替代 02-02-PLAN 的端到端 verify
  1. import 链路完整python -c "from aiapp.views import CredentialSlotAdminView; from aiapp.serializers import CredentialSlotSerializer" 同行无 ImportError
  2. URL 路由生效python manage.py shell -c "from django.urls import reverse; print(reverse('admin_credential_slot'))" 输出 /api/v1/admin/credential-slot/
  3. Django check 通过python manage.py check 无 ERRORS / WARNINGSW 级 noise 可忽略)
  4. Swagger schema 暴露(运行时验证留 02-02:本 plan 不启动 daphne但通过 import 检查保证 @swagger_auto_schema 装饰器无语法错误

reachability self-checkgoal-backward

  • truth #1GET 脱敏 200→ artifact: views.py CredentialSlotAdminView.get + serializers.py CredentialSlotSerializer + admin_urls.py path → reachable ✓
  • truth #2PUT 全字段覆写 + updated_at 刷新 + 响应脱敏)→ artifact: views.py CredentialSlotAdminView.putserializer.save + _build_response_data→ reachable ✓
  • truth #3PUT 在空记录场景 get_or_create→ artifact: CredentialSlot.get_solo()Phase 1 落地,本 plan 调用)→ reachable ✓
  • truth #4无 token → 401→ artifact: RedisTokenAuthenticationPhase 0 已在)+ DRF NotAuthenticated → reachable ✓
  • truth #5user token → 403→ artifact: views.py _ensure_admin is_staff 校验 → reachable ✓
  • truth #6swagger 路径条目 + access_token 脱敏 description→ artifact: views.py @swagger_auto_schema + _credential_slot_data_schema description → reachable ✓

<threat_model>

Trust Boundaries

Boundary Description
公网 → /api/v1/admin/credential-slot/ 来自 qy-lty-admin Web UI 的 HTTP 请求untrusted Authorization headeruntrusted PUT body
view → DBCredentialSlot 表) 内部信任边界;写入前必须经过 serializer.is_valid + admin 校验
view → 响应回 admin 端 出站脱敏边界access_token 必须经 mask_token 才能离开后端

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02-01 Spoofing Authorization header mitigate RedisTokenAuthentication 走 Redis admin_token:{token} key 验证TTL 30 天token 验证失败抛 AuthenticationFailed → 401
T-02-02 Elevation of Privilege user token 持有者调 admin 端点 mitigate view 内 _ensure_admin 早返回 403CONTEXT.md success #5 的核心防线
T-02-03 Information Disclosure GET 响应明文 access_token mitigate _build_response_data 强制走 mask_token(instance.access_token)serializer 不在 to_representation 内做脱敏避免责任分散
T-02-04 Information Disclosure PUT 响应回显明文 access_token mitigate PUT 路径 save() 后同样调 _build_response_data;不直接 return success_response(data=serializer.data) — 该写法会回显明文per Pitfall 3
T-02-05 Tampering mass assignment未声明字段 mitigate DRF ModelSerializer 默认拒绝未在 Meta.fields 声明的字段fields=['app_id', 'access_token', 'updated_at'] 三字段封死updated_at read_only 双重保险
T-02-06 Tampering PUT 重放 / 幂等性 accept 全字段覆写本身幂等(重放结果一致);不需要 ETag / If-Match
T-02-07 Denial of Service 暴力 PUT 消耗 DB accept 现有架构无限流PROJECT.md candidate priorities #2 跟踪);本 phase 不引入新依赖deferred ideas 已明确
T-02-08 Information Disclosure access_token 写入 access log请求体 / 响应体) accept本 phase+ transferPhase 3 本 phase 仅做响应脱敏;阿里云 access log 链路过滤由 Phase 3 落地CRED-06Phase 3 完成后此项关闭
</threat_model>

<success_criteria> 本 plan 落地完成的标志plan 内自验):

  • qy_lty/aiapp/serializers.py 顶部 from .models import ChatMessage, CredentialSlot,文件内含 class CredentialSlotSerializer(serializers.ModelSerializer):
  • qy_lty/aiapp/views.py 顶部 import 区含 CredentialSlot / CredentialSlotSerializer / mask_token / get_standardized_response_schema
  • qy_lty/aiapp/views.py 文件末尾含 class CredentialSlotAdminView(APIView):,含 GET/PUT 两个方法及各自 @swagger_auto_schema 装饰器
  • qy_lty/userapp/admin_urls.pypath('credential-slot/', CredentialSlotAdminView.as_view(), name='admin_credential_slot')
  • python manage.py check 通过
  • reverse('admin_credential_slot') 解析到 /api/v1/admin/credential-slot/
  • grep aiapp/views.pymask_token(instance.access_token) ≥ 1 次(脱敏调用点)
  • grep aiapp/views.pyif not request.user.is_staff ≥ 1 次admin 二次校验)
  • grep aiapp/views.py 不含 RetrieveUpdateAPIView(确认未走通用 view </success_criteria>
完成后创建 `.planning/phases/02-admin-rest/02-01-SUMMARY.md`,记录: - 改动文件清单aiapp/serializers.py / aiapp/views.py / userapp/admin_urls.py - 实际落地的 view 类全名 / 路由 path / 调用 mask_token 的位置 - 任何与本 PLAN 不一致的偏离(应为零;如有偏离说明原因) - 留给 02-02 的端到端 verify hookcurl 命令模板 + Django shell 程序化验收脚本片段)