@consiliency/agent-board-schema 1.9.0 → 1.12.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 +16 -0
- package/migrations/20260728000100_agent_board_enroll_federated_human.sql +313 -0
- package/migrations/20260729000100_agent_board_fedsession_autojoin_default_board.sql +364 -0
- package/migrations/20260730000100_agent_board_gatenull_service_role_bypass.sql +369 -0
- package/migrations/20260730000200_agent_board_membstate_authz.sql +195 -0
- package/package.json +1 -1
- package/schema-manifest.json +39 -3
- package/src/tooling.test.mjs +16 -2
- package/tests/agent_board_enrollment_audit_content.test.sql +5 -2
- package/tests/agent_board_fedsession.test.sql +314 -0
- package/tests/agent_board_fedsession_autojoin.test.sql +181 -0
- package/tests/agent_board_gatenull_membstate.test.sql +352 -0
|
@@ -76,10 +76,13 @@ select is(
|
|
|
76
76
|
-- ("X is an aggregate function"); restrict to callable functions +
|
|
77
77
|
-- procedures (the RPC surface this invariant is about).
|
|
78
78
|
and p.prokind in ('f', 'p')
|
|
79
|
-
|
|
79
|
+
-- The federation enroll RPCs WRITE the audit (enroll_federated_human added by
|
|
80
|
+
-- FEDSESSION, F-6); the invariant is that nothing on the send/session read path
|
|
81
|
+
-- touches it (asserted specifically in the next case).
|
|
82
|
+
and p.proname not in ('enroll_federated_identity', 'enroll_federated_human')
|
|
80
83
|
) d
|
|
81
84
|
where lower(d.def) like '%enrollment_federation_audit%'),
|
|
82
|
-
0, 'no agent_board RPC other than
|
|
85
|
+
0, 'no agent_board RPC other than the federation enroll RPCs references enrollment_federation_audit');
|
|
83
86
|
select is(
|
|
84
87
|
(select count(*)::int from (
|
|
85
88
|
select pg_get_functiondef(p.oid) as def
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
-- FEDSESSION pgTAP: enroll_federated_human() — service_role no-PoP federated-HUMAN
|
|
2
|
+
-- provisioning (Option A minter substrate).
|
|
3
|
+
--
|
|
4
|
+
-- Proves: the 5-arg signature + service_role-only privs; one provision mints a kind=human
|
|
5
|
+
-- SELF-owned agent (auth_user_id = owner_user_id = a synthetic uid) with a real auth.users
|
|
6
|
+
-- row + NO signing key + a mode='fedsession' audit row; the verified email lives ONLY in
|
|
7
|
+
-- auth.users.raw_user_meta_data (provenance-tagged, MERGED preserving slug) with just the
|
|
8
|
+
-- boolean email_verified in agents.metadata (no '@' anywhere in agents.metadata);
|
|
9
|
+
-- idempotency + L-4b (one agent per synthetic uid); current_agent_id() resolves the
|
|
10
|
+
-- no-credential session to the agent; the IDLINK shared-uid invariant against
|
|
11
|
+
-- enroll_federated_identity; and the provider/kind/subject/non-service rejection matrix.
|
|
12
|
+
-- Runs as postgres (service_role-equivalent), mirroring
|
|
13
|
+
-- agent_board_enroll_federated_identity.test.sql + agent_board_localauth.test.sql.
|
|
14
|
+
|
|
15
|
+
create extension if not exists pgtap with schema extensions;
|
|
16
|
+
set search_path = extensions, public, agent_board;
|
|
17
|
+
|
|
18
|
+
begin;
|
|
19
|
+
|
|
20
|
+
select plan(36);
|
|
21
|
+
|
|
22
|
+
-- ---------------------------------------------------------------------------
|
|
23
|
+
-- Phase 0 (superuser): function metadata + privileges
|
|
24
|
+
-- ---------------------------------------------------------------------------
|
|
25
|
+
-- 1
|
|
26
|
+
select has_function(
|
|
27
|
+
'agent_board', 'enroll_federated_human',
|
|
28
|
+
array['text','text','text','jsonb','text'],
|
|
29
|
+
'enroll_federated_human(text,text,text,jsonb,text) exists');
|
|
30
|
+
-- 2
|
|
31
|
+
select function_privs_are(
|
|
32
|
+
'agent_board', 'enroll_federated_human', array['text','text','text','jsonb','text'],
|
|
33
|
+
'service_role', array['EXECUTE'], 'service_role may EXECUTE enroll_federated_human');
|
|
34
|
+
-- 3
|
|
35
|
+
select function_privs_are(
|
|
36
|
+
'agent_board', 'enroll_federated_human', array['text','text','text','jsonb','text'],
|
|
37
|
+
'authenticated', array[]::text[], 'authenticated may NOT EXECUTE enroll_federated_human');
|
|
38
|
+
-- 4
|
|
39
|
+
select function_privs_are(
|
|
40
|
+
'agent_board', 'enroll_federated_human', array['text','text','text','jsonb','text'],
|
|
41
|
+
'anon', array[]::text[], 'anon may NOT EXECUTE enroll_federated_human');
|
|
42
|
+
|
|
43
|
+
-- ---------------------------------------------------------------------------
|
|
44
|
+
-- Fixtures + provision identity A (email_verified) and B (unverified)
|
|
45
|
+
-- ---------------------------------------------------------------------------
|
|
46
|
+
create temp table _f (
|
|
47
|
+
s text, iss text, aud text,
|
|
48
|
+
sub_a text, email_a text, sub_b text
|
|
49
|
+
) on commit drop;
|
|
50
|
+
grant all on table _f to authenticated, service_role;
|
|
51
|
+
|
|
52
|
+
create temp table _r (
|
|
53
|
+
label text, agent_id uuid, auth_user_id uuid, owner_user_id uuid,
|
|
54
|
+
handle text, kind text, status text, created_at timestamptz
|
|
55
|
+
) on commit drop;
|
|
56
|
+
grant all on table _r to authenticated, service_role;
|
|
57
|
+
|
|
58
|
+
do $$
|
|
59
|
+
declare
|
|
60
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
61
|
+
v_iss text; v_agent uuid; v_auth uuid; v_owner uuid; v_handle text;
|
|
62
|
+
v_kind text; v_status text; v_created timestamptz;
|
|
63
|
+
begin
|
|
64
|
+
v_iss := 'https://fed-hs-' || s || '.test';
|
|
65
|
+
insert into _f values (
|
|
66
|
+
s, v_iss, 'message-board',
|
|
67
|
+
'portal-user-' || s, 'alice-' || s || '@example.test', 'portal-user2-' || s);
|
|
68
|
+
|
|
69
|
+
-- Approved provider that permits BOTH agent + human (agent needed for the IDLINK case).
|
|
70
|
+
insert into agent_board.identity_providers
|
|
71
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
72
|
+
values
|
|
73
|
+
(v_iss, 'message-board', true, true, '{board:send}', '{board:x}', '{agent,human}');
|
|
74
|
+
|
|
75
|
+
-- Provision A (verified email present in the claim).
|
|
76
|
+
select e.agent_id, e.auth_user_id, e.handle, e.kind, e.status, e.created_at
|
|
77
|
+
into v_agent, v_auth, v_handle, v_kind, v_status, v_created
|
|
78
|
+
from agent_board.enroll_federated_human(
|
|
79
|
+
v_iss, 'message-board', 'portal-user-' || s,
|
|
80
|
+
jsonb_build_object('sub', 'portal-user-' || s, 'email', 'alice-' || s || '@example.test',
|
|
81
|
+
'email_verified', true, 'name', 'Alice ' || s),
|
|
82
|
+
'Alice ' || s) e;
|
|
83
|
+
select a.owner_user_id into v_owner from agent_board.agents a where a.id = v_agent;
|
|
84
|
+
insert into _r values ('A', v_agent, v_auth, v_owner, v_handle, v_kind, v_status, v_created);
|
|
85
|
+
|
|
86
|
+
-- Provision B (DIFFERENT identity, email_verified=false).
|
|
87
|
+
select e.agent_id, e.auth_user_id, e.handle, e.kind, e.status, e.created_at
|
|
88
|
+
into v_agent, v_auth, v_handle, v_kind, v_status, v_created
|
|
89
|
+
from agent_board.enroll_federated_human(
|
|
90
|
+
v_iss, 'message-board', 'portal-user2-' || s,
|
|
91
|
+
jsonb_build_object('sub', 'portal-user2-' || s, 'email', 'bob-' || s || '@example.test',
|
|
92
|
+
'email_verified', false),
|
|
93
|
+
null) e;
|
|
94
|
+
select a.owner_user_id into v_owner from agent_board.agents a where a.id = v_agent;
|
|
95
|
+
insert into _r values ('B', v_agent, v_auth, v_owner, v_handle, v_kind, v_status, v_created);
|
|
96
|
+
end
|
|
97
|
+
$$;
|
|
98
|
+
|
|
99
|
+
-- One provision mints a kind=human, self-owned, active agent -----------------------------
|
|
100
|
+
-- 5
|
|
101
|
+
select is((select count(*)::int from _r where label = 'A'), 1, 'provision A returned exactly one row');
|
|
102
|
+
-- 6
|
|
103
|
+
select is((select kind from _r where label = 'A'), 'human', 'agent A kind = human');
|
|
104
|
+
-- 7
|
|
105
|
+
select is(
|
|
106
|
+
(select a.agent_type from agent_board.agents a where a.id = (select agent_id from _r where label='A')),
|
|
107
|
+
'human', 'agent A agent_type = human');
|
|
108
|
+
-- 8
|
|
109
|
+
select is((select status from _r where label = 'A'), 'active', 'agent A status = active');
|
|
110
|
+
-- 9
|
|
111
|
+
select is(
|
|
112
|
+
(select auth_user_id from _r where label='A'),
|
|
113
|
+
(select owner_user_id from _r where label='A'),
|
|
114
|
+
'agent A is self-owned (auth_user_id = owner_user_id = the synthetic uid)');
|
|
115
|
+
-- 10
|
|
116
|
+
select is(
|
|
117
|
+
(select count(*)::int from auth.users u where u.id = (select auth_user_id from _r where label='A')),
|
|
118
|
+
1, 'the synthetic auth.users row for agent A exists');
|
|
119
|
+
-- 11
|
|
120
|
+
select is(
|
|
121
|
+
(select handle from _r where label='A'),
|
|
122
|
+
'fed-human-' || substr(
|
|
123
|
+
encode(extensions.digest(convert_to((select iss from _f) || '|' || (select sub_a from _f), 'UTF8'), 'sha256'), 'hex'),
|
|
124
|
+
1, 32),
|
|
125
|
+
'handle = fed-human-<sha256(issuer|subject)[:32]> (byte-identical to enroll_federated_identity)');
|
|
126
|
+
|
|
127
|
+
-- metadata carries the federation snapshot, boolean-only email, no PII --------------------
|
|
128
|
+
-- 12
|
|
129
|
+
select is(
|
|
130
|
+
(select a.metadata->'federation'->>'issuer' from agent_board.agents a where a.id=(select agent_id from _r where label='A')),
|
|
131
|
+
(select iss from _f), 'metadata.federation.issuer round-trips');
|
|
132
|
+
-- 13
|
|
133
|
+
select is(
|
|
134
|
+
(select a.metadata->'federation'->>'subject' from agent_board.agents a where a.id=(select agent_id from _r where label='A')),
|
|
135
|
+
(select sub_a from _f), 'metadata.federation.subject round-trips');
|
|
136
|
+
-- 14
|
|
137
|
+
select is(
|
|
138
|
+
(select a.metadata->'federation'->>'mode' from agent_board.agents a where a.id=(select agent_id from _r where label='A')),
|
|
139
|
+
'fedsession', 'metadata.federation.mode = fedsession');
|
|
140
|
+
-- 15
|
|
141
|
+
select is(
|
|
142
|
+
(select a.metadata->'federation'->>'email_verified' from agent_board.agents a where a.id=(select agent_id from _r where label='A')),
|
|
143
|
+
'true', 'verified identity -> metadata.federation.email_verified = true');
|
|
144
|
+
-- 16
|
|
145
|
+
select ok(
|
|
146
|
+
(select a.metadata::text not like '%@%' from agent_board.agents a where a.id=(select agent_id from _r where label='A')),
|
|
147
|
+
'agent A metadata carries NO plaintext email/@ (PII stays out of fleet-readable metadata)');
|
|
148
|
+
|
|
149
|
+
-- verified email + slug live (merged) in auth.users.raw_user_meta_data --------------------
|
|
150
|
+
-- 17
|
|
151
|
+
select is(
|
|
152
|
+
(select u.raw_user_meta_data->>'fedsession_email' from auth.users u where u.id=(select auth_user_id from _r where label='A')),
|
|
153
|
+
(select email_a from _f), 'verified email is stored in auth.users.raw_user_meta_data');
|
|
154
|
+
-- 18
|
|
155
|
+
select is(
|
|
156
|
+
(select u.raw_user_meta_data->>'fedsession_email_provenance' from auth.users u where u.id=(select auth_user_id from _r where label='A')),
|
|
157
|
+
'portal-claim-unverified', 'stored email is tagged provenance=portal-claim-unverified (F-12)');
|
|
158
|
+
-- 19 (F-4 merge preserved _ensure_agent_auth_user's slug)
|
|
159
|
+
select is(
|
|
160
|
+
(select u.raw_user_meta_data->>'slug' from auth.users u where u.id=(select auth_user_id from _r where label='A')),
|
|
161
|
+
(select handle from _r where label='A'),
|
|
162
|
+
'raw_user_meta_data merge preserved the _ensure_agent_auth_user slug (no overwrite)');
|
|
163
|
+
|
|
164
|
+
-- NO signing key; audit row written ------------------------------------------------------
|
|
165
|
+
-- 20
|
|
166
|
+
select is(
|
|
167
|
+
(select count(*)::int from agent_board.agent_signing_keys k where k.agent_id=(select agent_id from _r where label='A')),
|
|
168
|
+
0, 'agent A holds NO agent_signing_keys row (humans hold no key)');
|
|
169
|
+
-- 21
|
|
170
|
+
select is(
|
|
171
|
+
(select count(*)::int from agent_board.enrollment_federation_audit e
|
|
172
|
+
where e.agent_id=(select agent_id from _r where label='A') and e.mode='fedsession'),
|
|
173
|
+
1, 'provision A wrote exactly one mode=fedsession audit row');
|
|
174
|
+
|
|
175
|
+
-- Identity B: unverified boolean + PII guard ---------------------------------------------
|
|
176
|
+
-- 22
|
|
177
|
+
select is(
|
|
178
|
+
(select a.metadata->'federation'->>'email_verified' from agent_board.agents a where a.id=(select agent_id from _r where label='B')),
|
|
179
|
+
'false', 'unverified identity -> metadata.federation.email_verified = false');
|
|
180
|
+
-- 23
|
|
181
|
+
select ok(
|
|
182
|
+
(select a.metadata::text not like '%@%' from agent_board.agents a where a.id=(select agent_id from _r where label='B')),
|
|
183
|
+
'agent B metadata carries NO plaintext email/@');
|
|
184
|
+
|
|
185
|
+
-- Idempotency + L-4b ---------------------------------------------------------------------
|
|
186
|
+
-- 24
|
|
187
|
+
select is(
|
|
188
|
+
(select e.agent_id from agent_board.enroll_federated_human(
|
|
189
|
+
(select iss from _f), 'message-board', (select sub_a from _f),
|
|
190
|
+
jsonb_build_object('sub', (select sub_a from _f), 'email_verified', true), 'Alice again') e),
|
|
191
|
+
(select agent_id from _r where label='A'),
|
|
192
|
+
're-provision of the same (issuer,sub) is idempotent (same agent_id)');
|
|
193
|
+
-- 25
|
|
194
|
+
select is(
|
|
195
|
+
(select count(*)::int from agent_board.agents a where a.auth_user_id=(select auth_user_id from _r where label='A')),
|
|
196
|
+
1, 'still exactly one agent on the synthetic uid (L-4b)');
|
|
197
|
+
-- 26
|
|
198
|
+
select is(
|
|
199
|
+
(select count(*)::int from agent_board.agents a
|
|
200
|
+
where a.owner_user_id=(select auth_user_id from _r where label='A') and a.kind <> 'human'),
|
|
201
|
+
0, 'no non-human agent was ever created on this owner uid (kind is structural)');
|
|
202
|
+
|
|
203
|
+
-- IDLINK: enroll_federated_identity for the SAME (issuer,sub) shares the owner uid --------
|
|
204
|
+
create temp table _idlink (owner uuid) on commit drop;
|
|
205
|
+
do $$
|
|
206
|
+
declare
|
|
207
|
+
f record;
|
|
208
|
+
v_pub text; v_fp text; v_agent uuid;
|
|
209
|
+
begin
|
|
210
|
+
select * into f from _f;
|
|
211
|
+
-- A deterministic canonical 32-byte raw key + its raw-byte fingerprint (same construction
|
|
212
|
+
-- as the enroll_federated_identity B1 harness).
|
|
213
|
+
v_pub := rtrim(translate(encode(extensions.digest(convert_to('idlink-'||f.s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
214
|
+
v_fp := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
215
|
+
select e.agent_id into v_agent
|
|
216
|
+
from agent_board.enroll_federated_identity(
|
|
217
|
+
f.iss, 'message-board', f.sub_a, jsonb_build_object('sub', f.sub_a),
|
|
218
|
+
'idlink-'||f.s, v_pub, 'raw_ed25519_base64url', v_fp,
|
|
219
|
+
true, null, 'agent', '{board:send}', '{}') e;
|
|
220
|
+
insert into _idlink select a.owner_user_id from agent_board.agents a where a.id = v_agent;
|
|
221
|
+
end
|
|
222
|
+
$$;
|
|
223
|
+
-- 27
|
|
224
|
+
select is(
|
|
225
|
+
(select owner from _idlink),
|
|
226
|
+
(select auth_user_id from _r where label='A'),
|
|
227
|
+
'IDLINK: enroll_federated_identity for the same (issuer,sub) shares the fed-human owner uid');
|
|
228
|
+
|
|
229
|
+
-- current_agent_id() resolves the no-credential session to the fed-human agent -----------
|
|
230
|
+
set local role authenticated;
|
|
231
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
232
|
+
select set_config('request.jwt.claim.sub', (select auth_user_id::text from _r where label='A'), true);
|
|
233
|
+
-- 28
|
|
234
|
+
select is(
|
|
235
|
+
agent_board.current_agent_id(),
|
|
236
|
+
(select agent_id from _r where label='A'),
|
|
237
|
+
'current_agent_id() (no agent_credential_id claim) resolves to the fed-human agent');
|
|
238
|
+
-- 29 (non-service caller: authenticated lacks EXECUTE -> 42501)
|
|
239
|
+
select throws_ok(
|
|
240
|
+
$$ select agent_board.enroll_federated_human('https://x.test','message-board','portal-x','{}'::jsonb,null) $$,
|
|
241
|
+
'42501', null, 'a non-service (authenticated) caller is rejected with 42501');
|
|
242
|
+
reset role;
|
|
243
|
+
-- Clear BOTH request.jwt GUCs: reset role alone leaves request.jwt.claim.role set (it
|
|
244
|
+
-- persists as is_local), so auth.role() would stay 'authenticated' and _is_service_role()
|
|
245
|
+
-- would wrongly reject the following superuser calls (they must run as service_role-equiv).
|
|
246
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
247
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
248
|
+
|
|
249
|
+
-- Provider / kind / subject rejection matrix (superuser) ---------------------------------
|
|
250
|
+
-- 30 unknown (issuer, audience)
|
|
251
|
+
select throws_ok(
|
|
252
|
+
(select format($q$select agent_board.enroll_federated_human('https://nope-%s.test','message-board',%L,%L::jsonb,null)$q$,
|
|
253
|
+
s, sub_a, jsonb_build_object('sub', sub_a)::text) from _f),
|
|
254
|
+
'42501', null, 'unknown (issuer,audience) is rejected');
|
|
255
|
+
-- 31 null verified subject
|
|
256
|
+
select throws_ok(
|
|
257
|
+
(select format($q$select agent_board.enroll_federated_human(%L,'message-board',NULL,%L::jsonb,null)$q$,
|
|
258
|
+
iss, jsonb_build_object('x', 1)::text) from _f),
|
|
259
|
+
'22023', null, 'a null verified subject is rejected');
|
|
260
|
+
-- 32 subject contains a '|' (F-11 preimage-injection guard)
|
|
261
|
+
select throws_ok(
|
|
262
|
+
(select format($q$select agent_board.enroll_federated_human(%L,'message-board',%L,%L::jsonb,null)$q$,
|
|
263
|
+
iss, 'portal|evil', jsonb_build_object('sub','portal|evil')::text) from _f),
|
|
264
|
+
'22023', null, 'a subject containing a pipe separator is rejected (F-11)');
|
|
265
|
+
-- 32b subject is email-shaped ('@') (F-11 PII backstop; subject is stored fleet-readable)
|
|
266
|
+
select throws_ok(
|
|
267
|
+
(select format($q$select agent_board.enroll_federated_human(%L,'message-board',%L,%L::jsonb,null)$q$,
|
|
268
|
+
iss, 'evil@example.test', jsonb_build_object('sub','evil@example.test')::text) from _f),
|
|
269
|
+
'22023', null, 'an email-shaped (@) subject is rejected before it reaches fleet-readable metadata (F-11)');
|
|
270
|
+
|
|
271
|
+
-- provider present but NOT policy_approved -> 42501
|
|
272
|
+
do $$
|
|
273
|
+
declare s2 text := replace(substr(gen_random_uuid()::text,1,8),'-','');
|
|
274
|
+
begin
|
|
275
|
+
insert into agent_board.identity_providers
|
|
276
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
277
|
+
values ('https://fed-unapproved-'||s2||'.test','message-board', true, false, '{}','{}','{human}');
|
|
278
|
+
insert into _f(s,iss,aud,sub_a,email_a,sub_b)
|
|
279
|
+
values ('U'||s2, 'https://fed-unapproved-'||s2||'.test','message-board','portal-u-'||s2,null,null);
|
|
280
|
+
end $$;
|
|
281
|
+
-- 33
|
|
282
|
+
select throws_ok(
|
|
283
|
+
(select format($q$select agent_board.enroll_federated_human(%L,'message-board',%L,%L::jsonb,null)$q$,
|
|
284
|
+
iss, sub_a, jsonb_build_object('sub', sub_a)::text) from _f where s like 'U%'),
|
|
285
|
+
'42501', null, 'an un-approved (policy_approved=false) provider is rejected');
|
|
286
|
+
|
|
287
|
+
-- provider approved but allowed_agent_kinds EXCLUDES human (non-empty {agent}) -> 42501
|
|
288
|
+
do $$
|
|
289
|
+
declare s3 text := replace(substr(gen_random_uuid()::text,1,8),'-','');
|
|
290
|
+
begin
|
|
291
|
+
insert into agent_board.identity_providers
|
|
292
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
293
|
+
values ('https://fed-nohuman-'||s3||'.test','message-board', true, true, '{}','{}','{agent}');
|
|
294
|
+
insert into _f(s,iss,aud,sub_a,email_a,sub_b)
|
|
295
|
+
values ('N'||s3, 'https://fed-nohuman-'||s3||'.test','message-board','portal-n-'||s3,null,null);
|
|
296
|
+
end $$;
|
|
297
|
+
-- 34
|
|
298
|
+
select throws_ok(
|
|
299
|
+
(select format($q$select agent_board.enroll_federated_human(%L,'message-board',%L,%L::jsonb,null)$q$,
|
|
300
|
+
iss, sub_a, jsonb_build_object('sub', sub_a)::text) from _f where s like 'N%'),
|
|
301
|
+
'42501', null, 'a non-empty allowed_agent_kinds that excludes human is rejected');
|
|
302
|
+
|
|
303
|
+
-- Non-active identity cannot re-provision (no self-reactivation; F-13 lever) --------------
|
|
304
|
+
update agent_board.agents set status='suspended', updated_at=now()
|
|
305
|
+
where id = (select agent_id from _r where label='A');
|
|
306
|
+
-- 35
|
|
307
|
+
select throws_ok(
|
|
308
|
+
(select format($q$select agent_board.enroll_federated_human(%L,'message-board',%L,%L::jsonb,null)$q$,
|
|
309
|
+
iss, sub_a, jsonb_build_object('sub', sub_a)::text) from _f where sub_a like 'portal-user-%'),
|
|
310
|
+
'42501', null, 'a suspended (non-active) fed-human cannot re-provision a session (42501)');
|
|
311
|
+
|
|
312
|
+
select finish();
|
|
313
|
+
|
|
314
|
+
rollback;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
-- FEDSESSION-AUTOJOIN pgTAP: enroll_federated_human auto-joins a freshly provisioned
|
|
2
|
+
-- federated human to the provider's designated default board.
|
|
3
|
+
--
|
|
4
|
+
-- Proves: the additive default_board_id column + the service_role-only helper; a first
|
|
5
|
+
-- provision with an ACTIVE designated board yields exactly one baseline 'member'/'joined'
|
|
6
|
+
-- self-created membership tagged source=fedsession-autojoin; re-provision is idempotent (no
|
|
7
|
+
-- duplicate); a NULL default_board_id yields no membership; an INACTIVE designated board is
|
|
8
|
+
-- a non-fatal skip (the human still provisions, no membership); and a human who LEFT the
|
|
9
|
+
-- board is NOT re-joined on re-provision (first-time-only). Runs as postgres
|
|
10
|
+
-- (service_role-equivalent), mirroring agent_board_fedsession.test.sql.
|
|
11
|
+
|
|
12
|
+
create extension if not exists pgtap with schema extensions;
|
|
13
|
+
set search_path = extensions, public, agent_board;
|
|
14
|
+
|
|
15
|
+
begin;
|
|
16
|
+
|
|
17
|
+
select plan(14);
|
|
18
|
+
|
|
19
|
+
-- ---------------------------------------------------------------------------
|
|
20
|
+
-- Phase 0: metadata + privileges
|
|
21
|
+
-- ---------------------------------------------------------------------------
|
|
22
|
+
-- 1
|
|
23
|
+
select has_column('agent_board', 'identity_providers', 'default_board_id',
|
|
24
|
+
'identity_providers.default_board_id exists');
|
|
25
|
+
-- 2
|
|
26
|
+
select has_function('agent_board', '_fedsession_autojoin_default_board',
|
|
27
|
+
array['agent_board.identity_providers', 'uuid'],
|
|
28
|
+
'_fedsession_autojoin_default_board(identity_providers,uuid) exists');
|
|
29
|
+
-- 3
|
|
30
|
+
select function_privs_are('agent_board', '_fedsession_autojoin_default_board',
|
|
31
|
+
array['agent_board.identity_providers', 'uuid'], 'service_role', array['EXECUTE'],
|
|
32
|
+
'service_role may EXECUTE _fedsession_autojoin_default_board');
|
|
33
|
+
-- 4
|
|
34
|
+
select function_privs_are('agent_board', '_fedsession_autojoin_default_board',
|
|
35
|
+
array['agent_board.identity_providers', 'uuid'], 'authenticated', array[]::text[],
|
|
36
|
+
'authenticated may NOT EXECUTE _fedsession_autojoin_default_board');
|
|
37
|
+
|
|
38
|
+
-- ---------------------------------------------------------------------------
|
|
39
|
+
-- Fixtures + provisioning
|
|
40
|
+
-- ---------------------------------------------------------------------------
|
|
41
|
+
create temp table _aj (
|
|
42
|
+
board_active uuid, board_inactive uuid,
|
|
43
|
+
agent_h1 uuid, agent_h2 uuid, agent_h3 uuid,
|
|
44
|
+
iss1 text, sub1 text
|
|
45
|
+
) on commit drop;
|
|
46
|
+
grant all on table _aj to authenticated, service_role;
|
|
47
|
+
|
|
48
|
+
do $$
|
|
49
|
+
declare
|
|
50
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
51
|
+
v_owner_agent uuid;
|
|
52
|
+
v_board_active uuid; v_board_inactive uuid;
|
|
53
|
+
v_iss1 text; v_iss2 text; v_iss3 text;
|
|
54
|
+
v_sub1 text := 'aj-user1-';
|
|
55
|
+
v_h1 uuid; v_h2 uuid; v_h3 uuid;
|
|
56
|
+
begin
|
|
57
|
+
v_iss1 := 'https://aj1-' || s || '.test';
|
|
58
|
+
v_iss2 := 'https://aj2-' || s || '.test';
|
|
59
|
+
v_iss3 := 'https://aj3-' || s || '.test';
|
|
60
|
+
v_sub1 := 'aj-user1-' || s;
|
|
61
|
+
|
|
62
|
+
-- A board-owner agent (created_by for the seeded boards).
|
|
63
|
+
insert into agent_board.agents(agent_type, kind, handle, display_name, status)
|
|
64
|
+
values ('system', 'system'::agent_board.agent_kind, 'aj-owner-' || s, 'AJ Owner', 'active')
|
|
65
|
+
returning id into v_owner_agent;
|
|
66
|
+
|
|
67
|
+
-- An ACTIVE default board and an ARCHIVED (inactive) board.
|
|
68
|
+
insert into agent_board.boards(slug, name, created_by_agent_id, visibility, status)
|
|
69
|
+
values ('aj-active-' || s, 'AJ Active', v_owner_agent, 'private', 'active')
|
|
70
|
+
returning id into v_board_active;
|
|
71
|
+
insert into agent_board.boards(slug, name, created_by_agent_id, visibility, status)
|
|
72
|
+
values ('aj-inactive-' || s, 'AJ Inactive', v_owner_agent, 'private', 'archived')
|
|
73
|
+
returning id into v_board_inactive;
|
|
74
|
+
|
|
75
|
+
-- P1: default board = ACTIVE board -> provision H1 (should auto-join).
|
|
76
|
+
insert into agent_board.identity_providers
|
|
77
|
+
(issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
|
|
78
|
+
values (v_iss1, 'message-board', true, true, '{human}', v_board_active);
|
|
79
|
+
select e.agent_id into v_h1 from agent_board.enroll_federated_human(
|
|
80
|
+
v_iss1, 'message-board', v_sub1,
|
|
81
|
+
jsonb_build_object('sub', v_sub1, 'email_verified', false), 'H1') e;
|
|
82
|
+
|
|
83
|
+
-- P2: default board NULL -> provision H2 (no auto-join).
|
|
84
|
+
insert into agent_board.identity_providers
|
|
85
|
+
(issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
|
|
86
|
+
values (v_iss2, 'message-board', true, true, '{human}', null);
|
|
87
|
+
select e.agent_id into v_h2 from agent_board.enroll_federated_human(
|
|
88
|
+
v_iss2, 'message-board', 'aj-user2-' || s,
|
|
89
|
+
jsonb_build_object('sub', 'aj-user2-' || s, 'email_verified', false), 'H2') e;
|
|
90
|
+
|
|
91
|
+
-- P3: default board = INACTIVE board -> provision H3 (no auto-join; provision still succeeds).
|
|
92
|
+
insert into agent_board.identity_providers
|
|
93
|
+
(issuer, audience, supported, policy_approved, allowed_agent_kinds, default_board_id)
|
|
94
|
+
values (v_iss3, 'message-board', true, true, '{human}', v_board_inactive);
|
|
95
|
+
select e.agent_id into v_h3 from agent_board.enroll_federated_human(
|
|
96
|
+
v_iss3, 'message-board', 'aj-user3-' || s,
|
|
97
|
+
jsonb_build_object('sub', 'aj-user3-' || s, 'email_verified', false), 'H3') e;
|
|
98
|
+
|
|
99
|
+
-- Idempotent re-provision of H1 (same issuer/sub) -> must NOT add a second membership.
|
|
100
|
+
perform e.agent_id from agent_board.enroll_federated_human(
|
|
101
|
+
v_iss1, 'message-board', v_sub1,
|
|
102
|
+
jsonb_build_object('sub', v_sub1, 'email_verified', false), 'H1') e;
|
|
103
|
+
|
|
104
|
+
insert into _aj values (v_board_active, v_board_inactive, v_h1, v_h2, v_h3, v_iss1, v_sub1);
|
|
105
|
+
end
|
|
106
|
+
$$;
|
|
107
|
+
|
|
108
|
+
-- H1 auto-joined the ACTIVE default board ------------------------------------------------
|
|
109
|
+
-- 5
|
|
110
|
+
select is(
|
|
111
|
+
(select count(*)::int from agent_board.board_memberships m, _aj
|
|
112
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
|
|
113
|
+
1, 'H1 has exactly one membership on the active default board (idempotent after re-provision)');
|
|
114
|
+
-- 6
|
|
115
|
+
select is(
|
|
116
|
+
(select m.role from agent_board.board_memberships m, _aj
|
|
117
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
|
|
118
|
+
'member', 'auto-join role is baseline member (never owner/admin)');
|
|
119
|
+
-- 7
|
|
120
|
+
select is(
|
|
121
|
+
(select m.membership_state from agent_board.board_memberships m, _aj
|
|
122
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
|
|
123
|
+
'joined', 'auto-join membership_state is joined');
|
|
124
|
+
-- 8
|
|
125
|
+
select is(
|
|
126
|
+
(select m.created_by_agent_id from agent_board.board_memberships m, _aj
|
|
127
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
|
|
128
|
+
(select agent_h1 from _aj), 'auto-join created_by is the fed-human itself (self-join)');
|
|
129
|
+
-- 9
|
|
130
|
+
select is(
|
|
131
|
+
(select m.metadata->>'source' from agent_board.board_memberships m, _aj
|
|
132
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
|
|
133
|
+
'fedsession-autojoin', 'auto-join membership tagged source=fedsession-autojoin');
|
|
134
|
+
-- 10
|
|
135
|
+
select is(
|
|
136
|
+
(select count(distinct m.id)::int from agent_board.board_memberships m, _aj
|
|
137
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1),
|
|
138
|
+
1, 'no duplicate membership rows after re-provision (idempotent)');
|
|
139
|
+
|
|
140
|
+
-- NULL default board -> no auto-join -----------------------------------------------------
|
|
141
|
+
-- 11
|
|
142
|
+
select is(
|
|
143
|
+
(select count(*)::int from agent_board.board_memberships m, _aj
|
|
144
|
+
where m.agent_id = _aj.agent_h2),
|
|
145
|
+
0, 'H2 (provider default_board_id NULL) got NO auto-join membership');
|
|
146
|
+
|
|
147
|
+
-- INACTIVE default board -> non-fatal skip -----------------------------------------------
|
|
148
|
+
-- 12
|
|
149
|
+
select ok((select agent_h3 is not null from _aj),
|
|
150
|
+
'H3 provisioned successfully even though its designated default board is inactive');
|
|
151
|
+
-- 13
|
|
152
|
+
select is(
|
|
153
|
+
(select count(*)::int from agent_board.board_memberships m, _aj
|
|
154
|
+
where m.board_id = _aj.board_inactive and m.agent_id = _aj.agent_h3),
|
|
155
|
+
0, 'inactive default board yields NO auto-join (non-fatal skip)');
|
|
156
|
+
|
|
157
|
+
-- Left-respect: a human who LEFT the board is NOT re-joined on re-provision ---------------
|
|
158
|
+
update agent_board.board_memberships m
|
|
159
|
+
set membership_state = 'left', left_at = now()
|
|
160
|
+
from _aj
|
|
161
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1;
|
|
162
|
+
|
|
163
|
+
do $$
|
|
164
|
+
declare r record;
|
|
165
|
+
begin
|
|
166
|
+
select iss1, sub1 into r from _aj;
|
|
167
|
+
perform e.agent_id from agent_board.enroll_federated_human(
|
|
168
|
+
r.iss1, 'message-board', r.sub1,
|
|
169
|
+
jsonb_build_object('sub', r.sub1, 'email_verified', false), 'H1') e;
|
|
170
|
+
end
|
|
171
|
+
$$;
|
|
172
|
+
|
|
173
|
+
-- 14
|
|
174
|
+
select is(
|
|
175
|
+
(select count(*)::int from agent_board.board_memberships m, _aj
|
|
176
|
+
where m.board_id = _aj.board_active and m.agent_id = _aj.agent_h1
|
|
177
|
+
and m.membership_state = 'joined'),
|
|
178
|
+
0, 'a fed-human who LEFT the default board is NOT re-joined on re-provision (first-time-only)');
|
|
179
|
+
|
|
180
|
+
select * from finish();
|
|
181
|
+
rollback;
|