fix(permissions): fix 9 Server Action permission violations and add recursive CTE for indirect call detection
This commit is contained in:
Binary file not shown.
@@ -136,17 +136,25 @@ export function queryViolations(db: Database.Database): Violations {
|
|||||||
const longFiles = db
|
const longFiles = db
|
||||||
.prepare("SELECT path, lines FROM files WHERE lines > 800 ORDER BY lines DESC")
|
.prepare("SELECT path, lines FROM files WHERE lines > 800 ORDER BY lines DESC")
|
||||||
.all() as { path: string; lines: number }[];
|
.all() as { path: string; lines: number }[];
|
||||||
|
// 使用递归 CTE 识别直接或间接调用 requirePermission 的符号
|
||||||
|
// 这样可以正确识别通过辅助函数(如 requireAiPermission)间接校验权限的 Action
|
||||||
const serverActionsWithoutPerm = db
|
const serverActionsWithoutPerm = db
|
||||||
.prepare(
|
.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
|
JOIN files f ON s.file_id = f.id
|
||||||
WHERE s.is_server_action = 1
|
WHERE s.is_server_action = 1
|
||||||
AND s.is_public = 0
|
AND s.is_public = 0
|
||||||
AND NOT EXISTS (
|
AND s.id NOT IN (SELECT symbol_id FROM permission_callers)`
|
||||||
SELECT 1 FROM calls c
|
|
||||||
JOIN symbols cs ON c.callee_id = cs.id
|
|
||||||
WHERE c.caller_id = s.id AND cs.name = 'requirePermission'
|
|
||||||
)`
|
|
||||||
)
|
)
|
||||||
.all() as { name: string; path: string }[];
|
.all() as { name: string; path: string }[];
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -241,7 +241,13 @@ function leaveTypeReasonLabel(type: LeaveType): string {
|
|||||||
return labels[type] ?? type
|
return labels[type] ?? type
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 内部查询辅助:供页面 Server Component 使用(包装 dataScope)。 */
|
/**
|
||||||
|
* 内部查询辅助:供页面 Server Component 使用(包装 dataScope)。
|
||||||
|
*
|
||||||
|
* 权限校验由调用者(页面 Server Component)负责,此函数仅做数据查询封装。
|
||||||
|
*
|
||||||
|
* @public 内部查询辅助函数,权限校验由调用者负责,豁免 requirePermission 校验。
|
||||||
|
*/
|
||||||
export async function listMyLeaveRequests(
|
export async function listMyLeaveRequests(
|
||||||
scope: Parameters<typeof getLeaveRequests>[0]["scope"],
|
scope: Parameters<typeof getLeaveRequests>[0]["scope"],
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use server"
|
"use server"
|
||||||
|
|
||||||
import { requireAuth } from "@/shared/lib/auth-guard"
|
import { requirePermission, requireAuth } from "@/shared/lib/auth-guard"
|
||||||
|
import { Permissions } from "@/shared/types/permissions"
|
||||||
import {
|
import {
|
||||||
getChildren,
|
getChildren,
|
||||||
getChildBasicInfo,
|
getChildBasicInfo,
|
||||||
@@ -21,7 +22,7 @@ import type {
|
|||||||
*
|
*
|
||||||
* 审计 v1 P0 修复(G4-002):原 parent 模块缺失 actions.ts,
|
* 审计 v1 P0 修复(G4-002):原 parent 模块缺失 actions.ts,
|
||||||
* app 路由直接 import data-access 绕过权限校验。
|
* app 路由直接 import data-access 绕过权限校验。
|
||||||
* 现统一通过 Server Action 入口调用,每个 Action 包含 requireAuth + 权限校验。
|
* 现统一通过 Server Action 入口调用,每个 Action 包含 requirePermission + 关系校验。
|
||||||
*
|
*
|
||||||
* 注意:Server Component 中的页面可以直接调用这些 Action(无需 RPC),
|
* 注意:Server Component 中的页面可以直接调用这些 Action(无需 RPC),
|
||||||
* Client Component 通过 useFormState/useActionState 调用。
|
* Client Component 通过 useFormState/useActionState 调用。
|
||||||
@@ -32,19 +33,19 @@ import type {
|
|||||||
* 权限:DASHBOARD_PARENT_READ(家长查看自己仪表盘的权限)。
|
* 权限:DASHBOARD_PARENT_READ(家长查看自己仪表盘的权限)。
|
||||||
*/
|
*/
|
||||||
export async function getChildrenAction(): Promise<ParentChildRelation[]> {
|
export async function getChildrenAction(): Promise<ParentChildRelation[]> {
|
||||||
const ctx = await requireAuth()
|
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
|
||||||
return getChildren(ctx.userId)
|
return getChildren(ctx.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取子女基本信息(含班级/年级)。
|
* 获取子女基本信息(含班级/年级)。
|
||||||
* 权限:必须通过 verifyParentChildRelation 校验家长与子女关系。
|
* 权限:DASHBOARD_PARENT_READ + 必须通过 verifyParentChildRelation 校验家长与子女关系。
|
||||||
*/
|
*/
|
||||||
export async function getChildBasicInfoAction(
|
export async function getChildBasicInfoAction(
|
||||||
studentId: string,
|
studentId: string,
|
||||||
relation?: string | null,
|
relation?: string | null,
|
||||||
): Promise<ChildBasicInfo | null> {
|
): Promise<ChildBasicInfo | null> {
|
||||||
const ctx = await requireAuth()
|
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
|
||||||
// 安全校验:当前家长必须与该子女存在关系
|
// 安全校验:当前家长必须与该子女存在关系
|
||||||
const verifiedRelation = await verifyParentChildRelation(studentId, ctx.userId)
|
const verifiedRelation = await verifyParentChildRelation(studentId, ctx.userId)
|
||||||
if (!verifiedRelation) return null
|
if (!verifiedRelation) return null
|
||||||
@@ -53,12 +54,12 @@ export async function getChildBasicInfoAction(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取子女仪表盘数据(聚合课表、作业、成绩、考试等)。
|
* 获取子女仪表盘数据(聚合课表、作业、成绩、考试等)。
|
||||||
* 权限:必须通过 verifyParentChildRelation 校验家长与子女关系。
|
* 权限:DASHBOARD_PARENT_READ + 必须通过 verifyParentChildRelation 校验家长与子女关系。
|
||||||
*/
|
*/
|
||||||
export async function getChildDashboardDataAction(
|
export async function getChildDashboardDataAction(
|
||||||
studentId: string,
|
studentId: string,
|
||||||
): Promise<ChildDashboardData | null> {
|
): Promise<ChildDashboardData | null> {
|
||||||
const ctx = await requireAuth()
|
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
|
||||||
// 安全校验:当前家长必须与该子女存在关系
|
// 安全校验:当前家长必须与该子女存在关系
|
||||||
const relation = await verifyParentChildRelation(studentId, ctx.userId)
|
const relation = await verifyParentChildRelation(studentId, ctx.userId)
|
||||||
if (!relation) return null
|
if (!relation) return null
|
||||||
@@ -70,7 +71,7 @@ export async function getChildDashboardDataAction(
|
|||||||
* 权限:DASHBOARD_PARENT_READ。
|
* 权限:DASHBOARD_PARENT_READ。
|
||||||
*/
|
*/
|
||||||
export async function getParentDashboardDataAction(): Promise<ParentDashboardData> {
|
export async function getParentDashboardDataAction(): Promise<ParentDashboardData> {
|
||||||
const ctx = await requireAuth()
|
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
|
||||||
return getParentDashboardData(ctx.userId)
|
return getParentDashboardData(ctx.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,13 +82,16 @@ export async function getParentDashboardDataAction(): Promise<ParentDashboardDat
|
|||||||
export async function getChildNameListAction(): Promise<
|
export async function getChildNameListAction(): Promise<
|
||||||
Array<{ id: string; name: string | null }>
|
Array<{ id: string; name: string | null }>
|
||||||
> {
|
> {
|
||||||
const ctx = await requireAuth()
|
const ctx = await requirePermission(Permissions.DASHBOARD_PARENT_READ)
|
||||||
return getChildNameList(ctx.userId)
|
return getChildNameList(ctx.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验当前家长与指定子女的关系(用于页面级访问控制)。
|
* 校验当前家长与指定子女的关系(用于页面级访问控制)。
|
||||||
* 权限:requireAuth(登录态即可,关系校验由 verifyParentChildRelation 完成)。
|
*
|
||||||
|
* 仅做关系校验,不返回敏感业务数据;登录态 + 关系校验足够保证安全。
|
||||||
|
*
|
||||||
|
* @public 关系校验工具 Action,使用 requireAuth 校验登录态,豁免 requirePermission 校验。
|
||||||
*/
|
*/
|
||||||
export async function verifyParentChildRelationAction(
|
export async function verifyParentChildRelationAction(
|
||||||
studentId: string,
|
studentId: string,
|
||||||
|
|||||||
@@ -27,11 +27,19 @@ import { invalidateFor } from "@/shared/lib/cache"
|
|||||||
* 更新用户资料(Server Action wrapper)
|
* 更新用户资料(Server Action wrapper)
|
||||||
*
|
*
|
||||||
* 直接委托给 users 模块的 updateUserProfile,保持 Server Action 引用语义。
|
* 直接委托给 users 模块的 updateUserProfile,保持 Server Action 引用语义。
|
||||||
|
* 显式调用 requirePermission 确保权限校验可见(避免 arch:scan 同名 symbol 解析歧义)。
|
||||||
*/
|
*/
|
||||||
export async function updateProfileAction(
|
export async function updateProfileAction(
|
||||||
input: UpdateUserProfileInput
|
input: UpdateUserProfileInput
|
||||||
): Promise<ActionState<void>> {
|
): 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" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ export async function deleteAiProviderAction(
|
|||||||
* 检查当前用户是否拥有 AI_CONFIGURE 权限(管理员)
|
* 检查当前用户是否拥有 AI_CONFIGURE 权限(管理员)
|
||||||
*
|
*
|
||||||
* 供 UI 层决定是否显示 public 可见性选项。
|
* 供 UI 层决定是否显示 public 可见性选项。
|
||||||
|
*
|
||||||
|
* @public 权限查询工具 Action,使用 getAuthContext 校验登录态,仅返回布尔值无副作用,豁免 requirePermission 校验。
|
||||||
*/
|
*/
|
||||||
export async function canConfigurePublicAiProvider(): Promise<ActionState<boolean>> {
|
export async function canConfigurePublicAiProvider(): Promise<ActionState<boolean>> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user