fix(auth): reject MCP API-key sessions in user auth paths
With the MCP plugin enabled, payload.auth() can return a payload-mcp-api-keys doc instead of a User. Add an isPayloadUser type guard and use it in every server action, page, and route that treats the auth result as a real user, so API-key sessions can no longer pass user auth checks. hasRoles now explicitly requires a users-collection doc.
This commit is contained in:
parent
b70ff4dff8
commit
c0d00fc113
13 changed files with 40 additions and 15 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"use server";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||
|
|
@ -23,7 +24,7 @@ async function authenticate() {
|
|||
const hdrs = await headers();
|
||||
const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false });
|
||||
|
||||
if (!user) {
|
||||
if (!isPayloadUser(user)) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use server";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import type { Ticket } from "@/payload-types";
|
||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
|
|
@ -25,7 +26,7 @@ async function authenticate() {
|
|||
const hdrs = await headers();
|
||||
const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false });
|
||||
|
||||
if (!user) {
|
||||
if (!isPayloadUser(user)) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
|||
import { AppSidebar } from "@/components/frontend/blocks/AppSidebar";
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import { SiteHeader } from "@/components/frontend/SiteHeader";
|
||||
import { Metadata } from "next";
|
||||
|
|
@ -50,7 +51,8 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
|
|||
|
||||
const payloadConfig = await config;
|
||||
const payload = await getPayload({ config });
|
||||
const { user } = await payload.auth({ headers, canSetHeaders: false });
|
||||
const { user: authUser } = await payload.auth({ headers, canSetHeaders: false });
|
||||
const user = isPayloadUser(authUser) ? authUser : null;
|
||||
const impersonating = (await cookies()).has(IMPERSONATION_ACTIVE_COOKIE);
|
||||
|
||||
let xpLevel: ResolvedLevel = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use server";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import type { Asset, Loadout, LockerStorage, User } from "@/payload-types";
|
||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
|
|
@ -39,7 +40,7 @@ async function authenticate() {
|
|||
const hdrs = await headers();
|
||||
const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false });
|
||||
|
||||
if (!user) {
|
||||
if (!isPayloadUser(user)) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import type { Asset, Loadout, LockerStorage } from "@/payload-types";
|
||||
|
|
@ -13,10 +14,11 @@ export const metadata = {
|
|||
|
||||
export default async function LockerPage() {
|
||||
const payload = await getPayload({ config });
|
||||
const { user } = await payload.auth({
|
||||
const { user: authUser } = await payload.auth({
|
||||
headers: await nextHeaders(),
|
||||
canSetHeaders: false,
|
||||
});
|
||||
const user = isPayloadUser(authUser) ? authUser : null;
|
||||
|
||||
const userId = user?.id as number;
|
||||
const locker = await ensureLockerStorage(payload, userId);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use server";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import type { User } from "@/payload-types";
|
||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
|
|
@ -31,7 +32,7 @@ async function authenticate() {
|
|||
canSetHeaders: false,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
if (!isPayloadUser(user)) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import type { BankAccount, BankTransaction, Faction, Resource } from "@/payload-types";
|
||||
|
|
@ -13,10 +14,11 @@ export const metadata = {
|
|||
|
||||
export default async function BankingPage() {
|
||||
const payload = await getPayload({ config });
|
||||
const { user } = await payload.auth({
|
||||
const { user: authUser } = await payload.auth({
|
||||
headers: await nextHeaders(),
|
||||
canSetHeaders: false,
|
||||
});
|
||||
const user = isPayloadUser(authUser) ? authUser : null;
|
||||
|
||||
const accountsRes = await payload.find({
|
||||
collection: "bank-accounts",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"use server";
|
||||
|
||||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import type { Asset, LockerStorage, MarketListing, MarketNegotiation, User } from "@/payload-types";
|
||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
|
|
@ -37,7 +38,7 @@ async function authenticate() {
|
|||
const hdrs = await headers();
|
||||
const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false });
|
||||
|
||||
if (!user) {
|
||||
if (!isPayloadUser(user)) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import { headers as nextHeaders } from "next/headers";
|
||||
import type { Media, Rank, User } from "@/payload-types";
|
||||
|
|
@ -34,7 +35,8 @@ export type RosterAssignment = {
|
|||
export default async function RosterPage() {
|
||||
const headers = await nextHeaders();
|
||||
const payload = await getPayload({ config });
|
||||
const { user: currentUser } = await payload.auth({ headers, canSetHeaders: false });
|
||||
const { user: authUser } = await payload.auth({ headers, canSetHeaders: false });
|
||||
const currentUser = isPayloadUser(authUser) ? authUser : null;
|
||||
|
||||
const [profileRes, assignmentRes] = await Promise.all([
|
||||
payload.find({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import config from "@payload-config";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
import { getPayload } from "payload";
|
||||
import { removePresence, touchPresence } from "@/lib/realtime/presence";
|
||||
|
||||
|
|
@ -20,7 +21,7 @@ export async function POST(req: NextRequest) {
|
|||
const channel = channelFrom(req);
|
||||
if (!channel) return NextResponse.json({ ok: false, error: "Invalid channel." }, { status: 400 });
|
||||
const { user } = await authenticate(req);
|
||||
if (!user) return NextResponse.json({ ok: false }, { status: 401 });
|
||||
if (!isPayloadUser(user)) return NextResponse.json({ ok: false }, { status: 401 });
|
||||
touchPresence(channel, {
|
||||
id: user.id as number,
|
||||
name: user.payloadDisplayName || user.username || `Pilot #${user.id}`,
|
||||
|
|
@ -32,7 +33,7 @@ export async function DELETE(req: NextRequest) {
|
|||
const channel = channelFrom(req);
|
||||
if (!channel) return NextResponse.json({ ok: false, error: "Invalid channel." }, { status: 400 });
|
||||
const { user } = await authenticate(req);
|
||||
if (!user) return NextResponse.json({ ok: false }, { status: 401 });
|
||||
if (!isPayloadUser(user)) return NextResponse.json({ ok: false }, { status: 401 });
|
||||
removePresence(channel, user.id as number);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { CollectionConfig } from "payload";
|
||||
import { requirePermission } from "@/utils/access-control/hasPermission";
|
||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
|
||||
export const Technologies: CollectionConfig = {
|
||||
slug: "technologies",
|
||||
|
|
@ -32,7 +33,9 @@ export const Technologies: CollectionConfig = {
|
|||
],
|
||||
defaultValue: "in_progress",
|
||||
filterOptions: ({ options, req }) => {
|
||||
const roles = (req.user?.roles as string[] | undefined) ?? [];
|
||||
const roles = isPayloadUser(req.user)
|
||||
? ((req.user.roles as string[] | undefined) ?? [])
|
||||
: [];
|
||||
const canReadAll = roles.includes("admin") || roles.includes("developer");
|
||||
if (canReadAll) return options;
|
||||
return options.filter((option) =>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { User } from "@/payload-types";
|
||||
import { PayloadMcpApiKey, User } from "@/payload-types";
|
||||
|
||||
export default function hasRoles(roles: NonNullable<User["roles"]>, user: User | null | undefined) {
|
||||
if (!user) return false;
|
||||
export default function hasRoles(
|
||||
roles: NonNullable<User["roles"]>,
|
||||
user: User | PayloadMcpApiKey | null | undefined,
|
||||
) {
|
||||
if (!user || user.collection !== "users") return false;
|
||||
|
||||
return roles.some((role) => user.roles?.some((urole) => urole === role || urole === "developer"));
|
||||
}
|
||||
|
|
|
|||
5
src/utils/access-control/isPayloadUser.ts
Normal file
5
src/utils/access-control/isPayloadUser.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import type { PayloadMcpApiKey, User } from "@/payload-types";
|
||||
|
||||
export function isPayloadUser(user: User | PayloadMcpApiKey | null | undefined): user is User {
|
||||
return !!user && user.collection === "users";
|
||||
}
|
||||
Loading…
Reference in a new issue