feat(rbac): enforce admin access checks and document permission model
This commit is contained in:
105
apps/admin/src/lib/access.ts
Normal file
105
apps/admin/src/lib/access.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { hasPermission, normalizeRole, type PermissionScope, type Role } from "@cms/content/rbac"
|
||||
import { cookies, headers } from "next/headers"
|
||||
import type { NextRequest } from "next/server"
|
||||
|
||||
type RoutePermission = {
|
||||
permission: Parameters<typeof hasPermission>[1]
|
||||
scope: PermissionScope
|
||||
}
|
||||
|
||||
type GuardRule = {
|
||||
route: RegExp
|
||||
requirement: RoutePermission | null
|
||||
}
|
||||
|
||||
const guardRules: GuardRule[] = [
|
||||
{
|
||||
route: /^\/unauthorized(?:\/|$)/,
|
||||
requirement: null,
|
||||
},
|
||||
{
|
||||
route: /^\/todo(?:\/|$)/,
|
||||
requirement: {
|
||||
permission: "roadmap:read",
|
||||
scope: "global",
|
||||
},
|
||||
},
|
||||
{
|
||||
route: /^\/(?:$|\?)/,
|
||||
requirement: {
|
||||
permission: "dashboard:read",
|
||||
scope: "global",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function resolveDefaultRole(): Role | null {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return null
|
||||
}
|
||||
|
||||
return normalizeRole(process.env.CMS_DEV_ROLE ?? "admin")
|
||||
}
|
||||
|
||||
function resolveRoleFromRawValue(raw: string | null | undefined): Role | null {
|
||||
return normalizeRole(raw)
|
||||
}
|
||||
|
||||
export function resolveRoleFromRequest(request: NextRequest): Role | null {
|
||||
const roleFromCookie = request.cookies.get("cms_role")?.value
|
||||
const roleFromHeader = request.headers.get("x-cms-role")
|
||||
|
||||
const resolved = resolveRoleFromRawValue(roleFromCookie ?? roleFromHeader)
|
||||
|
||||
if (resolved) {
|
||||
return resolved
|
||||
}
|
||||
|
||||
return resolveDefaultRole()
|
||||
}
|
||||
|
||||
export async function resolveRoleFromServerContext(): Promise<Role | null> {
|
||||
const cookieStore = await cookies()
|
||||
const headerStore = await headers()
|
||||
|
||||
const roleFromCookie = cookieStore.get("cms_role")?.value
|
||||
const roleFromHeader = headerStore.get("x-cms-role")
|
||||
|
||||
const resolved = resolveRoleFromRawValue(roleFromCookie ?? roleFromHeader)
|
||||
|
||||
if (resolved) {
|
||||
return resolved
|
||||
}
|
||||
|
||||
return resolveDefaultRole()
|
||||
}
|
||||
|
||||
export function getRequiredPermission(pathname: string): RoutePermission {
|
||||
for (const rule of guardRules) {
|
||||
if (rule.route.test(pathname)) {
|
||||
return (
|
||||
rule.requirement ?? {
|
||||
permission: "dashboard:read",
|
||||
scope: "global",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
permission: "dashboard:read",
|
||||
scope: "global",
|
||||
}
|
||||
}
|
||||
|
||||
export function canAccessRoute(role: Role, pathname: string): boolean {
|
||||
const rule = guardRules.find((item) => item.route.test(pathname))
|
||||
|
||||
if (rule && rule.requirement === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
const requirement = getRequiredPermission(pathname)
|
||||
|
||||
return hasPermission(role, requirement.permission, requirement.scope)
|
||||
}
|
||||
Reference in New Issue
Block a user