|
@@ -372,6 +372,9 @@ MySQL (InnoDB) + Redis Cache
|
|
|
| TC-0114 | POST /api/perm/list | 默认分页 | `{"productCode":"p1"}` | page=1, pageSize=20 | 分支覆盖 | P1 | NormalizePage |
|
|
| TC-0114 | POST /api/perm/list | 默认分页 | `{"productCode":"p1"}` | page=1, pageSize=20 | 分支覆盖 | P1 | NormalizePage |
|
|
|
| TC-0115 | POST /api/perm/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
|
|
| TC-0115 | POST /api/perm/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
|
|
|
| TC-0116 | POST /api/perm/list | 不存在的productCode | `{"productCode":"notexist"}` | total=0, list=[] | 边界 | P1 | 空结果 |
|
|
| TC-0116 | POST /api/perm/list | 不存在的productCode | `{"productCode":"notexist"}` | total=0, list=[] | 边界 | P1 | 空结果 |
|
|
|
|
|
+| TC-1311 | POST /api/perm/list | 非超管不传productCode时从JWT获取 | `AdminCtx(pc)` + `{}` | 从JWT取productCode,返回本产品权限数据 | 安全 | P0 | 非超管productCode统一从JWT获取 |
|
|
|
|
|
+| TC-1312 | POST /api/perm/list | 非超管传了其他产品code时被忽略 | `AdminCtx(pc)` + `{"productCode":"other"}` | 忽略req.ProductCode,仍返回JWT产品数据 | 安全 | P0 | 非超管req.ProductCode被忽略 |
|
|
|
|
|
+| TC-1313 | POST /api/perm/list | 超管不传productCode时返回全量数据 | `SuperAdminCtx` + `{}` | 返回跨产品全量权限数据 | 正常路径 | P0 | 超管不传productCode走FindListByPage |
|
|
|
|
|
|
|
|
### 2.11 角色管理
|
|
### 2.11 角色管理
|
|
|
|
|
|
|
@@ -384,6 +387,9 @@ MySQL (InnoDB) + Redis Cache
|
|
|
| TC-0121 | POST /api/role/update | 不存在 | `{"id":9999,...}` | code=404 | 异常路径 | P0 | FindOne失败 |
|
|
| TC-0121 | POST /api/role/update | 不存在 | `{"id":9999,...}` | code=404 | 异常路径 | P0 | FindOne失败 |
|
|
|
| TC-0122 | POST /api/role/list | 正常查询 | `{"productCode":"p1","page":1,"pageSize":10}` | code=0 | 正常路径 | P0 | roleListLogic |
|
|
| TC-0122 | POST /api/role/list | 正常查询 | `{"productCode":"p1","page":1,"pageSize":10}` | code=0 | 正常路径 | P0 | roleListLogic |
|
|
|
| TC-0123 | POST /api/role/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
|
|
| TC-0123 | POST /api/role/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
|
|
|
|
|
+| TC-1308 | POST /api/role/list | 非超管不传productCode时从JWT获取 | `AdminCtx(pc)` + `{}` | 从JWT取productCode,返回本产品角色数据 | 安全 | P0 | 非超管productCode统一从JWT获取 |
|
|
|
|
|
+| TC-1309 | POST /api/role/list | 非超管传了其他产品code时被忽略 | `AdminCtx(pc)` + `{"productCode":"other"}` | 忽略req.ProductCode,仍返回JWT产品数据 | 安全 | P0 | 非超管req.ProductCode被忽略 |
|
|
|
|
|
+| TC-1310 | POST /api/role/list | 超管不传productCode时返回全量数据 | `SuperAdminCtx` + `{}` | 返回跨产品全量角色数据 | 正常路径 | P0 | 超管不传productCode走FindListByPage |
|
|
|
| TC-0124 | POST /api/role/detail | 正常查询 | `{"id":1}` | code=0, 含permIds | 正常路径 | P0 | roleDetailLogic |
|
|
| TC-0124 | POST /api/role/detail | 正常查询 | `{"id":1}` | code=0, 含permIds | 正常路径 | P0 | roleDetailLogic |
|
|
|
| TC-0125 | POST /api/role/detail | 不存在 | `{"id":9999}` | code=404 | 异常路径 | P0 | FindOne失败 |
|
|
| TC-0125 | POST /api/role/detail | 不存在 | `{"id":9999}` | code=404 | 异常路径 | P0 | FindOne失败 |
|
|
|
| TC-0730 | POST /api/role/* | 非超管 admin 把 roleA.PermsLevel 从 100 调到 10(数字变小 = 提升权级) | AdminCtx,PermsLevel 100→10 | 403 "非超管不能提升角色的权限级别",DB 保持 100 | 安全 | P0 | caller.IsSuperAdmin=false && newLevel<oldLevel(数字越小 = 权限越高);R12 后错误消息从"降低"修正为"提升" |
|
|
| TC-0730 | POST /api/role/* | 非超管 admin 把 roleA.PermsLevel 从 100 调到 10(数字变小 = 提升权级) | AdminCtx,PermsLevel 100→10 | 403 "非超管不能提升角色的权限级别",DB 保持 100 | 安全 | P0 | caller.IsSuperAdmin=false && newLevel<oldLevel(数字越小 = 权限越高);R12 后错误消息从"降低"修正为"提升" |
|
|
@@ -403,6 +409,8 @@ MySQL (InnoDB) + Redis Cache
|
|
|
| TC-1197 | POST /api/role/create | 非超管 product ADMIN 禁止创建 `permsLevel=1` 顶格角色 | `AdminCtx(pc)` + `{productCode:pc, name:...,permsLevel:1}` | `CodeError.Code()==403`;文案含 "权限级别为 1 的顶格角色";DB 无新角色 | 安全/纵向越权 | P0 | 顶格角色只能由 SuperAdmin 创建;若放行 ADMIN,其可"建 R_super + BindRoles 给下属" 绕开 `GuardRoleLevelAssignable` 的同级拦截,形成等价横向提权链路 |
|
|
| TC-1197 | POST /api/role/create | 非超管 product ADMIN 禁止创建 `permsLevel=1` 顶格角色 | `AdminCtx(pc)` + `{productCode:pc, name:...,permsLevel:1}` | `CodeError.Code()==403`;文案含 "权限级别为 1 的顶格角色";DB 无新角色 | 安全/纵向越权 | P0 | 顶格角色只能由 SuperAdmin 创建;若放行 ADMIN,其可"建 R_super + BindRoles 给下属" 绕开 `GuardRoleLevelAssignable` 的同级拦截,形成等价横向提权链路 |
|
|
|
| TC-1198 | POST /api/role/create | product ADMIN 创建 `permsLevel>=2` 次级角色放行 | `AdminCtx(pc)` + `{productCode:pc, name:..., permsLevel:2}` | 成功;DB `sys_role.permsLevel=2` | 正向回归 | P0 | 防 `GuardCreateRolePermsLevel` 过度收紧把合法业务路径也打死 |
|
|
| TC-1198 | POST /api/role/create | product ADMIN 创建 `permsLevel>=2` 次级角色放行 | `AdminCtx(pc)` + `{productCode:pc, name:..., permsLevel:2}` | 成功;DB `sys_role.permsLevel=2` | 正向回归 | P0 | 防 `GuardCreateRolePermsLevel` 过度收紧把合法业务路径也打死 |
|
|
|
| TC-1199 | POST /api/role/create | SuperAdmin 不受 `permsLevel=1` 约束 | `SuperAdminCtx()` + `{..., permsLevel:1}` | 成功;DB `sys_role.permsLevel=1` | 正向回归 | P0 | SuperAdmin 是顶格角色的唯一合法来源;若回滚把超管也拦住,系统将没有任何路径能初始化 permsLevel=1 的角色 |
|
|
| TC-1199 | POST /api/role/create | SuperAdmin 不受 `permsLevel=1` 约束 | `SuperAdminCtx()` + `{..., permsLevel:1}` | 成功;DB `sys_role.permsLevel=1` | 正向回归 | P0 | SuperAdmin 是顶格角色的唯一合法来源;若回滚把超管也拦住,系统将没有任何路径能初始化 permsLevel=1 的角色 |
|
|
|
|
|
+| TC-1316 | POST /api/role/create | 非超管不传productCode时从JWT获取并正常创建 | `AdminCtx(pc)` + `{name:..., permsLevel:5}` 不传productCode | 从JWT取productCode,创建成功,DB落盘productCode=pc | 正常路径 | P0 | 非超管productCode统一从JWT获取 |
|
|
|
|
|
+| TC-1317 | POST /api/role/create | 超管不传productCode时返回400 | `SuperAdminCtx` + `{name:..., permsLevel:5}` 不传productCode | `CodeError.Code()==400`,文案含 "必须指定产品编码" | 参数校验 | P0 | 超管写操作必须显式传入productCode |
|
|
|
| TC-1204 | POST /api/role/update | UpdateRole 重命名后旧 name 索引缓存必须失效 | 超管创建角色 name=A;第一次 Load 让 `sysRole:productCode:name:<pc>:A` 写入缓存;UpdateRole 把 name 改为 B;再次 `FindOneByProductCodeName(pc, "A")` | `FindOneByProductCodeName` 返回 `sqlx.ErrNotFound`(不得返回旧行数据);说明 post-commit `InvalidateRoleCache(oldName)` 已把 Redis 里的 `<pc>:A` 索引键清掉 | 缓存一致性/安全 | P0 | `UpdateWithOptLock` 内部只失效新 name 键;rename 路径的旧 name 键必须在 post-commit 由 `InvalidateRoleCache(prevName)` 显式清除,否则 Redis TTL 窗口内同名并发创建会命中幽灵快照 |
|
|
| TC-1204 | POST /api/role/update | UpdateRole 重命名后旧 name 索引缓存必须失效 | 超管创建角色 name=A;第一次 Load 让 `sysRole:productCode:name:<pc>:A` 写入缓存;UpdateRole 把 name 改为 B;再次 `FindOneByProductCodeName(pc, "A")` | `FindOneByProductCodeName` 返回 `sqlx.ErrNotFound`(不得返回旧行数据);说明 post-commit `InvalidateRoleCache(oldName)` 已把 Redis 里的 `<pc>:A` 索引键清掉 | 缓存一致性/安全 | P0 | `UpdateWithOptLock` 内部只失效新 name 键;rename 路径的旧 name 键必须在 post-commit 由 `InvalidateRoleCache(prevName)` 显式清除,否则 Redis TTL 窗口内同名并发创建会命中幽灵快照 |
|
|
|
|
|
|
|
|
### 2.12 删除角色 `POST /api/role/delete`
|
|
### 2.12 删除角色 `POST /api/role/delete`
|
|
@@ -558,8 +566,8 @@ MySQL (InnoDB) + Redis Cache
|
|
|
| TC-0203 | POST /api/user/updateStatus | 冻结自己 | id=当前登录userId | code=400, "不能修改自己的状态" | 自我保护 | P0 | callerId==req.Id |
|
|
| TC-0203 | POST /api/user/updateStatus | 冻结自己 | id=当前登录userId | code=400, "不能修改自己的状态" | 自我保护 | P0 | callerId==req.Id |
|
|
|
| TC-0204 | POST /api/user/updateStatus | 冻结超管 | id=超管 | code=403, "不能修改超级管理员的状态" | 超管保护 | P0 | IsSuperAdmin==1 |
|
|
| TC-0204 | POST /api/user/updateStatus | 冻结超管 | id=超管 | code=403, "不能修改超级管理员的状态" | 超管保护 | P0 | IsSuperAdmin==1 |
|
|
|
| TC-0205 | POST /api/user/list | userList-非超管仅可见产品成员 | ctx=ADMIN(非超管), productCode指定 | 仅返回该产品成员, 不返回非成员 | 安全 | P0 | FindListByProductMembers数据隔离 |
|
|
| TC-0205 | POST /api/user/list | userList-非超管仅可见产品成员 | ctx=ADMIN(非超管), productCode指定 | 仅返回该产品成员, 不返回非成员 | 安全 | P0 | FindListByProductMembers数据隔离 |
|
|
|
-| TC-0206 | POST /api/user/list | userList-非超管未指定productCode被拒绝 | ctx=ADMIN(非超管), productCode="" | 403 "必须指定产品编码" | 安全 | P0 | 强制productCode |
|
|
|
|
|
-| TC-0207 | POST /api/user/list | userList-非超管使用错误productCode被拒绝 | ctx=ADMIN, productCode!=ctx.ProductCode | 403 | 安全 | P0 | productCode一致性校验 |
|
|
|
|
|
|
|
+| TC-0206 | POST /api/user/list | userList-非超管不传productCode时从JWT获取 | ctx=ADMIN(非超管), productCode="" | 从JWT取productCode,正常返回本产品成员数据 | 安全 | P0 | 非超管productCode统一从JWT获取 |
|
|
|
|
|
+| TC-0207 | POST /api/user/list | userList-非超管传了其他产品code时被忽略 | ctx=ADMIN, productCode="other" | 忽略req.ProductCode,仍返回JWT产品数据 | 安全 | P0 | 非超管req.ProductCode被忽略 |
|
|
|
| TC-0208 | POST /api/user/bindRoles | bindRoles-permsLevel越权拒绝 | ctx=ADMIN(MinPermsLevel=50), role.permsLevel=1 | 403 "不能分配权限级别高于自身的角色" | 安全 | P0 | 角色权限级别越权防护 |
|
|
| TC-0208 | POST /api/user/bindRoles | bindRoles-permsLevel越权拒绝 | ctx=ADMIN(MinPermsLevel=50), role.permsLevel=1 | 403 "不能分配权限级别高于自身的角色" | 安全 | P0 | 角色权限级别越权防护 |
|
|
|
| TC-0209 | POST /api/user/bindRoles | bindRoles-超管可分配任意级别角色 | ctx=SuperAdmin, role.permsLevel=1 | 绑定成功 | 正常路径 | P0 | 超管无permsLevel限制 |
|
|
| TC-0209 | POST /api/user/bindRoles | bindRoles-超管可分配任意级别角色 | ctx=SuperAdmin, role.permsLevel=1 | 绑定成功 | 正常路径 | P0 | 超管无permsLevel限制 |
|
|
|
| TC-0210 | POST /api/user/setPerms | 同一权限ID冲突Effect被拒绝 | perms含[{permId:1,effect:"ALLOW"},{permId:1,effect:"DENY"}] | 400 "同一权限ID不能同时为 ALLOW 和 DENY" | 业务约束 | P0 | seen[permId]冲突检测 |
|
|
| TC-0210 | POST /api/user/setPerms | 同一权限ID冲突Effect被拒绝 | perms含[{permId:1,effect:"ALLOW"},{permId:1,effect:"DENY"}] | 400 "同一权限ID不能同时为 ALLOW 和 DENY" | 业务约束 | P0 | seen[permId]冲突检测 |
|
|
@@ -630,6 +638,9 @@ MySQL (InnoDB) + Redis Cache
|
|
|
| TC-0223 | POST /api/member/list | 成员用户已删除 | userId不存在于FindByIds结果 | username/nickname为空 | 分支覆盖 | P1 | userMap无对应key |
|
|
| TC-0223 | POST /api/member/list | 成员用户已删除 | userId不存在于FindByIds结果 | username/nickname为空 | 分支覆盖 | P1 | userMap无对应key |
|
|
|
| TC-0224 | POST /api/member/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
|
|
| TC-0224 | POST /api/member/list | pageSize超过上限 | `{"productCode":"p1","pageSize":200}` | 实际pageSize=100 | 边界 | P0 | NormalizePage cap |
|
|
|
| TC-0225 | POST /api/member/list | 空成员列表 | productCode下无成员 | total=0, list=[], 不调FindByIds | 分支覆盖 | P1 | userIds空 |
|
|
| TC-0225 | POST /api/member/list | 空成员列表 | productCode下无成员 | total=0, list=[], 不调FindByIds | 分支覆盖 | P1 | userIds空 |
|
|
|
|
|
+| TC-1305 | POST /api/member/list | 非超管不传productCode时从JWT获取 | `AdminCtx(pc)` + `{}` | 从JWT取productCode,返回本产品成员数据 | 安全 | P0 | 非超管productCode统一从JWT获取 |
|
|
|
|
|
+| TC-1306 | POST /api/member/list | 非超管传了其他产品code时被忽略 | `AdminCtx(pc)` + `{"productCode":"other"}` | 忽略req.ProductCode,仍返回JWT产品数据 | 安全 | P0 | 非超管req.ProductCode被忽略 |
|
|
|
|
|
+| TC-1307 | POST /api/member/list | 超管不传productCode时返回全量数据 | `SuperAdminCtx` + `{}` | 返回跨产品全量成员数据 | 正常路径 | P0 | 超管不传productCode走FindListByPage |
|
|
|
| TC-0226 | POST /api/member/remove | 正常移除+级联(事务内) | `{"id":1}` (含角色/权限) | code=0, user_role+user_perm同步清理 | 正常+事务 | P0 | TransactCtx全路径 |
|
|
| TC-0226 | POST /api/member/remove | 正常移除+级联(事务内) | `{"id":1}` (含角色/权限) | code=0, user_role+user_perm同步清理 | 正常+事务 | P0 | TransactCtx全路径 |
|
|
|
| TC-0227 | POST /api/member/remove | 跨产品隔离 | 用户在多产品有角色 | 仅清理该产品的 | 深度业务 | P0 | ForProductTx子查询 |
|
|
| TC-0227 | POST /api/member/remove | 跨产品隔离 | 用户在多产品有角色 | 仅清理该产品的 | 深度业务 | P0 | ForProductTx子查询 |
|
|
|
| TC-0228 | POST /api/member/remove | 成员不存在 | `{"id":9999}` | code=404, "成员不存在" | 异常路径 | P0 | FindOne失败 |
|
|
| TC-0228 | POST /api/member/remove | 成员不存在 | `{"id":9999}` | code=404, "成员不存在" | 异常路径 | P0 | FindOne失败 |
|
|
@@ -682,9 +693,11 @@ MySQL (InnoDB) + Redis Cache
|
|
|
| TC-1135 | POST /api/member/update | 降级事务失败(last-admin 400):sys_user.tokenVersion 不变 | 唯一启用 ADMIN,`{MemberType:"MEMBER"}` | 返回 400 "最后一个管理员";DB `sys_user.tokenVersion` 严格等于初值;`sys_product_member` 行内容也保持原状 | 事务回滚 | P0 | 关键:tokenVersion 增量必须与 member 更新在同一事务里;业务失败不得污染 tokenVersion |
|
|
| TC-1135 | POST /api/member/update | 降级事务失败(last-admin 400):sys_user.tokenVersion 不变 | 唯一启用 ADMIN,`{MemberType:"MEMBER"}` | 返回 400 "最后一个管理员";DB `sys_user.tokenVersion` 严格等于初值;`sys_product_member` 行内容也保持原状 | 事务回滚 | P0 | 关键:tokenVersion 增量必须与 member 更新在同一事务里;业务失败不得污染 tokenVersion |
|
|
|
| TC-1136 | POST /api/member/update | no-op 更新不递增 tokenVersion | 传进来的 memberType/status 与 DB 现值相同 | `tokenVersion` 不变;早退分支不进事务 | 正向/幂等 | P0 | `locked.MemberType==nextType && locked.Status==nextStatus` 早退 |
|
|
| TC-1136 | POST /api/member/update | no-op 更新不递增 tokenVersion | 传进来的 memberType/status 与 DB 现值相同 | `tokenVersion` 不变;早退分支不进事务 | 正向/幂等 | P0 | `locked.MemberType==nextType && locked.Status==nextStatus` 早退 |
|
|
|
| TC-1137 | POST /api/member/update | 降级成功后 post-commit 失效 sysUser id-key / username-key 两把缓存 | seed ADMIN→降级 MEMBER,先预热 `FindOne(id)` + `FindOneByUsername(name)` 把缓存灌入 Redis | 事务成功返回后两把 cache key 均被 DEL;下一次 FindOne 取到 DB 中递增后的 tokenVersion | 缓存一致性 | P0 | UD loader 下次 cache-miss 重建时不得从旧 sysUser 缓存把 tokenVersion 抹回 |
|
|
| TC-1137 | POST /api/member/update | 降级成功后 post-commit 失效 sysUser id-key / username-key 两把缓存 | seed ADMIN→降级 MEMBER,先预热 `FindOne(id)` + `FindOneByUsername(name)` 把缓存灌入 Redis | 事务成功返回后两把 cache key 均被 DEL;下一次 FindOne 取到 DB 中递增后的 tokenVersion | 缓存一致性 | P0 | UD loader 下次 cache-miss 重建时不得从旧 sysUser 缓存把 tokenVersion 抹回 |
|
|
|
-| TC-1107 | POST /api/member/add | 非 ADMIN caller + **不存在的 productCode**:必须 403(不是 404)以消除 productCode 枚举 oracle | `MemberCtx("other_product")` + `ProductCode="does_not_exist"` | `CodeError.Code()==403`(不是 404 "产品不存在");DB 无 `sys_product_member` 新增 | 安全/枚举 | P0 | 反回归:`RequireProductAdminFor` 必须先于 `SysProductModel.FindOneByCode` |
|
|
|
|
|
|
|
+| TC-1107 | POST /api/member/add | 非超管 req.ProductCode 被忽略,枚举攻击路径从根本上被堵死 | `AdminCtx("some_other_product")` + `ProductCode="does_not_exist"` | 忽略 req.ProductCode,使用 JWT 的 productCode 查询产品,不存在则 404;DB 无 `sys_product_member` 新增 | 安全/枚举 | P0 | 改造后非超管无法指定任意 productCode,枚举攻击路径不存在 |
|
|
|
| TC-1108 | POST /api/member/add | 非 ADMIN caller + 非法 `MemberType`:返回 403 而不是 400(权限优先于字面校验) | `MemberCtx` + `MemberType="INVALID"` | `CodeError.Code()==403`(不是 400 "无效的成员类型") | 安全/枚举 | P0 | 防通过 400/404 差分探测产品/用户存在性 |
|
|
| TC-1108 | POST /api/member/add | 非 ADMIN caller + 非法 `MemberType`:返回 403 而不是 400(权限优先于字面校验) | `MemberCtx` + `MemberType="INVALID"` | `CodeError.Code()==403`(不是 400 "无效的成员类型") | 安全/枚举 | P0 | 防通过 400/404 差分探测产品/用户存在性 |
|
|
|
| TC-1109 | POST /api/member/add | 超管 + 非法 `MemberType`:正常 400 | `SuperAdminCtx` + `MemberType="INVALID"`(产品存在) | `CodeError.Code()==400`,文案含 "无效的成员类型" | 正向回归 | P0 | 确认权限通过后仍走字面 400 检查,不误伤合法路径 |
|
|
| TC-1109 | POST /api/member/add | 超管 + 非法 `MemberType`:正常 400 | `SuperAdminCtx` + `MemberType="INVALID"`(产品存在) | `CodeError.Code()==400`,文案含 "无效的成员类型" | 正向回归 | P0 | 确认权限通过后仍走字面 400 检查,不误伤合法路径 |
|
|
|
|
|
+| TC-1314 | POST /api/member/add | 非超管不传productCode时从JWT获取并正常添加 | `AdminCtx(pc)` + `{userId:..., memberType:"MEMBER"}` 不传productCode | 从JWT取productCode,添加成功,DB落盘productCode=pc | 正常路径 | P0 | 非超管productCode统一从JWT获取 |
|
|
|
|
|
+| TC-1315 | POST /api/member/add | 超管不传productCode时返回400 | `SuperAdminCtx` + `{userId:..., memberType:"MEMBER"}` 不传productCode | `CodeError.Code()==400`,文案含 "必须指定产品编码" | 参数校验 | P0 | 超管写操作必须显式传入productCode |
|
|
|
| TC-1162 | POST /api/member/remove | 移除成员后被移除用户 sys_user.tokenVersion 必须 +1 | seed 2 个 ADMIN 绕过 last-admin,`{id: targetMemberId}` | DB `sys_user.tokenVersion` 严格 +1;`sys_product_member` 行被删;post-commit 产品成员缓存失效 | 安全/会话吊销 | P0 | 镜像 updateMember 的 tokenVersion 契约,避免被踢出产品后旧 access token 仍能访问该产品 |
|
|
| TC-1162 | POST /api/member/remove | 移除成员后被移除用户 sys_user.tokenVersion 必须 +1 | seed 2 个 ADMIN 绕过 last-admin,`{id: targetMemberId}` | DB `sys_user.tokenVersion` 严格 +1;`sys_product_member` 行被删;post-commit 产品成员缓存失效 | 安全/会话吊销 | P0 | 镜像 updateMember 的 tokenVersion 契约,避免被踢出产品后旧 access token 仍能访问该产品 |
|
|
|
| TC-1163 | POST /api/member/remove | 移除失败(last-admin 场景)时 tokenVersion 绝不得 +1 | 唯一启用 ADMIN,`{id: adminMemberId}` | 返回 400 "不能移除该产品的最后一个管理员";DB `sys_user.tokenVersion` 与初值严格相等;`sys_product_member` 行仍在 | 事务回滚 | P0 | tokenVersion 增量必须与 member 删除同事务;失败路径不得污染 tokenVersion 让合法会话被无故踢下线 |
|
|
| TC-1163 | POST /api/member/remove | 移除失败(last-admin 场景)时 tokenVersion 绝不得 +1 | 唯一启用 ADMIN,`{id: adminMemberId}` | 返回 400 "不能移除该产品的最后一个管理员";DB `sys_user.tokenVersion` 与初值严格相等;`sys_product_member` 行仍在 | 事务回滚 | P0 | tokenVersion 增量必须与 member 删除同事务;失败路径不得污染 tokenVersion 让合法会话被无故踢下线 |
|
|
|
|
|
|