131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
import type { LegacyApiKeySimple } from "@toolctrl/db/api-keys.server";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import { createServicePrincipal } from "../auth.js";
|
|
import { authorizeApiScope, createStoredApiKeyPrincipal, type ApiScopeResult } from "./scope.js";
|
|
|
|
const NOW = new Date("2026-01-15T12:00:00.000Z");
|
|
|
|
function buildApiKey(overrides: Partial<LegacyApiKeySimple> = {}): LegacyApiKeySimple {
|
|
return {
|
|
id: "api-key-1",
|
|
alias: "Production key",
|
|
expires: new Date("2026-02-01T00:00:00.000Z"),
|
|
tenantId: "tenant-1",
|
|
active: true,
|
|
entities: [
|
|
{
|
|
entityId: "entity-1",
|
|
create: true,
|
|
read: true,
|
|
update: true,
|
|
delete: false,
|
|
},
|
|
],
|
|
tenant: {
|
|
id: "tenant-1",
|
|
name: "Tenant One",
|
|
slug: "tenant-one",
|
|
deactivatedReason: null,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function authorize(apiKey: LegacyApiKeySimple, overrides: Partial<Parameters<typeof authorizeApiScope>[1]> = {}): ApiScopeResult {
|
|
return authorizeApiScope(createStoredApiKeyPrincipal(apiKey), {
|
|
tenantId: "tenant-1",
|
|
entityId: "entity-1",
|
|
operation: "read",
|
|
now: NOW,
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function expectRedactedDeny(result: ApiScopeResult, reason: string) {
|
|
expect(result).toEqual({
|
|
allowed: false,
|
|
principalType: "api-key",
|
|
reason,
|
|
code: "FORBIDDEN_API_KEY_SCOPE",
|
|
});
|
|
const serialized = JSON.stringify(result);
|
|
expect(serialized).not.toContain("plaintext-secret");
|
|
expect(serialized).not.toContain("hash-value");
|
|
expect(serialized).not.toContain("service-token");
|
|
}
|
|
|
|
describe("authorizeApiScope", () => {
|
|
test("allows service-token principals for every scoped operation", () => {
|
|
const principal = createServicePrincipal();
|
|
|
|
expect(
|
|
authorizeApiScope(principal, {
|
|
tenantId: "other-tenant",
|
|
entityId: "unknown-entity",
|
|
operation: "delete",
|
|
now: NOW,
|
|
}),
|
|
).toEqual({
|
|
allowed: true,
|
|
principalType: "service-token",
|
|
});
|
|
});
|
|
|
|
test("allows active stored API keys with matching tenant, entity, and CRUD permission", () => {
|
|
expect(authorize(buildApiKey(), { operation: "create" })).toEqual({
|
|
allowed: true,
|
|
principalType: "api-key",
|
|
});
|
|
});
|
|
|
|
test("denies inactive stored API keys", () => {
|
|
expectRedactedDeny(authorize(buildApiKey({ active: false })), "api_key_inactive");
|
|
});
|
|
|
|
test("denies expired stored API keys", () => {
|
|
expectRedactedDeny(authorize(buildApiKey({ expires: new Date("2026-01-01T00:00:00.000Z") })), "api_key_expired");
|
|
});
|
|
|
|
test("denies stored API keys for deactivated tenants", () => {
|
|
expectRedactedDeny(
|
|
authorize(
|
|
buildApiKey({
|
|
tenant: {
|
|
id: "tenant-1",
|
|
name: "Tenant One",
|
|
slug: "tenant-one",
|
|
deactivatedReason: "billing_issue",
|
|
},
|
|
}),
|
|
),
|
|
"tenant_deactivated",
|
|
);
|
|
});
|
|
|
|
test("denies stored API keys whose tenant does not match the request tenant", () => {
|
|
expectRedactedDeny(authorize(buildApiKey(), { tenantId: "tenant-2" }), "tenant_mismatch");
|
|
expectRedactedDeny(
|
|
authorize(
|
|
buildApiKey({
|
|
tenant: {
|
|
id: "tenant-2",
|
|
name: "Tenant Two",
|
|
slug: "tenant-two",
|
|
deactivatedReason: null,
|
|
},
|
|
}),
|
|
),
|
|
"tenant_mismatch",
|
|
);
|
|
});
|
|
|
|
test("denies stored API keys missing the requested entity permission", () => {
|
|
expectRedactedDeny(authorize(buildApiKey(), { entityId: "entity-2" }), "entity_permission_missing");
|
|
});
|
|
|
|
test("denies stored API keys missing the requested CRUD operation permission", () => {
|
|
expectRedactedDeny(authorize(buildApiKey(), { operation: "delete" }), "operation_permission_missing");
|
|
});
|
|
});
|