Files
HiveGrid-Wireframes/hivegrid-cloud/apps/api/src/auth/permission.test.ts
T

188 lines
5.5 KiB
TypeScript

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> = {}): 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> = {}): 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");
});
});