/** * ICD Test Runner — AuthZ Tier * Permission boundary tests: cross-user isolation, role enforcement. */ (function () { 'use strict'; var T = window.ICD; if (!T) return; T.runAuthz = async function () { if (!T.fixtures.ready) { if (typeof UI !== 'undefined') UI.toast('Provision test fixtures first', 'warning'); return; } var testUser = T.getFixtureUser('-user'); var teamAdmin = T.getFixtureUser('-tadmin'); var testAdmin = T.getFixtureUser('-admin2'); if (!testUser || !testUser.token) { T.results.push({ tier: 'authz', domain: 'setup', name: 'SKIP', status: 'fail', duration: 0, detail: 'Test user not provisioned or login failed (no token)' }); return; } // ── Regular user MUST be denied admin routes ── var adminGets = [ '/admin/stats', '/admin/users', '/admin/settings', '/admin/configs', '/admin/models', '/admin/personas', '/admin/teams', '/admin/groups', '/admin/packages', '/admin/providers/health', '/admin/routing/policies', '/admin/permissions', '/admin/roles', '/admin/vault/status', '/admin/storage/status', '/admin/audit', '/admin/pricing', '/admin/tasks', '/admin/projects', '/admin/channels/archived', '/admin/extensions' ]; for (var ai = 0; ai < adminGets.length; ai++) { await (async function (path) { await T.test('authz', 'admin-deny', 'user → GET ' + path + ' (expect 403)', async function () { var d = await T.authFetch(testUser.token, 'GET', path); T.assert(d._status === 403 || d._status === 401, 'expected 403/401 for non-admin, got ' + d._status); }); })(adminGets[ai]); } // ── Regular user MUST NOT be able to create admin resources ── await T.test('authz', 'admin-deny', 'user → POST /admin/users (expect 403)', async function () { var d = await T.authFetch(testUser.token, 'POST', '/admin/users', { username: 'should-not-exist', password: 'x', email: 'no@no.no', role: 'user' }); T.assert(d._status === 403 || d._status === 401, 'expected 403/401, got ' + d._status); }); await T.test('authz', 'admin-deny', 'user → POST /admin/teams (expect 403)', async function () { var d = await T.authFetch(testUser.token, 'POST', '/admin/teams', { name: 'should-not-exist' }); T.assert(d._status === 403 || d._status === 401, 'expected 403/401, got ' + d._status); }); await T.test('authz', 'admin-deny', 'user → POST /admin/groups (expect 403)', async function () { var d = await T.authFetch(testUser.token, 'POST', '/admin/groups', { name: 'should-not-exist', scope: 'global' }); T.assert(d._status === 403 || d._status === 401, 'expected 403/401, got ' + d._status); }); await T.test('authz', 'admin-deny', 'user → POST /admin/packages/install (expect 403)', async function () { var d = await T.authFetch(testUser.token, 'POST', '/admin/packages/install', {}); T.assert(d._status === 403 || d._status === 401 || d._status === 400, 'expected 403/401/400, got ' + d._status); }); await T.test('authz', 'admin-deny', 'user → PUT /admin/users/:id/role (expect 403)', async function () { var d = await T.authFetch(testUser.token, 'PUT', '/admin/users/' + testUser.id + '/role', { role: 'admin' }); T.assert(d._status === 403 || d._status === 401, 'CRITICAL: non-admin was able to escalate role! got ' + d._status); }); await T.test('authz', 'admin-deny', 'user → POST /admin/users/:id/reset-password (expect 403)', async function () { var d = await T.authFetch(testUser.token, 'POST', '/admin/users/' + testUser.id + '/reset-password', { new_password: 'hacked' }); T.assert(d._status === 403 || d._status === 401, 'CRITICAL: non-admin was able to reset passwords! got ' + d._status); }); // ── Regular user SHOULD be able to access their own stuff ── await T.test('authz', 'user-allow', 'user → GET /profile (expect 200)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/profile'); T.assertStatus(d, 200, 'profile'); T.assert(d.username === testUser.username, 'username mismatch'); }); await T.test('authz', 'user-allow', 'user → GET /channels (expect 200)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/channels'); T.assertStatus(d, 200, 'channels'); }); await T.test('authz', 'user-allow', 'user → GET /models/enabled (expect 200)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/models/enabled'); T.assertStatus(d, 200, 'models'); }); await T.test('authz', 'user-allow', 'user → GET /surfaces (expect 200)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/surfaces'); T.assertStatus(d, 200, 'surfaces'); }); await T.test('authz', 'user-allow', 'user → GET /notifications/unread-count (expect 200)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/notifications/unread-count'); T.assertStatus(d, 200, 'unread-count'); }); await T.test('authz', 'user-allow', 'user → GET /teams/mine (expect 200)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/teams/mine'); T.assertStatus(d, 200, 'teams/mine'); }); await T.test('authz', 'user-allow', 'user → GET /groups/mine (expect 200)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/groups/mine'); T.assertStatus(d, 200, 'groups/mine'); }); // ── Permission-gated operations ── await T.test('authz', 'perm-gate', 'user → POST /workflows (expect 403, no workflow.create)', async function () { var d = await T.authFetch(testUser.token, 'POST', '/workflows', { name: 'should-not-exist', slug: 'should-not-exist', entry_mode: 'team_only' }); T.assert(d._status === 403 || d._status === 401, 'expected 403 (no workflow.create), got ' + d._status); }); // ── Team admin can manage their team but NOT platform admin ── if (teamAdmin && teamAdmin.token && T.fixtures.team) { await T.test('authz', 'team-scope', 'teamAdmin → GET /teams/:id/members (expect 200)', async function () { var d = await T.authFetch(teamAdmin.token, 'GET', '/teams/' + T.fixtures.team.id + '/members'); T.assertStatus(d, 200, 'team-members'); }); await T.test('authz', 'team-scope', 'teamAdmin → GET /teams/:id/personas (expect 200)', async function () { var d = await T.authFetch(teamAdmin.token, 'GET', '/teams/' + T.fixtures.team.id + '/personas'); T.assertStatus(d, 200, 'team-personas'); }); await T.test('authz', 'team-scope', 'teamAdmin → GET /teams/:id/providers (expect 200)', async function () { var d = await T.authFetch(teamAdmin.token, 'GET', '/teams/' + T.fixtures.team.id + '/providers'); T.assertStatus(d, 200, 'team-providers'); }); await T.test('authz', 'team-scope', 'teamAdmin → GET /admin/stats (expect 403)', async function () { var d = await T.authFetch(teamAdmin.token, 'GET', '/admin/stats'); T.assert(d._status === 403 || d._status === 401, 'team admin should not have platform admin, got ' + d._status); }); await T.test('authz', 'team-scope', 'teamAdmin → PUT /admin/users/:id/role (expect 403)', async function () { var d = await T.authFetch(teamAdmin.token, 'PUT', '/admin/users/' + teamAdmin.id + '/role', { role: 'admin' }); T.assert(d._status === 403 || d._status === 401, 'CRITICAL: team admin was able to escalate to platform admin! got ' + d._status); }); } // ── Second admin CAN access admin routes ── if (testAdmin && testAdmin.token) { await T.test('authz', 'admin-allow', 'admin2 → GET /admin/stats (expect 200)', async function () { var d = await T.authFetch(testAdmin.token, 'GET', '/admin/stats'); T.assertStatus(d, 200, 'admin-stats'); }); await T.test('authz', 'admin-allow', 'admin2 → GET /admin/users (expect 200)', async function () { var d = await T.authFetch(testAdmin.token, 'GET', '/admin/users'); T.assertStatus(d, 200, 'admin-users'); }); } // ── Expired/invalid token ── await T.test('authz', 'token', 'bogus token → GET /profile (expect 401)', async function () { var d = await T.authFetch('this-is-not-a-valid-token', 'GET', '/profile'); T.assert(d._status === 401, 'expected 401 for bogus token, got ' + d._status); }); await T.test('authz', 'token', 'empty token → GET /profile (expect 401)', async function () { var d = await T.authFetch('', 'GET', '/profile'); T.assert(d._status === 401, 'expected 401 for empty token, got ' + d._status); }); // ── Cross-user resource isolation ── var userChannel = null; await T.test('authz', 'isolation', 'admin creates private channel', async function () { var d = await T.apiPost('/channels', { title: 'authz-private-' + Date.now(), type: 'direct' }); T.assertHasKey(d, 'id', 'channel'); userChannel = d.id; T.registerCleanup(function () { if (userChannel) return T.safeDelete('/channels/' + userChannel); }); }); if (userChannel && testUser.token) { await T.test('authz', 'isolation', 'user → GET /channels/:id (expect 403/404)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/channels/' + userChannel); T.assert(d._status === 403 || d._status === 404, 'user should not see another user\'s channel, got ' + d._status); }); await T.test('authz', 'isolation', 'user → DELETE /channels/:id (expect 403/404)', async function () { var d = await T.authFetch(testUser.token, 'DELETE', '/channels/' + userChannel); T.assert(d._status === 403 || d._status === 404, 'user should not delete another user\'s channel, got ' + d._status); }); await T.test('authz', 'isolation', 'user → GET /channels/:id/path (expect 403/404)', async function () { var d = await T.authFetch(testUser.token, 'GET', '/channels/' + userChannel + '/path'); T.assert(d._status === 403 || d._status === 404, 'user should not read another user\'s messages, got ' + d._status); }); } }; })();