@fuzdev/fuz_app 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +630 -0
- package/dist/actions/action_rpc.d.ts +29 -0
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +42 -6
- package/dist/actions/action_types.d.ts +2 -2
- package/dist/actions/cancel.d.ts +12 -13
- package/dist/actions/cancel.d.ts.map +1 -1
- package/dist/actions/cancel.js +10 -13
- package/dist/actions/heartbeat.d.ts +8 -13
- package/dist/actions/heartbeat.d.ts.map +1 -1
- package/dist/actions/heartbeat.js +5 -8
- package/dist/actions/register_action_ws.d.ts +3 -3
- package/dist/actions/register_action_ws.js +2 -2
- package/dist/actions/register_ws_endpoint.d.ts +4 -4
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +3 -3
- package/dist/actions/socket.svelte.d.ts +16 -16
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +15 -15
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.d.ts +15 -0
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +17 -0
- package/dist/auth/CLAUDE.md +923 -0
- package/dist/auth/account_action_specs.d.ts +216 -0
- package/dist/auth/account_action_specs.d.ts.map +1 -0
- package/dist/auth/account_action_specs.js +159 -0
- package/dist/auth/account_actions.d.ts +51 -0
- package/dist/auth/account_actions.d.ts.map +1 -0
- package/dist/auth/account_actions.js +119 -0
- package/dist/auth/account_queries.d.ts +6 -2
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +40 -4
- package/dist/auth/account_routes.d.ts +94 -16
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +108 -180
- package/dist/auth/account_schema.d.ts +85 -30
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +40 -8
- package/dist/auth/admin_action_specs.d.ts +674 -0
- package/dist/auth/admin_action_specs.d.ts.map +1 -0
- package/dist/auth/admin_action_specs.js +287 -0
- package/dist/auth/admin_actions.d.ts +69 -0
- package/dist/auth/admin_actions.d.ts.map +1 -0
- package/dist/auth/admin_actions.js +256 -0
- package/dist/auth/api_token.d.ts +10 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +9 -0
- package/dist/auth/api_token_queries.d.ts +3 -3
- package/dist/auth/api_token_queries.js +3 -3
- package/dist/auth/app_settings_schema.d.ts +4 -3
- package/dist/auth/app_settings_schema.d.ts.map +1 -1
- package/dist/auth/app_settings_schema.js +2 -1
- package/dist/auth/audit_log_routes.d.ts +14 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +22 -79
- package/dist/auth/audit_log_schema.d.ts +100 -29
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +83 -11
- package/dist/auth/bootstrap_routes.d.ts +14 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +10 -3
- package/dist/auth/cleanup.d.ts +63 -0
- package/dist/auth/cleanup.d.ts.map +1 -0
- package/dist/auth/cleanup.js +80 -0
- package/dist/auth/invite_schema.d.ts +11 -10
- package/dist/auth/invite_schema.d.ts.map +1 -1
- package/dist/auth/invite_schema.js +4 -3
- package/dist/auth/migrations.d.ts +6 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +28 -0
- package/dist/auth/permit_offer_action_specs.d.ts +364 -0
- package/dist/auth/permit_offer_action_specs.d.ts.map +1 -0
- package/dist/auth/permit_offer_action_specs.js +216 -0
- package/dist/auth/permit_offer_actions.d.ts +96 -0
- package/dist/auth/permit_offer_actions.d.ts.map +1 -0
- package/dist/auth/permit_offer_actions.js +428 -0
- package/dist/auth/permit_offer_notifications.d.ts +361 -0
- package/dist/auth/permit_offer_notifications.d.ts.map +1 -0
- package/dist/auth/permit_offer_notifications.js +179 -0
- package/dist/auth/permit_offer_queries.d.ts +165 -0
- package/dist/auth/permit_offer_queries.d.ts.map +1 -0
- package/dist/auth/permit_offer_queries.js +390 -0
- package/dist/auth/permit_offer_schema.d.ts +103 -0
- package/dist/auth/permit_offer_schema.d.ts.map +1 -0
- package/dist/auth/permit_offer_schema.js +142 -0
- package/dist/auth/permit_queries.d.ts +77 -14
- package/dist/auth/permit_queries.d.ts.map +1 -1
- package/dist/auth/permit_queries.js +119 -24
- package/dist/auth/session_queries.d.ts +4 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +4 -2
- package/dist/auth/signup_routes.d.ts +13 -0
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +14 -7
- package/dist/http/CLAUDE.md +584 -0
- package/dist/http/pending_effects.d.ts +29 -0
- package/dist/http/pending_effects.d.ts.map +1 -0
- package/dist/http/pending_effects.js +31 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/route_spec.js +4 -3
- package/dist/rate_limiter.d.ts +30 -0
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +25 -2
- package/dist/realtime/sse_auth_guard.d.ts +2 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +5 -3
- package/dist/testing/CLAUDE.md +668 -1
- package/dist/testing/admin_integration.d.ts +10 -7
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +382 -482
- package/dist/testing/app_server.d.ts +7 -6
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/attack_surface.d.ts +9 -3
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +4 -4
- package/dist/testing/audit_completeness.d.ts +6 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +158 -134
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +4 -33
- package/dist/testing/db.d.ts +1 -1
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +2 -0
- package/dist/testing/entities.d.ts +35 -13
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +17 -0
- package/dist/testing/integration.d.ts +10 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +352 -340
- package/dist/testing/integration_helpers.d.ts +16 -5
- package/dist/testing/integration_helpers.d.ts.map +1 -1
- package/dist/testing/integration_helpers.js +24 -4
- package/dist/testing/rate_limiting.d.ts +7 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +41 -10
- package/dist/testing/rpc_helpers.d.ts +153 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_helpers.js +184 -8
- package/dist/testing/sse_round_trip.d.ts +8 -0
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +10 -3
- package/dist/testing/standard.d.ts +9 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +6 -2
- package/dist/testing/surface_invariants.d.ts +7 -3
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +5 -4
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +9 -38
- package/dist/ui/AccountSessions.svelte +8 -4
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAccounts.svelte +61 -33
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminAuditLog.svelte +3 -2
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -1
- package/dist/ui/AdminInvites.svelte +3 -2
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +14 -9
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/AdminPermitHistory.svelte +3 -2
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +29 -25
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +351 -0
- package/dist/ui/OpenSignupToggle.svelte +6 -3
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/PermitOfferForm.svelte +141 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts +14 -0
- package/dist/ui/PermitOfferForm.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferHistory.svelte +109 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts +11 -0
- package/dist/ui/PermitOfferHistory.svelte.d.ts.map +1 -0
- package/dist/ui/PermitOfferInbox.svelte +121 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts +12 -0
- package/dist/ui/PermitOfferInbox.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +53 -3
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +39 -16
- package/dist/ui/admin_accounts_state.svelte.d.ts +118 -2
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +99 -23
- package/dist/ui/admin_invites_state.svelte.d.ts +47 -1
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +38 -26
- package/dist/ui/admin_sessions_state.svelte.d.ts +26 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +35 -21
- package/dist/ui/app_settings_state.svelte.d.ts +39 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +34 -18
- package/dist/ui/audit_log_state.svelte.d.ts +40 -3
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +36 -42
- package/dist/ui/auth_state.svelte.d.ts +4 -3
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +4 -1
- package/dist/ui/permit_offers_state.svelte.d.ts +125 -0
- package/dist/ui/permit_offers_state.svelte.d.ts.map +1 -0
- package/dist/ui/permit_offers_state.svelte.js +197 -0
- package/package.json +3 -3
- package/dist/auth/admin_routes.d.ts +0 -29
- package/dist/auth/admin_routes.d.ts.map +0 -1
- package/dist/auth/admin_routes.js +0 -226
- package/dist/auth/app_settings_routes.d.ts +0 -27
- package/dist/auth/app_settings_routes.d.ts.map +0 -1
- package/dist/auth/app_settings_routes.js +0 -66
- package/dist/auth/invite_routes.d.ts +0 -18
- package/dist/auth/invite_routes.d.ts.map +0 -1
- package/dist/auth/invite_routes.js +0 -129
|
@@ -3,11 +3,12 @@ import './assert_dev_env.js';
|
|
|
3
3
|
* Standard admin integration test suite for fuz_app admin routes.
|
|
4
4
|
*
|
|
5
5
|
* `describe_standard_admin_integration_tests` creates a composable test suite
|
|
6
|
-
* that exercises admin account listing, permit grant/revoke
|
|
6
|
+
* that exercises admin account listing, permit grant/revoke (via the RPC
|
|
7
|
+
* surface — see `permit_offer_create` / `permit_revoke`), session/token
|
|
7
8
|
* management, and audit log routes against a real PGlite database.
|
|
8
9
|
*
|
|
9
|
-
* Consumers call it with their route factory, session config,
|
|
10
|
-
* all admin route tests come for free.
|
|
10
|
+
* Consumers call it with their route factory, session config, role schema,
|
|
11
|
+
* and RPC endpoint specs — all admin route tests come for free.
|
|
11
12
|
*
|
|
12
13
|
* @module
|
|
13
14
|
*/
|
|
@@ -19,16 +20,13 @@ import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TA
|
|
|
19
20
|
import { find_auth_route, assert_response_matches_spec } from './integration_helpers.js';
|
|
20
21
|
import { run_migrations } from '../db/migrate.js';
|
|
21
22
|
import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
s.path.endsWith(suffix) &&
|
|
30
|
-
s.auth.type === 'role' &&
|
|
31
|
-
s.auth.role === 'admin');
|
|
23
|
+
import { rpc_call, rpc_call_non_browser, require_rpc_endpoint_path } from './rpc_helpers.js';
|
|
24
|
+
import { permit_offer_create_action_spec, permit_revoke_action_spec, } from '../auth/permit_offer_action_specs.js';
|
|
25
|
+
import { admin_account_list_action_spec, admin_session_list_action_spec, admin_session_revoke_all_action_spec, admin_token_revoke_all_action_spec, audit_log_list_action_spec, audit_log_permit_history_action_spec, } from '../auth/admin_action_specs.js';
|
|
26
|
+
import { account_token_create_action_spec, account_verify_action_spec, } from '../auth/account_action_specs.js';
|
|
27
|
+
import { query_grant_permit } from '../auth/permit_queries.js';
|
|
28
|
+
import { query_actor_by_account } from '../auth/account_queries.js';
|
|
29
|
+
import { query_accept_offer } from '../auth/permit_offer_queries.js';
|
|
32
30
|
/**
|
|
33
31
|
* Pick a web-grantable role for testing, preferring a non-admin app-defined role.
|
|
34
32
|
*/
|
|
@@ -47,21 +45,24 @@ const build_admin_test_app_options = (options, db, roles) => ({
|
|
|
47
45
|
create_route_specs: options.create_route_specs,
|
|
48
46
|
db,
|
|
49
47
|
roles: roles ?? [ROLE_KEEPER, ROLE_ADMIN],
|
|
50
|
-
app_options:
|
|
48
|
+
app_options: {
|
|
49
|
+
rpc_endpoints: options.rpc_endpoints,
|
|
50
|
+
...options.app_options,
|
|
51
|
+
},
|
|
51
52
|
});
|
|
52
53
|
/**
|
|
53
54
|
* Standard admin integration test suite for fuz_app admin routes.
|
|
54
55
|
*
|
|
55
|
-
* Exercises account listing, permit grant/revoke
|
|
56
|
-
* management, audit log routes, admin-to-admin isolation,
|
|
57
|
-
* schema validation.
|
|
58
|
-
*
|
|
59
|
-
* Each test group asserts that required routes exist, failing with a descriptive
|
|
60
|
-
* message if the consumer's route specs are misconfigured.
|
|
56
|
+
* Exercises account listing, permit grant/revoke (via RPC), session
|
|
57
|
+
* management, token management, audit log routes, admin-to-admin isolation,
|
|
58
|
+
* and response schema validation.
|
|
61
59
|
*
|
|
62
|
-
* @param options - session config, route factory,
|
|
60
|
+
* @param options - session config, route factory, role schema, RPC endpoints
|
|
63
61
|
*/
|
|
64
62
|
export const describe_standard_admin_integration_tests = (options) => {
|
|
63
|
+
// Hard-fail early so consumers see a clear setup error instead of a
|
|
64
|
+
// confusing test failure when `rpc_endpoints` is missing.
|
|
65
|
+
const rpc_path = require_rpc_endpoint_path(options.rpc_endpoints);
|
|
65
66
|
const init_schema = async (db) => {
|
|
66
67
|
await run_migrations(db, [AUTH_MIGRATION_NS]);
|
|
67
68
|
};
|
|
@@ -76,20 +77,13 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
76
77
|
let captured_route_specs = null;
|
|
77
78
|
afterAll(() => {
|
|
78
79
|
if (captured_route_specs) {
|
|
79
|
-
// Scope coverage to admin auth-related routes.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
'/audit-log',
|
|
87
|
-
'/audit-log/permit-history',
|
|
88
|
-
'/invites',
|
|
89
|
-
];
|
|
90
|
-
const admin_routes = captured_route_specs.filter((s) => (admin_suffixes.some((suffix) => s.path.endsWith(suffix)) ||
|
|
91
|
-
s.path.includes('/permits/:') ||
|
|
92
|
-
s.path.includes('/invites/:')) &&
|
|
80
|
+
// Scope coverage to admin auth-related routes. Post-2026-04-23
|
|
81
|
+
// RPC migration: account listing, session/token revoke-all,
|
|
82
|
+
// audit-log reads, and invite CRUD are RPC-only. The only
|
|
83
|
+
// admin REST route remaining is the optional
|
|
84
|
+
// `GET /audit-log/stream` SSE, plus the shared RPC endpoint
|
|
85
|
+
// path itself (admin methods live behind spec-level role auth).
|
|
86
|
+
const admin_routes = captured_route_specs.filter((s) => s.path.endsWith('/audit-log/stream') &&
|
|
93
87
|
s.auth.type === 'role' &&
|
|
94
88
|
s.auth.role === 'admin');
|
|
95
89
|
assert_error_coverage(error_collector, admin_routes.length > 0 ? admin_routes : captured_route_specs, { min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE });
|
|
@@ -102,279 +96,134 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
102
96
|
cookie: `${cookie_name}=${session_cookie}`,
|
|
103
97
|
...extra,
|
|
104
98
|
});
|
|
105
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Drive the full consent flow (admin offer → recipient accept) and
|
|
101
|
+
* return the materialized permit id. Accept is a direct transactional
|
|
102
|
+
* `query_accept_offer` call because the suite focuses on the admin
|
|
103
|
+
* side; exercising the recipient's UI-wired accept path is covered by
|
|
104
|
+
* `describe_rpc_round_trip_tests` + fuz_app's own action suite.
|
|
105
|
+
*/
|
|
106
|
+
const offer_and_accept = async (args) => {
|
|
107
|
+
const res = await rpc_call({
|
|
108
|
+
app: args.app,
|
|
109
|
+
path: rpc_path,
|
|
110
|
+
method: permit_offer_create_action_spec.method,
|
|
111
|
+
params: { to_account_id: args.to_account_id, role: args.role },
|
|
112
|
+
headers: args.admin_headers,
|
|
113
|
+
});
|
|
114
|
+
assert.ok(res.ok, `permit_offer_create failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
115
|
+
const offer = res.result.offer;
|
|
116
|
+
const accept_result = await get_db().transaction(async (tx) => query_accept_offer({ db: tx }, { offer_id: offer.id, to_account_id: args.to_account_id, ip: null }));
|
|
117
|
+
return { offer_id: offer.id, permit_id: accept_result.permit.id };
|
|
118
|
+
};
|
|
119
|
+
// --- 1. Admin account listing (RPC) ---
|
|
106
120
|
describe('admin account listing', () => {
|
|
107
121
|
test('admin can list all accounts', async () => {
|
|
108
122
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
109
|
-
const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
|
|
110
|
-
assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
|
|
111
123
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
112
|
-
const res = await
|
|
124
|
+
const res = await rpc_call({
|
|
125
|
+
app: test_app.app,
|
|
126
|
+
path: rpc_path,
|
|
127
|
+
method: admin_account_list_action_spec.method,
|
|
113
128
|
headers: test_app.create_session_headers(),
|
|
114
129
|
});
|
|
115
|
-
assert.
|
|
116
|
-
const
|
|
117
|
-
assert.ok(Array.isArray(
|
|
118
|
-
assert.ok(
|
|
119
|
-
assert.ok(Array.isArray(
|
|
130
|
+
assert.ok(res.ok, `admin_account_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
131
|
+
const result = res.result;
|
|
132
|
+
assert.ok(Array.isArray(result.accounts), 'Expected accounts array');
|
|
133
|
+
assert.ok(result.accounts.length >= 2, 'Expected at least 2 accounts');
|
|
134
|
+
assert.ok(Array.isArray(result.grantable_roles), 'Expected grantable_roles array');
|
|
120
135
|
// Verify user_two appears in the listing
|
|
121
|
-
const found =
|
|
136
|
+
const found = result.accounts.find((e) => e.account.id === user_two.account.id);
|
|
122
137
|
assert.ok(found, 'Expected user_two in accounts listing');
|
|
123
138
|
});
|
|
124
139
|
test('non-admin cannot list accounts', async () => {
|
|
125
140
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db(), [ROLE_KEEPER]));
|
|
126
141
|
captured_route_specs ??= test_app.route_specs;
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
const res = await rpc_call({
|
|
143
|
+
app: test_app.app,
|
|
144
|
+
path: rpc_path,
|
|
145
|
+
method: admin_account_list_action_spec.method,
|
|
130
146
|
headers: test_app.create_session_headers(),
|
|
131
147
|
});
|
|
148
|
+
assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
|
|
132
149
|
assert.strictEqual(res.status, 403);
|
|
133
|
-
const body = await res.clone().json();
|
|
134
|
-
assert.strictEqual(body.error, 'insufficient_permissions');
|
|
135
|
-
await error_collector.assert_and_record(test_app.route_specs, 'GET', accounts_route.path, res);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
// --- 2. Permit grant lifecycle ---
|
|
139
|
-
describe('permit grant lifecycle', () => {
|
|
140
|
-
test('admin can grant a web-grantable role', async () => {
|
|
141
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
142
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
143
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
144
|
-
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
145
|
-
const path = grant_route.path.replace(':account_id', user_two.account.id);
|
|
146
|
-
const res = await test_app.app.request(path, {
|
|
147
|
-
method: 'POST',
|
|
148
|
-
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
149
|
-
body: JSON.stringify({ role: grantable_role }),
|
|
150
|
-
});
|
|
151
|
-
assert.strictEqual(res.status, 200);
|
|
152
|
-
const body = await res.json();
|
|
153
|
-
assert.strictEqual(body.ok, true);
|
|
154
|
-
assert.ok(body.permit);
|
|
155
|
-
assert.strictEqual(body.permit.role, grantable_role);
|
|
156
|
-
assert.ok(body.permit.id, 'Expected permit id');
|
|
157
|
-
});
|
|
158
|
-
test('admin cannot grant a non-web-grantable role', async () => {
|
|
159
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
160
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
161
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
162
|
-
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
163
|
-
const path = grant_route.path.replace(':account_id', user_two.account.id);
|
|
164
|
-
const res = await test_app.app.request(path, {
|
|
165
|
-
method: 'POST',
|
|
166
|
-
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
167
|
-
body: JSON.stringify({ role: ROLE_KEEPER }),
|
|
168
|
-
});
|
|
169
|
-
assert.strictEqual(res.status, 403);
|
|
170
|
-
const body = await res.clone().json();
|
|
171
|
-
assert.strictEqual(body.error, 'role_not_web_grantable');
|
|
172
|
-
await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
|
|
173
|
-
});
|
|
174
|
-
test('granting same role twice is idempotent (returns same permit)', async () => {
|
|
175
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
176
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
177
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
178
|
-
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
179
|
-
const path = grant_route.path.replace(':account_id', user_two.account.id);
|
|
180
|
-
const headers = test_app.create_session_headers({ 'content-type': 'application/json' });
|
|
181
|
-
const body = JSON.stringify({ role: grantable_role });
|
|
182
|
-
// First grant
|
|
183
|
-
const res1 = await test_app.app.request(path, {
|
|
184
|
-
method: 'POST',
|
|
185
|
-
headers,
|
|
186
|
-
body,
|
|
187
|
-
});
|
|
188
|
-
assert.strictEqual(res1.status, 200);
|
|
189
|
-
const body1 = await res1.json();
|
|
190
|
-
assert.strictEqual(body1.ok, true);
|
|
191
|
-
const permit_id_1 = body1.permit.id;
|
|
192
|
-
// Second grant — same role, same account
|
|
193
|
-
const res2 = await test_app.app.request(path, {
|
|
194
|
-
method: 'POST',
|
|
195
|
-
headers,
|
|
196
|
-
body,
|
|
197
|
-
});
|
|
198
|
-
assert.strictEqual(res2.status, 200);
|
|
199
|
-
const body2 = await res2.json();
|
|
200
|
-
assert.strictEqual(body2.ok, true);
|
|
201
|
-
assert.strictEqual(body2.permit.id, permit_id_1, 'Expected same permit ID on idempotent grant');
|
|
202
|
-
assert.strictEqual(body2.permit.role, grantable_role);
|
|
203
|
-
});
|
|
204
|
-
test('grant with unknown role returns 400', async () => {
|
|
205
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
206
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
207
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
208
|
-
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
209
|
-
const path = grant_route.path.replace(':account_id', user_two.account.id);
|
|
210
|
-
const res = await test_app.app.request(path, {
|
|
211
|
-
method: 'POST',
|
|
212
|
-
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
213
|
-
body: JSON.stringify({ role: 'nonexistent_role' }),
|
|
214
|
-
});
|
|
215
|
-
assert.strictEqual(res.status, 400);
|
|
216
|
-
error_collector.record(test_app.route_specs, 'POST', grant_route.path, 400);
|
|
217
|
-
});
|
|
218
|
-
test('grant to nonexistent account returns 404', async () => {
|
|
219
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
220
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
221
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
222
|
-
const fake_id = '00000000-0000-0000-0000-000000000000';
|
|
223
|
-
const path = grant_route.path.replace(':account_id', fake_id);
|
|
224
|
-
const res = await test_app.app.request(path, {
|
|
225
|
-
method: 'POST',
|
|
226
|
-
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
227
|
-
body: JSON.stringify({ role: grantable_role }),
|
|
228
|
-
});
|
|
229
|
-
assert.strictEqual(res.status, 404);
|
|
230
|
-
const body = await res.clone().json();
|
|
231
|
-
assert.strictEqual(body.error, 'account_not_found');
|
|
232
|
-
await error_collector.assert_and_record(test_app.route_specs, 'POST', grant_route.path, res);
|
|
233
|
-
});
|
|
234
|
-
test('admin can revoke a permit', async () => {
|
|
235
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
236
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
237
|
-
const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
|
|
238
|
-
const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
|
|
239
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
240
|
-
assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
|
|
241
|
-
assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
|
|
242
|
-
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
243
|
-
const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
|
|
244
|
-
// Grant
|
|
245
|
-
const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
|
|
246
|
-
await test_app.app.request(grant_path, {
|
|
247
|
-
method: 'POST',
|
|
248
|
-
headers: admin_headers,
|
|
249
|
-
body: JSON.stringify({ role: grantable_role }),
|
|
250
|
-
});
|
|
251
|
-
// Find the permit ID via account listing
|
|
252
|
-
const list_res = await test_app.app.request(accounts_route.path, {
|
|
253
|
-
headers: test_app.create_session_headers(),
|
|
254
|
-
});
|
|
255
|
-
const list_body = await list_res.json();
|
|
256
|
-
const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
|
|
257
|
-
const permit = entry.permits.find((p) => p.role === grantable_role);
|
|
258
|
-
assert.ok(permit, 'Expected granted permit in listing');
|
|
259
|
-
// Revoke
|
|
260
|
-
const revoke_path = revoke_route.path
|
|
261
|
-
.replace(':account_id', user_two.account.id)
|
|
262
|
-
.replace(':permit_id', permit.id);
|
|
263
|
-
const revoke_res = await test_app.app.request(revoke_path, {
|
|
264
|
-
method: 'POST',
|
|
265
|
-
headers: test_app.create_session_headers(),
|
|
266
|
-
});
|
|
267
|
-
assert.strictEqual(revoke_res.status, 200);
|
|
268
|
-
const revoke_body = await revoke_res.json();
|
|
269
|
-
assert.strictEqual(revoke_body.ok, true);
|
|
270
|
-
assert.strictEqual(revoke_body.revoked, true);
|
|
271
|
-
});
|
|
272
|
-
test('revoking an already-revoked permit returns 404', async () => {
|
|
273
|
-
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
274
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
275
|
-
const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
|
|
276
|
-
const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
|
|
277
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
278
|
-
assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
|
|
279
|
-
assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
|
|
280
|
-
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
281
|
-
const admin_headers = test_app.create_session_headers({ 'content-type': 'application/json' });
|
|
282
|
-
// Grant
|
|
283
|
-
const grant_path = grant_route.path.replace(':account_id', user_two.account.id);
|
|
284
|
-
await test_app.app.request(grant_path, {
|
|
285
|
-
method: 'POST',
|
|
286
|
-
headers: admin_headers,
|
|
287
|
-
body: JSON.stringify({ role: grantable_role }),
|
|
288
|
-
});
|
|
289
|
-
// Find permit ID
|
|
290
|
-
const list_res = await test_app.app.request(accounts_route.path, {
|
|
291
|
-
headers: test_app.create_session_headers(),
|
|
292
|
-
});
|
|
293
|
-
const list_body = await list_res.json();
|
|
294
|
-
const entry = list_body.accounts.find((e) => e.account.id === user_two.account.id);
|
|
295
|
-
const permit = entry.permits.find((p) => p.role === grantable_role);
|
|
296
|
-
assert.ok(permit);
|
|
297
|
-
const revoke_path = revoke_route.path
|
|
298
|
-
.replace(':account_id', user_two.account.id)
|
|
299
|
-
.replace(':permit_id', permit.id);
|
|
300
|
-
// First revoke — succeeds
|
|
301
|
-
const first = await test_app.app.request(revoke_path, {
|
|
302
|
-
method: 'POST',
|
|
303
|
-
headers: test_app.create_session_headers(),
|
|
304
|
-
});
|
|
305
|
-
assert.strictEqual(first.status, 200);
|
|
306
|
-
// Second revoke — already revoked, returns 404
|
|
307
|
-
const second = await test_app.app.request(revoke_path, {
|
|
308
|
-
method: 'POST',
|
|
309
|
-
headers: test_app.create_session_headers(),
|
|
310
|
-
});
|
|
311
|
-
assert.strictEqual(second.status, 404);
|
|
312
|
-
const body = await second.clone().json();
|
|
313
|
-
assert.strictEqual(body.error, 'permit_not_found');
|
|
314
|
-
await error_collector.assert_and_record(test_app.route_specs, 'POST', revoke_route.path, second);
|
|
315
150
|
});
|
|
316
151
|
});
|
|
152
|
+
// --- 2. Permit grant/revoke lifecycle ---
|
|
153
|
+
// Permit grant/revoke are RPC-only (see `permit_offer_create` /
|
|
154
|
+
// `permit_revoke`). End-to-end coverage lives in
|
|
155
|
+
// `describe_rpc_round_trip_tests` + fuz_app's own
|
|
156
|
+
// `permit_offer_actions.db.test.ts` /
|
|
157
|
+
// `permit_offer_actions.notifications.revoke.db.test.ts`. The
|
|
158
|
+
// audit/isolation groups below exercise them as preconditions for
|
|
159
|
+
// cross-cutting checks (event emission, admin-to-admin isolation).
|
|
317
160
|
// --- 3. Admin session management ---
|
|
318
161
|
describe('admin session management', () => {
|
|
319
162
|
test('admin can list all active sessions', async () => {
|
|
320
163
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
321
|
-
const sessions_route = find_admin_route(test_app.route_specs, '/sessions', 'GET');
|
|
322
|
-
assert.ok(sessions_route, 'Expected admin GET /sessions route — ensure create_route_specs includes admin routes');
|
|
323
164
|
await test_app.create_account({ username: 'user_two' });
|
|
324
|
-
const res = await
|
|
165
|
+
const res = await rpc_call({
|
|
166
|
+
app: test_app.app,
|
|
167
|
+
path: rpc_path,
|
|
168
|
+
method: admin_session_list_action_spec.method,
|
|
325
169
|
headers: test_app.create_session_headers(),
|
|
326
170
|
});
|
|
327
|
-
assert.
|
|
328
|
-
const body =
|
|
171
|
+
assert.ok(res.ok, `admin_session_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
172
|
+
const body = res.result;
|
|
329
173
|
assert.ok(Array.isArray(body.sessions), 'Expected sessions array');
|
|
330
174
|
assert.ok(body.sessions.length >= 2, 'Expected sessions from multiple accounts');
|
|
331
175
|
});
|
|
332
176
|
test('admin can revoke all sessions for another account', async () => {
|
|
333
177
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
334
|
-
const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
|
|
335
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
336
|
-
assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
|
|
337
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
338
178
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
339
|
-
// Verify user_two's session works
|
|
340
|
-
const before = await
|
|
179
|
+
// Verify user_two's session works via `account_verify` RPC
|
|
180
|
+
const before = await rpc_call({
|
|
181
|
+
app: test_app.app,
|
|
182
|
+
path: rpc_path,
|
|
183
|
+
method: account_verify_action_spec.method,
|
|
341
184
|
headers: create_headers(user_two.session_cookie),
|
|
342
185
|
});
|
|
343
186
|
assert.strictEqual(before.status, 200);
|
|
344
|
-
// Admin revokes all sessions for user_two
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
187
|
+
// Admin revokes all sessions for user_two via RPC
|
|
188
|
+
const res = await rpc_call({
|
|
189
|
+
app: test_app.app,
|
|
190
|
+
path: rpc_path,
|
|
191
|
+
method: admin_session_revoke_all_action_spec.method,
|
|
192
|
+
params: { account_id: user_two.account.id },
|
|
348
193
|
headers: test_app.create_session_headers(),
|
|
349
194
|
});
|
|
350
|
-
assert.
|
|
351
|
-
const
|
|
352
|
-
assert.strictEqual(
|
|
353
|
-
assert.ok(
|
|
195
|
+
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
196
|
+
const result = res.result;
|
|
197
|
+
assert.strictEqual(result.ok, true);
|
|
198
|
+
assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
|
|
354
199
|
// Verify user_two's session no longer works
|
|
355
|
-
const after = await
|
|
200
|
+
const after = await rpc_call({
|
|
201
|
+
app: test_app.app,
|
|
202
|
+
path: rpc_path,
|
|
203
|
+
method: account_verify_action_spec.method,
|
|
356
204
|
headers: create_headers(user_two.session_cookie),
|
|
357
205
|
});
|
|
358
206
|
assert.strictEqual(after.status, 401);
|
|
359
207
|
});
|
|
360
208
|
test('admin revoking own sessions invalidates own session', async () => {
|
|
361
209
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const res = await test_app.app.request(path, {
|
|
369
|
-
method: 'POST',
|
|
210
|
+
// Admin revokes own sessions via RPC
|
|
211
|
+
const res = await rpc_call({
|
|
212
|
+
app: test_app.app,
|
|
213
|
+
path: rpc_path,
|
|
214
|
+
method: admin_session_revoke_all_action_spec.method,
|
|
215
|
+
params: { account_id: test_app.backend.account.id },
|
|
370
216
|
headers: test_app.create_session_headers(),
|
|
371
217
|
});
|
|
372
|
-
assert.
|
|
373
|
-
const
|
|
374
|
-
assert.strictEqual(
|
|
375
|
-
assert.ok(
|
|
218
|
+
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
219
|
+
const result = res.result;
|
|
220
|
+
assert.strictEqual(result.ok, true);
|
|
221
|
+
assert.ok(result.count >= 1, 'Expected at least 1 revoked session');
|
|
376
222
|
// Admin's own session should no longer work
|
|
377
|
-
const after = await
|
|
223
|
+
const after = await rpc_call({
|
|
224
|
+
app: test_app.app,
|
|
225
|
+
path: rpc_path,
|
|
226
|
+
method: account_verify_action_spec.method,
|
|
378
227
|
headers: test_app.create_session_headers(),
|
|
379
228
|
});
|
|
380
229
|
assert.strictEqual(after.status, 401);
|
|
@@ -384,92 +233,97 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
384
233
|
describe('admin token management', () => {
|
|
385
234
|
test('admin can revoke all tokens for another account', async () => {
|
|
386
235
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
387
|
-
const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
|
|
388
|
-
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
389
|
-
assert.ok(revoke_tokens_route, 'Expected admin POST /tokens/revoke-all route — ensure create_route_specs includes admin routes');
|
|
390
|
-
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
391
236
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
392
|
-
// Verify user_two's bearer token works
|
|
393
|
-
const before = await
|
|
394
|
-
|
|
237
|
+
// Verify user_two's bearer token works via `account_verify` RPC
|
|
238
|
+
const before = await rpc_call_non_browser({
|
|
239
|
+
app: test_app.app,
|
|
240
|
+
path: rpc_path,
|
|
241
|
+
method: account_verify_action_spec.method,
|
|
242
|
+
headers: { authorization: `Bearer ${user_two.api_token}` },
|
|
395
243
|
});
|
|
396
244
|
assert.strictEqual(before.status, 200);
|
|
397
|
-
// Admin revokes all tokens for user_two
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
245
|
+
// Admin revokes all tokens for user_two via RPC
|
|
246
|
+
const res = await rpc_call({
|
|
247
|
+
app: test_app.app,
|
|
248
|
+
path: rpc_path,
|
|
249
|
+
method: admin_token_revoke_all_action_spec.method,
|
|
250
|
+
params: { account_id: user_two.account.id },
|
|
401
251
|
headers: test_app.create_session_headers(),
|
|
402
252
|
});
|
|
403
|
-
assert.
|
|
404
|
-
const
|
|
405
|
-
assert.strictEqual(
|
|
406
|
-
assert.ok(
|
|
253
|
+
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
254
|
+
const result = res.result;
|
|
255
|
+
assert.strictEqual(result.ok, true);
|
|
256
|
+
assert.ok(result.count >= 1, 'Expected at least 1 revoked token');
|
|
407
257
|
// Verify user_two's bearer token no longer works
|
|
408
|
-
const after = await
|
|
409
|
-
|
|
258
|
+
const after = await rpc_call_non_browser({
|
|
259
|
+
app: test_app.app,
|
|
260
|
+
path: rpc_path,
|
|
261
|
+
method: account_verify_action_spec.method,
|
|
262
|
+
headers: { authorization: `Bearer ${user_two.api_token}` },
|
|
410
263
|
});
|
|
411
264
|
assert.strictEqual(after.status, 401);
|
|
412
265
|
});
|
|
413
266
|
});
|
|
414
|
-
// --- 5. Audit log
|
|
415
|
-
describe('audit log
|
|
267
|
+
// --- 5. Audit log RPC reads ---
|
|
268
|
+
describe('audit log RPC reads', () => {
|
|
416
269
|
test('admin can list audit log events', async () => {
|
|
417
270
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
271
|
+
const res = await rpc_call({
|
|
272
|
+
app: test_app.app,
|
|
273
|
+
path: rpc_path,
|
|
274
|
+
method: audit_log_list_action_spec.method,
|
|
421
275
|
headers: test_app.create_session_headers(),
|
|
422
276
|
});
|
|
423
|
-
assert.
|
|
424
|
-
const body =
|
|
277
|
+
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
278
|
+
const body = res.result;
|
|
425
279
|
assert.ok(Array.isArray(body.events), 'Expected events array');
|
|
426
280
|
});
|
|
427
281
|
test('audit log supports event_type filter', async () => {
|
|
428
282
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
432
|
-
assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
|
|
433
|
-
// Create a grant to produce an audit event
|
|
283
|
+
// Admin offer emits `permit_offer_create`. The downstream
|
|
284
|
+
// `permit_grant` only fires on accept — out of scope for this test.
|
|
434
285
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
286
|
+
const offer_res = await rpc_call({
|
|
287
|
+
app: test_app.app,
|
|
288
|
+
path: rpc_path,
|
|
289
|
+
method: permit_offer_create_action_spec.method,
|
|
290
|
+
params: { to_account_id: user_two.account.id, role: grantable_role },
|
|
291
|
+
headers: test_app.create_session_headers(),
|
|
440
292
|
});
|
|
441
|
-
|
|
442
|
-
const res = await
|
|
293
|
+
assert.ok(offer_res.ok, 'permit_offer_create should succeed');
|
|
294
|
+
const res = await rpc_call({
|
|
295
|
+
app: test_app.app,
|
|
296
|
+
path: rpc_path,
|
|
297
|
+
method: audit_log_list_action_spec.method,
|
|
298
|
+
params: { event_type: 'permit_offer_create' },
|
|
443
299
|
headers: test_app.create_session_headers(),
|
|
444
300
|
});
|
|
445
|
-
assert.
|
|
446
|
-
const body =
|
|
447
|
-
assert.ok(
|
|
448
|
-
assert.ok(body.events.length >= 1, 'Expected at least 1 permit_grant event');
|
|
301
|
+
assert.ok(res.ok, `audit_log_list failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
302
|
+
const body = res.result;
|
|
303
|
+
assert.ok(body.events.length >= 1, 'Expected at least 1 permit_offer_create event');
|
|
449
304
|
for (const event of body.events) {
|
|
450
|
-
assert.strictEqual(event.event_type, '
|
|
305
|
+
assert.strictEqual(event.event_type, 'permit_offer_create');
|
|
451
306
|
}
|
|
452
307
|
});
|
|
453
308
|
test('admin can view permit history', async () => {
|
|
454
309
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
458
|
-
assert.ok(history_route, 'Expected admin GET /audit-log/permit-history route — ensure create_route_specs includes admin routes');
|
|
459
|
-
// Create a grant to produce audit data
|
|
310
|
+
// Drive the full consent flow so `permit_grant` lands in the audit log
|
|
311
|
+
// — `query_audit_log_list_permit_history` filters to (permit_grant, permit_revoke).
|
|
460
312
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
});
|
|
467
|
-
const res = await
|
|
313
|
+
await offer_and_accept({
|
|
314
|
+
app: test_app.app,
|
|
315
|
+
admin_headers: test_app.create_session_headers(),
|
|
316
|
+
to_account_id: user_two.account.id,
|
|
317
|
+
role: grantable_role,
|
|
318
|
+
});
|
|
319
|
+
const res = await rpc_call({
|
|
320
|
+
app: test_app.app,
|
|
321
|
+
path: rpc_path,
|
|
322
|
+
method: audit_log_permit_history_action_spec.method,
|
|
468
323
|
headers: test_app.create_session_headers(),
|
|
469
324
|
});
|
|
470
|
-
assert.
|
|
471
|
-
const body =
|
|
472
|
-
assert.ok(Array.isArray(body.events), 'Expected events array');
|
|
325
|
+
assert.ok(res.ok, `audit_log_permit_history failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
326
|
+
const body = res.result;
|
|
473
327
|
assert.ok(body.events.length >= 1, 'Expected at least 1 permit history event');
|
|
474
328
|
});
|
|
475
329
|
});
|
|
@@ -477,85 +331,147 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
477
331
|
describe('admin audit trail', () => {
|
|
478
332
|
test('permit revoke creates audit event', async () => {
|
|
479
333
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
480
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
481
|
-
const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
|
|
482
|
-
const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
|
|
483
|
-
const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
|
|
484
|
-
assert.ok(grant_route, 'Expected admin POST /permits/grant route — ensure create_route_specs includes admin routes');
|
|
485
|
-
assert.ok(revoke_route, 'Expected admin POST /permits/:permit_id/revoke route — ensure create_route_specs includes admin routes');
|
|
486
|
-
assert.ok(accounts_route, 'Expected admin GET /accounts route — ensure create_route_specs includes admin routes');
|
|
487
|
-
assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
|
|
488
334
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
335
|
+
const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
|
|
336
|
+
assert.ok(target_actor);
|
|
337
|
+
const permit = await query_grant_permit({ db: get_db() }, {
|
|
338
|
+
actor_id: target_actor.id,
|
|
339
|
+
role: grantable_role,
|
|
340
|
+
granted_by: test_app.backend.actor.id,
|
|
341
|
+
});
|
|
342
|
+
// Revoke via RPC
|
|
343
|
+
const revoke_res = await rpc_call({
|
|
344
|
+
app: test_app.app,
|
|
345
|
+
path: rpc_path,
|
|
346
|
+
method: permit_revoke_action_spec.method,
|
|
347
|
+
params: { actor_id: target_actor.id, permit_id: permit.id },
|
|
499
348
|
headers: test_app.create_session_headers(),
|
|
500
349
|
});
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
await test_app.app.request(revoke_path, {
|
|
509
|
-
method: 'POST',
|
|
350
|
+
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
351
|
+
// Check audit log for permit_revoke event
|
|
352
|
+
const audit_res = await rpc_call({
|
|
353
|
+
app: test_app.app,
|
|
354
|
+
path: rpc_path,
|
|
355
|
+
method: audit_log_list_action_spec.method,
|
|
356
|
+
params: { event_type: 'permit_revoke' },
|
|
510
357
|
headers: test_app.create_session_headers(),
|
|
511
358
|
});
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
assert.strictEqual(audit_res.status, 200);
|
|
515
|
-
const audit_body = await audit_res.json();
|
|
359
|
+
assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
|
|
360
|
+
const audit_body = audit_res.result;
|
|
516
361
|
assert.ok(audit_body.events.length >= 1, 'Expected permit_revoke audit event');
|
|
517
362
|
assert.strictEqual(audit_body.events[0].event_type, 'permit_revoke');
|
|
518
363
|
});
|
|
519
364
|
test('admin session revoke-all creates audit event', async () => {
|
|
520
365
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
521
|
-
const revoke_sessions_route = find_admin_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
|
|
522
|
-
const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
|
|
523
|
-
assert.ok(revoke_sessions_route, 'Expected admin POST /sessions/revoke-all route — ensure create_route_specs includes admin routes');
|
|
524
|
-
assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
|
|
525
366
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
526
|
-
// Revoke all sessions for user_two
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
367
|
+
// Revoke all sessions for user_two via RPC
|
|
368
|
+
const revoke_res = await rpc_call({
|
|
369
|
+
app: test_app.app,
|
|
370
|
+
path: rpc_path,
|
|
371
|
+
method: admin_session_revoke_all_action_spec.method,
|
|
372
|
+
params: { account_id: user_two.account.id },
|
|
530
373
|
headers: test_app.create_session_headers(),
|
|
531
374
|
});
|
|
375
|
+
assert.ok(revoke_res.ok, `admin_session_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
532
376
|
// Check audit log
|
|
533
|
-
const audit_res = await
|
|
534
|
-
|
|
535
|
-
|
|
377
|
+
const audit_res = await rpc_call({
|
|
378
|
+
app: test_app.app,
|
|
379
|
+
path: rpc_path,
|
|
380
|
+
method: audit_log_list_action_spec.method,
|
|
381
|
+
params: { event_type: 'session_revoke_all' },
|
|
382
|
+
headers: test_app.create_session_headers(),
|
|
383
|
+
});
|
|
384
|
+
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
385
|
+
const audit_body = audit_res.result;
|
|
536
386
|
assert.ok(audit_body.events.length >= 1, 'Expected session_revoke_all audit event');
|
|
537
387
|
assert.strictEqual(audit_body.events[0].event_type, 'session_revoke_all');
|
|
538
388
|
});
|
|
539
389
|
test('admin token revoke-all creates audit event', async () => {
|
|
540
390
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
541
|
-
const revoke_tokens_route = find_admin_route(test_app.route_specs, '/tokens/revoke-all', 'POST');
|
|
542
|
-
const audit_route = find_admin_route(test_app.route_specs, '/audit-log', 'GET');
|
|
543
|
-
assert.ok(revoke_tokens_route, 'Expected admin POST /tokens/revoke-all route — ensure create_route_specs includes admin routes');
|
|
544
|
-
assert.ok(audit_route, 'Expected admin GET /audit-log route — ensure create_route_specs includes admin routes');
|
|
545
391
|
const user_two = await test_app.create_account({ username: 'user_two' });
|
|
546
|
-
// Revoke all tokens for user_two
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
392
|
+
// Revoke all tokens for user_two via RPC
|
|
393
|
+
const revoke_res = await rpc_call({
|
|
394
|
+
app: test_app.app,
|
|
395
|
+
path: rpc_path,
|
|
396
|
+
method: admin_token_revoke_all_action_spec.method,
|
|
397
|
+
params: { account_id: user_two.account.id },
|
|
550
398
|
headers: test_app.create_session_headers(),
|
|
551
399
|
});
|
|
400
|
+
assert.ok(revoke_res.ok, `admin_token_revoke_all failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
552
401
|
// Check audit log
|
|
553
|
-
const audit_res = await
|
|
554
|
-
|
|
555
|
-
|
|
402
|
+
const audit_res = await rpc_call({
|
|
403
|
+
app: test_app.app,
|
|
404
|
+
path: rpc_path,
|
|
405
|
+
method: audit_log_list_action_spec.method,
|
|
406
|
+
params: { event_type: 'token_revoke_all' },
|
|
407
|
+
headers: test_app.create_session_headers(),
|
|
408
|
+
});
|
|
409
|
+
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
410
|
+
const audit_body = audit_res.result;
|
|
556
411
|
assert.ok(audit_body.events.length >= 1, 'Expected token_revoke_all audit event');
|
|
557
412
|
assert.strictEqual(audit_body.events[0].event_type, 'token_revoke_all');
|
|
558
413
|
});
|
|
414
|
+
test('admin session revoke-all 404 emits failure audit', async () => {
|
|
415
|
+
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
416
|
+
// `Uuid = z.uuid()` is v4-strict; use a valid v4 shape so we hit the
|
|
417
|
+
// handler's account lookup rather than failing at param validation.
|
|
418
|
+
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa01';
|
|
419
|
+
const res = await rpc_call({
|
|
420
|
+
app: test_app.app,
|
|
421
|
+
path: rpc_path,
|
|
422
|
+
method: admin_session_revoke_all_action_spec.method,
|
|
423
|
+
params: { account_id: missing_id },
|
|
424
|
+
headers: test_app.create_session_headers(),
|
|
425
|
+
});
|
|
426
|
+
assert.ok(!res.ok, 'Expected 404 for missing account');
|
|
427
|
+
assert.strictEqual(res.status, 404);
|
|
428
|
+
assert.strictEqual(res.error.data.reason, 'account_not_found');
|
|
429
|
+
// Failure audit row should be visible on the audit-log feed.
|
|
430
|
+
// `target_account_id` is null (FK prevents referencing a missing id)
|
|
431
|
+
// — the probed id is preserved under `metadata.attempted_account_id`.
|
|
432
|
+
const audit_res = await rpc_call({
|
|
433
|
+
app: test_app.app,
|
|
434
|
+
path: rpc_path,
|
|
435
|
+
method: audit_log_list_action_spec.method,
|
|
436
|
+
params: { event_type: 'session_revoke_all' },
|
|
437
|
+
headers: test_app.create_session_headers(),
|
|
438
|
+
});
|
|
439
|
+
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
440
|
+
const audit_body = audit_res.result;
|
|
441
|
+
const failure = audit_body.events.find((e) => e.outcome === 'failure');
|
|
442
|
+
assert.ok(failure, 'Expected a failure-outcome session_revoke_all audit event');
|
|
443
|
+
assert.strictEqual(failure.target_account_id, null);
|
|
444
|
+
assert.strictEqual(failure.metadata.reason, 'account_not_found');
|
|
445
|
+
assert.strictEqual(failure.metadata.attempted_account_id, missing_id);
|
|
446
|
+
});
|
|
447
|
+
test('admin token revoke-all 404 emits failure audit', async () => {
|
|
448
|
+
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
449
|
+
const missing_id = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa02';
|
|
450
|
+
const res = await rpc_call({
|
|
451
|
+
app: test_app.app,
|
|
452
|
+
path: rpc_path,
|
|
453
|
+
method: admin_token_revoke_all_action_spec.method,
|
|
454
|
+
params: { account_id: missing_id },
|
|
455
|
+
headers: test_app.create_session_headers(),
|
|
456
|
+
});
|
|
457
|
+
assert.ok(!res.ok, 'Expected 404 for missing account');
|
|
458
|
+
assert.strictEqual(res.status, 404);
|
|
459
|
+
assert.strictEqual(res.error.data.reason, 'account_not_found');
|
|
460
|
+
const audit_res = await rpc_call({
|
|
461
|
+
app: test_app.app,
|
|
462
|
+
path: rpc_path,
|
|
463
|
+
method: audit_log_list_action_spec.method,
|
|
464
|
+
params: { event_type: 'token_revoke_all' },
|
|
465
|
+
headers: test_app.create_session_headers(),
|
|
466
|
+
});
|
|
467
|
+
assert.ok(audit_res.ok, 'audit_log_list should succeed');
|
|
468
|
+
const audit_body = audit_res.result;
|
|
469
|
+
const failure = audit_body.events.find((e) => e.outcome === 'failure');
|
|
470
|
+
assert.ok(failure, 'Expected a failure-outcome token_revoke_all audit event');
|
|
471
|
+
assert.strictEqual(failure.target_account_id, null);
|
|
472
|
+
assert.strictEqual(failure.metadata.reason, 'account_not_found');
|
|
473
|
+
assert.strictEqual(failure.metadata.attempted_account_id, missing_id);
|
|
474
|
+
});
|
|
559
475
|
});
|
|
560
476
|
// --- 7. Audit log completeness ---
|
|
561
477
|
describe('audit log completeness', () => {
|
|
@@ -563,26 +479,13 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
563
479
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
564
480
|
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
565
481
|
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
566
|
-
const grant_route = find_admin_route(test_app.route_specs, '/permits/grant', 'POST');
|
|
567
|
-
const revoke_route = find_admin_route(test_app.route_specs, '/permits/:permit_id/revoke', 'POST');
|
|
568
|
-
const accounts_route = find_admin_route(test_app.route_specs, '/accounts', 'GET');
|
|
569
|
-
const create_token_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
|
|
570
482
|
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
//
|
|
574
|
-
if (!login_route ||
|
|
575
|
-
!logout_route ||
|
|
576
|
-
!grant_route ||
|
|
577
|
-
!revoke_route ||
|
|
578
|
-
!accounts_route ||
|
|
579
|
-
!create_token_route ||
|
|
580
|
-
!password_route)
|
|
483
|
+
// skip if required routes are missing (consumer may not wire all routes).
|
|
484
|
+
// Token creation goes through `account_token_create` RPC — always wired
|
|
485
|
+
// because `rpc_endpoints` is required at the suite level.
|
|
486
|
+
if (!login_route || !logout_route || !password_route)
|
|
581
487
|
return;
|
|
582
488
|
const user_two = await test_app.create_account({ username: 'audit_user' });
|
|
583
|
-
const admin_headers = test_app.create_session_headers({
|
|
584
|
-
'content-type': 'application/json',
|
|
585
|
-
});
|
|
586
489
|
// 1. login (user_two logs in)
|
|
587
490
|
const login_res = await test_app.app.request(login_route.path, {
|
|
588
491
|
method: 'POST',
|
|
@@ -609,36 +512,35 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
609
512
|
},
|
|
610
513
|
});
|
|
611
514
|
}
|
|
612
|
-
// 3.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
515
|
+
// 3. offer permit (admin offers grantable_role to user_two) — full
|
|
516
|
+
// consentful flow: offer + accept so both `permit_offer_create` and
|
|
517
|
+
// `permit_grant` audit events land.
|
|
518
|
+
const { permit_id } = await offer_and_accept({
|
|
519
|
+
app: test_app.app,
|
|
520
|
+
admin_headers: test_app.create_session_headers(),
|
|
521
|
+
to_account_id: user_two.account.id,
|
|
522
|
+
role: grantable_role,
|
|
523
|
+
});
|
|
524
|
+
// 4. revoke permit (RPC)
|
|
525
|
+
const target_actor = await query_actor_by_account({ db: get_db() }, user_two.account.id);
|
|
526
|
+
assert.ok(target_actor);
|
|
527
|
+
const revoke_res = await rpc_call({
|
|
528
|
+
app: test_app.app,
|
|
529
|
+
path: rpc_path,
|
|
530
|
+
method: permit_revoke_action_spec.method,
|
|
531
|
+
params: { actor_id: target_actor.id, permit_id },
|
|
621
532
|
headers: test_app.create_session_headers(),
|
|
622
533
|
});
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
await test_app.app.request(rev_path, {
|
|
632
|
-
method: 'POST',
|
|
633
|
-
headers: test_app.create_session_headers(),
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
// 5. create token
|
|
637
|
-
await test_app.app.request(create_token_route.path, {
|
|
638
|
-
method: 'POST',
|
|
639
|
-
headers: admin_headers,
|
|
640
|
-
body: JSON.stringify({ name: 'audit-test-token' }),
|
|
534
|
+
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
535
|
+
// 5. create token (RPC)
|
|
536
|
+
const token_res = await rpc_call({
|
|
537
|
+
app: test_app.app,
|
|
538
|
+
path: rpc_path,
|
|
539
|
+
method: account_token_create_action_spec.method,
|
|
540
|
+
params: { name: 'audit-test-token' },
|
|
541
|
+
headers: test_app.create_session_headers(),
|
|
641
542
|
});
|
|
543
|
+
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|
|
642
544
|
// 6. password change
|
|
643
545
|
await test_app.app.request(password_route.path, {
|
|
644
546
|
method: 'POST',
|
|
@@ -673,16 +575,23 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
673
575
|
origin: 'http://localhost:5173',
|
|
674
576
|
cookie: `${cookie_name}=${relogin_match[1]}`,
|
|
675
577
|
};
|
|
676
|
-
const audit_res = await
|
|
578
|
+
const audit_res = await rpc_call({
|
|
579
|
+
app: test_app.app,
|
|
580
|
+
path: rpc_path,
|
|
581
|
+
method: audit_log_list_action_spec.method,
|
|
677
582
|
headers: relogin_headers,
|
|
678
583
|
});
|
|
679
|
-
assert.
|
|
680
|
-
const audit_body =
|
|
584
|
+
assert.ok(audit_res.ok, `audit_log_list failed: ${audit_res.ok ? '' : JSON.stringify(audit_res.error)}`);
|
|
585
|
+
const audit_body = audit_res.result;
|
|
681
586
|
const events = audit_body.events;
|
|
682
|
-
// check that each operation produced at least one event
|
|
587
|
+
// check that each operation produced at least one event.
|
|
588
|
+
// `permit_offer_create` fires on the admin RPC; `permit_grant`
|
|
589
|
+
// fires when the recipient accepts (driven by offer_and_accept).
|
|
683
590
|
const expected_types = [
|
|
684
591
|
'login',
|
|
685
592
|
'logout',
|
|
593
|
+
'permit_offer_create',
|
|
594
|
+
'permit_offer_accept',
|
|
686
595
|
'permit_grant',
|
|
687
596
|
'permit_revoke',
|
|
688
597
|
'token_create',
|
|
@@ -697,7 +606,7 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
697
606
|
});
|
|
698
607
|
// --- 8. Admin-to-admin isolation ---
|
|
699
608
|
describe('admin-to-admin isolation', () => {
|
|
700
|
-
test('admin
|
|
609
|
+
test('admin B revoking own permit via RPC succeeds', async () => {
|
|
701
610
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
702
611
|
captured_route_specs ??= test_app.route_specs;
|
|
703
612
|
// Bootstrap user is admin A. Create admin B.
|
|
@@ -705,36 +614,24 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
705
614
|
username: 'admin_b_iso',
|
|
706
615
|
roles: ['admin'],
|
|
707
616
|
});
|
|
708
|
-
//
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
assert.ok(grant_body.permit, 'Expected permit in grant response');
|
|
722
|
-
const permit_id = grant_body.permit.id;
|
|
723
|
-
// Admin B revokes their own permit via admin route — should succeed
|
|
724
|
-
const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
725
|
-
s.path.includes('/permits/:permit_id/revoke') &&
|
|
726
|
-
s.auth.type === 'role' &&
|
|
727
|
-
s.auth.role === 'admin');
|
|
728
|
-
assert.ok(revoke_route, 'Expected POST /permits/:permit_id/revoke admin route');
|
|
729
|
-
const revoke_res = await test_app.app.request(revoke_route.path
|
|
730
|
-
.replace(':account_id', admin_b.account.id)
|
|
731
|
-
.replace(':permit_id', permit_id), {
|
|
732
|
-
method: 'POST',
|
|
617
|
+
// Seed an active permit directly — the revoke IDOR check is the
|
|
618
|
+
// subject of this test, not the grant→accept cycle.
|
|
619
|
+
const permit = await query_grant_permit({ db: get_db() }, {
|
|
620
|
+
actor_id: admin_b.actor.id,
|
|
621
|
+
role: grantable_role,
|
|
622
|
+
granted_by: test_app.backend.actor.id,
|
|
623
|
+
});
|
|
624
|
+
// Admin B revokes their own permit via RPC — should succeed
|
|
625
|
+
const revoke_res = await rpc_call({
|
|
626
|
+
app: test_app.app,
|
|
627
|
+
path: rpc_path,
|
|
628
|
+
method: permit_revoke_action_spec.method,
|
|
629
|
+
params: { actor_id: admin_b.actor.id, permit_id: permit.id },
|
|
733
630
|
headers: create_headers(admin_b.session_cookie),
|
|
734
631
|
});
|
|
735
|
-
assert.
|
|
736
|
-
const
|
|
737
|
-
assert.strictEqual(
|
|
632
|
+
assert.ok(revoke_res.ok, `permit_revoke failed: ${revoke_res.ok ? '' : JSON.stringify(revoke_res.error)}`);
|
|
633
|
+
const result = revoke_res.result;
|
|
634
|
+
assert.strictEqual(result.revoked, true);
|
|
738
635
|
});
|
|
739
636
|
test('admin revoke-all sessions for another admin works', async () => {
|
|
740
637
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -742,17 +639,18 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
742
639
|
username: 'admin_b_sess',
|
|
743
640
|
roles: ['admin'],
|
|
744
641
|
});
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
method:
|
|
642
|
+
// Admin A revokes all of admin B's sessions via RPC
|
|
643
|
+
const res = await rpc_call({
|
|
644
|
+
app: test_app.app,
|
|
645
|
+
path: rpc_path,
|
|
646
|
+
method: admin_session_revoke_all_action_spec.method,
|
|
647
|
+
params: { account_id: admin_b.account.id },
|
|
750
648
|
headers: create_headers(test_app.backend.session_cookie),
|
|
751
649
|
});
|
|
752
|
-
assert.
|
|
753
|
-
const
|
|
754
|
-
assert.ok(typeof
|
|
755
|
-
assert.ok(
|
|
650
|
+
assert.ok(res.ok, `admin_session_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
651
|
+
const result = res.result;
|
|
652
|
+
assert.ok(typeof result.count === 'number', 'Expected count field in response');
|
|
653
|
+
assert.ok(result.count >= 1, 'Expected at least 1 session revoked');
|
|
756
654
|
});
|
|
757
655
|
test('admin revoke-all tokens for another admin works', async () => {
|
|
758
656
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
@@ -760,39 +658,41 @@ export const describe_standard_admin_integration_tests = (options) => {
|
|
|
760
658
|
username: 'admin_b_tok',
|
|
761
659
|
roles: ['admin'],
|
|
762
660
|
});
|
|
763
|
-
// Admin B creates an API token
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
method: 'POST',
|
|
661
|
+
// Admin B creates an API token via RPC
|
|
662
|
+
const token_res = await rpc_call({
|
|
663
|
+
app: test_app.app,
|
|
664
|
+
path: rpc_path,
|
|
665
|
+
method: account_token_create_action_spec.method,
|
|
666
|
+
params: { name: 'admin-b-token' },
|
|
667
|
+
headers: create_headers(admin_b.session_cookie),
|
|
668
|
+
});
|
|
669
|
+
assert.ok(token_res.ok, `account_token_create failed: ${token_res.ok ? '' : JSON.stringify(token_res.error)}`);
|
|
670
|
+
// Admin A revokes all of admin B's tokens via RPC
|
|
671
|
+
const res = await rpc_call({
|
|
672
|
+
app: test_app.app,
|
|
673
|
+
path: rpc_path,
|
|
674
|
+
method: admin_token_revoke_all_action_spec.method,
|
|
675
|
+
params: { account_id: admin_b.account.id },
|
|
779
676
|
headers: create_headers(test_app.backend.session_cookie),
|
|
780
677
|
});
|
|
781
|
-
assert.
|
|
782
|
-
const
|
|
783
|
-
assert.ok(typeof
|
|
678
|
+
assert.ok(res.ok, `admin_token_revoke_all failed: ${res.ok ? '' : JSON.stringify(res.error)}`);
|
|
679
|
+
const result = res.result;
|
|
680
|
+
assert.ok(typeof result.count === 'number', 'Expected count field in response');
|
|
681
|
+
// Token was created above, so revoke should evict at least one.
|
|
682
|
+
assert.ok(result.count >= 1, 'Expected at least 1 token revoked');
|
|
784
683
|
});
|
|
785
684
|
test('non-admin cannot access admin routes for another account', async () => {
|
|
786
685
|
const test_app = await create_test_app(build_admin_test_app_options(options, get_db()));
|
|
787
686
|
const regular_user = await test_app.create_account({ username: 'regular_user_iso' });
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
687
|
+
// Regular user tries to list accounts via the admin RPC — should 403
|
|
688
|
+
const res = await rpc_call({
|
|
689
|
+
app: test_app.app,
|
|
690
|
+
path: rpc_path,
|
|
691
|
+
method: admin_account_list_action_spec.method,
|
|
792
692
|
headers: create_headers(regular_user.session_cookie),
|
|
793
693
|
});
|
|
694
|
+
assert.ok(!res.ok, 'Expected admin_account_list to fail for non-admin');
|
|
794
695
|
assert.strictEqual(res.status, 403);
|
|
795
|
-
error_collector.record(test_app.route_specs, 'GET', accounts_route.path, 403);
|
|
796
696
|
});
|
|
797
697
|
});
|
|
798
698
|
// --- 8a. Error coverage: unauthenticated access to admin routes ---
|