All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
229 lines
10 KiB
JavaScript
229 lines
10 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
}
|
|
};
|
|
})();
|