bcca7686ee
REQ-001: SSO session via WorkOS (getAuthorizationUrl + exchangeCodeForSession) REQ-002: tenant provisioning on first signup (admin role) REQ-003: invitations via single-use acceptance link REQ-004: RBAC role assignment (admin/operator/viewer) REQ-005: RBAC enforcement at API gateway from first endpoint (/api/me) ---ci--- phase: 2 milestone: v0.1 status: execute ---/ci---
145 lines
5.8 KiB
TypeScript
145 lines
5.8 KiB
TypeScript
/**
|
|
* Auth middleware tests — REQ-005.
|
|
*
|
|
* - extracts the user from the `coreci_session` cookie on a valid session.
|
|
* - returns `unauthorized` when no cookie is present.
|
|
* - returns `unauthorized` when the cookie is invalid/tampered.
|
|
* - returns `forbidden` when the role lacks the required permission.
|
|
* - returns `authorized` for a viewer hitting a read route, `forbidden` for
|
|
* a viewer hitting an admin route.
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
import type { DbClient } from "@coreci/db";
|
|
import { bootTestDb, seedTenantAndUser, type TestDb } from "./helpers.js";
|
|
import { createSession } from "../src/sessions.js";
|
|
import { createAuthMiddleware, authenticate, SESSION_COOKIE, type AuthRequest } from "../src/middleware.js";
|
|
|
|
const SIGNING_KEY = "mw-test-session-signing-key";
|
|
|
|
function reqFor(method: string, path: string, cookie: string | undefined): AuthRequest {
|
|
return {
|
|
method,
|
|
path,
|
|
getCookie(name: string) {
|
|
return name === SESSION_COOKIE ? cookie : undefined;
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("auth middleware (REQ-005)", () => {
|
|
let testDb: TestDb;
|
|
let db: DbClient;
|
|
let tenantId: string;
|
|
let adminUserId: string;
|
|
|
|
beforeAll(async () => {
|
|
testDb = await bootTestDb();
|
|
db = testDb.db;
|
|
const seeded = await seedTenantAndUser(db, "org_mw", "mw-admin@example.com");
|
|
tenantId = seeded.tenantId;
|
|
adminUserId = seeded.userId;
|
|
});
|
|
afterAll(async () => {
|
|
await testDb.teardown();
|
|
});
|
|
|
|
it("returns unauthorized when no cookie is present", async () => {
|
|
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
|
|
const r = await mw(reqFor("GET", "/api/me", undefined));
|
|
expect(r.status).toBe("unauthorized");
|
|
expect(r).toMatchObject({ status: "unauthorized", reason: "no_cookie" });
|
|
});
|
|
|
|
it("returns unauthorized for an invalid/tampered cookie", async () => {
|
|
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
|
|
const r = await mw(reqFor("GET", "/api/me", "garbage.token.value"));
|
|
expect(r.status).toBe("unauthorized");
|
|
expect(r).toMatchObject({ status: "unauthorized", reason: "invalid_session" });
|
|
});
|
|
|
|
it("extracts user + authorizes a viewer on a read route", async () => {
|
|
// Create a viewer session.
|
|
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "viewer", {
|
|
lifetimeSeconds: 60,
|
|
});
|
|
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
|
|
const r = await mw(reqFor("GET", "/api/me", token));
|
|
expect(r.status).toBe("authorized");
|
|
if (r.status === "authorized") {
|
|
expect(r.user.userId).toBe(adminUserId);
|
|
expect(r.user.tenantId).toBe(tenantId);
|
|
expect(r.user.role).toBe("viewer");
|
|
expect(r.decision.required).toBe("read");
|
|
}
|
|
});
|
|
|
|
it("forbids a viewer on an admin route (POST /api/byom)", async () => {
|
|
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "viewer", {
|
|
lifetimeSeconds: 60,
|
|
});
|
|
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
|
|
const r = await mw(reqFor("POST", "/api/byom", token));
|
|
expect(r.status).toBe("forbidden");
|
|
if (r.status === "forbidden") {
|
|
expect(r.decision.required).toBe("admin");
|
|
expect(r.user.role).toBe("viewer");
|
|
}
|
|
});
|
|
|
|
it("authorizes an operator on a read route but forbids admin routes", async () => {
|
|
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "operator", {
|
|
lifetimeSeconds: 60,
|
|
});
|
|
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
|
|
expect((await mw(reqFor("GET", "/api/me", token))).status).toBe("authorized");
|
|
expect((await mw(reqFor("GET", "/api/targets", token))).status).toBe("authorized");
|
|
expect((await mw(reqFor("POST", "/api/byom", token))).status).toBe("forbidden");
|
|
expect((await mw(reqFor("POST", "/api/relay/issue-token", token))).status).toBe("forbidden");
|
|
});
|
|
|
|
it("authorizes an admin on every listed route", async () => {
|
|
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "admin", {
|
|
lifetimeSeconds: 60,
|
|
});
|
|
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
|
|
for (const [method, path] of [
|
|
["GET", "/api/me"],
|
|
["POST", "/api/byom"],
|
|
["GET", "/api/team"],
|
|
["POST", "/api/team"],
|
|
["PATCH", "/api/team/00000000-0000-0000-0000-0000000000aa"],
|
|
["POST", "/api/invitations"],
|
|
["GET", "/api/invitations"],
|
|
["POST", "/api/relay/issue-token"],
|
|
["GET", "/api/targets"],
|
|
] as const) {
|
|
const r = await mw(reqFor(method, path, token));
|
|
expect(r.status, `${method} ${path}`).toBe("authorized");
|
|
}
|
|
});
|
|
|
|
it("role change is enforced on the next API call (REQ-004)", async () => {
|
|
// Start as admin, hit an admin route (allowed).
|
|
const { token, sessionId } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "admin", {
|
|
lifetimeSeconds: 60,
|
|
});
|
|
const mw = createAuthMiddleware(db, { sessionSigningKey: SIGNING_KEY });
|
|
expect((await mw(reqFor("POST", "/api/byom", token))).status).toBe("authorized");
|
|
|
|
// Simulate a role change: update the sessions row role to viewer.
|
|
await db.query("UPDATE sessions SET role = 'viewer' WHERE id = $1", [sessionId]);
|
|
|
|
// The very next call is now forbidden — RBAC enforced from the gateway.
|
|
const r = await mw(reqFor("POST", "/api/byom", token));
|
|
expect(r.status).toBe("forbidden");
|
|
});
|
|
|
|
it("authenticate() standalone helper matches createAuthMiddleware", async () => {
|
|
const { token } = await createSession(db, SIGNING_KEY, adminUserId, tenantId, "admin", {
|
|
lifetimeSeconds: 60,
|
|
});
|
|
const r = await authenticate(db, SIGNING_KEY, reqFor("GET", "/api/me", token));
|
|
expect(r.status).toBe("authorized");
|
|
});
|
|
}); |