Explorar o código

S-cfc-230: Implement teacher.data.isolation (SC-T-13) for family-list data isolation

Sisyphus hai 3 horas
pai
achega
b5df051275

+ 22 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java

@@ -7,6 +7,7 @@ import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
 import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.PlanActivationService;
 import com.etotem.cfc.service.PlanActivationService;
+import com.etotem.cfc.service.GuideFamilyAccessGuard;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
@@ -40,6 +41,10 @@ public class PlanReviewController {
     @Resource
     @Resource
     private com.etotem.cfc.service.PlannerCertificationGuard plannerCertificationGuard;
     private com.etotem.cfc.service.PlannerCertificationGuard plannerCertificationGuard;
 
 
+    /** 规划师-家庭绑定统一授权判定点(全项目唯一,SC-T-13 数据隔离) */
+    @Resource
+    private GuideFamilyAccessGuard guideFamilyAccessGuard;
+
     @Operation(summary = "查看家庭的方案列表")
     @Operation(summary = "查看家庭的方案列表")
     @PostMapping("/family-list")
     @PostMapping("/family-list")
     public Result<List<TaskPlanInstance>> listFamilyPlans(@RequestBody Map<String, Object> body,
     public Result<List<TaskPlanInstance>> listFamilyPlans(@RequestBody Map<String, Object> body,
@@ -62,6 +67,23 @@ public class PlanReviewController {
                 return Result.noFamily("请先创建或加入家庭");
                 return Result.noFamily("请先创建或加入家庭");
             }
             }
         }
         }
+        // SC-T-13 数据隔离(teacher.data.isolation):teacher 仅可访问其绑定家庭
+        // (guide_families status='binding' isBound 校验,统一授权判定点 GuideFamilyAccessGuard)。
+        // 非绑定 familyId(含 body.familyId 越权目标)在触达方案查询层前 403 拒绝,防止 IDOR 跨家庭泄漏。
+        if (!"admin".equals(role)) {
+            Result<Void> denied;
+            if (guideFamilyAccessGuard != null) {
+                denied = guideFamilyAccessGuard.checkBinding(userId, filterFamilyId);
+            } else {
+                // 守卫不可用(如无 Spring 上下文的契约单测夹具)→ 失败关闭:收敛到 JWT 家庭,
+                // 请求家庭 ≠ JWT 家庭视为越权拒绝(与 isBound 对越权目标的判定一致,仅测试环境触发)
+                denied = filterFamilyId != null && filterFamilyId.equals(jwtFamilyId)
+                        ? null : Result.error(403, "无权访问该家庭数据");
+            }
+            if (denied != null) {
+                return Result.error(denied.getCode(), denied.getMessage());
+            }
+        }
         LambdaQueryWrapper<TaskPlanInstance> w = new LambdaQueryWrapper<TaskPlanInstance>()
         LambdaQueryWrapper<TaskPlanInstance> w = new LambdaQueryWrapper<TaskPlanInstance>()
                 .orderByDesc(TaskPlanInstance::getCreatedAt);
                 .orderByDesc(TaskPlanInstance::getCreatedAt);
         if (filterFamilyId != null) {
         if (filterFamilyId != null) {