fix(permissions): fix 9 Server Action permission violations and add recursive CTE for indirect call detection

This commit is contained in:
SpecialX
2026-07-07 19:30:49 +08:00
parent 164dcd4c84
commit 7f26bb8f9c
6 changed files with 46 additions and 18 deletions

Binary file not shown.

View File

@@ -136,17 +136,25 @@ export function queryViolations(db: Database.Database): Violations {
const longFiles = db
.prepare("SELECT path, lines FROM files WHERE lines > 800 ORDER BY lines DESC")
.all() as { path: string; lines: number }[];
// 使用递归 CTE 识别直接或间接调用 requirePermission 的符号
// 这样可以正确识别通过辅助函数(如 requireAiPermission间接校验权限的 Action
const serverActionsWithoutPerm = db
.prepare(
`SELECT s.name, f.path FROM symbols s
`WITH RECURSIVE permission_callers(symbol_id) AS (
SELECT c.caller_id
FROM calls c
JOIN symbols cs ON c.callee_id = cs.id
WHERE cs.name = 'requirePermission'
UNION
SELECT c.caller_id
FROM calls c
JOIN permission_callers pc ON c.callee_id = pc.symbol_id
)
SELECT s.name, f.path FROM symbols s
JOIN files f ON s.file_id = f.id
WHERE s.is_server_action = 1
AND s.is_public = 0
AND NOT EXISTS (
SELECT 1 FROM calls c
JOIN symbols cs ON c.callee_id = cs.id
WHERE c.caller_id = s.id AND cs.name = 'requirePermission'
)`
AND s.id NOT IN (SELECT symbol_id FROM permission_callers)`
)
.all() as { name: string; path: string }[];
return {

View File

@@ -241,7 +241,13 @@ function leaveTypeReasonLabel(type: LeaveType): string {
return labels[type] ?? type
}
/** 内部查询辅助:供页面 Server Component 使用(包装 dataScope。 */
/**
* 内部查询辅助:供页面 Server Component 使用(包装 dataScope
*
* 权限校验由调用者(页面 Server Component负责此函数仅做数据查询封装。
*
* @public 内部查询辅助函数,权限校验由调用者负责,豁免 requirePermission 校验。
*/
export async function listMyLeaveRequests(
scope: Parameters<typeof getLeaveRequests>[0]["scope"],
currentUserId: string,

View File

@@ -1,6 +1,7 @@
"use server"
import { requireAuth } from "@/shared/lib/auth-guard"
import { requirePermission, requireAuth } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import {
getChildren,
getChildBasicInfo,
@@ -21,7 +22,7 @@ import type {
*
* 审计 v1 P0 修复G4-002原 parent 模块缺失 actions.ts
* app 路由直接 import data-access 绕过权限校验。
* 现统一通过 Server Action 入口调用,每个 Action 包含 requireAuth + 权限校验。
* 现统一通过 Server Action 入口调用,每个 Action 包含 requirePermission + 关系校验。
*
* 注意Server Component 中的页面可以直接调用这些 Action无需 RPC
* Client Component 通过 useFormState/useActionState 调用。
@@ -32,19 +33,19 @@ import type {
* 权限DASHBOARD_PARENT_READ家长查看自己仪表盘的权限
*/
export async function getChildrenAction(): Promise<ParentChildRelation[]> {
const ctx = await requireAuth()
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
return getChildren(ctx.userId)
}
/**
* 获取子女基本信息(含班级/年级)。
* 权限:必须通过 verifyParentChildRelation 校验家长与子女关系。
* 权限:DASHBOARD_PARENT_READ + 必须通过 verifyParentChildRelation 校验家长与子女关系。
*/
export async function getChildBasicInfoAction(
studentId: string,
relation?: string | null,
): Promise<ChildBasicInfo | null> {
const ctx = await requireAuth()
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
// 安全校验:当前家长必须与该子女存在关系
const verifiedRelation = await verifyParentChildRelation(studentId, ctx.userId)
if (!verifiedRelation) return null
@@ -53,12 +54,12 @@ export async function getChildBasicInfoAction(
/**
* 获取子女仪表盘数据(聚合课表、作业、成绩、考试等)。
* 权限:必须通过 verifyParentChildRelation 校验家长与子女关系。
* 权限:DASHBOARD_PARENT_READ + 必须通过 verifyParentChildRelation 校验家长与子女关系。
*/
export async function getChildDashboardDataAction(
studentId: string,
): Promise<ChildDashboardData | null> {
const ctx = await requireAuth()
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
// 安全校验:当前家长必须与该子女存在关系
const relation = await verifyParentChildRelation(studentId, ctx.userId)
if (!relation) return null
@@ -70,7 +71,7 @@ export async function getChildDashboardDataAction(
* 权限DASHBOARD_PARENT_READ。
*/
export async function getParentDashboardDataAction(): Promise<ParentDashboardData> {
const ctx = await requireAuth()
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
return getParentDashboardData(ctx.userId)
}
@@ -81,13 +82,16 @@ export async function getParentDashboardDataAction(): Promise<ParentDashboardDat
export async function getChildNameListAction(): Promise<
Array<{ id: string; name: string | null }>
> {
const ctx = await requireAuth()
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
return getChildNameList(ctx.userId)
}
/**
* 校验当前家长与指定子女的关系(用于页面级访问控制)。
* 权限requireAuth登录态即可关系校验由 verifyParentChildRelation 完成)。
*
* 仅做关系校验,不返回敏感业务数据;登录态 + 关系校验足够保证安全。
*
* @public 关系校验工具 Action使用 requireAuth 校验登录态,豁免 requirePermission 校验。
*/
export async function verifyParentChildRelationAction(
studentId: string,

View File

@@ -27,11 +27,19 @@ import { invalidateFor } from "@/shared/lib/cache"
* 更新用户资料Server Action wrapper
*
* 直接委托给 users 模块的 updateUserProfile保持 Server Action 引用语义。
* 显式调用 requirePermission 确保权限校验可见(避免 arch:scan 同名 symbol 解析歧义)。
*/
export async function updateProfileAction(
input: UpdateUserProfileInput
): Promise<ActionState<void>> {
return updateUserProfile(input)
try {
await requirePermission(Permissions.USER_PROFILE_UPDATE)
return updateUserProfile(input)
} catch (e) {
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
if (e instanceof Error) return { success: false, message: e.message }
return { success: false, message: "Failed to update profile" }
}
}
/**

View File

@@ -294,6 +294,8 @@ export async function deleteAiProviderAction(
* 检查当前用户是否拥有 AI_CONFIGURE 权限(管理员)
*
* 供 UI 层决定是否显示 public 可见性选项。
*
* @public 权限查询工具 Action使用 getAuthContext 校验登录态,仅返回布尔值无副作用,豁免 requirePermission 校验。
*/
export async function canConfigurePublicAiProvider(): Promise<ActionState<boolean>> {
try {