import type { LegacyApiKeySimple } from "@toolctrl/db/api-keys.server"; import type { LegacyEntity } from "@toolctrl/db/entities.server"; import { describe, expect, test } from "vitest"; import { createServicePrincipal } from "../auth.js"; import { authorizeApiEntityRowScope, authorizeApiRelationshipScope } from "./permission.js"; import { createStoredApiKeyPrincipal, type ApiScopeResult } from "./scope.js"; const NOW = new Date("2026-01-15T12:00:00.000Z"); function buildEntity(overrides: Partial = {}): LegacyEntity { return { id: "entity-1", name: "contacts", description: null, url: null, multiTenant: true, isPublic: false, useAsUser: false, createdById: null, createdAt: new Date("2026-01-01T00:00:00.000Z"), updatedAt: new Date("2026-01-01T00:00:00.000Z"), ...overrides, }; } function buildApiKey(overrides: Partial = {}): 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, }, { entityId: "child-entity", create: true, read: true, update: false, delete: false, }, ], tenant: { id: "tenant-1", name: "Tenant One", slug: "tenant-one", deactivatedReason: null, }, ...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-value"); } describe("authorizeApiEntityRowScope", () => { test("allows entity row operations when the stored API key has the entity operation scope", () => { const result = authorizeApiEntityRowScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: "tenant-1", entity: buildEntity(), operation: "read", now: NOW, }); expect(result).toEqual({ allowed: true, principalType: "api-key", }); }); test("denies entity row operations when the stored API key lacks the entity operation scope", () => { const result = authorizeApiEntityRowScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: "tenant-1", entity: buildEntity(), operation: "delete", now: NOW, }); expectRedactedDeny(result, "operation_permission_missing"); }); test("fails closed for malformed legacy entities without an id", () => { expect(() => authorizeApiEntityRowScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: "tenant-1", entity: buildEntity({ id: "" }), operation: "read", now: NOW, }), ).toThrow("Entity is missing an id."); }); test("denies stored API keys when tenant scope cannot be resolved", () => { const result = authorizeApiEntityRowScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: null, entity: buildEntity(), operation: "read", now: NOW, }); expectRedactedDeny(result, "tenant_mismatch"); }); }); describe("authorizeApiRelationshipScope", () => { test("allows service-token principals for relationship operations without entity scopes", () => { const result = authorizeApiRelationshipScope(createServicePrincipal(), { tenantId: "tenant-1", parentEntityId: "missing-parent", childEntityId: "missing-child", operation: "delete", }); expect(result).toEqual({ allowed: true, principalType: "service-token", }); }); test("denies relationship operations when the parent entity lacks the requested operation", () => { const result = authorizeApiRelationshipScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: "tenant-1", parentEntityId: "missing-parent", childEntityId: "child-entity", operation: "read", now: NOW, }); expectRedactedDeny(result, "entity_permission_missing"); }); test("denies relationship operations when the child entity lacks the requested operation", () => { const result = authorizeApiRelationshipScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: "tenant-1", parentEntityId: "entity-1", childEntityId: "child-entity", operation: "update", now: NOW, }); expectRedactedDeny(result, "operation_permission_missing"); }); test("allows relationship operations when parent and child entities both include the requested operation", () => { const result = authorizeApiRelationshipScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: "tenant-1", parentEntityId: "entity-1", childEntityId: "child-entity", operation: "create", now: NOW, }); expect(result).toEqual({ allowed: true, principalType: "api-key", }); }); test("denies stored API keys for relationship operations when tenant scope cannot be resolved", () => { const result = authorizeApiRelationshipScope(createStoredApiKeyPrincipal(buildApiKey()), { tenantId: null, parentEntityId: "entity-1", childEntityId: "child-entity", operation: "read", now: NOW, }); expectRedactedDeny(result, "tenant_mismatch"); }); });