156 lines
6 KiB
TypeScript
156 lines
6 KiB
TypeScript
import type { Access, AccessArgs, AccessResult, Payload, PayloadRequest } from "payload";
|
|
import type { Permission } from "@/permissions";
|
|
import {
|
|
canAccessAdminPanel as canAccessAdminPanelByPermissions,
|
|
hasPermission,
|
|
isAdminPanelRequest,
|
|
requireAdminPageAccess,
|
|
} from "@/utils/access-control/hasPermission";
|
|
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
|
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
|
|
|
/**
|
|
* Division-scoped access control.
|
|
*
|
|
* Division membership is established by the two qualification helpers:
|
|
* - Intelligence: `hasIntelligenceQualification` (intel-domain RBAC permissions
|
|
* or the "intelligence" profile qualification)
|
|
* - Logistics: `hasLogisticsQualification` (logistics-domain RBAC permissions
|
|
* or a "logistics" profile qualification)
|
|
*
|
|
* Explicit RBAC permissions always still work on their own - these helpers are
|
|
* a union (permission OR qualification OR superuser), never a replacement.
|
|
*/
|
|
|
|
/**
|
|
* Collections whose admin panel is delegated to a division. Division members
|
|
* get full admin-panel access to exactly these collections (and nothing else,
|
|
* because `requireAdminPageAccess` still gates every other collection's
|
|
* admin-panel read behind `admin:<slug>:manage` permissions).
|
|
*
|
|
* NOTE: `assets`, `resources`, and `vehicles` are intentionally NOT here.
|
|
* Admin-panel access to those collections is gated solely by the
|
|
* `admin:<slug>:manage` "Manage Admin Page" permission (via
|
|
* `requireAdminPageAccess`), not by division membership. This prevents
|
|
* division members (e.g. an intelligence-officer role that also holds the
|
|
* logistics qualification) from viewing those backend pages without the
|
|
* explicit manage permission.
|
|
*/
|
|
const DIVISION_ADMIN_QUALIFICATIONS: Record<
|
|
string,
|
|
(payload: Payload, user: { id: number | string }) => Promise<boolean>
|
|
> = {
|
|
technologies: hasIntelligenceQualification,
|
|
};
|
|
|
|
/**
|
|
* Access factory: allows users holding the explicit permission OR members of
|
|
* the Intelligence division.
|
|
*/
|
|
export function requireIntelligencePermission(permission: Permission) {
|
|
return async ({ req }: { req: PayloadRequest }): Promise<boolean> => {
|
|
if (await hasPermission(req.payload, req.user, permission)) return true;
|
|
if (!req.user) return false;
|
|
return hasIntelligenceQualification(req.payload, req.user);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Access factory: allows users holding the explicit permission OR members of
|
|
* the Logistics division.
|
|
*/
|
|
export function requireLogisticsPermission(permission: Permission) {
|
|
return async ({ req }: { req: PayloadRequest }): Promise<boolean> => {
|
|
if (await hasPermission(req.payload, req.user, permission)) return true;
|
|
if (!req.user) return false;
|
|
return hasLogisticsQualification(req.payload, req.user);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Field-level access factory for approval-gating fields (approval status,
|
|
* is-live flags, ...). Only "super users" may write them: superuser roles or
|
|
* holders of the `system:admin-access` permission (the Admin tier).
|
|
*
|
|
* Used as both `create` and `update` field access: on create the field is
|
|
* stripped from non-superuser payloads so the schema default (e.g.
|
|
* "in_progress") applies; on update attempts to change the value are ignored.
|
|
*/
|
|
export function requireApprovalPermission() {
|
|
return async ({ req }: { req: PayloadRequest }): Promise<boolean> =>
|
|
hasPermission(req.payload, req.user, "system:admin-access");
|
|
}
|
|
|
|
/**
|
|
* Field-level access factory for approval-status select fields.
|
|
*
|
|
* Anyone with the collection's update permission may set the status to
|
|
* "In Progress" or "Ready for Review". Only superusers (holders of
|
|
* `system:admin-access`) may set it to final states ("Approved",
|
|
* "Rejected", "Revision Requested").
|
|
*/
|
|
export function approvalStatusFieldAccess(collectionSlug: string) {
|
|
const fn = async ({ req, data }: { req: PayloadRequest; data?: Record<string, unknown> }): Promise<boolean> => {
|
|
const value = data?.approvalStatus as string | undefined;
|
|
const isSuperuser = await hasPermission(req.payload, req.user, "system:admin-access");
|
|
if (isSuperuser) return true;
|
|
|
|
const allowedValues = ["in_progress", "ready_for_review"];
|
|
if (!value || allowedValues.includes(value)) {
|
|
return await hasPermission(req.payload, req.user, `${collectionSlug}:update` as Permission);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
return {
|
|
create: fn,
|
|
update: fn,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Same contract as `requireAdminPageAccess`, with one addition: for the four
|
|
* division-scoped collections an admin-panel request also passes when the user
|
|
* qualifies for the owning division. Non-admin (REST/API) requests keep the
|
|
* original read access behavior unchanged.
|
|
*/
|
|
export function scopedAdminPageAccess(
|
|
collectionSlug: string,
|
|
readAccess?: Access | boolean,
|
|
): Access {
|
|
const base = requireAdminPageAccess(collectionSlug, readAccess);
|
|
const divisionCheck = DIVISION_ADMIN_QUALIFICATIONS[collectionSlug];
|
|
|
|
if (!divisionCheck) return base;
|
|
|
|
return async (args: AccessArgs): Promise<AccessResult> => {
|
|
const { req } = args;
|
|
if (req.user && isAdminPanelRequest(req)) {
|
|
try {
|
|
if (await divisionCheck(req.payload, req.user)) return true;
|
|
} catch {
|
|
// Qualification lookup failed; fall through to the permission-based
|
|
// decision instead of failing the request outright.
|
|
}
|
|
}
|
|
return base(args);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Division-aware admin panel gate: permission-based access (superuser or any
|
|
* `admin:<slug>:manage` permission) OR membership of a division that owns at
|
|
* least one admin panel collection.
|
|
*/
|
|
export async function canAccessAdminPanel(
|
|
payload: Payload,
|
|
user: { id: number | string; roleDocs?: unknown } | null | undefined,
|
|
): Promise<boolean> {
|
|
if (!user) return false;
|
|
if (await canAccessAdminPanelByPermissions(payload, user)) return true;
|
|
for (const divisionCheck of Object.values(DIVISION_ADMIN_QUALIFICATIONS)) {
|
|
if (await divisionCheck(payload, user)) return true;
|
|
}
|
|
return false;
|
|
}
|