Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
693 lines
29 KiB
JavaScript
693 lines
29 KiB
JavaScript
// Space Access Control Tests
|
|
// Comprehensive tests for checkUserSpaceAccess, checkUserAccessViaTableV2,
|
|
// getSpacesByUser filtering, and user_access_permissions
|
|
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
import { dbAll, dbGet, dbRun, destroyAdapter, resetAdapter } from '../database/connection.js';
|
|
import {
|
|
checkUserSpaceAccess,
|
|
checkUserAccessViaTableV2,
|
|
getUserAccessData,
|
|
canAccessSpace,
|
|
canAccessProject,
|
|
canAccessTable,
|
|
canAccessColumn,
|
|
} from '../services/space/access.js';
|
|
import { getSpacesByUser } from '../services/space/crud.js';
|
|
|
|
// ─── ID mapping: use 90000+ range to avoid collisions with production data ───
|
|
// Users: 90001 (owner), 90002 (admin), 90010 (regular), 90011 (vika)
|
|
// Spaces: 90001, 90011, 90033, 90035, 90050, 90060, 90070
|
|
// Tables: 90100, 90200, 90300, 90400, 90500, 90600
|
|
|
|
const U_OWNER = 90001;
|
|
const U_ADMIN = 90002;
|
|
const U_REGULAR = 90010;
|
|
const U_VIKA = 90011;
|
|
|
|
const ALL_USER_IDS = [U_OWNER, U_ADMIN, U_REGULAR, U_VIKA];
|
|
const ALL_SPACE_IDS = [90001, 90011, 90033, 90035, 90050, 90060, 90070];
|
|
const ALL_TABLE_IDS = [90100, 90200, 90300, 90400, 90500, 90600];
|
|
|
|
// ─── Minimal schema setup ───
|
|
|
|
async function setupMinimalSchema() {
|
|
await resetAdapter();
|
|
// Tables already exist in PostgreSQL — just clean up previous test data
|
|
await cleanupTestData();
|
|
}
|
|
|
|
async function cleanupTestData() {
|
|
try {
|
|
await dbRun(`DELETE FROM user_access_permissions WHERE user_id IN (${ALL_USER_IDS.join(',')})`);
|
|
await dbRun(`DELETE FROM table_rows WHERE base_id LIKE 'test-%'`);
|
|
for (const tid of ALL_TABLE_IDS) {
|
|
await dbRun(`DELETE FROM table_columns WHERE table_id = $1`, [tid]);
|
|
await dbRun(`DELETE FROM universal_tables WHERE id = $1`, [tid]);
|
|
await dbRun(`DELETE FROM projects WHERE id = $1`, [tid]);
|
|
}
|
|
for (const sid of ALL_SPACE_IDS) {
|
|
await dbRun(`DELETE FROM dashboards WHERE space_id = $1`, [sid]);
|
|
}
|
|
await dbRun(`DELETE FROM spaces WHERE id IN (${ALL_SPACE_IDS.join(',')})`);
|
|
await dbRun(`DELETE FROM users WHERE id IN (${ALL_USER_IDS.join(',')})`);
|
|
} catch (e) {
|
|
// Ignore cleanup errors
|
|
}
|
|
}
|
|
|
|
// ─── Helpers ───
|
|
|
|
let _rowCounter = 1;
|
|
|
|
async function createUser(id, email, role = 'user') {
|
|
await dbRun(
|
|
`INSERT INTO users (id, email, name, role, password_hash, encryption_key_encrypted)
|
|
VALUES (?, ?, ?, ?, 'hash', 'none')
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
[id, email, email.split('@')[0], role]
|
|
);
|
|
}
|
|
|
|
async function createSpaceRow(id, name, ownerId, type = 'business', visibility = 'internal', accessControl = null) {
|
|
await dbRun(
|
|
`INSERT INTO spaces (id, owner_id, name, type, visibility, access_control)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[id, ownerId, name, type, visibility, accessControl ? JSON.stringify(accessControl) : null]
|
|
);
|
|
await dbRun(
|
|
`INSERT INTO dashboards (space_id, name, icon, is_default, order_index)
|
|
VALUES (?, ?, '📊', 1, 0)`,
|
|
[id, `${name} Overview`]
|
|
);
|
|
}
|
|
|
|
async function createUsersTable(tableId) {
|
|
// Need a project for the table
|
|
await dbRun(`INSERT INTO projects (id, name, type, owner_id) VALUES (?, 'TestProject', 'default', $2) ON CONFLICT (id) DO NOTHING`, [tableId, U_OWNER]);
|
|
await dbRun(`INSERT INTO universal_tables (id, project_id, name) VALUES (?, ?, 'Access Users')`, [tableId, tableId]);
|
|
|
|
const columns = [
|
|
[tableId * 10 + 1, 'system_user_id'],
|
|
[tableId * 10 + 2, 'email'],
|
|
[tableId * 10 + 3, 'name'],
|
|
[tableId * 10 + 4, 'role'],
|
|
[tableId * 10 + 5, 'active'],
|
|
[tableId * 10 + 6, 'allowed_spaces'],
|
|
[tableId * 10 + 7, 'denied_spaces'],
|
|
[tableId * 10 + 8, 'allowed_projects'],
|
|
[tableId * 10 + 9, 'denied_projects'],
|
|
];
|
|
for (const [colId, colName] of columns) {
|
|
await dbRun(
|
|
`INSERT INTO table_columns (id, table_id, column_name, type)
|
|
VALUES (?, ?, ?, 'text')`,
|
|
[colId, tableId, colName]
|
|
);
|
|
}
|
|
}
|
|
|
|
async function addUserRow(tableId, userId, email, role, active = true, extras = {}) {
|
|
const colPrefix = tableId * 10;
|
|
const data = {
|
|
system_user_id: userId,
|
|
email,
|
|
name: email.split('@')[0],
|
|
role,
|
|
active,
|
|
allowed_spaces: [],
|
|
denied_spaces: [],
|
|
allowed_projects: [],
|
|
denied_projects: [],
|
|
...extras,
|
|
};
|
|
// Also store by column ID (so both name and id lookups work)
|
|
data[String(colPrefix + 1)] = userId;
|
|
data[String(colPrefix + 2)] = email;
|
|
data[String(colPrefix + 4)] = role;
|
|
data[String(colPrefix + 5)] = active;
|
|
|
|
const baseId = `test-${tableId}-${userId}-${_rowCounter++}`;
|
|
await dbRun(
|
|
`INSERT INTO table_rows (table_id, base_id, data) VALUES (?, ?, ?)`,
|
|
[tableId, baseId, JSON.stringify(data)]
|
|
);
|
|
}
|
|
|
|
async function grantPermission(userId, spaceId, accessLevel, grantedBy = U_OWNER) {
|
|
await dbRun(
|
|
`INSERT INTO user_access_permissions (user_id, space_id, access_level, granted_by)
|
|
VALUES (?, ?, ?, ?)`,
|
|
[userId, spaceId, accessLevel, grantedBy]
|
|
);
|
|
}
|
|
|
|
// ─── Tests ───
|
|
|
|
describe('Space Access Control', () => {
|
|
beforeEach(async () => {
|
|
_rowCounter = 1;
|
|
await setupMinimalSchema();
|
|
await createUser(U_OWNER, 'owner@test.com', 'owner');
|
|
await createUser(U_ADMIN, 'admin@test.com', 'admin');
|
|
await createUser(U_REGULAR, 'regular@test.com', 'user');
|
|
await createUser(U_VIKA, 'vika@test.com', 'user');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTestData();
|
|
await destroyAdapter();
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 1. checkUserSpaceAccess — basic rules
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('checkUserSpaceAccess — basic rules', () => {
|
|
test('owner always has access', async () => {
|
|
const space = { id: 90001, owner_id: U_REGULAR, type: 'business', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_REGULAR, 'user', space, null)).toBe(true);
|
|
});
|
|
|
|
test('non-owner without access_control is denied', async () => {
|
|
const space = { id: 90001, owner_id: U_OWNER, type: 'business', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_REGULAR, 'user', space, null)).toBe(false);
|
|
});
|
|
|
|
test('admin role can see admin spaces', async () => {
|
|
const space = { id: 90001, owner_id: U_OWNER, type: 'admin', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_ADMIN, 'admin', space, null)).toBe(true);
|
|
});
|
|
|
|
test('owner role can see admin spaces', async () => {
|
|
const space = { id: 90001, owner_id: 99999, type: 'admin', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_OWNER, 'owner', space, null)).toBe(true);
|
|
});
|
|
|
|
test('regular user cannot see admin spaces', async () => {
|
|
const space = { id: 90001, owner_id: U_OWNER, type: 'admin', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_REGULAR, 'user', space, null)).toBe(false);
|
|
});
|
|
|
|
test('personal spaces — only owner can access', async () => {
|
|
const space = { id: 90001, owner_id: U_REGULAR, type: 'personal', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_REGULAR, 'user', space, null)).toBe(true);
|
|
expect(await checkUserSpaceAccess(U_VIKA, 'user', space, null)).toBe(false);
|
|
// Even admin cannot see other's personal space
|
|
expect(await checkUserSpaceAccess(U_ADMIN, 'admin', space, null)).toBe(false);
|
|
});
|
|
|
|
test('open visibility — any authenticated user has access', async () => {
|
|
const space = { id: 90001, owner_id: U_OWNER, type: 'business', visibility: 'open' };
|
|
expect(await checkUserSpaceAccess(U_REGULAR, 'user', space, null)).toBe(true);
|
|
expect(await checkUserSpaceAccess(U_VIKA, 'user', space, null)).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 2. user_access_permissions — explicit grants
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('checkUserSpaceAccess — explicit permissions (user_access_permissions)', () => {
|
|
test('explicit admin grant gives access', async () => {
|
|
await grantPermission(U_VIKA, 90035, 'admin');
|
|
const space = { id: 90035, owner_id: U_OWNER, type: 'business', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_VIKA, 'user', space, null)).toBe(true);
|
|
});
|
|
|
|
test('explicit viewer grant gives access', async () => {
|
|
await grantPermission(U_VIKA, 90035, 'viewer');
|
|
const space = { id: 90035, owner_id: U_OWNER, type: 'business', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_VIKA, 'user', space, null)).toBe(true);
|
|
});
|
|
|
|
test('explicit denied blocks access', async () => {
|
|
await grantPermission(U_VIKA, 90035, 'denied');
|
|
const space = { id: 90035, owner_id: U_OWNER, type: 'business', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_VIKA, 'user', space, null)).toBe(false);
|
|
});
|
|
|
|
test('no explicit permission — no access', async () => {
|
|
const space = { id: 90035, owner_id: U_OWNER, type: 'business', visibility: 'internal' };
|
|
expect(await checkUserSpaceAccess(U_VIKA, 'user', space, null)).toBe(false);
|
|
});
|
|
|
|
test('explicit permission checked BEFORE table access_control', async () => {
|
|
const TABLE_ID = 90100;
|
|
await createUsersTable(TABLE_ID);
|
|
// User has denied in table but admin in explicit permissions
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'denied');
|
|
await grantPermission(U_VIKA, 90035, 'admin');
|
|
|
|
const ac = {
|
|
enabled: true,
|
|
users_table_id: TABLE_ID,
|
|
role_column_id: String(TABLE_ID * 10 + 4),
|
|
role_mappings: [
|
|
{ columnValue: 'denied', accessLevel: 'denied' },
|
|
{ columnValue: 'admin', accessLevel: 'admin' },
|
|
],
|
|
};
|
|
const space = { id: 90035, owner_id: U_OWNER, type: 'business', visibility: 'internal' };
|
|
// Explicit permission (step 3.6) should grant access before table check (step 6)
|
|
expect(await checkUserSpaceAccess(U_VIKA, 'user', space, ac)).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 3. checkUserAccessViaTableV2 — table-based access
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('checkUserAccessViaTableV2', () => {
|
|
const TABLE_ID = 90200;
|
|
|
|
beforeEach(async () => {
|
|
await createUsersTable(TABLE_ID);
|
|
});
|
|
|
|
const makeAC = (overrides = {}) => ({
|
|
enabled: true,
|
|
users_table_id: TABLE_ID,
|
|
role_column_id: String(TABLE_ID * 10 + 4),
|
|
role_mappings: [
|
|
{ columnValue: 'owner', accessLevel: 'owner' },
|
|
{ columnValue: 'admin', accessLevel: 'admin' },
|
|
{ columnValue: 'editor', accessLevel: 'editor' },
|
|
{ columnValue: 'viewer', accessLevel: 'viewer' },
|
|
{ columnValue: 'denied', accessLevel: 'denied' },
|
|
],
|
|
...overrides,
|
|
});
|
|
|
|
test('user with admin role → allowed', async () => {
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin');
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC());
|
|
expect(r.allowed).toBe(true);
|
|
expect(r.accessLevel).toBe('admin');
|
|
});
|
|
|
|
test('user with viewer role → allowed', async () => {
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'viewer');
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC());
|
|
expect(r.allowed).toBe(true);
|
|
expect(r.accessLevel).toBe('viewer');
|
|
});
|
|
|
|
test('user with denied role → blocked', async () => {
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'denied');
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC());
|
|
expect(r.allowed).toBe(false);
|
|
expect(r.accessLevel).toBe('denied');
|
|
});
|
|
|
|
test('user not in table → blocked', async () => {
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC());
|
|
expect(r.allowed).toBe(false);
|
|
});
|
|
|
|
test('inactive user → blocked', async () => {
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin', false);
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC());
|
|
expect(r.allowed).toBe(false);
|
|
expect(r.accessLevel).toBe('denied');
|
|
});
|
|
|
|
test('matching by email when system_user_id not set', async () => {
|
|
const data = {
|
|
email: 'vika@test.com',
|
|
role: 'editor',
|
|
active: true,
|
|
[String(TABLE_ID * 10 + 2)]: 'vika@test.com',
|
|
[String(TABLE_ID * 10 + 4)]: 'editor',
|
|
};
|
|
const baseId = `test-email-match-${_rowCounter++}`;
|
|
await dbRun(
|
|
`INSERT INTO table_rows (table_id, base_id, data) VALUES (?, ?, ?)`,
|
|
[TABLE_ID, baseId, JSON.stringify(data)]
|
|
);
|
|
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC());
|
|
expect(r.allowed).toBe(true);
|
|
expect(r.accessLevel).toBe('editor');
|
|
});
|
|
|
|
test('role_column_id as column name works', async () => {
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin');
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC({ role_column_id: 'role' }));
|
|
expect(r.allowed).toBe(true);
|
|
expect(r.accessLevel).toBe('admin');
|
|
});
|
|
|
|
test('unmapped role → blocked', async () => {
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'superuser');
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, makeAC());
|
|
expect(r.allowed).toBe(false);
|
|
});
|
|
|
|
test('missing config → blocked', async () => {
|
|
const r = await checkUserAccessViaTableV2(U_VIKA, { enabled: true });
|
|
expect(r.allowed).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 4. getSpacesByUser — end-to-end filtering
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('getSpacesByUser — filtering', () => {
|
|
const TABLE_ID = 90300;
|
|
|
|
const makeAC = () => ({
|
|
enabled: true,
|
|
users_table_id: TABLE_ID,
|
|
role_column_id: String(TABLE_ID * 10 + 4),
|
|
role_mappings: [
|
|
{ columnValue: 'owner', accessLevel: 'owner' },
|
|
{ columnValue: 'admin', accessLevel: 'admin' },
|
|
{ columnValue: 'editor', accessLevel: 'editor' },
|
|
{ columnValue: 'viewer', accessLevel: 'viewer' },
|
|
{ columnValue: 'denied', accessLevel: 'denied' },
|
|
],
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await createUsersTable(TABLE_ID);
|
|
});
|
|
|
|
test('owner sees their own spaces', async () => {
|
|
await createSpaceRow(90001, 'My Space', U_VIKA, 'business');
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.some(s => s.id === 90001)).toBe(true);
|
|
expect(spaces.find(s => s.id === 90001).user_access_level).toBe('owner_owner');
|
|
});
|
|
|
|
test('user with table-based admin sees the space', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin');
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const s35 = spaces.find(s => s.id === 90035);
|
|
expect(s35).toBeDefined();
|
|
expect(s35.user_access_level).toBe('admin');
|
|
});
|
|
|
|
test('user NOT in table and no perm → space NOT shown', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.find(s => s.id === 90035)).toBeUndefined();
|
|
});
|
|
|
|
test('user with explicit admin perm sees the space', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal');
|
|
await grantPermission(U_VIKA, 90035, 'admin');
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const s35 = spaces.find(s => s.id === 90035);
|
|
expect(s35).toBeDefined();
|
|
expect(s35.user_access_level).toBe('admin');
|
|
});
|
|
|
|
test('user with explicit perm + access_control enabled sees space', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await grantPermission(U_VIKA, 90035, 'admin');
|
|
// User NOT in users table, but has explicit perm
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const s35 = spaces.find(s => s.id === 90035);
|
|
expect(s35).toBeDefined();
|
|
});
|
|
|
|
test('user_access_level from explicit perm when no table access', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal');
|
|
await grantPermission(U_VIKA, 90035, 'editor');
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const s35 = spaces.find(s => s.id === 90035);
|
|
expect(s35).toBeDefined();
|
|
expect(s35.user_access_level).toBe('editor');
|
|
});
|
|
|
|
test('table level overrides explicit perm for access_level when both exist', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin');
|
|
await grantPermission(U_VIKA, 90035, 'viewer');
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const s35 = spaces.find(s => s.id === 90035);
|
|
expect(s35).toBeDefined();
|
|
// Table-based admin takes priority over explicit viewer
|
|
expect(s35.user_access_level).toBe('admin');
|
|
});
|
|
|
|
test('open visibility space shown to everyone', async () => {
|
|
await createSpaceRow(90050, 'Open Space', U_OWNER, 'business', 'open');
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.some(s => s.id === 90050)).toBe(true);
|
|
});
|
|
|
|
test('personal space hidden from non-owner', async () => {
|
|
await createSpaceRow(90060, 'Personal', U_REGULAR, 'personal');
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.find(s => s.id === 90060)).toBeUndefined();
|
|
});
|
|
|
|
test('admin user sees admin-type spaces', async () => {
|
|
await createSpaceRow(90001, 'Admin Space', U_OWNER, 'admin');
|
|
const spacesAdmin = await getSpacesByUser(U_ADMIN, 'admin');
|
|
expect(spacesAdmin.some(s => s.id === 90001)).toBe(true);
|
|
|
|
const spacesUser = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spacesUser.find(s => s.id === 90001)).toBeUndefined();
|
|
});
|
|
|
|
test('denied in table → space NOT shown', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'denied');
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.find(s => s.id === 90035)).toBeUndefined();
|
|
});
|
|
|
|
test('inactive in table → space NOT shown', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin', false);
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.find(s => s.id === 90035)).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 5. getUserAccessData + canAccessSpace/Project/Table/Column
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('canAccessSpace / canAccessProject / canAccessTable / canAccessColumn', () => {
|
|
test('admin with no restrictions → access', () => {
|
|
const d = { role: 'admin', active: true, allowed_spaces: [], denied_spaces: [] };
|
|
expect(canAccessSpace(d, 90035)).toBe(true);
|
|
});
|
|
|
|
test('denied role → no access', () => {
|
|
const d = { role: 'denied', active: true, allowed_spaces: [], denied_spaces: [] };
|
|
expect(canAccessSpace(d, 90035)).toBe(false);
|
|
});
|
|
|
|
test('inactive → no access', () => {
|
|
const d = { role: 'admin', active: false, allowed_spaces: [], denied_spaces: [] };
|
|
expect(canAccessSpace(d, 90035)).toBe(false);
|
|
});
|
|
|
|
test('denied_spaces takes priority over allowed_spaces', () => {
|
|
const d = { role: 'editor', active: true, allowed_spaces: ['90035'], denied_spaces: ['90035'] };
|
|
expect(canAccessSpace(d, 90035)).toBe(false);
|
|
});
|
|
|
|
test('allowed_spaces whitelist works', () => {
|
|
const d = { role: 'editor', active: true, allowed_spaces: ['90035', '90036'], denied_spaces: [] };
|
|
expect(canAccessSpace(d, 90035)).toBe(true);
|
|
expect(canAccessSpace(d, 99)).toBe(false);
|
|
});
|
|
|
|
test('empty allowed list = allow all', () => {
|
|
const d = { role: 'editor', active: true, allowed_spaces: [], denied_spaces: [] };
|
|
expect(canAccessSpace(d, 90035)).toBe(true);
|
|
expect(canAccessSpace(d, 99)).toBe(true);
|
|
});
|
|
|
|
test('canAccessProject follows same logic', () => {
|
|
const d = { role: 'editor', active: true, allowed_projects: ['10'], denied_projects: [] };
|
|
expect(canAccessProject(d, 10)).toBe(true);
|
|
expect(canAccessProject(d, 99)).toBe(false);
|
|
});
|
|
|
|
test('canAccessTable — denied takes priority', () => {
|
|
const d = { role: 'viewer', active: true, allowed_tables: [], denied_tables: ['100'] };
|
|
expect(canAccessTable(d, 100)).toBe(false);
|
|
expect(canAccessTable(d, 200)).toBe(true);
|
|
});
|
|
|
|
test('canAccessColumn — admin bypasses unless denied', () => {
|
|
expect(canAccessColumn({ role: 'admin', active: true, allowed_columns: [], denied_columns: [] }, 1)).toBe(true);
|
|
expect(canAccessColumn({ role: 'admin', active: true, allowed_columns: [], denied_columns: ['5'] }, 5)).toBe(false);
|
|
});
|
|
|
|
test('null data → false for all', () => {
|
|
expect(canAccessSpace(null, 1)).toBe(false);
|
|
expect(canAccessProject(null, 1)).toBe(false);
|
|
expect(canAccessTable(null, 1)).toBe(false);
|
|
expect(canAccessColumn(null, 1)).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 6. getUserAccessData
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('getUserAccessData', () => {
|
|
const TABLE_ID = 90400;
|
|
|
|
beforeEach(async () => {
|
|
await createUsersTable(TABLE_ID);
|
|
});
|
|
|
|
const makeAC = () => ({
|
|
enabled: true,
|
|
users_table_id: TABLE_ID,
|
|
user_id_column: 'system_user_id',
|
|
});
|
|
|
|
test('returns user data with role and arrays', async () => {
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin', true, {
|
|
allowed_spaces: ['90035'],
|
|
denied_spaces: ['99'],
|
|
});
|
|
const data = await getUserAccessData(U_VIKA, makeAC());
|
|
expect(data).not.toBeNull();
|
|
expect(data.role).toBe('admin');
|
|
expect(data.active).toBe(true);
|
|
});
|
|
|
|
test('returns null for non-existent user', async () => {
|
|
const data = await getUserAccessData(999, makeAC());
|
|
expect(data).toBeNull();
|
|
});
|
|
|
|
test('returns null when disabled', async () => {
|
|
const data = await getUserAccessData(U_VIKA, { enabled: false });
|
|
expect(data).toBeNull();
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 7. Bug reproduction: Vika scenario
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('BUG REPRODUCTION: Vika (user 11) + space 35', () => {
|
|
const TABLE_ID = 90500;
|
|
|
|
const makeAC = () => ({
|
|
enabled: true,
|
|
users_table_id: TABLE_ID,
|
|
role_column_id: String(TABLE_ID * 10 + 4),
|
|
role_mappings: [
|
|
{ columnValue: 'owner', accessLevel: 'owner' },
|
|
{ columnValue: 'admin', accessLevel: 'admin' },
|
|
{ columnValue: 'editor', accessLevel: 'editor' },
|
|
{ columnValue: 'viewer', accessLevel: 'viewer' },
|
|
{ columnValue: 'denied', accessLevel: 'denied' },
|
|
],
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await createUsersTable(TABLE_ID);
|
|
});
|
|
|
|
test('user in table + explicit perm → sees space with correct level', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin');
|
|
await grantPermission(U_VIKA, 90035, 'admin');
|
|
|
|
const hasAccess = await checkUserSpaceAccess(U_VIKA, 'user',
|
|
{ id: 90035, owner_id: U_OWNER, type: 'business', visibility: 'internal' }, makeAC());
|
|
expect(hasAccess).toBe(true);
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const s35 = spaces.find(s => s.id === 90035);
|
|
expect(s35).toBeDefined();
|
|
expect(s35.user_access_level).toBe('admin');
|
|
});
|
|
|
|
test('user only in table → sees space', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin');
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.find(s => s.id === 90035)).toBeDefined();
|
|
});
|
|
|
|
test('user only has explicit perm (not in table) → sees space', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await grantPermission(U_VIKA, 90035, 'admin');
|
|
|
|
const hasAccess = await checkUserSpaceAccess(U_VIKA, 'user',
|
|
{ id: 90035, owner_id: U_OWNER, type: 'business', visibility: 'internal' }, makeAC());
|
|
expect(hasAccess).toBe(true);
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
expect(spaces.find(s => s.id === 90035)).toBeDefined();
|
|
});
|
|
|
|
test('EDGE: access_control enabled, user not in table, explicit perm → correct user_access_level', async () => {
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await grantPermission(U_VIKA, 90035, 'admin');
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const s35 = spaces.find(s => s.id === 90035);
|
|
expect(s35).toBeDefined();
|
|
expect(s35.user_access_level).toBe('admin');
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// 8. Full scenario: Multiple spaces
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
describe('Full scenario: multiple spaces, correct filtering', () => {
|
|
const TABLE_ID = 90600;
|
|
|
|
const makeAC = () => ({
|
|
enabled: true,
|
|
users_table_id: TABLE_ID,
|
|
role_column_id: String(TABLE_ID * 10 + 4),
|
|
role_mappings: [
|
|
{ columnValue: 'admin', accessLevel: 'admin' },
|
|
{ columnValue: 'viewer', accessLevel: 'viewer' },
|
|
{ columnValue: 'denied', accessLevel: 'denied' },
|
|
],
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await createUsersTable(TABLE_ID);
|
|
});
|
|
|
|
test('user sees exactly the right spaces', async () => {
|
|
// Admin space — user (role=user) should NOT see
|
|
await createSpaceRow(90001, 'Admin Space', U_OWNER, 'admin');
|
|
// Another admin space
|
|
await createSpaceRow(90011, 'Development', U_OWNER, 'admin');
|
|
// Personal owned by user
|
|
await createSpaceRow(90033, 'Personal Space', U_VIKA, 'personal');
|
|
// Business with table-based access
|
|
await createSpaceRow(90035, 'SIXTYNINE', U_OWNER, 'business', 'internal', makeAC());
|
|
await addUserRow(TABLE_ID, U_VIKA, 'vika@test.com', 'admin');
|
|
// Business without access — should NOT see
|
|
await createSpaceRow(90070, 'Marketing', U_OWNER, 'business');
|
|
|
|
const spaces = await getSpacesByUser(U_VIKA, 'user');
|
|
const ids = spaces.map(s => s.id);
|
|
|
|
expect(ids).toContain(90033); // own personal
|
|
expect(ids).toContain(90035); // table-based admin
|
|
expect(ids).not.toContain(90001); // admin space
|
|
expect(ids).not.toContain(90011); // admin space
|
|
expect(ids).not.toContain(90070); // no access
|
|
});
|
|
});
|
|
});
|