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 type { CollectionConfig, Payload, PayloadRequest, Where } from "payload";
|
||||||
import { requirePermission, hasPermission } from "@/utils/access-control/hasPermission";
|
import { requirePermission, hasPermission, isSuperuser } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
type AssignmentRef = { id: number } | number;
|
type AssignmentRef = { id: number } | number;
|
||||||
|
|
||||||
|
const DRAFT_STATUSES = ["Concept", "Planning"];
|
||||||
|
|
||||||
async function isInAssignments(
|
async function isInAssignments(
|
||||||
payload: Payload,
|
payload: Payload,
|
||||||
userId: number,
|
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 = {
|
export const Missions: CollectionConfig = {
|
||||||
slug: "missions",
|
slug: "missions",
|
||||||
admin: {
|
admin: {
|
||||||
|
|
@ -71,45 +123,8 @@ export const Missions: CollectionConfig = {
|
||||||
delete: async ({ req }) => {
|
delete: async ({ req }) => {
|
||||||
return await hasPermission(req.payload, req.user, "missions:delete");
|
return await hasPermission(req.payload, req.user, "missions:delete");
|
||||||
},
|
},
|
||||||
read: async ({ req, data }) => {
|
read: async ({ req }: { req: PayloadRequest }): Promise<boolean | Where> => {
|
||||||
if (!req.user) return false;
|
return missionReadFilter(req);
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue