feat: Implement Phase 1 Event & Session Overhaul (Guest Ingress & Audits)

- Allowed guest accounts to be evaluated in `forward-auth`
- Validated `guest` account's `customScopes` and rejected ungranted access
- Added Array parameterization and `UNION` query in `getDashboardApps`
- Mapped `customScopes` to `getDashboardApps` in the UI route `/dashboard`
- Wired web and CLI joins in `events.ts` to `auditWrapper.auditLog` using correct schema (`event.id`, `{slug, method}`)
- Added `auditWrapper.auditLog` unit test validations in `events.test.ts`
- Added guest session scope unit tests in `forward_auth.test.ts`
- Moved Markdown tasks logic from `tasks/new/` to `tasks/complete/`

Co-authored-by: mrteye <1945243+mrteye@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot] 2026-08-26 23:34:00 +00:00
parent e88512edad
commit eed5c8a0fd
7 changed files with 214 additions and 19 deletions

View File

@ -117,11 +117,35 @@ forwardAuthRoutes.get("/api/forward-auth", async (c) => {
WHERE id = ${auth.userId}
`.then((res: any) => res[0]);
if (!user || user.account_status !== "active") {
if (
!user ||
(user.account_status !== "active" && user.account_status !== "guest")
) {
return c.text("Forbidden: Account inactive", 403);
}
// 3. Resolve Grants and Roles
let scopes = "";
if (
user.account_status === "guest" ||
(auth.customScopes && auth.customScopes.length > 0)
) {
// Guest or delegated session handling
const customScopes = auth.customScopes || [];
const hasAppScope = customScopes.includes(`app:${appRecord.name}`) ||
customScopes.includes("*");
if (!hasAppScope) {
return c.text(
"Forbidden: Access denied to this application (Guest/Delegated)",
403,
);
}
scopes = customScopes.filter(Boolean).join(",") || "viewer";
} else {
// Standard user handling
const globalAdmin = await isGlobalAdmin(auth.userId);
const grantRole = await getUserGrant(auth.userId, appRecord.id);
@ -129,9 +153,10 @@ forwardAuthRoutes.get("/api/forward-auth", async (c) => {
return c.text("Forbidden: Access denied to this application", 403);
}
const scopes = [
scopes = [
...new Set([grantRole, globalAdmin ? "admin" : null].filter(Boolean)),
].join(",");
}
// 4. Inject Headers
c.header("X-Forwarded-User", user.username);

View File

@ -8,6 +8,8 @@ import {
getCookieDomain,
hasScope,
} from "../auth-session.ts";
import { getClientIp } from "../middleware.ts";
import { auditWrapper } from "../audit.ts";
import { EventJoinPage } from "../../ui/components/EventJoinPage.tsx";
import { EventSplashPage } from "../../ui/components/EventSplashPage.tsx";
@ -243,6 +245,13 @@ eventRoutes.post("/api/join", async (c) => {
}),
);
auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, {
slug: event.slug,
name: event.name,
seatNumber: event.seats_claimed,
method: "web",
}, getClientIp(c));
deleteCookie(c, "session_id", { path: "/" });
const rpID = Deno.env.get("RP_ID");
const cookieDomain = getCookieDomain(rpID);
@ -343,6 +352,13 @@ eventRoutes.get("/join/:slug", async (c) => {
}),
);
auditWrapper.auditLog(guestUuid, "event_seat_claimed", event.id, {
slug: event.slug,
name: event.name,
seatNumber: event.seats_claimed,
method: "cli",
}, getClientIp(c));
if (format === "env") {
return c.text(
`export AUTH_YES_TOKEN="${sessionId}"\nexport AUTH_YES_USER="${username}"\n`,

View File

@ -93,10 +93,27 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
() => Promise.resolve("OK" as any),
);
const { auditWrapper } = await import("../audit.ts");
let auditCalled = false;
let auditPayload: any = null;
const auditStub = stub(
auditWrapper,
"auditLog",
(_userId, action, resource, metadata, _ip) => {
if (action === "event_seat_claimed") {
auditCalled = true;
auditPayload = { resource, metadata };
}
},
);
try {
const res = await app.request("/api/join", {
method: "POST",
headers: { "Content-Type": "application/json" },
headers: {
"Content-Type": "application/json",
"X-Forwarded-For": "192.168.1.1",
},
body: JSON.stringify({ code: "749-123" }),
});
@ -109,9 +126,15 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
const cookies = res.headers.get("set-cookie");
assertExists(cookies);
assert(cookies.includes(`session_id=${json.token};`));
assert(auditCalled);
assertEquals(auditPayload.resource, "event-uuid-1");
assertEquals(auditPayload.metadata.slug, "deno-lab");
assertEquals(auditPayload.metadata.method, "web");
} finally {
sqlWrapper.sql = originalSql;
valkeySetexStub.restore();
auditStub.restore();
}
},
);
@ -145,18 +168,39 @@ Deno.test("Multi-Claim Event Passes & Join Endpoints", async (t) => {
() => Promise.resolve("OK" as any),
);
const { auditWrapper } = await import("../audit.ts");
let auditCalled = false;
let auditPayload: any = null;
const auditStub = stub(
auditWrapper,
"auditLog",
(_userId, action, resource, metadata, _ip) => {
if (action === "event_seat_claimed") {
auditCalled = true;
auditPayload = { resource, metadata };
}
},
);
try {
const res = await app.request("/join/deno-lab?format=env", {
method: "GET",
headers: { "X-Forwarded-For": "192.168.1.2" },
});
assertEquals(res.status, 200);
const text = await res.text();
assert(text.includes('export AUTH_YES_TOKEN="ay_sess_'));
assert(text.includes('export AUTH_YES_USER="guest_deno-lab_2"'));
assert(auditCalled);
assertEquals(auditPayload.resource, "event-uuid-1");
assertEquals(auditPayload.metadata.slug, "deno-lab");
assertEquals(auditPayload.metadata.method, "cli");
} finally {
sqlWrapper.sql = originalSql;
valkeySetexStub.restore();
auditStub.restore();
}
},
);

View File

@ -268,6 +268,94 @@ Deno.test("Tier 1 & 2: GET /api/forward-auth - Dynamic Bypass (bypass_paths)", a
restoreMockSql();
});
Deno.test("Tier 1 & 2: GET /api/forward-auth - Guest session with app scope allowed", async () => {
const mockUser = {
id: "guest-id-1",
username: "guest_event_1",
account_status: "guest",
};
setMockSql(() => Promise.resolve([mockUser]));
const valkeyStub = stub(valkey, "get", (key: any) => {
const k = String(key);
if (k.startsWith("auth:app_by_host:")) {
return Promise.resolve(
JSON.stringify({ id: "app-id-1", name: "test-app" }),
);
}
return Promise.resolve(
JSON.stringify({
uuid: "guest-id-1",
username: "guest_event_1",
account_status: "guest",
customScopes: ["app:test-app", "viewer"],
}),
);
});
const req = new Request("http://localhost/api/forward-auth", {
headers: {
Cookie: "session_id=mock-guest-session",
"X-Forwarded-Host": "test.app.local",
},
});
const res = await app.request(req);
assertEquals(res.status, 200);
assertEquals(res.headers.get("X-Forwarded-User"), "guest_event_1");
assertEquals(res.headers.get("X-Forwarded-User-Id"), "guest-id-1");
assertEquals(res.headers.get("X-Forwarded-Scopes"), "app:test-app,viewer");
assertEquals(res.headers.get("X-Forwarded-App-Id"), "app-id-1");
restoreMockSql();
valkeyStub.restore();
});
Deno.test("Tier 1 & 2: GET /api/forward-auth - Guest session without app scope rejected (403)", async () => {
const mockUser = {
id: "guest-id-1",
username: "guest_event_1",
account_status: "guest",
};
setMockSql(() => Promise.resolve([mockUser]));
const valkeyStub = stub(valkey, "get", (key: any) => {
const k = String(key);
if (k.startsWith("auth:app_by_host:")) {
return Promise.resolve(
JSON.stringify({ id: "app-id-1", name: "test-app" }),
);
}
return Promise.resolve(
JSON.stringify({
uuid: "guest-id-1",
username: "guest_event_1",
account_status: "guest",
customScopes: ["app:other-app", "viewer"],
}),
);
});
const req = new Request("http://localhost/api/forward-auth", {
headers: {
Cookie: "session_id=mock-guest-session",
"X-Forwarded-Host": "test.app.local",
},
});
const res = await app.request(req);
assertEquals(res.status, 403);
assertEquals(
await res.text(),
"Forbidden: Access denied to this application (Guest/Delegated)",
);
restoreMockSql();
valkeyStub.restore();
});
Deno.test("Tier 1 & 2: POST /api/guests/sandbox - Creates guest session", async () => {
const { valkey } = await import("../valkey.ts");
// Mock valkey.setex to prevent connection errors during tests

View File

@ -1,6 +1,10 @@
import { sql } from "../server/db.ts";
export async function getDashboardApps(userId: string, isAdmin: boolean) {
export async function getDashboardApps(
userId: string,
isAdmin: boolean,
customScopes?: string[],
) {
if (isAdmin) {
return await sql`
SELECT id, name, description, domain, 'Admin' as role
@ -8,6 +12,23 @@ export async function getDashboardApps(userId: string, isAdmin: boolean) {
WHERE domain IS NOT NULL
ORDER BY name ASC
` as any[];
} else {
const appNames = (customScopes || [])
.filter((s) => s.startsWith("app:"))
.map((s) => s.split(":")[1]);
if (appNames.length > 0) {
return await sql`
SELECT a.id, a.name, a.description, a.domain, g.role
FROM apps a
JOIN grants g ON a.id = g.app_id
WHERE g.user_id = ${userId} AND a.domain IS NOT NULL
UNION
SELECT id, name, description, domain, 'Guest (Viewer)' as role
FROM apps
WHERE domain IS NOT NULL AND name = ANY(${appNames}::text[])
ORDER BY name ASC
` as any[];
} else {
return await sql`
SELECT a.id, a.name, a.description, a.domain, g.role
@ -18,6 +39,7 @@ export async function getDashboardApps(userId: string, isAdmin: boolean) {
` as any[];
}
}
}
export async function getSessionApps() {
return await sql`

View File

@ -151,7 +151,7 @@ uiApp.get("/dashboard", async (c) => {
if (authRes instanceof Response) return authRes;
const { auth, isAdmin } = authRes;
const apps = await getDashboardApps(auth.userId, isAdmin);
const apps = await getDashboardApps(auth.userId, isAdmin, auth.customScopes);
return c.html(AppLaunchpadPage({ apps: apps as any, isAdmin }));
});