126 lines
5 KiB
JavaScript
126 lines
5 KiB
JavaScript
|
|
import { test } from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { isTestDbConfigured, setupTestDb } from '../helpers/setup-db.js';
|
||
|
|
import {
|
||
|
|
isAdmin,
|
||
|
|
accessibleProjectIds,
|
||
|
|
projectLevel,
|
||
|
|
assertProjectAccess,
|
||
|
|
} from '../../src/auth/authz.js';
|
||
|
|
|
||
|
|
const SKIP = !isTestDbConfigured() && 'TEST_DATABASE_URL not set';
|
||
|
|
|
||
|
|
// ── isAdmin (pure, no DB) ───────────────────────────────────────────────────
|
||
|
|
test('isAdmin true only for role admin', () => {
|
||
|
|
assert.equal(isAdmin({ role: 'admin' }), true);
|
||
|
|
assert.equal(isAdmin({ role: 'editor' }), false);
|
||
|
|
assert.equal(isAdmin({ role: 'viewer' }), false);
|
||
|
|
assert.equal(isAdmin(null), false);
|
||
|
|
assert.equal(isAdmin(undefined), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Seed helpers shared across the DB-backed cases.
|
||
|
|
async function seed(pool) {
|
||
|
|
const proj = async (name) =>
|
||
|
|
(await pool.query(
|
||
|
|
`INSERT INTO projects (name, s3_prefix) VALUES ($1, $1) RETURNING id`, [name]
|
||
|
|
)).rows[0].id;
|
||
|
|
const user = async (username, role) =>
|
||
|
|
(await pool.query(
|
||
|
|
`INSERT INTO users (username, password_hash, role) VALUES ($1, 'x', $2) RETURNING id`,
|
||
|
|
[username, role]
|
||
|
|
)).rows[0].id;
|
||
|
|
const group = async (name) =>
|
||
|
|
(await pool.query(`INSERT INTO groups (name) VALUES ($1) RETURNING id`, [name])).rows[0].id;
|
||
|
|
const grantUser = (pid, uid, level) =>
|
||
|
|
pool.query(
|
||
|
|
`INSERT INTO project_access (project_id, subject_type, subject_id, level)
|
||
|
|
VALUES ($1, 'user', $2, $3)`, [pid, uid, level]);
|
||
|
|
const grantGroup = (pid, gid, level) =>
|
||
|
|
pool.query(
|
||
|
|
`INSERT INTO project_access (project_id, subject_type, subject_id, level)
|
||
|
|
VALUES ($1, 'group', $2, $3)`, [pid, gid, level]);
|
||
|
|
const addToGroup = (uid, gid) =>
|
||
|
|
pool.query(`INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2)`, [uid, gid]);
|
||
|
|
return { proj, user, group, grantUser, grantGroup, addToGroup };
|
||
|
|
}
|
||
|
|
|
||
|
|
test('admin sees all projects, every project at edit', { skip: SKIP }, async () => {
|
||
|
|
const pool = await setupTestDb();
|
||
|
|
try {
|
||
|
|
const s = await seed(pool);
|
||
|
|
await s.proj('Alpha'); await s.proj('Beta');
|
||
|
|
const admin = { id: await s.user('adm', 'admin'), role: 'admin' };
|
||
|
|
|
||
|
|
const acc = await accessibleProjectIds(admin, pool);
|
||
|
|
assert.equal(acc.all, true);
|
||
|
|
assert.equal(await projectLevel(admin, '00000000-0000-4000-8000-000000000001', pool), 'edit');
|
||
|
|
await assertProjectAccess(admin, '00000000-0000-4000-8000-000000000001', 'edit', pool); // no throw
|
||
|
|
} finally { await pool.end(); }
|
||
|
|
});
|
||
|
|
|
||
|
|
test('direct user grant scopes access and respects level', { skip: SKIP }, async () => {
|
||
|
|
const pool = await setupTestDb();
|
||
|
|
try {
|
||
|
|
const s = await seed(pool);
|
||
|
|
const alpha = await s.proj('Alpha');
|
||
|
|
const beta = await s.proj('Beta');
|
||
|
|
const u = { id: await s.user('bob', 'editor'), role: 'editor' };
|
||
|
|
await s.grantUser(alpha, u.id, 'view');
|
||
|
|
|
||
|
|
const acc = await accessibleProjectIds(u, pool);
|
||
|
|
assert.equal(acc.all, false);
|
||
|
|
assert.deepEqual([...acc.ids], [alpha]);
|
||
|
|
assert.equal(await projectLevel(u, alpha, pool), 'view');
|
||
|
|
assert.equal(await projectLevel(u, beta, pool), null);
|
||
|
|
|
||
|
|
await assertProjectAccess(u, alpha, 'view', pool); // ok
|
||
|
|
await assert.rejects(() => assertProjectAccess(u, alpha, 'edit', pool), e => e.status === 403);
|
||
|
|
await assert.rejects(() => assertProjectAccess(u, beta, 'view', pool), e => e.status === 403);
|
||
|
|
} finally { await pool.end(); }
|
||
|
|
});
|
||
|
|
|
||
|
|
test('group grant flows through membership', { skip: SKIP }, async () => {
|
||
|
|
const pool = await setupTestDb();
|
||
|
|
try {
|
||
|
|
const s = await seed(pool);
|
||
|
|
const alpha = await s.proj('Alpha');
|
||
|
|
const u = { id: await s.user('carol', 'viewer'), role: 'viewer' };
|
||
|
|
const g = await s.group('broadcasters');
|
||
|
|
await s.addToGroup(u.id, g);
|
||
|
|
await s.grantGroup(alpha, g, 'edit');
|
||
|
|
|
||
|
|
assert.equal(await projectLevel(u, alpha, pool), 'edit');
|
||
|
|
const acc = await accessibleProjectIds(u, pool);
|
||
|
|
assert.deepEqual([...acc.ids], [alpha]);
|
||
|
|
await assertProjectAccess(u, alpha, 'edit', pool); // ok via group
|
||
|
|
} finally { await pool.end(); }
|
||
|
|
});
|
||
|
|
|
||
|
|
test('effective level is the max of direct + group grants', { skip: SKIP }, async () => {
|
||
|
|
const pool = await setupTestDb();
|
||
|
|
try {
|
||
|
|
const s = await seed(pool);
|
||
|
|
const alpha = await s.proj('Alpha');
|
||
|
|
const u = { id: await s.user('dan', 'editor'), role: 'editor' };
|
||
|
|
const g = await s.group('team');
|
||
|
|
await s.addToGroup(u.id, g);
|
||
|
|
await s.grantUser(alpha, u.id, 'view'); // direct: view
|
||
|
|
await s.grantGroup(alpha, g, 'edit'); // group: edit → wins
|
||
|
|
|
||
|
|
assert.equal(await projectLevel(u, alpha, pool), 'edit');
|
||
|
|
} finally { await pool.end(); }
|
||
|
|
});
|
||
|
|
|
||
|
|
test('user with no grants sees nothing', { skip: SKIP }, async () => {
|
||
|
|
const pool = await setupTestDb();
|
||
|
|
try {
|
||
|
|
const s = await seed(pool);
|
||
|
|
await s.proj('Alpha');
|
||
|
|
const u = { id: await s.user('eve', 'viewer'), role: 'viewer' };
|
||
|
|
|
||
|
|
const acc = await accessibleProjectIds(u, pool);
|
||
|
|
assert.equal(acc.ids.size, 0);
|
||
|
|
} finally { await pool.end(); }
|
||
|
|
});
|