@consiliency/agent-board-schema 1.2.0 → 1.4.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/bin/agent-board-schema.js +0 -0
- package/fixtures/already-applied.json +108 -0
- package/migrations/20260714000200_agent_board_workload_providers_rls.sql +35 -0
- package/migrations/20260717000100_agent_board_board_role.sql +180 -0
- package/migrations/20260717000200_agent_board_thread_acl.sql +149 -0
- package/migrations/20260717000300_agent_board_agent_lineage.sql +54 -0
- package/migrations/20260717000400_agent_board_delegation_policies.sql +175 -0
- package/migrations/20260718000100_agent_board_send_authz_config.sql +172 -0
- package/migrations/20260718000200_agent_board_send_role_gate.sql +804 -0
- package/migrations/20260718000300_agent_board_fleet_policy_default_deny.sql +192 -0
- package/migrations/20260718000400_agent_board_grant_ceiling_wiring.sql +58 -0
- package/migrations/20260718000500_agent_board_fleet_agent_session_class.sql +24 -0
- package/migrations/20260718000600_agent_board_mint_fleet_agent_session.sql +172 -0
- package/migrations/20260718000700_agent_board_nonfleet_session_reaper.sql +35 -0
- package/migrations/20260718000800_agent_board_regrant_scoped_send.sql +24 -0
- package/migrations/20260718000900_agent_board_agents_status_revoked.sql +24 -0
- package/migrations/20260718001000_agent_board_delegation_enforcement.sql +296 -0
- package/migrations/20260718001100_agent_board_revoke_agent_subtree.sql +238 -0
- package/migrations/20260718001200_agent_board_resigning_refresh.sql +129 -0
- package/migrations/20260718001300_agent_board_identity_providers.sql +70 -0
- package/migrations/20260718001400_agent_board_enrollment_federation_audit.sql +50 -0
- package/migrations/20260718001500_agent_board_enroll_federated_identity.sql +220 -0
- package/migrations/20260719000100_agent_board_signing_challenge_pending_cap.sql +124 -0
- package/migrations/20260719000200_agent_board_signing_challenge_reaper.sql +34 -0
- package/migrations/20260719000300_agent_board_enroll_rate_cap.sql +72 -0
- package/migrations/20260719001000_agent_board_identity_providers_portal_contract.sql +38 -0
- package/migrations/20260719001100_agent_board_enroll_federated_identity_pop.sql +267 -0
- package/migrations/20260719001200_agent_board_enroll_federated_identity_raw_fingerprint.sql +296 -0
- package/migrations/20260719001300_agent_board_enroll_rate_events_reaper_rls.sql +62 -0
- package/package.json +9 -9
- package/schema-manifest.json +247 -4
- package/src/tooling.test.mjs +42 -1
- package/tests/agent_board_agent_lineage.test.sql +73 -0
- package/tests/agent_board_agents_status_revoked.test.sql +60 -0
- package/tests/agent_board_auth_rpcs.test.sql +17 -16
- package/tests/agent_board_board_role.test.sql +129 -0
- package/tests/agent_board_bundle_endpoint_compat.test.sql +18 -6
- package/tests/agent_board_canary_signed_fleet_enforcement.test.sql +8 -8
- package/tests/agent_board_delegation_enforcement.test.sql +175 -0
- package/tests/agent_board_delegation_policies.test.sql +119 -0
- package/tests/agent_board_enroll_federated_identity.test.sql +217 -0
- package/tests/agent_board_enroll_pop_binding.test.sql +131 -0
- package/tests/agent_board_enroll_rate_cap.test.sql +53 -0
- package/tests/agent_board_enrollment_audit_content.test.sql +134 -0
- package/tests/agent_board_fleet_agent_session_class.test.sql +64 -0
- package/tests/agent_board_fleet_policy_default_deny.test.sql +149 -0
- package/tests/agent_board_fleet_registry.test.sql +2 -2
- package/tests/agent_board_identity_providers.test.sql +104 -0
- package/tests/agent_board_inbox_rpcs.test.sql +6 -6
- package/tests/agent_board_mint_fleet_agent_session.test.sql +290 -0
- package/tests/agent_board_nonfleet_session_reaper.test.sql +61 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +130 -0
- package/tests/agent_board_resigning_refresh.test.sql +73 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +188 -0
- package/tests/agent_board_schema_version.test.sql +2 -2
- package/tests/agent_board_schema_version_rpc.test.sql +1 -1
- package/tests/agent_board_scoped_send_regrant.test.sql +138 -0
- package/tests/agent_board_send_authz_config.test.sql +84 -0
- package/tests/agent_board_send_authz_matrix.test.sql +159 -0
- package/tests/agent_board_send_role_gate.test.sql +177 -0
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +87 -0
- package/tests/agent_board_signing_challenge_reaper.test.sql +74 -0
- package/tests/agent_board_thread_acl.test.sql +129 -0
- package/tests/agent_board_workload_providers_rls.test.sql +40 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-1 pgTAP: _current_agent_can_send_to_board, the board-role send
|
|
2
|
+
-- gate on the board-target sends, boardless-send invariance, and service_role
|
|
3
|
+
-- bypass telemetry with the frozen origin discriminator.
|
|
4
|
+
--
|
|
5
|
+
-- Runs as the postgres db role; request.jwt.claim.role toggles _is_service_role()
|
|
6
|
+
-- (''/service_role => true, 'authenticated' => false), request.jwt.claim.sub picks
|
|
7
|
+
-- the acting agent.
|
|
8
|
+
create extension if not exists pgtap with schema extensions;
|
|
9
|
+
set search_path = extensions, public, agent_board;
|
|
10
|
+
|
|
11
|
+
begin;
|
|
12
|
+
|
|
13
|
+
select plan(16);
|
|
14
|
+
|
|
15
|
+
create temp table _srg_fixture (
|
|
16
|
+
board uuid,
|
|
17
|
+
member_agent uuid,
|
|
18
|
+
observer_agent uuid,
|
|
19
|
+
nonmember_agent uuid,
|
|
20
|
+
recipient_agent uuid,
|
|
21
|
+
member_user uuid,
|
|
22
|
+
observer_user uuid,
|
|
23
|
+
nonmember_user uuid,
|
|
24
|
+
thread_member uuid,
|
|
25
|
+
thread_observer uuid,
|
|
26
|
+
thread_dm uuid
|
|
27
|
+
) on commit drop;
|
|
28
|
+
|
|
29
|
+
do $$
|
|
30
|
+
declare
|
|
31
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
32
|
+
v_member_user uuid;
|
|
33
|
+
v_observer_user uuid;
|
|
34
|
+
v_nonmember_user uuid;
|
|
35
|
+
v_member uuid := gen_random_uuid();
|
|
36
|
+
v_observer uuid := gen_random_uuid();
|
|
37
|
+
v_nonmember uuid := gen_random_uuid();
|
|
38
|
+
v_recipient uuid := gen_random_uuid();
|
|
39
|
+
v_board uuid;
|
|
40
|
+
v_thread_member uuid := gen_random_uuid();
|
|
41
|
+
v_thread_observer uuid := gen_random_uuid();
|
|
42
|
+
v_thread_dm uuid := gen_random_uuid();
|
|
43
|
+
begin
|
|
44
|
+
v_member_user := public.ensure_github_user('srg-member-' || v_suffix, 'srg-member-' || v_suffix || '@example.test', 'srg-member-' || v_suffix, 'SRG Member');
|
|
45
|
+
v_observer_user := public.ensure_github_user('srg-observer-' || v_suffix, 'srg-observer-' || v_suffix || '@example.test', 'srg-observer-' || v_suffix, 'SRG Observer');
|
|
46
|
+
v_nonmember_user := public.ensure_github_user('srg-nonmember-' || v_suffix, 'srg-nonmember-' || v_suffix || '@example.test', 'srg-nonmember-' || v_suffix, 'SRG Nonmember');
|
|
47
|
+
|
|
48
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
49
|
+
values
|
|
50
|
+
(v_member, 'agent', 'srg-member-' || v_suffix, 'SRG Member', 'active', v_member_user, v_member_user),
|
|
51
|
+
(v_observer, 'agent', 'srg-observer-' || v_suffix, 'SRG Observer', 'active', v_observer_user, v_observer_user),
|
|
52
|
+
(v_nonmember, 'agent', 'srg-nonmember-' || v_suffix, 'SRG Nonmember', 'active', v_nonmember_user, v_nonmember_user),
|
|
53
|
+
(v_recipient, 'agent', 'srg-recipient-' || v_suffix, 'SRG Recipient', 'active', null, null);
|
|
54
|
+
|
|
55
|
+
select board_id into v_board
|
|
56
|
+
from agent_board.create_board(v_member, 'srg-board-' || v_suffix, 'SRG Board', 'send role gate test', '{}'::jsonb, 'private');
|
|
57
|
+
|
|
58
|
+
-- member: role member; observer: role observer. Both active.
|
|
59
|
+
insert into agent_board.board_memberships(board_id, agent_id, membership_state, created_by_agent_id, role)
|
|
60
|
+
values
|
|
61
|
+
(v_board, v_member, 'joined', v_member, 'member'),
|
|
62
|
+
(v_board, v_observer, 'joined', v_member, 'observer');
|
|
63
|
+
|
|
64
|
+
-- Threads each sending actor can access (inherit mode; creator has access).
|
|
65
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
66
|
+
values
|
|
67
|
+
(v_thread_member, 'coordination', v_member, 'SRG member thread'),
|
|
68
|
+
(v_thread_observer, 'coordination', v_observer, 'SRG observer thread'),
|
|
69
|
+
(v_thread_dm, 'coordination', v_member, 'SRG dm thread');
|
|
70
|
+
|
|
71
|
+
insert into _srg_fixture values
|
|
72
|
+
(v_board, v_member, v_observer, v_nonmember, v_recipient,
|
|
73
|
+
v_member_user, v_observer_user, v_nonmember_user,
|
|
74
|
+
v_thread_member, v_thread_observer, v_thread_dm);
|
|
75
|
+
end
|
|
76
|
+
$$;
|
|
77
|
+
|
|
78
|
+
-- 1. _current_agent_can_send_to_board -----------------------------------------
|
|
79
|
+
select has_function('agent_board', '_current_agent_can_send_to_board', array['uuid'], '_current_agent_can_send_to_board exists');
|
|
80
|
+
|
|
81
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
82
|
+
select is(
|
|
83
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
84
|
+
true, 'service_role may always send to board');
|
|
85
|
+
|
|
86
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
87
|
+
select set_config('request.jwt.claim.sub', (select member_user from _srg_fixture)::text, true);
|
|
88
|
+
select is(
|
|
89
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
90
|
+
true, 'active member (role member) may send to board');
|
|
91
|
+
|
|
92
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
93
|
+
select is(
|
|
94
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
95
|
+
false, 'observer board member may not send to board');
|
|
96
|
+
|
|
97
|
+
select set_config('request.jwt.claim.sub', (select nonmember_user from _srg_fixture)::text, true);
|
|
98
|
+
select is(
|
|
99
|
+
agent_board._current_agent_can_send_to_board((select board from _srg_fixture)),
|
|
100
|
+
false, 'non-member may not send to board');
|
|
101
|
+
|
|
102
|
+
-- 2. Board-target sends enforce the role gate on the authenticated path -------
|
|
103
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
104
|
+
select throws_ok(
|
|
105
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body')$q$,
|
|
106
|
+
thread_observer, observer_agent, board) from _srg_fixture),
|
|
107
|
+
'42501', 'board send requires a non-observer board role',
|
|
108
|
+
'post_board_message rejects an observer board member');
|
|
109
|
+
|
|
110
|
+
select set_config('request.jwt.claim.sub', (select member_user from _srg_fixture)::text, true);
|
|
111
|
+
select lives_ok(
|
|
112
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body')$q$,
|
|
113
|
+
thread_member, member_agent, board) from _srg_fixture),
|
|
114
|
+
'post_board_message admits a member board member');
|
|
115
|
+
|
|
116
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
117
|
+
select throws_ok(
|
|
118
|
+
(select format($q$select agent_board.create_tasking_delivery(%L::uuid, %L::uuid, 'title', null, null, %L::uuid)$q$,
|
|
119
|
+
thread_observer, observer_agent, board) from _srg_fixture),
|
|
120
|
+
'42501', 'board send requires a non-observer board role',
|
|
121
|
+
'create_tasking_delivery (board target) rejects an observer board member');
|
|
122
|
+
|
|
123
|
+
select set_config('request.jwt.claim.sub', (select member_user from _srg_fixture)::text, true);
|
|
124
|
+
select lives_ok(
|
|
125
|
+
(select format($q$select agent_board.create_tasking_delivery(%L::uuid, %L::uuid, 'title', null, null, %L::uuid)$q$,
|
|
126
|
+
thread_member, member_agent, board) from _srg_fixture),
|
|
127
|
+
'create_tasking_delivery (board target) admits a member board member');
|
|
128
|
+
|
|
129
|
+
-- 3. Boardless direct send is unaffected by the role gate ---------------------
|
|
130
|
+
-- The observer holds only an observer board role, but a board-less DM never
|
|
131
|
+
-- consults the board role gate.
|
|
132
|
+
select set_config('request.jwt.claim.sub', (select observer_user from _srg_fixture)::text, true);
|
|
133
|
+
select lives_ok(
|
|
134
|
+
(select format($q$select agent_board.send_direct_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body')$q$,
|
|
135
|
+
thread_observer, observer_agent, recipient_agent) from _srg_fixture),
|
|
136
|
+
'send_direct_message on a board-less thread is unaffected by the board role gate (observer sender admitted)');
|
|
137
|
+
|
|
138
|
+
-- 4. service_role bypass telemetry: exactly one row, origin from p_metadata ----
|
|
139
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
140
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
141
|
+
select lives_ok(
|
|
142
|
+
(select format($q$select agent_board.post_board_message(%L::uuid, %L::uuid, %L::uuid, 'sub', 'body', 'comment', 'medium', null, %L::jsonb)$q$,
|
|
143
|
+
thread_member, member_agent, board,
|
|
144
|
+
jsonb_build_object('signed_write', jsonb_build_object('origin', 'ef'))) from _srg_fixture),
|
|
145
|
+
'service_role post_board_message succeeds (guarded bypass)');
|
|
146
|
+
|
|
147
|
+
select is(
|
|
148
|
+
(select count(*)::int from agent_board.send_authz_telemetry
|
|
149
|
+
where kind = 'service_role_bypass'
|
|
150
|
+
and operation = 'post_board_message'
|
|
151
|
+
and actor_agent_id = (select member_agent from _srg_fixture)),
|
|
152
|
+
1, 'exactly one service_role_bypass telemetry row is recorded for the send');
|
|
153
|
+
|
|
154
|
+
select is(
|
|
155
|
+
(select origin from agent_board.send_authz_telemetry
|
|
156
|
+
where kind = 'service_role_bypass' and operation = 'post_board_message'
|
|
157
|
+
and actor_agent_id = (select member_agent from _srg_fixture)
|
|
158
|
+
order by recorded_at desc limit 1),
|
|
159
|
+
'ef', 'bypass telemetry origin is resolved from p_metadata signed_write origin');
|
|
160
|
+
|
|
161
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
162
|
+
|
|
163
|
+
-- 5. Enforcement live (MBSESSION): the 8 core sends are re-granted to
|
|
164
|
+
-- authenticated; the board-role gate asserted above still governs who may send.
|
|
165
|
+
select function_privs_are(
|
|
166
|
+
'agent_board', 'post_board_message', array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
|
|
167
|
+
'authenticated', array['EXECUTE'], 'post_board_message execute is granted to authenticated (scoped send live)');
|
|
168
|
+
select function_privs_are(
|
|
169
|
+
'agent_board', 'create_tasking_delivery', array['uuid','uuid','text','text','uuid[]','uuid','text','text','text','jsonb'],
|
|
170
|
+
'authenticated', array['EXECUTE'], 'create_tasking_delivery execute is granted to authenticated (scoped send live)');
|
|
171
|
+
select function_privs_are(
|
|
172
|
+
'agent_board', 'send_direct_message', array['uuid','uuid','uuid','text','text','text','text','text','jsonb'],
|
|
173
|
+
'authenticated', array['EXECUTE'], 'send_direct_message execute is granted to authenticated (scoped send live)');
|
|
174
|
+
|
|
175
|
+
select finish();
|
|
176
|
+
|
|
177
|
+
rollback;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- MBHARDEN SL-1 pgTAP: issue_signing_key_challenge enforces a per-key cap of
|
|
2
|
+
-- N=16 concurrently-pending challenges; <=16 issue as before, the 17th is
|
|
3
|
+
-- rejected, and expired/consumed challenges do not count toward the cap.
|
|
4
|
+
create extension if not exists pgtap with schema extensions;
|
|
5
|
+
set search_path = extensions, public, agent_board;
|
|
6
|
+
|
|
7
|
+
begin;
|
|
8
|
+
|
|
9
|
+
select plan(4);
|
|
10
|
+
|
|
11
|
+
select has_function(
|
|
12
|
+
'agent_board', 'issue_signing_key_challenge', array['uuid', 'text', 'interval'],
|
|
13
|
+
'issue_signing_key_challenge exists');
|
|
14
|
+
|
|
15
|
+
-- Block 1: 16 pending challenges are allowed; the 17th is rejected by the cap.
|
|
16
|
+
do $$
|
|
17
|
+
declare
|
|
18
|
+
v_agent uuid := '00000000-0000-0000-0000-0000000000c1';
|
|
19
|
+
v_i int;
|
|
20
|
+
v_c record;
|
|
21
|
+
begin
|
|
22
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
23
|
+
values (v_agent, 'agent', 'pcap-agent', 'PCAP Agent');
|
|
24
|
+
insert into agent_board.agent_signing_keys(
|
|
25
|
+
agent_id, key_id, key_version, public_key, public_key_format, fingerprint)
|
|
26
|
+
values (
|
|
27
|
+
v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url',
|
|
28
|
+
'sha256:' || repeat('Z', 43));
|
|
29
|
+
|
|
30
|
+
set local role service_role;
|
|
31
|
+
|
|
32
|
+
for v_i in 1..16 loop
|
|
33
|
+
select * into v_c
|
|
34
|
+
from agent_board.issue_signing_key_challenge(v_agent, 'primary', interval '5 minutes');
|
|
35
|
+
if v_c.challenge_id is null then
|
|
36
|
+
raise exception 'pending challenge % was not issued below the cap', v_i;
|
|
37
|
+
end if;
|
|
38
|
+
end loop;
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
perform *
|
|
42
|
+
from agent_board.issue_signing_key_challenge(v_agent, 'primary', interval '5 minutes');
|
|
43
|
+
raise exception 'the 17th pending challenge was NOT rejected by the cap';
|
|
44
|
+
exception
|
|
45
|
+
when others then
|
|
46
|
+
if sqlerrm !~ 'challenge_pending_cap' then
|
|
47
|
+
raise;
|
|
48
|
+
end if;
|
|
49
|
+
end;
|
|
50
|
+
end
|
|
51
|
+
$$;
|
|
52
|
+
reset role;
|
|
53
|
+
|
|
54
|
+
select pass('16 pending challenges issue below the cap');
|
|
55
|
+
select pass('the 17th concurrently-pending challenge is rejected (challenge_pending_cap)');
|
|
56
|
+
|
|
57
|
+
-- Expire one pending challenge (as owner), then a new challenge succeeds: expired
|
|
58
|
+
-- rows do not count toward the cap.
|
|
59
|
+
update agent_board.agent_signing_key_challenges
|
|
60
|
+
set expires_at = now() - interval '1 minute'
|
|
61
|
+
where id = (
|
|
62
|
+
select id from agent_board.agent_signing_key_challenges
|
|
63
|
+
where agent_id = '00000000-0000-0000-0000-0000000000c1'
|
|
64
|
+
and proof_state = 'pending'
|
|
65
|
+
order by created_at
|
|
66
|
+
limit 1);
|
|
67
|
+
|
|
68
|
+
do $$
|
|
69
|
+
declare
|
|
70
|
+
v_agent uuid := '00000000-0000-0000-0000-0000000000c1';
|
|
71
|
+
v_c record;
|
|
72
|
+
begin
|
|
73
|
+
set local role service_role;
|
|
74
|
+
select * into v_c
|
|
75
|
+
from agent_board.issue_signing_key_challenge(v_agent, 'primary', interval '5 minutes');
|
|
76
|
+
if v_c.challenge_id is null then
|
|
77
|
+
raise exception 'a new challenge was rejected even though an expired row freed a slot';
|
|
78
|
+
end if;
|
|
79
|
+
end
|
|
80
|
+
$$;
|
|
81
|
+
reset role;
|
|
82
|
+
|
|
83
|
+
select pass('an expired pending challenge frees a slot under the cap');
|
|
84
|
+
|
|
85
|
+
select finish();
|
|
86
|
+
|
|
87
|
+
rollback;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
-- MBHARDEN SL-1 pgTAP: reap_expired_signing_key_challenges deletes only rows
|
|
2
|
+
-- whose expires_at is older than the grace argument, leaves unexpired rows, returns
|
|
3
|
+
-- the affected count, and is revoked from authenticated.
|
|
4
|
+
create extension if not exists pgtap with schema extensions;
|
|
5
|
+
set search_path = extensions, public, agent_board;
|
|
6
|
+
|
|
7
|
+
begin;
|
|
8
|
+
|
|
9
|
+
select plan(9);
|
|
10
|
+
|
|
11
|
+
select has_function(
|
|
12
|
+
'agent_board', 'reap_expired_signing_key_challenges', array['interval'],
|
|
13
|
+
'reap_expired_signing_key_challenges(interval) exists');
|
|
14
|
+
|
|
15
|
+
-- Seed: one agent + key, then three challenges at old (>7d) / recent (<7d) / future
|
|
16
|
+
-- expires_at. All are pending (proof_state='pending', consumed_at null) — valid per
|
|
17
|
+
-- the consumed-shape check regardless of expires_at.
|
|
18
|
+
do $$
|
|
19
|
+
declare
|
|
20
|
+
v_agent uuid := gen_random_uuid();
|
|
21
|
+
v_key uuid;
|
|
22
|
+
begin
|
|
23
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
24
|
+
values (v_agent, 'agent', 'reap-chal-agent', 'REAP CHAL Agent');
|
|
25
|
+
insert into agent_board.agent_signing_keys(
|
|
26
|
+
agent_id, key_id, key_version, public_key, public_key_format, fingerprint)
|
|
27
|
+
values (
|
|
28
|
+
v_agent, 'primary', 1, repeat('A', 43), 'raw_ed25519_base64url',
|
|
29
|
+
'sha256:' || repeat('Z', 43))
|
|
30
|
+
returning id into v_key;
|
|
31
|
+
|
|
32
|
+
insert into agent_board.agent_signing_key_challenges(
|
|
33
|
+
signing_key_id, agent_id, challenge_nonce, challenge_bytes, expires_at)
|
|
34
|
+
values
|
|
35
|
+
(v_key, v_agent, 'nonce-old', 'bytes-old', now() - interval '30 days'),
|
|
36
|
+
(v_key, v_agent, 'nonce-recent', 'bytes-recent', now() - interval '1 hour'),
|
|
37
|
+
(v_key, v_agent, 'nonce-future', 'bytes-future', now() + interval '10 minutes');
|
|
38
|
+
end
|
|
39
|
+
$$;
|
|
40
|
+
|
|
41
|
+
-- Default 7-day grace: removes only the 30-day-old row.
|
|
42
|
+
select is(
|
|
43
|
+
agent_board.reap_expired_signing_key_challenges(),
|
|
44
|
+
1::bigint, 'default (7 day) reap removes exactly the one row expired > 7 days ago');
|
|
45
|
+
select ok(
|
|
46
|
+
not exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-old'),
|
|
47
|
+
'the 30-day-old challenge was deleted');
|
|
48
|
+
select ok(
|
|
49
|
+
exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-recent'),
|
|
50
|
+
'the 1-hour-old challenge survives the 7-day grace');
|
|
51
|
+
select ok(
|
|
52
|
+
exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-future'),
|
|
53
|
+
'the not-yet-expired challenge survives the 7-day grace');
|
|
54
|
+
|
|
55
|
+
-- Zero grace: removes any past-expires_at row (the recent one) but not the future one.
|
|
56
|
+
select is(
|
|
57
|
+
agent_board.reap_expired_signing_key_challenges(interval '0 seconds'),
|
|
58
|
+
1::bigint, 'zero-grace reap removes exactly the remaining past-expires_at row');
|
|
59
|
+
select ok(
|
|
60
|
+
not exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-recent'),
|
|
61
|
+
'the 1-hour-old challenge is deleted under zero grace');
|
|
62
|
+
select ok(
|
|
63
|
+
exists (select 1 from agent_board.agent_signing_key_challenges where challenge_nonce = 'nonce-future'),
|
|
64
|
+
'a not-yet-expired challenge is never reaped');
|
|
65
|
+
|
|
66
|
+
-- Privilege: execute revoked from authenticated.
|
|
67
|
+
select function_privs_are(
|
|
68
|
+
'agent_board', 'reap_expired_signing_key_challenges', array['interval'],
|
|
69
|
+
'authenticated', array[]::text[],
|
|
70
|
+
'reap_expired_signing_key_challenges execute is revoked from authenticated');
|
|
71
|
+
|
|
72
|
+
select finish();
|
|
73
|
+
|
|
74
|
+
rollback;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
-- MBAUTHZ SL-2 (Lane B) pgTAP: threads.access_mode, thread_memberships (+RLS),
|
|
2
|
+
-- and the redefined _current_agent_can_access_thread.
|
|
3
|
+
--
|
|
4
|
+
-- The suite runs as the postgres db role (superuser, so it can call the
|
|
5
|
+
-- grant-revoked access helper), and simulates callers by toggling
|
|
6
|
+
-- request.jwt.claim.role / request.jwt.claim.sub: role='authenticated' makes
|
|
7
|
+
-- _is_service_role() false so the real access logic runs; role='service_role'
|
|
8
|
+
-- exercises the short-circuit.
|
|
9
|
+
create extension if not exists pgtap with schema extensions;
|
|
10
|
+
set search_path = extensions, public, agent_board;
|
|
11
|
+
|
|
12
|
+
begin;
|
|
13
|
+
|
|
14
|
+
select plan(14);
|
|
15
|
+
|
|
16
|
+
create temp table _thread_acl_fixture (
|
|
17
|
+
thread_inherit uuid,
|
|
18
|
+
thread_restricted uuid,
|
|
19
|
+
creator_user uuid,
|
|
20
|
+
member_user uuid,
|
|
21
|
+
stranger_user uuid,
|
|
22
|
+
expired_user uuid,
|
|
23
|
+
revoked_user uuid
|
|
24
|
+
) on commit drop;
|
|
25
|
+
|
|
26
|
+
do $$
|
|
27
|
+
declare
|
|
28
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
29
|
+
v_creator_user uuid;
|
|
30
|
+
v_member_user uuid;
|
|
31
|
+
v_stranger_user uuid;
|
|
32
|
+
v_expired_user uuid;
|
|
33
|
+
v_revoked_user uuid;
|
|
34
|
+
v_creator uuid := gen_random_uuid();
|
|
35
|
+
v_member uuid := gen_random_uuid();
|
|
36
|
+
v_stranger uuid := gen_random_uuid();
|
|
37
|
+
v_expired uuid := gen_random_uuid();
|
|
38
|
+
v_revoked uuid := gen_random_uuid();
|
|
39
|
+
v_thread_inherit uuid := gen_random_uuid();
|
|
40
|
+
v_thread_restricted uuid := gen_random_uuid();
|
|
41
|
+
begin
|
|
42
|
+
v_creator_user := public.ensure_github_user('acl-creator-' || v_suffix, 'acl-creator-' || v_suffix || '@example.test', 'acl-creator-' || v_suffix, 'ACL Creator');
|
|
43
|
+
v_member_user := public.ensure_github_user('acl-member-' || v_suffix, 'acl-member-' || v_suffix || '@example.test', 'acl-member-' || v_suffix, 'ACL Member');
|
|
44
|
+
v_stranger_user := public.ensure_github_user('acl-stranger-' || v_suffix, 'acl-stranger-' || v_suffix || '@example.test', 'acl-stranger-' || v_suffix, 'ACL Stranger');
|
|
45
|
+
v_expired_user := public.ensure_github_user('acl-expired-' || v_suffix, 'acl-expired-' || v_suffix || '@example.test', 'acl-expired-' || v_suffix, 'ACL Expired');
|
|
46
|
+
v_revoked_user := public.ensure_github_user('acl-revoked-' || v_suffix, 'acl-revoked-' || v_suffix || '@example.test', 'acl-revoked-' || v_suffix, 'ACL Revoked');
|
|
47
|
+
|
|
48
|
+
insert into agent_board.agents(id, agent_type, handle, display_name, status, auth_user_id, owner_user_id)
|
|
49
|
+
values
|
|
50
|
+
(v_creator, 'agent', 'acl-creator-' || v_suffix, 'ACL Creator', 'active', v_creator_user, v_creator_user),
|
|
51
|
+
(v_member, 'agent', 'acl-member-' || v_suffix, 'ACL Member', 'active', v_member_user, v_member_user),
|
|
52
|
+
(v_stranger, 'agent', 'acl-stranger-' || v_suffix, 'ACL Stranger', 'active', v_stranger_user, v_stranger_user),
|
|
53
|
+
(v_expired, 'agent', 'acl-expired-' || v_suffix, 'ACL Expired', 'active', v_expired_user, v_expired_user),
|
|
54
|
+
(v_revoked, 'agent', 'acl-revoked-' || v_suffix, 'ACL Revoked', 'active', v_revoked_user, v_revoked_user);
|
|
55
|
+
|
|
56
|
+
-- inherit thread (default access_mode) and a restricted thread.
|
|
57
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject)
|
|
58
|
+
values (v_thread_inherit, 'coordination', v_creator, 'ACL inherit thread');
|
|
59
|
+
insert into agent_board.threads(id, thread_type, created_by_agent_id, subject, access_mode)
|
|
60
|
+
values (v_thread_restricted, 'coordination', v_creator, 'ACL restricted thread', 'restricted');
|
|
61
|
+
|
|
62
|
+
-- ACL on the restricted thread: active member, expired member, revoked member.
|
|
63
|
+
insert into agent_board.thread_memberships(thread_id, agent_id, role, granted_by_agent_id, expires_at, revoked_at)
|
|
64
|
+
values
|
|
65
|
+
(v_thread_restricted, v_member, 'member', v_creator, null, null),
|
|
66
|
+
(v_thread_restricted, v_expired, 'member', v_creator, now() - interval '1 hour', null),
|
|
67
|
+
(v_thread_restricted, v_revoked, 'member', v_creator, null, now() - interval '1 hour');
|
|
68
|
+
|
|
69
|
+
insert into _thread_acl_fixture values
|
|
70
|
+
(v_thread_inherit, v_thread_restricted, v_creator_user, v_member_user, v_stranger_user, v_expired_user, v_revoked_user);
|
|
71
|
+
end
|
|
72
|
+
$$;
|
|
73
|
+
|
|
74
|
+
-- Structure -----------------------------------------------------------------
|
|
75
|
+
select has_column('agent_board', 'threads', 'access_mode', 'threads has access_mode');
|
|
76
|
+
select col_type_is('agent_board', 'threads', 'access_mode', 'text', 'access_mode is text');
|
|
77
|
+
select col_has_check('agent_board', 'threads', 'access_mode', 'access_mode is check-constrained');
|
|
78
|
+
select is(
|
|
79
|
+
(select access_mode from agent_board.threads t join _thread_acl_fixture f on t.id = f.thread_inherit),
|
|
80
|
+
'inherit', 'access_mode defaults to inherit');
|
|
81
|
+
select has_table('agent_board', 'thread_memberships', 'thread_memberships table exists');
|
|
82
|
+
select has_column('agent_board', 'thread_memberships', 'thread_id', 'thread_memberships has thread_id');
|
|
83
|
+
select is(
|
|
84
|
+
(select relrowsecurity from pg_class where oid = 'agent_board.thread_memberships'::regclass),
|
|
85
|
+
true, 'RLS is enabled on thread_memberships');
|
|
86
|
+
|
|
87
|
+
-- inherit mode: historical OR preserved -------------------------------------
|
|
88
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
89
|
+
select set_config('request.jwt.claim.sub', (select creator_user from _thread_acl_fixture)::text, true);
|
|
90
|
+
select is(
|
|
91
|
+
agent_board._current_agent_can_access_thread((select thread_inherit from _thread_acl_fixture)),
|
|
92
|
+
true, 'inherit: creator retains access');
|
|
93
|
+
|
|
94
|
+
select set_config('request.jwt.claim.sub', (select stranger_user from _thread_acl_fixture)::text, true);
|
|
95
|
+
select is(
|
|
96
|
+
agent_board._current_agent_can_access_thread((select thread_inherit from _thread_acl_fixture)),
|
|
97
|
+
false, 'inherit: unrelated agent has no access');
|
|
98
|
+
|
|
99
|
+
-- service_role short-circuit: restricted thread, no membership -> allowed ----
|
|
100
|
+
select set_config('request.jwt.claim.role', 'service_role', true);
|
|
101
|
+
select is(
|
|
102
|
+
agent_board._current_agent_can_access_thread((select thread_restricted from _thread_acl_fixture)),
|
|
103
|
+
true, 'service_role bypasses restricted access');
|
|
104
|
+
|
|
105
|
+
-- restricted mode -----------------------------------------------------------
|
|
106
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
107
|
+
select set_config('request.jwt.claim.sub', (select creator_user from _thread_acl_fixture)::text, true);
|
|
108
|
+
select is(
|
|
109
|
+
agent_board._current_agent_can_access_thread((select thread_restricted from _thread_acl_fixture)),
|
|
110
|
+
false, 'restricted: creator without an ACL grant is denied');
|
|
111
|
+
|
|
112
|
+
select set_config('request.jwt.claim.sub', (select member_user from _thread_acl_fixture)::text, true);
|
|
113
|
+
select is(
|
|
114
|
+
agent_board._current_agent_can_access_thread((select thread_restricted from _thread_acl_fixture)),
|
|
115
|
+
true, 'restricted: active ACL member is granted');
|
|
116
|
+
|
|
117
|
+
select set_config('request.jwt.claim.sub', (select expired_user from _thread_acl_fixture)::text, true);
|
|
118
|
+
select is(
|
|
119
|
+
agent_board._current_agent_can_access_thread((select thread_restricted from _thread_acl_fixture)),
|
|
120
|
+
false, 'restricted: expired ACL member is denied');
|
|
121
|
+
|
|
122
|
+
select set_config('request.jwt.claim.sub', (select revoked_user from _thread_acl_fixture)::text, true);
|
|
123
|
+
select is(
|
|
124
|
+
agent_board._current_agent_can_access_thread((select thread_restricted from _thread_acl_fixture)),
|
|
125
|
+
false, 'restricted: revoked ACL member is denied');
|
|
126
|
+
|
|
127
|
+
select finish();
|
|
128
|
+
|
|
129
|
+
rollback;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
select plan(5);
|
|
3
|
+
|
|
4
|
+
-- After the corrective, workload_providers must have RLS ENABLED so a
|
|
5
|
+
-- RESTRICTIVE deny (the b2 client-JWT overlay on adopters that carry it) can
|
|
6
|
+
-- actually enforce — it is inert on an RLS-disabled table.
|
|
7
|
+
|
|
8
|
+
select ok(
|
|
9
|
+
(select relrowsecurity from pg_class c join pg_namespace n on n.oid=c.relnamespace
|
|
10
|
+
where n.nspname='agent_board' and c.relname='workload_providers'),
|
|
11
|
+
'workload_providers has RLS enabled');
|
|
12
|
+
|
|
13
|
+
select is(
|
|
14
|
+
(select count(*)::int from pg_policy pol join pg_class c on c.oid=pol.polrelid
|
|
15
|
+
join pg_namespace n on n.oid=c.relnamespace
|
|
16
|
+
where n.nspname='agent_board' and c.relname='workload_providers'
|
|
17
|
+
and pol.polname='workload_providers_authenticated_read'),
|
|
18
|
+
1, 'workload_providers_authenticated_read policy exists');
|
|
19
|
+
|
|
20
|
+
select is(
|
|
21
|
+
(select pg_get_expr(polqual, polrelid) from pg_policy pol join pg_class c on c.oid=pol.polrelid
|
|
22
|
+
join pg_namespace n on n.oid=c.relnamespace
|
|
23
|
+
where n.nspname='agent_board' and c.relname='workload_providers'
|
|
24
|
+
and pol.polname='workload_providers_authenticated_read'),
|
|
25
|
+
'true', 'authenticated read predicate is true (global registry, fleet-readable)');
|
|
26
|
+
|
|
27
|
+
select is(
|
|
28
|
+
(select count(*)::int from pg_policy pol join pg_class c on c.oid=pol.polrelid
|
|
29
|
+
join pg_namespace n on n.oid=c.relnamespace
|
|
30
|
+
where n.nspname='agent_board' and c.relname='workload_providers'
|
|
31
|
+
and pol.polname='workload_providers_deny_anon'),
|
|
32
|
+
1, 'workload_providers_deny_anon policy exists');
|
|
33
|
+
|
|
34
|
+
-- canonical fleet-read grant is preserved
|
|
35
|
+
select ok(
|
|
36
|
+
has_table_privilege('authenticated', 'agent_board.workload_providers', 'SELECT'),
|
|
37
|
+
'authenticated retains SELECT grant on workload_providers');
|
|
38
|
+
|
|
39
|
+
select * from finish();
|
|
40
|
+
rollback;
|