All checks were successful
CI/CD / detect-changes (pull_request) Successful in 23s
CI/CD / test-frontend (pull_request) Successful in 26s
CI/CD / test-go-pg (pull_request) Successful in 2m35s
CI/CD / test-sqlite (pull_request) Successful in 3m19s
CI/CD / build-and-deploy (pull_request) Successful in 1m46s
sw.testing SDK module — structured test framework for surface runners: - suite/test registration, lifecycle hooks (beforeAll/afterAll/beforeEach/afterEach) - Assertion library (ok/eq/neq/gt/match/throws/status/shape/arrayOf) - Auto-cleanup via track(type, id) with LIFO deletion - Three result statuses: passed/failed/warned - Structured JSON results with timing, warnings, cleanup stats test-runner manifest type: - ValidateManifest accepts "test-runner" packages - Excluded from sidebar nav (extensionNavItems filters surface/full only) - DB migrations: SQLite 013, Postgres 014 (CHECK constraint) ICD runner migration (kernel-only): - Migrated from T.test()/T.assert() to sw.testing.suite() - Stripped extension-dependent tests (channels, notes, personas, etc.) - Kernel suites: smoke, crud, authz, security, providers, packaging, sdk - Deleted ui.js + css (rendering delegated to registry surface) SDK runner migration (kernel-only): - Migrated from T.dualTest()/T.domains to sw.testing.suite() - Stripped extension domains (belong in v0.7.2 package runners) - Kernel suites: misc, workflows, admin, packages, connections, deps, composition Runner registry surface (/s/test-runners): - Admin-only dashboard discovering test-runner packages - Run All / per-runner Run buttons, real-time results - Export Failures / Export Full Results (JSON download) - Dark mode styling using kernel CSS variables - Suite count polling for async runner script loading 141 passed, 0 failed on fresh minimal install. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
241 lines
10 KiB
JavaScript
241 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;
|
|
if (!window.sw || !window.sw.testing) return;
|
|
|
|
sw.testing.suite('icd/authz', async function (s) {
|
|
if (!T.fixtures.ready) {
|
|
s.test('setup: fixtures required', async function (t) {
|
|
t.skip('Provision test fixtures first');
|
|
});
|
|
return;
|
|
}
|
|
|
|
var testUser = T.getFixtureUser('-user');
|
|
var teamAdmin = T.getFixtureUser('-tadmin');
|
|
var testAdmin = T.getFixtureUser('-admin2');
|
|
|
|
if (!testUser || !testUser.token) {
|
|
s.test('setup: fixture user token', async function (t) {
|
|
t.skip('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++) {
|
|
(function (path) {
|
|
s.test('admin-deny: user → GET ' + path + ' (expect 403)', async function (t) {
|
|
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 ──
|
|
|
|
s.test('admin-deny: user → POST /admin/users (expect 403)', async function (t) {
|
|
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);
|
|
});
|
|
|
|
s.test('admin-deny: user → POST /admin/teams (expect 403)', async function (t) {
|
|
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);
|
|
});
|
|
|
|
s.test('admin-deny: user → POST /admin/groups (expect 403)', async function (t) {
|
|
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);
|
|
});
|
|
|
|
s.test('admin-deny: user → POST /admin/packages/install (expect 403)', async function (t) {
|
|
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);
|
|
});
|
|
|
|
s.test('admin-deny: user → PUT /admin/users/:id/role (expect 403)', async function (t) {
|
|
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);
|
|
});
|
|
|
|
s.test('admin-deny: user → POST /admin/users/:id/reset-password (expect 403)', async function (t) {
|
|
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 ──
|
|
|
|
s.test('user-allow: user → GET /profile (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testUser.token, 'GET', '/profile');
|
|
T.assertStatus(d, 200, 'profile');
|
|
T.assert(d.username === testUser.username, 'username mismatch');
|
|
});
|
|
|
|
s.test('user-allow: user → GET /channels (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testUser.token, 'GET', '/channels');
|
|
T.assertStatus(d, 200, 'channels');
|
|
});
|
|
|
|
s.test('user-allow: user → GET /models/enabled (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testUser.token, 'GET', '/models/enabled');
|
|
T.assertStatus(d, 200, 'models');
|
|
});
|
|
|
|
s.test('user-allow: user → GET /surfaces (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testUser.token, 'GET', '/surfaces');
|
|
T.assertStatus(d, 200, 'surfaces');
|
|
});
|
|
|
|
s.test('user-allow: user → GET /notifications/unread-count (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testUser.token, 'GET', '/notifications/unread-count');
|
|
T.assertStatus(d, 200, 'unread-count');
|
|
});
|
|
|
|
s.test('user-allow: user → GET /teams/mine (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testUser.token, 'GET', '/teams/mine');
|
|
T.assertStatus(d, 200, 'teams/mine');
|
|
});
|
|
|
|
s.test('user-allow: user → GET /groups/mine (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testUser.token, 'GET', '/groups/mine');
|
|
T.assertStatus(d, 200, 'groups/mine');
|
|
});
|
|
|
|
// ── Permission-gated operations ──
|
|
|
|
s.test('perm-gate: user → POST /workflows (expect 403, no workflow.create)', async function (t) {
|
|
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) {
|
|
s.test('team-scope: teamAdmin → GET /teams/:id/members (expect 200)', async function (t) {
|
|
var d = await T.authFetch(teamAdmin.token, 'GET', '/teams/' + T.fixtures.team.id + '/members');
|
|
T.assertStatus(d, 200, 'team-members');
|
|
});
|
|
|
|
s.test('team-scope: teamAdmin → GET /teams/:id/personas (expect 200)', async function (t) {
|
|
var d = await T.authFetch(teamAdmin.token, 'GET', '/teams/' + T.fixtures.team.id + '/personas');
|
|
T.assertStatus(d, 200, 'team-personas');
|
|
});
|
|
|
|
s.test('team-scope: teamAdmin → GET /teams/:id/providers (expect 200)', async function (t) {
|
|
var d = await T.authFetch(teamAdmin.token, 'GET', '/teams/' + T.fixtures.team.id + '/providers');
|
|
T.assertStatus(d, 200, 'team-providers');
|
|
});
|
|
|
|
s.test('team-scope: teamAdmin → GET /admin/stats (expect 403)', async function (t) {
|
|
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);
|
|
});
|
|
|
|
s.test('team-scope: teamAdmin → PUT /admin/users/:id/role (expect 403)', async function (t) {
|
|
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) {
|
|
s.test('admin-allow: admin2 → GET /admin/stats (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testAdmin.token, 'GET', '/admin/stats');
|
|
T.assertStatus(d, 200, 'admin-stats');
|
|
});
|
|
|
|
s.test('admin-allow: admin2 → GET /admin/users (expect 200)', async function (t) {
|
|
var d = await T.authFetch(testAdmin.token, 'GET', '/admin/users');
|
|
T.assertStatus(d, 200, 'admin-users');
|
|
});
|
|
}
|
|
|
|
// ── Expired/invalid token ──
|
|
|
|
s.test('token: bogus token → GET /profile (expect 401)', async function (t) {
|
|
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);
|
|
});
|
|
|
|
s.test('token: empty token → GET /profile (expect 401)', async function (t) {
|
|
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;
|
|
s.test('isolation: admin creates private channel', async function (t) {
|
|
var cleanup = [];
|
|
try {
|
|
var d = await T.apiPost('/channels', { title: 'authz-private-' + Date.now(), type: 'direct' });
|
|
T.assertHasKey(d, 'id', 'channel');
|
|
userChannel = d.id;
|
|
cleanup.push(function () { return T.safeDelete('/channels/' + userChannel); });
|
|
} finally {
|
|
for (var i = 0; i < cleanup.length; i++) { try { await cleanup[i](); } catch (e) { /* ok */ } }
|
|
}
|
|
});
|
|
|
|
if (testUser.token) {
|
|
s.test('isolation: user → GET /channels/:id (expect 403/404)', async function (t) {
|
|
if (!userChannel) { t.skip('parent channel not created'); return; }
|
|
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);
|
|
});
|
|
|
|
s.test('isolation: user → DELETE /channels/:id (expect 403/404)', async function (t) {
|
|
if (!userChannel) { t.skip('parent channel not created'); return; }
|
|
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);
|
|
});
|
|
|
|
s.test('isolation: user → GET /channels/:id/path (expect 403/404)', async function (t) {
|
|
if (!userChannel) { t.skip('parent channel not created'); return; }
|
|
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);
|
|
});
|
|
}
|
|
});
|
|
})();
|