39948-vm/backend/tests/integration/access-policy.test.ts
2026-07-01 15:45:38 +02:00

364 lines
9.3 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import type { TestContext } from 'node:test';
import type { Transaction } from 'sequelize';
import db from '../../src/db/models/index.ts';
import AccessPolicy from '../../src/services/access-policy.ts';
import AccessPolicyAuditService from '../../src/services/access-policy-audit.ts';
import type {
CurrentUser,
PermissionRecord,
ProjectCreatePayload,
ProjectModelRecord,
ProductionPresentationVisibility,
RoleModelRecord,
UserModelRecord,
UserProductionPresentationAccessPayload,
UserRecord,
} from '../../src/types/index.ts';
const suffix = `${Date.now()}-${process.pid}`;
void test.after(async () => {
await db.sequelize.close();
});
async function authenticateWithTimeout(timeoutMs = 1500): Promise<void> {
let timeoutId: NodeJS.Timeout | undefined;
const timeout = new Promise<never>((_, reject) => {
timeoutId = setTimeout(
() => reject(new Error(`Database unavailable after ${timeoutMs}ms`)),
timeoutMs,
);
});
try {
await Promise.race([db.sequelize.authenticate(), timeout]);
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
}
}
async function withTransaction(
t: TestContext,
callback: (transaction: Transaction) => Promise<void>,
): Promise<void> {
try {
await authenticateWithTimeout();
} catch (error) {
const message = error instanceof Error ? error.message : 'unknown error';
t.skip(`Database unavailable: ${message}`);
return;
}
const transaction = await db.sequelize.transaction();
try {
await callback(transaction);
} finally {
await transaction.rollback();
}
}
async function createRole(
name: string,
transaction: Transaction,
): Promise<RoleModelRecord> {
return db.roles.create({ name }, { transaction });
}
function createPermission(
name: string,
transaction: Transaction,
): Promise<PermissionRecord> {
return db.permissions.create({ name }, { transaction });
}
interface CreateUserOptions {
email: string;
role: RoleModelRecord;
}
async function createUser(
{ email, role }: CreateUserOptions,
transaction: Transaction,
): Promise<UserModelRecord> {
const user = await db.users.create(
{
email,
password: 'not-used-in-test',
emailVerified: true,
},
{ transaction },
);
await user.setApp_role(role, { transaction });
return user;
}
interface CreateProjectOptions {
slug: string;
visibility: ProductionPresentationVisibility;
}
async function createProject(
{ slug, visibility }: CreateProjectOptions,
transaction: Transaction,
): Promise<ProjectModelRecord> {
const data: ProjectCreatePayload = {
id: undefined,
name: `Test ${slug}`,
slug,
description: undefined,
logo_url: undefined,
favicon_url: undefined,
og_image_url: undefined,
design_width: undefined,
design_height: undefined,
production_presentation_visibility: visibility,
importHash: null,
createdById: null,
updatedById: null,
};
return db.projects.create(data, { transaction });
}
function requireUserModelRecord(
user: UserModelRecord | null,
): UserModelRecord {
if (!user) {
throw new Error('Expected test user to exist.');
}
return user;
}
function toCurrentUser(user: UserRecord): CurrentUser {
const currentUser: CurrentUser = { id: user.id };
if (typeof user.email === 'string') {
currentUser.email = user.email;
}
if (user.app_role !== undefined) {
currentUser.app_role = user.app_role;
}
if (user.custom_permissions !== undefined) {
currentUser.custom_permissions = user.custom_permissions;
}
if (user.app_role_permissions !== undefined) {
currentUser.app_role_permissions = user.app_role_permissions;
}
return currentUser;
}
function createProductionAccessPayload(
userId: string,
projectId: string,
): UserProductionPresentationAccessPayload {
const now = new Date();
return {
userId,
projectId,
createdAt: now,
updatedAt: now,
createdById: null,
updatedById: null,
};
}
void test('guest can view public production presentation only', async (t) => {
await withTransaction(t, async (transaction) => {
const publicProject = await createProject(
{
slug: `test-public-runtime-access-${suffix}`,
visibility: 'public',
},
transaction,
);
const privateProject = await createProject(
{
slug: `test-private-runtime-access-${suffix}`,
visibility: 'private',
},
transaction,
);
assert.equal(
await AccessPolicy.canViewProductionPresentation(
null,
publicProject.slug,
{ transaction },
),
true,
);
assert.equal(
await AccessPolicy.canViewProductionPresentation(
null,
privateProject.slug,
{ transaction },
),
false,
);
});
});
void test('public user can view granted private production presentation', async (t) => {
await withTransaction(t, async (transaction) => {
const publicRole = await createRole('Public', transaction);
const publicUser = await createUser(
{
email: `public-granted-${suffix}@example.test`,
role: publicRole,
},
transaction,
);
const privateProject = await createProject(
{
slug: `test-private-granted-runtime-access-${suffix}`,
visibility: 'private',
},
transaction,
);
await db.production_presentation_access.create(
createProductionAccessPayload(publicUser.id, privateProject.id),
{ transaction },
);
const authUser = await db.users.findOne({
where: { id: publicUser.id },
include: [
{ association: 'app_role', include: [{ association: 'permissions' }] },
{ association: 'custom_permissions' },
],
transaction,
});
const authUserRecord = requireUserModelRecord(authUser);
assert.equal(
await AccessPolicy.canViewProductionPresentation(
toCurrentUser(authUserRecord.get({ plain: true })),
privateProject.slug,
{ transaction },
),
true,
);
});
});
void test('internal user with permission can use admin api and view private presentation', async (t) => {
await withTransaction(t, async (transaction) => {
const role = await createRole('Content Reviewer', transaction);
const permission = await createPermission(
`TEST_READ_PROJECTS_${suffix}`,
transaction,
);
await role.setPermissions([permission], { transaction });
const internalUser = await createUser(
{
email: `internal-access-${suffix}@example.test`,
role,
},
transaction,
);
const privateProject = await createProject(
{
slug: `test-private-internal-runtime-access-${suffix}`,
visibility: 'private',
},
transaction,
);
const authUser = await db.users.findOne({
where: { id: internalUser.id },
include: [
{ association: 'app_role', include: [{ association: 'permissions' }] },
{ association: 'custom_permissions' },
],
transaction,
});
const plainUser = toCurrentUser(
requireUserModelRecord(authUser).get({ plain: true }),
);
assert.equal(AccessPolicy.canUseAdminApi(plainUser), true);
assert.equal(
await AccessPolicy.canViewProductionPresentation(
plainUser,
privateProject.slug,
{ transaction },
),
true,
);
});
});
void test('audit finds and cleanup removes stale Public grants', async (t) => {
await withTransaction(t, async (transaction) => {
const publicRole = await createRole('Public', transaction);
const internalRole = await createRole('Tour Designer', transaction);
const permission = await createPermission(
`TEST_READ_USERS_${suffix}`,
transaction,
);
await publicRole.setPermissions([permission], { transaction });
const publicUser = await createUser(
{
email: `public-stale-${suffix}@example.test`,
role: publicRole,
},
transaction,
);
await publicUser.setCustom_permissions([permission], { transaction });
const internalUser = await createUser(
{
email: `internal-stale-grant-${suffix}@example.test`,
role: internalRole,
},
transaction,
);
const privateProject = await createProject(
{
slug: `test-private-stale-grant-${suffix}`,
visibility: 'private',
},
transaction,
);
await db.production_presentation_access.create(
createProductionAccessPayload(internalUser.id, privateProject.id),
{ transaction },
);
const report = await AccessPolicyAuditService.findViolations({
transaction,
});
assert.ok(report.publicRolePermissions.length >= 1);
assert.ok(report.publicUsersWithCustomPermissions.length >= 1);
assert.ok(report.productionPresentationAccessForNonPublicUsers.length >= 1);
const cleanup = await AccessPolicyAuditService.cleanupViolations({
transaction,
});
assert.ok(cleanup.removedPublicRolePermissions >= 1);
assert.ok(cleanup.clearedPublicUserCustomPermissions >= 1);
assert.ok(cleanup.removedNonPublicProductionPresentationGrants >= 1);
const after = await AccessPolicyAuditService.findViolations({
transaction,
});
assert.equal(AccessPolicyAuditService.hasViolations(after), false);
});
});