fix(missions): enforce visibility rules in collection read access
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
09b074734f
commit
8654eb40fa
1 changed files with 56 additions and 41 deletions
|
|
@ -1,8 +1,10 @@
|
|||
import type { CollectionConfig, Payload } from "payload";
|
||||
import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission";
|
||||
import type { CollectionConfig, Payload, PayloadRequest, Where } from "payload";
|
||||
import { requirePermission, hasPermission, isSuperuser } from "@/utils/access-control/hasPermission";
|
||||
|
||||
type AssignmentRef = { id: number } | number;
|
||||
|
||||
const DRAFT_STATUSES = ["Concept", "Planning"];
|
||||
|
||||
async function isInAssignments(
|
||||
payload: Payload,
|
||||
userId: number,
|
||||
|
|
@ -32,6 +34,56 @@ function extractIds(refs: unknown[] | undefined): number[] {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-document read access for missions, expressed as a Payload Where
|
||||
* constraint so it applies to both list queries and single-doc reads.
|
||||
*
|
||||
* Visibility rules (per ownershipAndStatus.visibility):
|
||||
* - authors / zeus always see their own missions.
|
||||
* - unit: every authenticated user.
|
||||
* - leadership: command or intel assignment members (+ authors/zeus).
|
||||
* - intel: intel assignment members (+ authors/zeus).
|
||||
* - private: authors/zeus only.
|
||||
*
|
||||
* Drafts (Concept / Planning) are only visible to authors/zeus and
|
||||
* command/intel members. Superusers bypass everything.
|
||||
*/
|
||||
async function missionReadFilter(req: PayloadRequest): Promise<boolean | Where> {
|
||||
if (!req.user) return false;
|
||||
if (await isSuperuser(req.payload, req.user)) return true;
|
||||
|
||||
const userId = req.user.id as number;
|
||||
|
||||
const gameRules = await req.payload.findGlobal({ slug: "game-rules" });
|
||||
const commandIds = extractIds(gameRules.commandAssignments as AssignmentRef[] | undefined);
|
||||
const intelIds = extractIds(gameRules.intelAssignments as AssignmentRef[] | undefined);
|
||||
|
||||
const [isCommand, isIntel] = await Promise.all([
|
||||
isInAssignments(req.payload, userId, commandIds),
|
||||
isInAssignments(req.payload, userId, intelIds),
|
||||
]);
|
||||
|
||||
const isLeader = isCommand || isIntel;
|
||||
|
||||
const notDraft = { "ownershipAndStatus.status": { not_in: DRAFT_STATUSES } };
|
||||
|
||||
const or: Where["or"] = [
|
||||
{ "ownershipAndStatus.authors": { in: [userId] } },
|
||||
{ "ownershipAndStatus.zeus": { in: [userId] } },
|
||||
{ and: [notDraft, { "ownershipAndStatus.visibility": { equals: "unit" } }] },
|
||||
];
|
||||
|
||||
if (isLeader) {
|
||||
or.push({ and: [notDraft, { "ownershipAndStatus.visibility": { equals: "leadership" } }] });
|
||||
or.push({ "ownershipAndStatus.status": { in: DRAFT_STATUSES } });
|
||||
}
|
||||
if (isIntel) {
|
||||
or.push({ and: [notDraft, { "ownershipAndStatus.visibility": { equals: "intel" } }] });
|
||||
}
|
||||
|
||||
return { or };
|
||||
}
|
||||
|
||||
export const Missions: CollectionConfig = {
|
||||
slug: "missions",
|
||||
admin: {
|
||||
|
|
@ -71,45 +123,8 @@ export const Missions: CollectionConfig = {
|
|||
delete: async ({ req }) => {
|
||||
return await hasPermission(req.payload, req.user, "missions:delete");
|
||||
},
|
||||
read: async ({ req, data }) => {
|
||||
if (!req.user) return false;
|
||||
if (await hasPermission(req.payload, req.user, "missions:read")) return true;
|
||||
|
||||
const userId = req.user.id;
|
||||
|
||||
const gameRules = await req.payload.findGlobal({ slug: "game-rules" });
|
||||
const commandIds = extractIds(gameRules.commandAssignments as AssignmentRef[] | undefined);
|
||||
const intelIds = extractIds(gameRules.intelAssignments as AssignmentRef[] | undefined);
|
||||
|
||||
const [isCommand, isIntel] = await Promise.all([
|
||||
isInAssignments(req.payload, userId, commandIds),
|
||||
isInAssignments(req.payload, userId, intelIds),
|
||||
]);
|
||||
|
||||
const authorIds = extractIds(data?.ownershipAndStatus?.authors as unknown[] | undefined);
|
||||
const zeusIds = extractIds(data?.ownershipAndStatus?.zeus as unknown[] | undefined);
|
||||
const isAuthorOrZeus = authorIds.includes(userId) || zeusIds.includes(userId);
|
||||
|
||||
const status = data?.ownershipAndStatus?.status;
|
||||
const isDraft = status === "Concept" || status === "Planning";
|
||||
|
||||
if (isDraft) {
|
||||
return isAuthorOrZeus || isCommand || isIntel;
|
||||
}
|
||||
|
||||
const visibility = data?.ownershipAndStatus?.visibility;
|
||||
switch (visibility) {
|
||||
case "unit":
|
||||
return true;
|
||||
case "leadership":
|
||||
return isCommand || isIntel || isAuthorOrZeus;
|
||||
case "intel":
|
||||
return isIntel || isAuthorOrZeus;
|
||||
case "private":
|
||||
return isAuthorOrZeus;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
read: async ({ req }: { req: PayloadRequest }): Promise<boolean | Where> => {
|
||||
return missionReadFilter(req);
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue