1
0
Fork 0
polaris-task-force/src/utils/access-control/divisionAccess.ts

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;
}