@consiliency/agent-board-schema 1.53.0 → 1.55.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/fixtures/already-applied.json +615 -607
- package/migrations/20261004000100_agent_board_callback_consumer_intent.sql +52 -0
- package/migrations/20261005000100_agent_board_enroll_token_single_use.sql +886 -0
- package/package.json +1 -1
- package/schema-manifest.json +21 -3
- package/src/tooling.test.mjs +5 -2
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +35 -35
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +5 -5
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +32 -32
- package/tests/agent_board_enroll_federated_identity.test.sql +11 -10
- package/tests/agent_board_enroll_pop_binding.test.sql +12 -12
- package/tests/agent_board_enroll_resumption.test.sql +1 -1
- package/tests/agent_board_enroll_token_single_use.test.sql +538 -0
- package/tests/agent_board_enrollment_audit_content.test.sql +3 -3
- package/tests/agent_board_federated_lock_order_class.test.sql +5 -5
- package/tests/agent_board_fedsession.test.sql +1 -1
- package/tests/agent_board_fk_lock_edges.test.sql +23 -0
- package/tests/agent_board_identity_providers.test.sql +3 -2
- package/tests/agent_board_portal_offline_invariant.test.sql +1 -1
- package/tests/agent_board_roomlink.test.sql +2 -2
- package/tests/agent_board_write_enforcement_class.test.sql +5 -2
- package/tests/catalog/function_execute_privileges.json +12 -3
- package/tests/catalog/p_metadata_parameters.json +6 -2
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +1 -1
- package/tests/isolation/expected/atomic_fleet_authority_races.out +8 -8
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +2 -2
- package/tests/isolation/expected/atomic_fleet_board_archive.out +2 -2
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +2 -2
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +2 -2
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +22 -22
- package/tests/isolation/expected/atomic_fleet_onboarding.out +2 -2
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +1 -1
- package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +1 -1
- package/tests/isolation/expected/enroll_token_fleet_abort.out +81 -0
- package/tests/isolation/expected/enroll_token_pk_backstop.out +36 -0
- package/tests/isolation/expected/enroll_token_placement.out +86 -0
- package/tests/isolation/expected/enroll_token_rollback_unstrands.out +59 -0
- package/tests/isolation/expected/enroll_token_route_race_fleet_first.out +72 -0
- package/tests/isolation/expected/enroll_token_route_race_identity_first.out +72 -0
- package/tests/isolation/expected/enroll_token_same_key_fleet_then_identity.out +73 -0
- package/tests/isolation/expected/enroll_token_same_key_identity_then_fleet.out +73 -0
- package/tests/isolation/expected/enroll_token_same_key_resume.out +59 -0
- package/tests/isolation/expected/enroll_token_same_token_race.out +58 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +2 -2
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +1 -1
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +2 -2
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +2 -2
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +2 -2
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +2 -2
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +2 -2
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +2 -2
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +2 -2
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +2 -2
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +1 -1
- package/tests/isolation/specs/enroll_token_fleet_abort.spec +138 -0
- package/tests/isolation/specs/enroll_token_pk_backstop.spec +37 -0
- package/tests/isolation/specs/enroll_token_placement.spec +136 -0
- package/tests/isolation/specs/enroll_token_rollback_unstrands.spec +71 -0
- package/tests/isolation/specs/enroll_token_route_race_fleet_first.spec +119 -0
- package/tests/isolation/specs/enroll_token_route_race_identity_first.spec +120 -0
- package/tests/isolation/specs/enroll_token_same_key_fleet_then_identity.spec +112 -0
- package/tests/isolation/specs/enroll_token_same_key_identity_then_fleet.spec +113 -0
- package/tests/isolation/specs/enroll_token_same_key_resume.spec +70 -0
- package/tests/isolation/specs/enroll_token_same_token_race.spec +80 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +1 -1
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
-- Consiliency/message-board#521 pgTAP: A VERIFIED PORTAL TOKEN ENROLLS AT MOST ONE KEY.
|
|
2
|
+
--
|
|
3
|
+
-- /enroll's proof-of-possession is the new key's signature over the token, so whoever
|
|
4
|
+
-- holds a token can mint a fresh key and sign it again; the #176 replay path is keyed on
|
|
5
|
+
-- the KEY and a fresh key walks past it. 20261005000100 binds each token (the sha256 of
|
|
6
|
+
-- its RS256 signing input, computed by the Edge Function) to one key fingerprint, in ONE
|
|
7
|
+
-- place: enroll_federated_identity, after its owner lock, before the found/not-found
|
|
8
|
+
-- split. This file pins the behaviour (T1-T10), the placement, and the class (EC-T1..4).
|
|
9
|
+
--
|
|
10
|
+
-- Rule 3 throughout: every allow-side call is `lives_ok(...)` followed by a SEPARATE state
|
|
11
|
+
-- check, never a bare `is((select ... from rpc(...)))` (#113 -- that form aborts the file
|
|
12
|
+
-- instead of failing it). Every call expected to raise goes through pg_temp.try(), a
|
|
13
|
+
-- BEGIN/EXCEPTION subtransaction that records '00000' when the call does NOT raise, so a
|
|
14
|
+
-- vanished raise fails an assertion rather than matching an absent row. Result tables are
|
|
15
|
+
-- declared up front, never CTAS'd from the call under test. Counts are scoped to this
|
|
16
|
+
-- file's own fixtures (its issuer, its fingerprints, its token hashes).
|
|
17
|
+
create extension if not exists pgtap with schema extensions;
|
|
18
|
+
set search_path = extensions, public, agent_board;
|
|
19
|
+
|
|
20
|
+
begin;
|
|
21
|
+
|
|
22
|
+
select plan(66);
|
|
23
|
+
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
-- Fixtures
|
|
26
|
+
-- ---------------------------------------------------------------------------
|
|
27
|
+
create temp table _v (name text primary key, val text) on commit drop;
|
|
28
|
+
create temp table _err (label text primary key, state text, msg text) on commit drop;
|
|
29
|
+
create temp table _r (
|
|
30
|
+
label text, agent_id uuid, auth_user_id uuid, provider_id text, signing_key_id uuid,
|
|
31
|
+
granted_scopes text[], key_id text, key_version int, agent_handle text,
|
|
32
|
+
invite_redeemed boolean
|
|
33
|
+
) on commit drop;
|
|
34
|
+
create temp table _f (label text, agent_id uuid, identity_disposition text) on commit drop;
|
|
35
|
+
|
|
36
|
+
create function pg_temp.v(p_name text) returns text language sql stable as
|
|
37
|
+
$$ select val from _v where name = p_name $$;
|
|
38
|
+
|
|
39
|
+
-- A token identity: lowercase hex sha256, exactly what the Edge Function sends.
|
|
40
|
+
create function pg_temp.tok(p_label text) returns text language sql immutable as
|
|
41
|
+
$$ select encode(sha256(convert_to('tok521-test-token-' || p_label, 'UTF8')), 'hex') $$;
|
|
42
|
+
|
|
43
|
+
-- The exact identity-route call, as SQL text. The hash and expiry are passed as SQL
|
|
44
|
+
-- expressions so the T9 gate cases can pass NULL and malformed values through the same
|
|
45
|
+
-- builder the allow-side cases use.
|
|
46
|
+
create function pg_temp.enroll_sql(
|
|
47
|
+
p_sub text, p_slug text, p_key int, p_hash_sql text, p_exp_sql text default null
|
|
48
|
+
) returns text language sql stable as
|
|
49
|
+
$$
|
|
50
|
+
select format(
|
|
51
|
+
$q$select %L::text, * from agent_board.enroll_federated_identity(%L,'message-board',%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,%s,%s,null,'agent','{board:send}','{}')$q$,
|
|
52
|
+
p_slug, pg_temp.v('iss'), p_sub,
|
|
53
|
+
jsonb_build_object('iss', pg_temp.v('iss'), 'aud', 'message-board', 'sub', p_sub)::text,
|
|
54
|
+
p_slug, pg_temp.v('pub' || p_key), pg_temp.v('fp' || p_key),
|
|
55
|
+
p_hash_sql, coalesce(p_exp_sql, quote_literal(pg_temp.v('exp')) || '::timestamptz'))
|
|
56
|
+
$$;
|
|
57
|
+
|
|
58
|
+
-- The exact fleet-route call. `p_inv` names one of the fixture invites; its bounds fix the
|
|
59
|
+
-- authorized handle, which is the slug.
|
|
60
|
+
create function pg_temp.fleet_sql(
|
|
61
|
+
p_sub text, p_inv text, p_key int, p_hash_sql text, p_exp_sql text default null
|
|
62
|
+
) returns text language sql stable as
|
|
63
|
+
$$
|
|
64
|
+
select format(
|
|
65
|
+
$q$select %L::text, r.agent_id, r.identity_disposition from agent_board.onboard_federated_fleet_agent(%L,'message-board',%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,%s,%s,null,'agent','{board:send}','{}',%L,%L::uuid,%L::uuid,'codex','phase-loop','message-board','sha256:tok521-repo','worker','phase_worker',%L) r$q$,
|
|
66
|
+
p_inv, pg_temp.v('iss'), p_sub,
|
|
67
|
+
jsonb_build_object('iss', pg_temp.v('iss'), 'aud', 'message-board', 'sub', p_sub)::text,
|
|
68
|
+
pg_temp.v('handle_' || p_inv), pg_temp.v('pub' || p_key), pg_temp.v('fp' || p_key),
|
|
69
|
+
p_hash_sql, coalesce(p_exp_sql, quote_literal(pg_temp.v('exp')) || '::timestamptz'),
|
|
70
|
+
pg_temp.v('code_' || p_inv), pg_temp.v('board'), pg_temp.v('machine'),
|
|
71
|
+
pg_temp.v('handle_' || p_inv))
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
create function pg_temp.try(p_label text, p_sql text) returns void language plpgsql as
|
|
75
|
+
$$
|
|
76
|
+
begin
|
|
77
|
+
begin
|
|
78
|
+
execute p_sql;
|
|
79
|
+
insert into _err values (p_label, '00000', '(no error raised)');
|
|
80
|
+
exception when others then
|
|
81
|
+
insert into _err values (p_label, sqlstate, sqlerrm);
|
|
82
|
+
end;
|
|
83
|
+
end
|
|
84
|
+
$$;
|
|
85
|
+
|
|
86
|
+
-- The fingerprint a token is bound to, or NULL.
|
|
87
|
+
create function pg_temp.bound(p_label text) returns text language sql stable as
|
|
88
|
+
$$ select agent_public_key_fingerprint from agent_board.enroll_token_uses where token_sha256 = pg_temp.tok(p_label) $$;
|
|
89
|
+
|
|
90
|
+
do $$
|
|
91
|
+
declare
|
|
92
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
93
|
+
v_creator uuid := gen_random_uuid();
|
|
94
|
+
v_board uuid;
|
|
95
|
+
v_domain uuid;
|
|
96
|
+
v_machine uuid;
|
|
97
|
+
v_code text;
|
|
98
|
+
v_pub text;
|
|
99
|
+
i int;
|
|
100
|
+
inv text;
|
|
101
|
+
begin
|
|
102
|
+
insert into _v values
|
|
103
|
+
('s', s), ('iss', 'https://tok521-' || s || '.test'),
|
|
104
|
+
('suba', 'portal|tok521-a-' || s), ('subr', 'portal|tok521-r-' || s),
|
|
105
|
+
-- One fixed expiry for every allow-side call, whole seconds so the stored value
|
|
106
|
+
-- round-trips exactly (T1 compares it).
|
|
107
|
+
('exp', (date_trunc('second', now()) + interval '1 hour')::text),
|
|
108
|
+
('creator', v_creator::text);
|
|
109
|
+
|
|
110
|
+
insert into agent_board.identity_providers
|
|
111
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
112
|
+
values (pg_temp.v('iss'), 'message-board', true, true, '{board:send}', '{}', '{agent}');
|
|
113
|
+
|
|
114
|
+
for i in 1..14 loop
|
|
115
|
+
v_pub := rtrim(translate(encode(gen_random_bytes(32), 'base64'), '+/', '-_'), '=');
|
|
116
|
+
insert into _v values ('pub' || i, v_pub), ('fp' || i, agent_board._raw_key_fingerprint(v_pub));
|
|
117
|
+
end loop;
|
|
118
|
+
|
|
119
|
+
-- Fleet fixture: creator, board, trust domain, machine, and one bounded invite per
|
|
120
|
+
-- fleet scenario (each invite fixes one authorized handle).
|
|
121
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status)
|
|
122
|
+
values (v_creator, 'agent', 'tok521-creator-' || s, 'Tok521 Creator', 'active');
|
|
123
|
+
select board_id into v_board from agent_board.create_board(
|
|
124
|
+
v_creator, 'tok521-board-' || s, 'Tok521 Board', '#521 pgTAP', '{}'::jsonb, 'private');
|
|
125
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
126
|
+
values ('tok521-domain-' || s, 'Tok521 Domain', 'tailnet', v_creator, 'active', now())
|
|
127
|
+
returning id into v_domain;
|
|
128
|
+
insert into agent_board.fleet_machines(trust_domain_id, owner_agent_id, machine_label, machine_fingerprint,
|
|
129
|
+
lifecycle_state, enrollment_source, enrolled_at)
|
|
130
|
+
values (v_domain, v_creator, 'claw', 'sha256:tok521-machine-' || s, 'active', 'portal', now())
|
|
131
|
+
returning id into v_machine;
|
|
132
|
+
insert into _v values ('board', v_board::text), ('machine', v_machine::text);
|
|
133
|
+
|
|
134
|
+
foreach inv in array array['f', 'x', 'y', 'g'] loop
|
|
135
|
+
select code into v_code from agent_board.create_fleet_onboarding_invite(
|
|
136
|
+
v_board, v_machine, 'codex', 'phase-loop', 'message-board', 'sha256:tok521-repo',
|
|
137
|
+
'worker', 'phase_worker', 'tok521-' || inv || '-' || s, 10, now() + interval '1 hour',
|
|
138
|
+
v_creator, '{}'::jsonb);
|
|
139
|
+
insert into _v values ('code_' || inv, v_code), ('handle_' || inv, 'tok521-' || inv || '-' || s);
|
|
140
|
+
end loop;
|
|
141
|
+
end
|
|
142
|
+
$$;
|
|
143
|
+
|
|
144
|
+
-- ---------------------------------------------------------------------------
|
|
145
|
+
-- T1 -- first use: the token binds to the key it enrolls
|
|
146
|
+
-- ---------------------------------------------------------------------------
|
|
147
|
+
select lives_ok(
|
|
148
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-one-' || pg_temp.v('s'), 1, quote_literal(pg_temp.tok('T')))),
|
|
149
|
+
'T1: identity (T, K1) enrolls');
|
|
150
|
+
select is(
|
|
151
|
+
(select count(*)::int from agent_board.agent_signing_keys where fingerprint = pg_temp.v('fp1')),
|
|
152
|
+
1, 'T1: exactly one K1 key');
|
|
153
|
+
select is(
|
|
154
|
+
(select count(*)::int from agent_board.enrollment_federation_audit e
|
|
155
|
+
where e.agent_id = (select agent_id from _r where label like 'tok521-one-%')),
|
|
156
|
+
1, 'T1: exactly one audit row for the new agent');
|
|
157
|
+
select is(pg_temp.bound('T'), pg_temp.v('fp1'), 'T1: the token row binds T to fp(K1)');
|
|
158
|
+
select is(
|
|
159
|
+
(select token_expires_at from agent_board.enroll_token_uses where token_sha256 = pg_temp.tok('T')),
|
|
160
|
+
pg_temp.v('exp')::timestamptz, 'T1: the token row records the verified expiry');
|
|
161
|
+
|
|
162
|
+
-- ---------------------------------------------------------------------------
|
|
163
|
+
-- T2 -- same token, same key: an ordinary retry, resumption unchanged
|
|
164
|
+
-- ---------------------------------------------------------------------------
|
|
165
|
+
select lives_ok(
|
|
166
|
+
(select 'insert into _r ' || replace(pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-one-' || pg_temp.v('s'), 1, quote_literal(pg_temp.tok('T'))),
|
|
167
|
+
'select ''tok521-one-', 'select ''retry-tok521-one-')),
|
|
168
|
+
'T2: identity (T, K1) again lives (same-key retry)');
|
|
169
|
+
select is(
|
|
170
|
+
(select agent_id from _r where label like 'retry-%'),
|
|
171
|
+
(select agent_id from _r where label like 'tok521-one-%'),
|
|
172
|
+
'T2: the retry resumes the SAME agent');
|
|
173
|
+
select is(
|
|
174
|
+
(select count(*)::int from agent_board.agent_signing_keys where fingerprint = pg_temp.v('fp1')),
|
|
175
|
+
1, 'T2: still exactly one K1 key');
|
|
176
|
+
select is(
|
|
177
|
+
(select count(*)::int from agent_board.enroll_token_uses where token_sha256 = pg_temp.tok('T')),
|
|
178
|
+
1, 'T2: still exactly one row for T');
|
|
179
|
+
|
|
180
|
+
-- ---------------------------------------------------------------------------
|
|
181
|
+
-- T3 -- same token, DIFFERENT key: MBT01, and nothing is created
|
|
182
|
+
-- ---------------------------------------------------------------------------
|
|
183
|
+
select pg_temp.try('T3', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-three-' || pg_temp.v('s'), 2, quote_literal(pg_temp.tok('T'))));
|
|
184
|
+
select is((select state from _err where label = 'T3'), 'MBT01',
|
|
185
|
+
'T3: identity (T, K2) after (T, K1) is refused with SQLSTATE MBT01');
|
|
186
|
+
select is(
|
|
187
|
+
(select count(*)::int from agent_board.agent_signing_keys where fingerprint = pg_temp.v('fp2')),
|
|
188
|
+
0, 'T3: no K2 key was created');
|
|
189
|
+
select is(
|
|
190
|
+
(select count(*)::int from agent_board.agents a where a.handle like 'tok521-three-%'),
|
|
191
|
+
0, 'T3: no new agent was created');
|
|
192
|
+
select is(pg_temp.bound('T'), pg_temp.v('fp1'), 'T3: the token row still binds T to fp(K1)');
|
|
193
|
+
|
|
194
|
+
-- ---------------------------------------------------------------------------
|
|
195
|
+
-- T4 -- a NEW token enrolls a second key: the legitimate path
|
|
196
|
+
-- ---------------------------------------------------------------------------
|
|
197
|
+
select lives_ok(
|
|
198
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-four-' || pg_temp.v('s'), 2, quote_literal(pg_temp.tok('T4')))),
|
|
199
|
+
'T4: identity (T'', K2) enrolls a second agent');
|
|
200
|
+
select isnt(
|
|
201
|
+
(select agent_id from _r where label like 'tok521-four-%'),
|
|
202
|
+
(select agent_id from _r where label like 'tok521-one-%'),
|
|
203
|
+
'T4: it is a DIFFERENT agent');
|
|
204
|
+
select is(pg_temp.bound('T4'), pg_temp.v('fp2'), 'T4: the new token binds to fp(K2)');
|
|
205
|
+
|
|
206
|
+
-- ---------------------------------------------------------------------------
|
|
207
|
+
-- T5 -- one KEY may consume many tokens (#176 retry after a lost response), but
|
|
208
|
+
-- binding covers resumption: a token that resumed K1 cannot then create K3
|
|
209
|
+
-- ---------------------------------------------------------------------------
|
|
210
|
+
select lives_ok(
|
|
211
|
+
(select 'insert into _r ' || replace(pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-one-' || pg_temp.v('s'), 1, quote_literal(pg_temp.tok('T5'))),
|
|
212
|
+
'select ''tok521-one-', 'select ''t5-tok521-one-')),
|
|
213
|
+
'T5: (T'''', K1) -- an already-enrolled key under a NEW token -- lives');
|
|
214
|
+
select is(
|
|
215
|
+
(select agent_id from _r where label like 't5-%'),
|
|
216
|
+
(select agent_id from _r where label like 'tok521-one-%'),
|
|
217
|
+
'T5: it resumes the same agent');
|
|
218
|
+
select is(pg_temp.bound('T5'), pg_temp.v('fp1'), 'T5: and binds the new token to fp(K1)');
|
|
219
|
+
select pg_temp.try('T5-K3', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-five-' || pg_temp.v('s'), 3, quote_literal(pg_temp.tok('T5'))));
|
|
220
|
+
select is((select state from _err where label = 'T5-K3'), 'MBT01',
|
|
221
|
+
'T5: (T'''', K3) is refused MBT01 -- a token that RESUMED a key is bound as firmly as one that created it');
|
|
222
|
+
select is(
|
|
223
|
+
(select count(*)::int from agent_board.agent_signing_keys where fingerprint = pg_temp.v('fp3')),
|
|
224
|
+
0, 'T5: no K3 key was created');
|
|
225
|
+
|
|
226
|
+
-- ---------------------------------------------------------------------------
|
|
227
|
+
-- T6 -- the gates around the bind still run for the same key
|
|
228
|
+
-- ---------------------------------------------------------------------------
|
|
229
|
+
-- Two different claims, kept apart because they prove different things. The human-revocation
|
|
230
|
+
-- gate runs BEFORE the bind, so its 42501 shows only that the bind did not displace it. The
|
|
231
|
+
-- key-revoked case is the fall-through proof: resumption gate (b) runs AFTER the bind, and a
|
|
232
|
+
-- same-key bind that returned early would skip it (T7 is the same proof for gate (c)).
|
|
233
|
+
select lives_ok(
|
|
234
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('subr'), 'tok521-six-' || pg_temp.v('s'), 6, quote_literal(pg_temp.tok('T6')))),
|
|
235
|
+
'T6: setup -- (T6, K6) enrolls for the subject about to be revoked');
|
|
236
|
+
insert into agent_board.fleet_human_revocations(issuer, subject, revoked_by_agent_id, revocation_reason)
|
|
237
|
+
values (pg_temp.v('iss'), pg_temp.v('subr'), pg_temp.v('creator')::uuid, '#521 pgTAP T6');
|
|
238
|
+
select pg_temp.try('T6-revoked-human', pg_temp.enroll_sql(pg_temp.v('subr'), 'tok521-six-' || pg_temp.v('s'), 6, quote_literal(pg_temp.tok('T6'))));
|
|
239
|
+
select is((select state from _err where label = 'T6-revoked-human'), '42501',
|
|
240
|
+
'T6: the revocation gate BEFORE the bind still refuses the same (T6, K6) once the human is revoked (42501)');
|
|
241
|
+
|
|
242
|
+
select lives_ok(
|
|
243
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-seven-' || pg_temp.v('s'), 7, quote_literal(pg_temp.tok('T7')))),
|
|
244
|
+
'T6: setup -- (T7, K7) enrolls a key about to be revoked');
|
|
245
|
+
update agent_board.agent_signing_keys
|
|
246
|
+
set status = 'revoked', proof_state = 'revoked', revoked_at = now()
|
|
247
|
+
where fingerprint = pg_temp.v('fp7');
|
|
248
|
+
select pg_temp.try('T6-revoked-key', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-seven-' || pg_temp.v('s'), 7, quote_literal(pg_temp.tok('T7'))));
|
|
249
|
+
select ok(
|
|
250
|
+
(select state = '23505' and msg like '%is not resumable%' from _err where label = 'T6-revoked-key'),
|
|
251
|
+
'T6: the same (T7, K7) after its key is revoked is refused 23505 NOT RESUMABLE (the bind no-op did not return early)');
|
|
252
|
+
|
|
253
|
+
-- ---------------------------------------------------------------------------
|
|
254
|
+
-- T7 -- fall-through, frozen contract: a stamped enrollment still refuses drift
|
|
255
|
+
-- ---------------------------------------------------------------------------
|
|
256
|
+
select pg_temp.try('T7', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-one-renamed-' || pg_temp.v('s'), 1, quote_literal(pg_temp.tok('T'))));
|
|
257
|
+
select ok(
|
|
258
|
+
(select state = '23505' and msg like '%frozen identity contract%' from _err where label = 'T7'),
|
|
259
|
+
'T7: (T, K1) with a DIFFERENT slug still raises the 23505 frozen-contract refusal');
|
|
260
|
+
|
|
261
|
+
-- ---------------------------------------------------------------------------
|
|
262
|
+
-- T8 -- the fleet route, and both directions across routes
|
|
263
|
+
-- ---------------------------------------------------------------------------
|
|
264
|
+
select lives_ok(
|
|
265
|
+
(select 'insert into _f ' || pg_temp.fleet_sql(pg_temp.v('suba'), 'f', 4, quote_literal(pg_temp.tok('Tf')))),
|
|
266
|
+
'T8: fleet (Tf, K4) onboards');
|
|
267
|
+
select is((select identity_disposition from _f where label = 'f'), 'created',
|
|
268
|
+
'T8: fleet (Tf, K4) created its identity');
|
|
269
|
+
select pg_temp.try('T8-fleet-K5', pg_temp.fleet_sql(pg_temp.v('suba'), 'f', 5, quote_literal(pg_temp.tok('Tf'))));
|
|
270
|
+
select is((select state from _err where label = 'T8-fleet-K5'), 'MBT01',
|
|
271
|
+
'T8: fleet (Tf, K5) after fleet (Tf, K4) is refused MBT01');
|
|
272
|
+
select is(
|
|
273
|
+
(select count(*)::int from agent_board.agent_signing_keys where fingerprint = pg_temp.v('fp5')),
|
|
274
|
+
0, 'T8: the refused onboarding left no K5 key');
|
|
275
|
+
select is(
|
|
276
|
+
(select count(*)::int from agent_board.fleet_machine_agents b where b.machine_id = pg_temp.v('machine')::uuid),
|
|
277
|
+
1, 'T8: the machine carries exactly one binding (K4''s), none for K5');
|
|
278
|
+
select is(
|
|
279
|
+
(select count(*)::int from agent_board.fleet_harness_registrations h where h.machine_id = pg_temp.v('machine')::uuid),
|
|
280
|
+
1, 'T8: exactly one harness registration on the machine (K4''s, reused -- none added by the refused call)');
|
|
281
|
+
select is(
|
|
282
|
+
(select count(*)::int from agent_board.fleet_repo_scopes r
|
|
283
|
+
join agent_board.fleet_harness_registrations h on h.id = r.harness_registration_id
|
|
284
|
+
where h.machine_id = pg_temp.v('machine')::uuid),
|
|
285
|
+
1, 'T8: exactly one repo scope on the machine (none added by the refused call)');
|
|
286
|
+
|
|
287
|
+
select lives_ok(
|
|
288
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), pg_temp.v('handle_x'), 8, quote_literal(pg_temp.tok('Tx')))),
|
|
289
|
+
'T8: identity (Tx, K8) enrolls');
|
|
290
|
+
select lives_ok(
|
|
291
|
+
(select 'insert into _f ' || pg_temp.fleet_sql(pg_temp.v('suba'), 'x', 8, quote_literal(pg_temp.tok('Tx')))),
|
|
292
|
+
'T8: fleet (Tx, K8) after identity (Tx, K8) lives -- same token, same key, across routes');
|
|
293
|
+
select is((select identity_disposition from _f where label = 'x'), 'reused',
|
|
294
|
+
'T8: and it REUSES the identity-route identity');
|
|
295
|
+
select pg_temp.try('T8-cross-K9', pg_temp.fleet_sql(pg_temp.v('suba'), 'x', 9, quote_literal(pg_temp.tok('Tx'))));
|
|
296
|
+
select is((select state from _err where label = 'T8-cross-K9'), 'MBT01',
|
|
297
|
+
'T8: fleet (Tx, K9) after identity (Tx, K8) is refused MBT01');
|
|
298
|
+
|
|
299
|
+
select lives_ok(
|
|
300
|
+
(select 'insert into _f ' || pg_temp.fleet_sql(pg_temp.v('suba'), 'y', 10, quote_literal(pg_temp.tok('Ty')))),
|
|
301
|
+
'T8: fleet (Ty, K10) onboards');
|
|
302
|
+
select lives_ok(
|
|
303
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), pg_temp.v('handle_y'), 10, quote_literal(pg_temp.tok('Ty')))),
|
|
304
|
+
'T8: identity (Ty, K10) after fleet (Ty, K10) lives');
|
|
305
|
+
select is(
|
|
306
|
+
(select agent_id from _r where label = pg_temp.v('handle_y')),
|
|
307
|
+
(select agent_id from _f where label = 'y'),
|
|
308
|
+
'T8: and resumes the fleet-created identity');
|
|
309
|
+
|
|
310
|
+
-- ---------------------------------------------------------------------------
|
|
311
|
+
-- T9 -- the bind's input gates, on BOTH routes, and nothing is written
|
|
312
|
+
-- ---------------------------------------------------------------------------
|
|
313
|
+
select pg_temp.try('T9-i-null-hash', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-nine-' || pg_temp.v('s'), 11, 'null'));
|
|
314
|
+
select pg_temp.try('T9-i-nonhex', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-nine-' || pg_temp.v('s'), 11, quote_literal(repeat('g', 64))));
|
|
315
|
+
select pg_temp.try('T9-i-63', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-nine-' || pg_temp.v('s'), 11, quote_literal(left(pg_temp.tok('T9'), 63))));
|
|
316
|
+
select pg_temp.try('T9-i-null-exp', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-nine-' || pg_temp.v('s'), 11, quote_literal(pg_temp.tok('T9')), 'null'));
|
|
317
|
+
select pg_temp.try('T9-i-expired', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-nine-' || pg_temp.v('s'), 11, quote_literal(pg_temp.tok('T9')), 'now()'));
|
|
318
|
+
select pg_temp.try('T9-f-null-hash', pg_temp.fleet_sql(pg_temp.v('suba'), 'g', 11, 'null'));
|
|
319
|
+
select pg_temp.try('T9-f-nonhex', pg_temp.fleet_sql(pg_temp.v('suba'), 'g', 11, quote_literal(repeat('g', 64))));
|
|
320
|
+
select pg_temp.try('T9-f-63', pg_temp.fleet_sql(pg_temp.v('suba'), 'g', 11, quote_literal(left(pg_temp.tok('T9'), 63))));
|
|
321
|
+
select pg_temp.try('T9-f-null-exp', pg_temp.fleet_sql(pg_temp.v('suba'), 'g', 11, quote_literal(pg_temp.tok('T9')), 'null'));
|
|
322
|
+
select pg_temp.try('T9-f-expired', pg_temp.fleet_sql(pg_temp.v('suba'), 'g', 11, quote_literal(pg_temp.tok('T9')), 'now()'));
|
|
323
|
+
select is(
|
|
324
|
+
(select string_agg(label || '=' || state, ' ' order by label) from _err where label like 'T9-i-%'),
|
|
325
|
+
'T9-i-63=22023 T9-i-expired=22023 T9-i-nonhex=22023 T9-i-null-exp=22023 T9-i-null-hash=22023',
|
|
326
|
+
'T9: identity route -- null hash, non-hex hash, 63-char hash, null expiry and expiry <= now() each raise 22023');
|
|
327
|
+
select is(
|
|
328
|
+
(select string_agg(label || '=' || state, ' ' order by label) from _err where label like 'T9-f-%'),
|
|
329
|
+
'T9-f-63=22023 T9-f-expired=22023 T9-f-nonhex=22023 T9-f-null-exp=22023 T9-f-null-hash=22023',
|
|
330
|
+
'T9: fleet route -- the same five inputs each raise 22023');
|
|
331
|
+
select is(
|
|
332
|
+
(select count(*)::int from agent_board.agent_signing_keys where fingerprint = pg_temp.v('fp11'))
|
|
333
|
+
+ (select count(*)::int from agent_board.enroll_token_uses where token_sha256 = pg_temp.tok('T9')),
|
|
334
|
+
0, 'T9: none of the ten refused calls wrote a key or a token row');
|
|
335
|
+
|
|
336
|
+
-- ---------------------------------------------------------------------------
|
|
337
|
+
-- T10 -- the bounded prune: 50 per call, and never a row inside the 1-hour margin
|
|
338
|
+
-- ---------------------------------------------------------------------------
|
|
339
|
+
insert into agent_board.enroll_token_uses(token_sha256, agent_public_key_fingerprint, token_expires_at)
|
|
340
|
+
select pg_temp.tok('old-' || i), 'sha256:tok521-prune', now() - interval '2 hours' - make_interval(secs => i)
|
|
341
|
+
from generate_series(1, 60) i;
|
|
342
|
+
insert into agent_board.enroll_token_uses(token_sha256, agent_public_key_fingerprint, token_expires_at)
|
|
343
|
+
values (pg_temp.tok('recent'), 'sha256:tok521-prune', now() - interval '30 minutes');
|
|
344
|
+
select lives_ok(
|
|
345
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-ten-' || pg_temp.v('s'), 12, quote_literal(pg_temp.tok('T10')))),
|
|
346
|
+
'T10: one enrollment runs the prune');
|
|
347
|
+
select is(
|
|
348
|
+
(select count(*)::int from agent_board.enroll_token_uses
|
|
349
|
+
where token_sha256 in (select pg_temp.tok('old-' || i) from generate_series(1, 60) i)),
|
|
350
|
+
10, 'T10: exactly 50 of the 60 rows expired more than an hour ago were deleted');
|
|
351
|
+
-- A second enrollment drains the 10 old rows left over, and the recent row is then the ONLY
|
|
352
|
+
-- expired row within reach of the batch. The first call alone cannot pin the margin: with 60
|
|
353
|
+
-- older rows ahead of it in expiry order, a 50-row batch never reaches the recent row even
|
|
354
|
+
-- when the margin is gone (measured: removing the margin left the first call's assertions
|
|
355
|
+
-- green). This is the call where a missing margin deletes it.
|
|
356
|
+
select lives_ok(
|
|
357
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-ten-b-' || pg_temp.v('s'), 13, quote_literal(pg_temp.tok('T10b')))),
|
|
358
|
+
'T10: a second enrollment runs the prune again');
|
|
359
|
+
select is(
|
|
360
|
+
(select count(*)::int from agent_board.enroll_token_uses
|
|
361
|
+
where token_sha256 in (select pg_temp.tok('old-' || i) from generate_series(1, 60) i)),
|
|
362
|
+
0, 'T10: the second prune drained the remaining 10 old rows');
|
|
363
|
+
select is(
|
|
364
|
+
(select count(*)::int from agent_board.enroll_token_uses where token_sha256 = pg_temp.tok('recent')),
|
|
365
|
+
1, 'T10: the row expired only 30 minutes ago is kept even when the batch reaches it (the clock-skew margin)');
|
|
366
|
+
|
|
367
|
+
-- ---------------------------------------------------------------------------
|
|
368
|
+
-- T11 -- the bind decides expiry at the time it RUNS, not when its transaction began
|
|
369
|
+
-- ---------------------------------------------------------------------------
|
|
370
|
+
-- now() is fixed at transaction start; a real request can start, wait on the owner lock for
|
|
371
|
+
-- as long as another enrollment holds it, and only then bind. This file's transaction began
|
|
372
|
+
-- long before this point (and sleeps to make the margin certain), so now() < exp <
|
|
373
|
+
-- clock_timestamp() below: the state of a request whose token expired while it waited. With
|
|
374
|
+
-- a now()-based refusal it would bind; had the prune meanwhile removed the token's old row,
|
|
375
|
+
-- that would be a second key under one token (#524 round 1).
|
|
376
|
+
select pg_sleep(0.2);
|
|
377
|
+
select pg_temp.try('T11-late', pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-eleven-' || pg_temp.v('s'), 14,
|
|
378
|
+
quote_literal(pg_temp.tok('T11')), $$now() + interval '100 milliseconds'$$));
|
|
379
|
+
select ok(
|
|
380
|
+
(select state = '22023' and msg like '%has expired%' from _err where label = 'T11-late'),
|
|
381
|
+
'T11: a token whose exp passed while the request waited (now() < exp <= clock_timestamp()) is refused 22023 expired');
|
|
382
|
+
select is(
|
|
383
|
+
(select count(*)::int from agent_board.enroll_token_uses where token_sha256 = pg_temp.tok('T11')),
|
|
384
|
+
0, 'T11: and no row was written for it');
|
|
385
|
+
-- The prune reads the SAME clock, so its window matches the refusal: a row whose exp is
|
|
386
|
+
-- between now() - 1h and clock_timestamp() - 1h is one the refusal already rejects, and it
|
|
387
|
+
-- is deleted. A now()-based cutoff would keep it (harmless, but the two clocks would differ).
|
|
388
|
+
insert into agent_board.enroll_token_uses(token_sha256, agent_public_key_fingerprint, token_expires_at)
|
|
389
|
+
values (pg_temp.tok('T11-prune'), 'sha256:tok521-prune', now() - interval '1 hour' + interval '100 milliseconds');
|
|
390
|
+
select lives_ok(
|
|
391
|
+
(select 'insert into _r ' || pg_temp.enroll_sql(pg_temp.v('suba'), 'tok521-eleven-b-' || pg_temp.v('s'), 14, quote_literal(pg_temp.tok('T11b')))),
|
|
392
|
+
'T11: an enrollment runs the prune');
|
|
393
|
+
select is(
|
|
394
|
+
(select count(*)::int from agent_board.enroll_token_uses where token_sha256 = pg_temp.tok('T11-prune')),
|
|
395
|
+
0, 'T11: the prune cutoff is clock_timestamp() - 1 hour, the same clock the refusal uses');
|
|
396
|
+
|
|
397
|
+
-- ---------------------------------------------------------------------------
|
|
398
|
+
-- PLACEMENT -- the bind sits after the owner lock and before the found/not-found split
|
|
399
|
+
-- ---------------------------------------------------------------------------
|
|
400
|
+
-- Over stripped_body(), so a comment cannot satisfy it. Above the owner lock the identity
|
|
401
|
+
-- route deadlocks against the fleet route (enroll_token_placement.spec shows the runtime
|
|
402
|
+
-- half); below `for update of a, k` it would cover creation only, not resumption.
|
|
403
|
+
select ok(
|
|
404
|
+
(with b as (
|
|
405
|
+
select lower(agent_board_test.stripped_body(p.oid)) as body
|
|
406
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
407
|
+
where n.nspname = 'agent_board' and p.proname = 'enroll_federated_identity')
|
|
408
|
+
select count(*) = 1
|
|
409
|
+
and bool_and(strpos(body, 'from auth.users u where u.id = v_owner_user_id for update') > 0
|
|
410
|
+
and strpos(body, 'from auth.users u where u.id = v_owner_user_id for update')
|
|
411
|
+
< strpos(body, 'perform agent_board._enroll_token_bind(')
|
|
412
|
+
and strpos(body, 'perform agent_board._enroll_token_bind(')
|
|
413
|
+
< strpos(body, 'for update of a, k'))
|
|
414
|
+
from b),
|
|
415
|
+
'PLACEMENT: enroll_federated_identity binds the token AFTER its owner lock and BEFORE the found/not-found split');
|
|
416
|
+
|
|
417
|
+
-- ---------------------------------------------------------------------------
|
|
418
|
+
-- EC-T1 -- every API-executable route that accepts a key fingerprint carries the token,
|
|
419
|
+
-- enumerated from pg_proc
|
|
420
|
+
-- ---------------------------------------------------------------------------
|
|
421
|
+
select is(
|
|
422
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
423
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
424
|
+
where n.nspname = 'agent_board'
|
|
425
|
+
and p.proargnames && array['p_agent_public_key_fingerprint']
|
|
426
|
+
and (has_function_privilege('anon', p.oid, 'EXECUTE')
|
|
427
|
+
or has_function_privilege('authenticated', p.oid, 'EXECUTE')
|
|
428
|
+
or has_function_privilege('service_role', p.oid, 'EXECUTE'))),
|
|
429
|
+
'enroll_federated_identity, onboard_federated_fleet_agent',
|
|
430
|
+
'EC-T1: exactly two API-executable routes accept a key fingerprint, one overload each (pg_proc-derived)');
|
|
431
|
+
select is(
|
|
432
|
+
(select string_agg(p.proname || ':' || (
|
|
433
|
+
array_position(p.proargnames, 'p_token_sha256') is not null
|
|
434
|
+
and array_position(p.proargnames, 'p_token_expires_at') is not null
|
|
435
|
+
and array_position(p.proargnames, 'p_token_sha256') <= p.pronargs - p.pronargdefaults
|
|
436
|
+
and array_position(p.proargnames, 'p_token_expires_at') <= p.pronargs - p.pronargdefaults)::text,
|
|
437
|
+
', ' order by p.proname)
|
|
438
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
439
|
+
where n.nspname = 'agent_board'
|
|
440
|
+
and p.proargnames && array['p_agent_public_key_fingerprint']
|
|
441
|
+
and (has_function_privilege('anon', p.oid, 'EXECUTE')
|
|
442
|
+
or has_function_privilege('authenticated', p.oid, 'EXECUTE')
|
|
443
|
+
or has_function_privilege('service_role', p.oid, 'EXECUTE'))),
|
|
444
|
+
'enroll_federated_identity:true, onboard_federated_fleet_agent:true',
|
|
445
|
+
'EC-T1: each carries p_token_sha256 and p_token_expires_at with NO default');
|
|
446
|
+
|
|
447
|
+
-- ---------------------------------------------------------------------------
|
|
448
|
+
-- EC-T2 -- the signing-key creator set, each member justified POSITIVELY
|
|
449
|
+
-- ---------------------------------------------------------------------------
|
|
450
|
+
select is(
|
|
451
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
452
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
453
|
+
where n.nspname = 'agent_board' and p.prokind = 'f'
|
|
454
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.stmt_re('insert into', 'agent_signing_keys')),
|
|
455
|
+
'_enroll_federated_identity_once, mint_delegated_child',
|
|
456
|
+
'EC-T2: exactly two functions insert signing keys (the EC-2 form)');
|
|
457
|
+
select is(
|
|
458
|
+
(select string_agg(r || '=' || has_function_privilege(r, p.oid, 'EXECUTE')::text, ' ' order by r)
|
|
459
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace,
|
|
460
|
+
unnest(array['anon', 'authenticated', 'service_role']) r
|
|
461
|
+
where n.nspname = 'agent_board' and p.proname = '_enroll_federated_identity_once'),
|
|
462
|
+
'anon=false authenticated=false service_role=false',
|
|
463
|
+
'EC-T2: _enroll_federated_identity_once is EXECUTE-able by no API role');
|
|
464
|
+
select is(
|
|
465
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
466
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
467
|
+
where n.nspname = 'agent_board' and p.proname <> '_enroll_federated_identity_once'
|
|
468
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_enroll_federated_identity_once')),
|
|
469
|
+
'enroll_federated_identity',
|
|
470
|
+
'EC-T2: _enroll_federated_identity_once''s only caller is enroll_federated_identity');
|
|
471
|
+
-- Counted per CALL SITE, not per calling function: a second bind call inside
|
|
472
|
+
-- enroll_federated_identity is a no-op under fall-through, so only this count sees it.
|
|
473
|
+
select is(
|
|
474
|
+
(select string_agg(p.proname || '=' || regexp_count(agent_board_test.stripped_body(p.oid), agent_board_test.call_re('_enroll_token_bind')), ', ' order by p.proname)
|
|
475
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
476
|
+
where n.nspname = 'agent_board' and p.proname <> '_enroll_token_bind'
|
|
477
|
+
and agent_board_test.stripped_body(p.oid) ~ agent_board_test.call_re('_enroll_token_bind')),
|
|
478
|
+
'enroll_federated_identity=1',
|
|
479
|
+
'EC-T2: _enroll_token_bind has exactly ONE call site in the schema, in enroll_federated_identity (one guard site)');
|
|
480
|
+
select ok(
|
|
481
|
+
(select bool_and('p_root_agent_id' = any(p.proargnames)
|
|
482
|
+
and not (p.proargnames && array['p_issuer', 'p_subject', 'p_human_identity_claim']))
|
|
483
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
484
|
+
where n.nspname = 'agent_board' and p.proname = 'mint_delegated_child'),
|
|
485
|
+
'EC-T2: mint_delegated_child''s authority input is a parent agent, not a Portal token');
|
|
486
|
+
|
|
487
|
+
-- ---------------------------------------------------------------------------
|
|
488
|
+
-- EC-T3 -- the token table and its only writer are unreachable from the API
|
|
489
|
+
-- ---------------------------------------------------------------------------
|
|
490
|
+
select is(
|
|
491
|
+
(select count(*)::int
|
|
492
|
+
from unnest(array['anon', 'authenticated', 'service_role']) r,
|
|
493
|
+
unnest(array['SELECT', 'INSERT', 'UPDATE', 'DELETE']) pr
|
|
494
|
+
where has_table_privilege(r, 'agent_board.enroll_token_uses', pr)),
|
|
495
|
+
0, 'EC-T3: no API role holds SELECT/INSERT/UPDATE/DELETE on enroll_token_uses');
|
|
496
|
+
select ok(
|
|
497
|
+
(select relrowsecurity from pg_class where oid = 'agent_board.enroll_token_uses'::regclass),
|
|
498
|
+
'EC-T3: RLS is enabled on enroll_token_uses');
|
|
499
|
+
select is(
|
|
500
|
+
(select string_agg(a.attname, ',')
|
|
501
|
+
from pg_constraint c
|
|
502
|
+
join pg_attribute a on a.attrelid = c.conrelid and a.attnum = any(c.conkey)
|
|
503
|
+
where c.conrelid = 'agent_board.enroll_token_uses'::regclass and c.contype = 'p'),
|
|
504
|
+
'token_sha256', 'EC-T3: the primary key (the fail-closed backstop) is on token_sha256');
|
|
505
|
+
select is(
|
|
506
|
+
(select string_agg(r || '=' || has_function_privilege(r, p.oid, 'EXECUTE')::text, ' ' order by r)
|
|
507
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace,
|
|
508
|
+
unnest(array['anon', 'authenticated', 'service_role']) r
|
|
509
|
+
where n.nspname = 'agent_board' and p.proname = '_enroll_token_bind'),
|
|
510
|
+
'anon=false authenticated=false service_role=false',
|
|
511
|
+
'EC-T3: _enroll_token_bind is EXECUTE-able by no API role');
|
|
512
|
+
|
|
513
|
+
-- ---------------------------------------------------------------------------
|
|
514
|
+
-- EC-T4 -- the recreated and new functions kept their comment, owner and ACL
|
|
515
|
+
-- ---------------------------------------------------------------------------
|
|
516
|
+
select is(
|
|
517
|
+
(select string_agg(p.proname, ', ' order by p.proname)
|
|
518
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
519
|
+
where n.nspname = 'agent_board'
|
|
520
|
+
and p.proname in ('enroll_federated_identity', 'onboard_federated_fleet_agent',
|
|
521
|
+
'_enroll_federated_identity_once', '_enroll_token_bind')
|
|
522
|
+
and obj_description(p.oid, 'pg_proc') like '%#521%'
|
|
523
|
+
and p.proowner = (select q.proowner from pg_proc q join pg_namespace m on m.oid = q.pronamespace
|
|
524
|
+
where m.nspname = 'agent_board' and q.proname = '_enroll_federated_identity_once')),
|
|
525
|
+
'_enroll_federated_identity_once, _enroll_token_bind, enroll_federated_identity, onboard_federated_fleet_agent',
|
|
526
|
+
'EC-T4: all four carry a #521 comment and the same owner as _enroll_federated_identity_once');
|
|
527
|
+
select is(
|
|
528
|
+
(select string_agg(p.proname || ':' || r || '=' || has_function_privilege(r, p.oid, 'EXECUTE')::text, ' ' order by p.proname, r)
|
|
529
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace,
|
|
530
|
+
unnest(array['anon', 'authenticated', 'public', 'service_role']) r
|
|
531
|
+
where n.nspname = 'agent_board'
|
|
532
|
+
and p.proname in ('enroll_federated_identity', 'onboard_federated_fleet_agent')),
|
|
533
|
+
'enroll_federated_identity:anon=false enroll_federated_identity:authenticated=false enroll_federated_identity:public=false enroll_federated_identity:service_role=true '
|
|
534
|
+
|| 'onboard_federated_fleet_agent:anon=false onboard_federated_fleet_agent:authenticated=false onboard_federated_fleet_agent:public=false onboard_federated_fleet_agent:service_role=true',
|
|
535
|
+
'EC-T4: the recreated RPCs are EXECUTE-able by service_role only (DROP discarded the ACL; it was restated)');
|
|
536
|
+
|
|
537
|
+
select * from finish();
|
|
538
|
+
rollback;
|
|
@@ -31,7 +31,7 @@ begin
|
|
|
31
31
|
select e.agent_id into v_agent
|
|
32
32
|
from agent_board.enroll_federated_identity(
|
|
33
33
|
'https://mock-portal-'||s||'.test', 'message-board', 'portal|'||s,
|
|
34
|
-
v_claim, 'acfx-'||s, v_pub, 'raw_ed25519_base64url', v_fp, true, null, 'human', '{board:send}', '{board:x}') e;
|
|
34
|
+
v_claim, 'acfx-'||s, v_pub, 'raw_ed25519_base64url', v_fp, true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1), null, 'human', '{board:send}', '{board:x}') e;
|
|
35
35
|
|
|
36
36
|
insert into _acfx values (v_agent, v_claim);
|
|
37
37
|
end
|
|
@@ -120,10 +120,10 @@ begin
|
|
|
120
120
|
values ('https://rls-portal-'||s||'.test','message-board',true,true,'{board:send}','{board:x}','{agent,human}');
|
|
121
121
|
select e.agent_id, e.auth_user_id into v_a_agent, v_a_user
|
|
122
122
|
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|a-'||s,
|
|
123
|
-
jsonb_build_object('iss','https://rls-portal-'||s||'.test','aud','message-board','sub','portal|a-'||s,'name','A'), 'rls-a-'||s, v_pub_a,'raw_ed25519_base64url',v_fp_a,true,null,'human','{board:send}','{board:x}') e;
|
|
123
|
+
jsonb_build_object('iss','https://rls-portal-'||s||'.test','aud','message-board','sub','portal|a-'||s,'name','A'), 'rls-a-'||s, v_pub_a,'raw_ed25519_base64url',v_fp_a,true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1),null,'human','{board:send}','{board:x}') e;
|
|
124
124
|
select e.agent_id into v_b_agent
|
|
125
125
|
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|b-'||s,
|
|
126
|
-
jsonb_build_object('iss','https://rls-portal-'||s||'.test','aud','message-board','sub','portal|b-'||s,'name','B'), 'rls-b-'||s, v_pub_b,'raw_ed25519_base64url',v_fp_b,true,null,'human','{board:send}','{board:x}') e;
|
|
126
|
+
jsonb_build_object('iss','https://rls-portal-'||s||'.test','aud','message-board','sub','portal|b-'||s,'name','B'), 'rls-b-'||s, v_pub_b,'raw_ed25519_base64url',v_fp_b,true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1),null,'human','{board:send}','{board:x}') e;
|
|
127
127
|
insert into _rls values (v_a_agent, v_a_user, v_b_agent);
|
|
128
128
|
end
|
|
129
129
|
$$;
|
|
@@ -321,7 +321,7 @@ select lives_ok(
|
|
|
321
321
|
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
322
322
|
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
323
323
|
'lo194-a', public.test_pubkey('lo194-k1'), 'raw_ed25519_base64url',
|
|
324
|
-
public.test_key_fingerprint('lo194-k1'), true, null, 'agent',
|
|
324
|
+
public.test_key_fingerprint('lo194-k1'), true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1), null, 'agent',
|
|
325
325
|
ARRAY[]::text[], ARRAY[]::text[], 'lo194-code-1') $$,
|
|
326
326
|
'R-1: fresh federated enrollment with a usable invite succeeds');
|
|
327
327
|
select is((select invite_redeemed from _e1), true,
|
|
@@ -342,7 +342,7 @@ select lives_ok(
|
|
|
342
342
|
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
343
343
|
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
344
344
|
'lo194-b', public.test_pubkey('lo194-k2'), 'raw_ed25519_base64url',
|
|
345
|
-
public.test_key_fingerprint('lo194-k2'), true, null, 'agent',
|
|
345
|
+
public.test_key_fingerprint('lo194-k2'), true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1), null, 'agent',
|
|
346
346
|
ARRAY[]::text[], ARRAY[]::text[], 'lo194-no-such-code') $$,
|
|
347
347
|
'R-4: an unusable invite code still enrolls (soft-honor preserved)');
|
|
348
348
|
select is((select invite_redeemed from _e2), false,
|
|
@@ -368,7 +368,7 @@ select throws_ok(
|
|
|
368
368
|
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
369
369
|
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
370
370
|
'lo194-c', public.test_pubkey('lo194-k3'), 'raw_ed25519_base64url',
|
|
371
|
-
public.test_key_fingerprint('lo194-k3'), true, null, 'agent',
|
|
371
|
+
public.test_key_fingerprint('lo194-k3'), true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1), null, 'agent',
|
|
372
372
|
ARRAY[]::text[], ARRAY[]::text[], 'lo194-code-2') $$,
|
|
373
373
|
'40001', null,
|
|
374
374
|
'R-6: class 40 during the CREATE-path redemption propagates to the caller');
|
|
@@ -380,7 +380,7 @@ select throws_ok(
|
|
|
380
380
|
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
381
381
|
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
382
382
|
'lo194-a', public.test_pubkey('lo194-k1'), 'raw_ed25519_base64url',
|
|
383
|
-
public.test_key_fingerprint('lo194-k1'), true, null, 'agent',
|
|
383
|
+
public.test_key_fingerprint('lo194-k1'), true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1), null, 'agent',
|
|
384
384
|
ARRAY[]::text[], ARRAY[]::text[], 'lo194-code-3') $$,
|
|
385
385
|
'40001', null,
|
|
386
386
|
'R-7: class 40 during the REPLAY-path redemption propagates to the caller');
|
|
@@ -395,7 +395,7 @@ select lives_ok(
|
|
|
395
395
|
'https://lockorder194.test', 'message-board', 'sub-lo194',
|
|
396
396
|
jsonb_build_object('iss','https://lockorder194.test','aud','message-board','sub','sub-lo194'),
|
|
397
397
|
'lo194-a', public.test_pubkey('lo194-k1'), 'raw_ed25519_base64url',
|
|
398
|
-
public.test_key_fingerprint('lo194-k1'), true, null, 'agent',
|
|
398
|
+
public.test_key_fingerprint('lo194-k1'), true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1), null, 'agent',
|
|
399
399
|
ARRAY[]::text[], ARRAY[]::text[], 'lo194-no-such-code') $$,
|
|
400
400
|
'R-8: replay with an unusable code still succeeds');
|
|
401
401
|
select is((select invite_redeemed from _e3), false,
|
|
@@ -233,7 +233,7 @@ begin
|
|
|
233
233
|
from agent_board.enroll_federated_identity(
|
|
234
234
|
f.iss, 'message-board', f.sub_a, jsonb_build_object('iss',f.iss,'aud','message-board','sub', f.sub_a),
|
|
235
235
|
'idlink-'||f.s, v_pub, 'raw_ed25519_base64url', v_fp,
|
|
236
|
-
true, null, 'agent', '{board:send}', '{}') e;
|
|
236
|
+
true, md5(random()::text) || md5(random()::text), now() + make_interval(hours => 1), null, 'agent', '{board:send}', '{}') e;
|
|
237
237
|
insert into _idlink select a.owner_user_id from agent_board.agents a where a.id = v_agent;
|
|
238
238
|
end
|
|
239
239
|
$$;
|