forked from EduCraft/curriculum-project-hub
feat: secure organization Feishu credentials
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import Fastify from "fastify";
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mintSessionToken, sessionCookieHeader } from "../../src/admin/routes/authRoutes.js";
|
||||
import { registerAdminPlugin } from "../../src/admin/plugin.js";
|
||||
import { FeishuReadinessError } from "../../src/connections/feishuReadiness.js";
|
||||
import { DEFAULT_ORG_ID, prisma, resetDb, testSecretEnvelope } from "./helpers.js";
|
||||
|
||||
const SESSION_SECRET = "integration-test-session-secret";
|
||||
|
||||
async function buildApp(readinessProbe: (input: {
|
||||
readonly appId: string;
|
||||
readonly appSecret: string;
|
||||
readonly botOpenId: string;
|
||||
}) => Promise<void>) {
|
||||
const app = Fastify({ logger: false });
|
||||
await registerAdminPlugin(app, {
|
||||
prisma,
|
||||
sessionSecret: SESSION_SECRET,
|
||||
publicBaseUrl: "http://127.0.0.1:8788",
|
||||
feishuAppId: "cli_legacy_auth_only",
|
||||
feishuAppSecret: "legacy-auth-secret",
|
||||
projectWorkspaceRoot: "/tmp/cph-test-workspaces",
|
||||
secretEnvelope: testSecretEnvelope,
|
||||
feishuConnectionReadinessProbe: readinessProbe,
|
||||
cookieSecure: false,
|
||||
});
|
||||
await app.ready();
|
||||
return app;
|
||||
}
|
||||
|
||||
async function seedUser(role: "OWNER" | "ADMIN" | "MEMBER"): Promise<string> {
|
||||
const id = `feishu-${role.toLowerCase()}`;
|
||||
const openId = `ou_feishu_${role.toLowerCase()}`;
|
||||
await prisma.user.create({ data: { id, feishuOpenId: openId, displayName: id } });
|
||||
await prisma.organizationMembership.create({
|
||||
data: { organizationId: DEFAULT_ORG_ID, userId: id, role },
|
||||
});
|
||||
return mintSessionToken({ userId: id, feishuOpenId: openId }, SESSION_SECRET);
|
||||
}
|
||||
|
||||
describe("Organization Feishu Application Connection API", () => {
|
||||
beforeEach(async () => {
|
||||
await resetDb();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
it("accepts write-only credentials and returns only redacted metadata", async () => {
|
||||
const token = await seedUser("OWNER");
|
||||
const readiness = vi.fn(async () => {});
|
||||
const app = await buildApp(readiness);
|
||||
const credential = {
|
||||
appId: "cli_customer",
|
||||
appSecret: "api-secret-never-return",
|
||||
botOpenId: "ou_customer_bot",
|
||||
verificationToken: "verification-never-return",
|
||||
encryptKey: "encrypt-key-never-return",
|
||||
};
|
||||
try {
|
||||
const created = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/org/test-default/feishu-application-connection",
|
||||
headers: {
|
||||
cookie: sessionCookieHeader(token),
|
||||
"content-type": "application/json",
|
||||
},
|
||||
payload: credential,
|
||||
});
|
||||
|
||||
expect(created.statusCode).toBe(201);
|
||||
expect(created.json()).toMatchObject({
|
||||
status: "ACTIVE",
|
||||
activeVersion: 1,
|
||||
keyId: "test-active",
|
||||
});
|
||||
for (const plaintext of Object.values(credential)) {
|
||||
expect(created.body).not.toContain(plaintext);
|
||||
}
|
||||
expect(readiness).toHaveBeenCalledWith({
|
||||
appId: credential.appId,
|
||||
appSecret: credential.appSecret,
|
||||
botOpenId: credential.botOpenId,
|
||||
verificationToken: credential.verificationToken,
|
||||
encryptKey: credential.encryptKey,
|
||||
schemaVersion: 1,
|
||||
});
|
||||
|
||||
const read = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/org/test-default/feishu-application-connection",
|
||||
headers: { cookie: sessionCookieHeader(token) },
|
||||
});
|
||||
expect(read.statusCode).toBe(200);
|
||||
expect(read.json()).toEqual({ connection: expect.objectContaining({ activeVersion: 1 }) });
|
||||
for (const plaintext of Object.values(credential)) {
|
||||
expect(read.body).not.toContain(plaintext);
|
||||
}
|
||||
|
||||
const disabled = await app.inject({
|
||||
method: "DELETE",
|
||||
url: "/api/org/test-default/feishu-application-connection",
|
||||
headers: { cookie: sessionCookieHeader(token) },
|
||||
});
|
||||
expect(disabled.statusCode).toBe(200);
|
||||
expect(disabled.json()).toMatchObject({ status: "DISABLED", activeVersion: 1 });
|
||||
for (const plaintext of Object.values(credential)) {
|
||||
expect(disabled.body).not.toContain(plaintext);
|
||||
}
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects members and does not persist credentials rejected by Feishu", async () => {
|
||||
const memberToken = await seedUser("MEMBER");
|
||||
const app = await buildApp(async () => {
|
||||
throw new FeishuReadinessError(
|
||||
"feishu_readiness_rejected",
|
||||
"Feishu bot identity does not match the configured botOpenId",
|
||||
"provider_rejection",
|
||||
);
|
||||
});
|
||||
const payload = {
|
||||
appId: "cli_rejected",
|
||||
appSecret: "rejected-secret",
|
||||
botOpenId: "ou_rejected",
|
||||
};
|
||||
try {
|
||||
const denied = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/org/test-default/feishu-application-connection",
|
||||
headers: {
|
||||
cookie: sessionCookieHeader(memberToken),
|
||||
"content-type": "application/json",
|
||||
},
|
||||
payload,
|
||||
});
|
||||
expect(denied.statusCode).toBe(403);
|
||||
|
||||
const adminToken = await seedUser("ADMIN");
|
||||
const rejected = await app.inject({
|
||||
method: "PUT",
|
||||
url: "/api/org/test-default/feishu-application-connection",
|
||||
headers: {
|
||||
cookie: sessionCookieHeader(adminToken),
|
||||
"content-type": "application/json",
|
||||
},
|
||||
payload,
|
||||
});
|
||||
expect(rejected.statusCode).toBe(400);
|
||||
expect(rejected.json()).toMatchObject({ error: { code: "feishu_credential_rejected" } });
|
||||
await expect(prisma.organizationFeishuApplicationConnection.count()).resolves.toBe(0);
|
||||
await expect(prisma.feishuApplicationCredentialVersion.count()).resolves.toBe(0);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user