forked from EduCraft/curriculum-project-hub
feat: add deployable alpha silo
This commit is contained in:
@@ -9,12 +9,14 @@ import {
|
||||
sessionCookieHeader,
|
||||
} from "../../src/admin/routes/authRoutes.js";
|
||||
import { OAUTH_STATE_COOKIE_NAME, signOAuthState } from "../../src/admin/auth/session.js";
|
||||
import { DEFAULT_ORG_ID, prisma, resetDb, testSecretEnvelope } from "./helpers.js";
|
||||
import { DEFAULT_ORG_ID, prisma, resetDb, seedTestOrganization, testSecretEnvelope } from "./helpers.js";
|
||||
import { FeishuApplicationConnectionService } from "../../src/connections/feishuApplicationConnections.js";
|
||||
import { upsertScopedFeishuIdentity } from "../../src/feishu/identityNamespace.js";
|
||||
|
||||
const SESSION_SECRET = "integration-test-session-secret";
|
||||
const PUBLIC_BASE = "http://127.0.0.1:8788";
|
||||
|
||||
async function buildApp(fetchImpl?: typeof fetch) {
|
||||
async function buildApp(fetchImpl?: typeof fetch, allowLegacyFeishuOAuth = true) {
|
||||
const app = Fastify({ logger: false });
|
||||
await registerAdminPlugin(app, {
|
||||
prisma,
|
||||
@@ -24,6 +26,7 @@ async function buildApp(fetchImpl?: typeof fetch) {
|
||||
feishuAppSecret: "secret_test",
|
||||
projectWorkspaceRoot: "/tmp/cph-test-workspaces",
|
||||
secretEnvelope: testSecretEnvelope,
|
||||
allowLegacyFeishuOAuth,
|
||||
cookieSecure: false,
|
||||
...(fetchImpl !== undefined ? { fetchImpl } : {}),
|
||||
});
|
||||
@@ -186,6 +189,25 @@ describe("admin auth + org API guards", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not expose unscoped OAuth when the explicit compatibility switch is off", async () => {
|
||||
const app = await buildApp(undefined, false);
|
||||
try {
|
||||
const start = await app.inject({ method: "GET", url: "/auth/feishu" });
|
||||
expect(start.statusCode).toBe(404);
|
||||
const nonce = "legacy-disabled";
|
||||
const state = signOAuthState({ nonce, returnTo: "/admin" }, SESSION_SECRET);
|
||||
const callback = await app.inject({
|
||||
method: "GET",
|
||||
url: `/auth/feishu/callback?code=ok&state=${encodeURIComponent(state)}`,
|
||||
headers: { cookie: `${OAUTH_STATE_COOKIE_NAME}=${nonce}` },
|
||||
});
|
||||
expect(callback.statusCode).toBe(302);
|
||||
expect(callback.headers.location).toContain("oauth_failed");
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("OAuth callback upserts user and sets session cookie", async () => {
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
@@ -230,6 +252,94 @@ describe("admin auth + org API guards", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("binds Organization OAuth state, identity, and session to the intended connection", async () => {
|
||||
await seedUser("scoped-owner", "legacy_scoped_owner", "OWNER");
|
||||
const connections = new FeishuApplicationConnectionService(prisma, testSecretEnvelope, async () => {});
|
||||
const connection = await connections.rotateCustomerApplication({
|
||||
organizationId: DEFAULT_ORG_ID,
|
||||
actorUserId: "scoped-owner",
|
||||
appId: "cli_customer_oauth",
|
||||
appSecret: "customer-oauth-secret",
|
||||
botOpenId: "ou_customer_bot",
|
||||
});
|
||||
const identity = await upsertScopedFeishuIdentity(prisma, {
|
||||
connectionId: connection.id,
|
||||
openId: "ou_scoped_user",
|
||||
displayName: "Invited User",
|
||||
});
|
||||
await prisma.organizationMembership.create({
|
||||
data: { organizationId: DEFAULT_ORG_ID, userId: identity.userId, role: "ADMIN" },
|
||||
});
|
||||
await seedTestOrganization("org_scoped_other", "scoped-other");
|
||||
await prisma.organizationMembership.create({
|
||||
data: { organizationId: "org_scoped_other", userId: identity.userId, role: "ADMIN" },
|
||||
});
|
||||
const fetchImpl = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.includes("/oauth/token")) {
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ client_id: "cli_customer_oauth" });
|
||||
return Response.json({ code: 0, access_token: "scoped-user-token" });
|
||||
}
|
||||
if (url.includes("/user_info")) {
|
||||
return Response.json({
|
||||
code: 0,
|
||||
data: {
|
||||
open_id: "ou_scoped_user",
|
||||
union_id: "on_scoped_union",
|
||||
name: "Scoped User",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected ${url}`);
|
||||
});
|
||||
const app = await buildApp(fetchImpl as unknown as typeof fetch);
|
||||
try {
|
||||
const start = await app.inject({
|
||||
method: "GET",
|
||||
url: "/auth/feishu/test-default?returnTo=/admin/org/test-default/settings",
|
||||
});
|
||||
expect(start.statusCode).toBe(302);
|
||||
const authorize = new URL(String(start.headers.location));
|
||||
expect(authorize.searchParams.get("client_id")).toBe("cli_customer_oauth");
|
||||
const state = authorize.searchParams.get("state");
|
||||
expect(state).not.toBeNull();
|
||||
const nonceCookie = cookiePair(start.headers["set-cookie"], OAUTH_STATE_COOKIE_NAME);
|
||||
|
||||
const callback = await app.inject({
|
||||
method: "GET",
|
||||
url: `/auth/feishu/callback?code=ok&state=${encodeURIComponent(state!)}`,
|
||||
headers: { cookie: nonceCookie },
|
||||
});
|
||||
expect(callback.statusCode).toBe(302);
|
||||
expect(callback.headers.location).toBe("/admin/org/test-default/settings");
|
||||
const sessionCookie = cookiePair(callback.headers["set-cookie"], "cph_session");
|
||||
const me = await app.inject({ method: "GET", url: "/api/me", headers: { cookie: sessionCookie } });
|
||||
expect(me.statusCode).toBe(200);
|
||||
expect(me.json()).toMatchObject({
|
||||
user: { id: identity.userId, displayName: "Scoped User" },
|
||||
organizations: [expect.objectContaining({ slug: "test-default", role: "ADMIN" })],
|
||||
});
|
||||
expect((me.json() as { organizations: unknown[] }).organizations).toHaveLength(1);
|
||||
const crossOrganization = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/org/scoped-other",
|
||||
headers: { cookie: sessionCookie },
|
||||
});
|
||||
expect(crossOrganization.statusCode).toBe(403);
|
||||
expect(crossOrganization.json()).toMatchObject({ error: { code: "forbidden" } });
|
||||
await expect(prisma.feishuUserIdentity.findUniqueOrThrow({
|
||||
where: { id: identity.identityId },
|
||||
select: { unionId: true },
|
||||
})).resolves.toEqual({ unionId: "on_scoped_union" });
|
||||
|
||||
await connections.disable({ organizationId: DEFAULT_ORG_ID, actorUserId: "scoped-owner" });
|
||||
const revoked = await app.inject({ method: "GET", url: "/api/me", headers: { cookie: sessionCookie } });
|
||||
expect(revoked.statusCode).toBe(401);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("unknown org slug returns 404 for admin", async () => {
|
||||
await seedUser("u-admin", "ou_admin", "ADMIN");
|
||||
const app = await buildApp();
|
||||
@@ -249,3 +359,10 @@ describe("admin auth + org API guards", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function cookiePair(header: string | string[] | undefined, name: string): string {
|
||||
const values = Array.isArray(header) ? header : [header ?? ""];
|
||||
const cookie = values.find((value) => value.startsWith(`${name}=`));
|
||||
if (cookie === undefined) throw new Error(`missing cookie: ${name}`);
|
||||
return cookie.split(";", 1)[0]!;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user