268 lines
7.0 KiB
JavaScript
268 lines
7.0 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
|
|
const db = require('../../src/db/models');
|
|
const AccessPolicy = require('../../src/services/access-policy');
|
|
const AccessPolicyAuditService = require('../../src/services/access-policy-audit');
|
|
|
|
const suffix = `${Date.now()}-${process.pid}`;
|
|
|
|
test.after(async () => {
|
|
await db.sequelize.close();
|
|
});
|
|
|
|
async function authenticateWithTimeout(timeoutMs = 1500) {
|
|
let timeoutId;
|
|
const timeout = new Promise((_, reject) => {
|
|
timeoutId = setTimeout(
|
|
() => reject(new Error(`Database unavailable after ${timeoutMs}ms`)),
|
|
timeoutMs,
|
|
);
|
|
});
|
|
|
|
try {
|
|
await Promise.race([db.sequelize.authenticate(), timeout]);
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
|
|
async function withTransaction(t, callback) {
|
|
try {
|
|
await authenticateWithTimeout();
|
|
} catch (error) {
|
|
t.skip(`Database unavailable: ${error.message}`);
|
|
return;
|
|
}
|
|
|
|
const transaction = await db.sequelize.transaction();
|
|
try {
|
|
await callback(transaction);
|
|
} finally {
|
|
await transaction.rollback();
|
|
}
|
|
}
|
|
|
|
async function createRole(name, transaction) {
|
|
return db.roles.create({ name }, { transaction });
|
|
}
|
|
|
|
async function createPermission(name, transaction) {
|
|
return db.permissions.create({ name }, { transaction });
|
|
}
|
|
|
|
async function createUser({ email, role }, transaction) {
|
|
const user = await db.users.create(
|
|
{
|
|
email,
|
|
password: 'not-used-in-test',
|
|
emailVerified: true,
|
|
},
|
|
{ transaction },
|
|
);
|
|
await user.setApp_role(role, { transaction });
|
|
return user;
|
|
}
|
|
|
|
async function createProject({ slug, visibility }, transaction) {
|
|
return db.projects.create(
|
|
{
|
|
name: `Test ${slug}`,
|
|
slug,
|
|
production_presentation_visibility: visibility,
|
|
},
|
|
{ transaction },
|
|
);
|
|
}
|
|
|
|
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,
|
|
);
|
|
});
|
|
});
|
|
|
|
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(
|
|
{
|
|
userId: publicUser.id,
|
|
projectId: privateProject.id,
|
|
},
|
|
{ transaction },
|
|
);
|
|
|
|
const authUser = await db.users.findOne({
|
|
where: { id: publicUser.id },
|
|
include: [
|
|
{ association: 'app_role', include: [{ association: 'permissions' }] },
|
|
{ association: 'custom_permissions' },
|
|
],
|
|
transaction,
|
|
});
|
|
|
|
assert.equal(
|
|
await AccessPolicy.canViewProductionPresentation(
|
|
authUser.get({ plain: true }),
|
|
privateProject.slug,
|
|
{ transaction },
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
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 = authUser.get({ plain: true });
|
|
|
|
assert.equal(AccessPolicy.canUseAdminApi(plainUser), true);
|
|
assert.equal(
|
|
await AccessPolicy.canViewProductionPresentation(
|
|
plainUser,
|
|
privateProject.slug,
|
|
{ transaction },
|
|
),
|
|
true,
|
|
);
|
|
});
|
|
});
|
|
|
|
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(
|
|
{
|
|
userId: internalUser.id,
|
|
projectId: 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);
|
|
});
|
|
});
|