@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,131 @@
|
|
|
1
|
+
-- MBPORTALGATE SL-2 pgTAP: the PoP-binding suite for enroll_federated_identity.
|
|
2
|
+
--
|
|
3
|
+
-- Proves the proof-of-possession + fingerprint contract the /enroll EF depends on:
|
|
4
|
+
-- * the DB-recomputed sha256:base64url fingerprint is BYTE-IDENTICAL to the value an
|
|
5
|
+
-- independent client (WebCrypto) computes over the same base64url key STRING
|
|
6
|
+
-- (client<->DB round-trip);
|
|
7
|
+
-- * p_pop_verified must be true (false / null -> raise, no writes);
|
|
8
|
+
-- * the RPC recomputes-and-asserts the fingerprint (a tampered fingerprint -> raise);
|
|
9
|
+
-- * a duplicate fingerprint (a key already enrolled) -> raise, dedup on the UNIQUE index;
|
|
10
|
+
-- * malformed / placeholder / wrong-format keys -> raise;
|
|
11
|
+
-- * p_actor_sub is audit-only: present-non-empty succeeds + is captured; empty -> raise.
|
|
12
|
+
-- Runs as postgres (service_role-equivalent for _is_service_role()).
|
|
13
|
+
create extension if not exists pgtap with schema extensions;
|
|
14
|
+
set search_path = extensions, public, agent_board;
|
|
15
|
+
|
|
16
|
+
begin;
|
|
17
|
+
|
|
18
|
+
select plan(18);
|
|
19
|
+
|
|
20
|
+
-- Client<->DB fingerprint round-trip (B2 raw-byte scheme): the DB derivation over the
|
|
21
|
+
-- DECODED 32 key bytes must equal the constant an independent tool (Node crypto,
|
|
22
|
+
-- matching WebCrypto + the client child_keygen keyRef) computed over the same raw bytes.
|
|
23
|
+
select is(
|
|
24
|
+
'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate('eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0','-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'='),
|
|
25
|
+
'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
|
|
26
|
+
'the DB raw-byte fingerprint derivation round-trips the independently-computed client value (key 1)');
|
|
27
|
+
select is(
|
|
28
|
+
'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate('yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk','-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'='),
|
|
29
|
+
'sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA',
|
|
30
|
+
'the DB raw-byte fingerprint derivation round-trips the independently-computed client value (key 2)');
|
|
31
|
+
|
|
32
|
+
create temp table _p (
|
|
33
|
+
iss text, aud text, sub text, claim jsonb,
|
|
34
|
+
pub1 text, fp1 text, pub2 text, fp2 text
|
|
35
|
+
) on commit drop;
|
|
36
|
+
do $$
|
|
37
|
+
declare s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
38
|
+
begin
|
|
39
|
+
insert into agent_board.identity_providers
|
|
40
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
41
|
+
values ('https://fed-pop-'||s||'.test', 'message-board', true, true, '{board:send}', '{}', '{agent,human}');
|
|
42
|
+
insert into _p values (
|
|
43
|
+
'https://fed-pop-'||s||'.test', 'message-board', 'portal|'||s,
|
|
44
|
+
jsonb_build_object('sub','portal|'||s,'act',jsonb_build_object('sub','act|'||s)),
|
|
45
|
+
'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f0', 'sha256:z8d5LQPfBMYnfZfvu8wVz684_jkp578-SPDU2OMObS8',
|
|
46
|
+
'yd_DIFGWGusVg3SbktDJQ1bE5UWvtR33xln4b05JIhk', 'sha256:NeVC2vx1MbTY56jUwJcLLr6YwB4IKCERsXWbUwhf5pA');
|
|
47
|
+
end
|
|
48
|
+
$$;
|
|
49
|
+
|
|
50
|
+
-- Happy path with a present, non-empty actor_sub -> succeeds and is audited (key 1).
|
|
51
|
+
select lives_ok(
|
|
52
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,%L,'agent','{board:send}','{}')$q$,
|
|
53
|
+
iss, aud, sub, claim::text, 'pop-ok', pub1, fp1, 'act|present') from _p),
|
|
54
|
+
'enroll with a present non-empty actor_sub and verified PoP succeeds');
|
|
55
|
+
select is(
|
|
56
|
+
(select e.human_identity_claim->'act'->>'sub' from agent_board.enrollment_federation_audit e
|
|
57
|
+
join agent_board.agents a on a.id=e.agent_id where a.handle='pop-ok'),
|
|
58
|
+
(select claim->'act'->>'sub' from _p),
|
|
59
|
+
'the act.sub is captured (audit-only) in the enrollment claim snapshot');
|
|
60
|
+
|
|
61
|
+
-- Duplicate fingerprint: re-enrolling key 1 raises (dedup on the UNIQUE index).
|
|
62
|
+
select throws_ok(
|
|
63
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
64
|
+
iss, aud, sub, claim::text, 'pop-dup', pub1, fp1) from _p),
|
|
65
|
+
'23505', null, 'a duplicate fingerprint (key already enrolled) is rejected');
|
|
66
|
+
select is(
|
|
67
|
+
(select count(*)::int from agent_board.agent_signing_keys k where k.fingerprint = (select fp1 from _p)),
|
|
68
|
+
1, 'the duplicate attempt did not enroll a second key with the same fingerprint');
|
|
69
|
+
|
|
70
|
+
-- B2: a NON-CANONICAL base64url encoding of an already-enrolled key (same 32 raw bytes,
|
|
71
|
+
-- 2 don't-care low bits flipped) is rejected as non-canonical (22023) BEFORE the dedup
|
|
72
|
+
-- check — so one private key can NEVER back multiple agents (the 4-strings-one-key
|
|
73
|
+
-- aliasing attack that broke revocation isolation).
|
|
74
|
+
select throws_ok(
|
|
75
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
76
|
+
iss, aud, sub, claim::text, 'pop-alias1', 'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f1', fp1) from _p),
|
|
77
|
+
'22023', null, 'a non-canonical base64url alias of an enrolled key is rejected (aliasing attack blocked)');
|
|
78
|
+
select throws_ok(
|
|
79
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
80
|
+
iss, aud, sub, claim::text, 'pop-alias2', 'eRHZmllEy_TZHAG2zB-Qe_a5BFLkaavMTazWWfaE3f2', fp1) from _p),
|
|
81
|
+
'22023', null, 'a second non-canonical alias of the same key is also rejected');
|
|
82
|
+
select is(
|
|
83
|
+
(select count(*)::int from agent_board.agent_signing_keys k where k.fingerprint = (select fp1 from _p)),
|
|
84
|
+
1, 'the aliasing attempts enrolled NO extra key/agent (one private key -> exactly one agent)');
|
|
85
|
+
|
|
86
|
+
-- PoP gate: p_pop_verified false / null -> raise, and NO agent is written.
|
|
87
|
+
select throws_ok(
|
|
88
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,false,null,'agent','{board:send}','{}')$q$,
|
|
89
|
+
iss, aud, sub, claim::text, 'pop-false', pub2, fp2) from _p),
|
|
90
|
+
'42501', null, 'p_pop_verified=false is refused');
|
|
91
|
+
select is(
|
|
92
|
+
(select count(*)::int from agent_board.agents a where a.handle = 'pop-false'),
|
|
93
|
+
0, 'a refused (pop=false) enrollment wrote no agent (no partial write)');
|
|
94
|
+
select throws_ok(
|
|
95
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,NULL,null,'agent','{board:send}','{}')$q$,
|
|
96
|
+
iss, aud, sub, claim::text, 'pop-null', pub2, fp2) from _p),
|
|
97
|
+
'42501', null, 'p_pop_verified=null is refused');
|
|
98
|
+
|
|
99
|
+
-- Fingerprint tamper: supplied fingerprint != in-DB recompute -> raise, no write.
|
|
100
|
+
select throws_ok(
|
|
101
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
102
|
+
iss, aud, sub, claim::text, 'pop-tamper', pub2, 'sha256:'||repeat('B',43)) from _p),
|
|
103
|
+
'22023', null, 'a tampered fingerprint (!= in-DB derive of the key) is rejected');
|
|
104
|
+
select is(
|
|
105
|
+
(select count(*)::int from agent_board.agents a where a.handle = 'pop-tamper'),
|
|
106
|
+
0, 'a tampered-fingerprint enrollment wrote no agent');
|
|
107
|
+
|
|
108
|
+
-- Malformed / placeholder / wrong-format keys -> raise.
|
|
109
|
+
select throws_ok(
|
|
110
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,'not-a-valid-43char-key','raw_ed25519_base64url','sha256:'||%L,true,null,'agent','{board:send}','{}')$q$,
|
|
111
|
+
iss, aud, sub, claim::text, 'pop-malformed', repeat('C',43)) from _p),
|
|
112
|
+
'22023', null, 'a malformed (non-43-base64url) public key is rejected');
|
|
113
|
+
select throws_ok(
|
|
114
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
115
|
+
iss, aud, sub, claim::text, 'pop-zero', repeat('A',43),
|
|
116
|
+
'sha256:'||rtrim(translate(encode(extensions.digest(convert_to(repeat('A',43),'UTF8'),'sha256'),'base64'),'+/','-_'),'=')) from _p),
|
|
117
|
+
'22023', null, 'the all-zero small-order placeholder key is rejected');
|
|
118
|
+
select throws_ok(
|
|
119
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'spki_pem',%L,true,null,'agent','{board:send}','{}')$q$,
|
|
120
|
+
iss, aud, sub, claim::text, 'pop-fmt', pub2, fp2) from _p),
|
|
121
|
+
'22023', null, 'a non-raw_ed25519_base64url public_key_format is rejected');
|
|
122
|
+
|
|
123
|
+
-- actor_sub present-but-empty -> raise (present means a real delegated actor).
|
|
124
|
+
select throws_ok(
|
|
125
|
+
(select format($q$select agent_board.enroll_federated_identity(%L,%L,%L,%L::jsonb,%L,%L,'raw_ed25519_base64url',%L,true,'','agent','{board:send}','{}')$q$,
|
|
126
|
+
iss, aud, sub, claim::text, 'pop-emptyact', pub2, fp2) from _p),
|
|
127
|
+
'22023', null, 'a present-but-empty actor_sub is rejected');
|
|
128
|
+
|
|
129
|
+
select finish();
|
|
130
|
+
|
|
131
|
+
rollback;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- MBHARDEN SL-1 pgTAP: check_and_bump_enroll_rate allows <=limit calls per source
|
|
2
|
+
-- per rolling window and rejects past the limit; sources are independent; the
|
|
3
|
+
-- reject path records nothing; and execute is revoked from anon/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(10);
|
|
10
|
+
|
|
11
|
+
select has_function(
|
|
12
|
+
'agent_board', 'check_and_bump_enroll_rate', array['text', 'integer', 'interval'],
|
|
13
|
+
'check_and_bump_enroll_rate(text, integer, interval) exists');
|
|
14
|
+
select has_table(
|
|
15
|
+
'agent_board', 'enroll_rate_events', 'enroll_rate_events counter table exists');
|
|
16
|
+
|
|
17
|
+
-- Source A: limit 3 in a 1-minute window. First three allowed, fourth rejected.
|
|
18
|
+
select is(
|
|
19
|
+
agent_board.check_and_bump_enroll_rate('src-a', 3, interval '1 minute'),
|
|
20
|
+
true, '1st call for src-a is allowed');
|
|
21
|
+
select is(
|
|
22
|
+
agent_board.check_and_bump_enroll_rate('src-a', 3, interval '1 minute'),
|
|
23
|
+
true, '2nd call for src-a is allowed');
|
|
24
|
+
select is(
|
|
25
|
+
agent_board.check_and_bump_enroll_rate('src-a', 3, interval '1 minute'),
|
|
26
|
+
true, '3rd call for src-a is allowed (at the limit)');
|
|
27
|
+
select is(
|
|
28
|
+
agent_board.check_and_bump_enroll_rate('src-a', 3, interval '1 minute'),
|
|
29
|
+
false, '4th call for src-a is rejected (over the cap)');
|
|
30
|
+
|
|
31
|
+
-- A different source is bounded independently.
|
|
32
|
+
select is(
|
|
33
|
+
agent_board.check_and_bump_enroll_rate('src-b', 3, interval '1 minute'),
|
|
34
|
+
true, 'a different source is counted independently');
|
|
35
|
+
|
|
36
|
+
-- The reject path records nothing: src-a still has exactly its 3 allowed events.
|
|
37
|
+
select is(
|
|
38
|
+
(select count(*)::int from agent_board.enroll_rate_events where source = 'src-a'),
|
|
39
|
+
3, 'the over-cap reject recorded no event (only the 3 allowed remain)');
|
|
40
|
+
|
|
41
|
+
-- Privilege: execute revoked from anon and authenticated.
|
|
42
|
+
select function_privs_are(
|
|
43
|
+
'agent_board', 'check_and_bump_enroll_rate', array['text', 'integer', 'interval'],
|
|
44
|
+
'anon', array[]::text[],
|
|
45
|
+
'check_and_bump_enroll_rate execute is revoked from anon');
|
|
46
|
+
select function_privs_are(
|
|
47
|
+
'agent_board', 'check_and_bump_enroll_rate', array['text', 'integer', 'interval'],
|
|
48
|
+
'authenticated', array[]::text[],
|
|
49
|
+
'check_and_bump_enroll_rate execute is revoked from authenticated');
|
|
50
|
+
|
|
51
|
+
select finish();
|
|
52
|
+
|
|
53
|
+
rollback;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
-- MBFED SL-2 pgTAP (Lane B): enrollment-audit content + no-write-path-read invariant.
|
|
2
|
+
--
|
|
3
|
+
-- Proves enroll_federated_identity writes exactly ONE enrollment_federation_audit row
|
|
4
|
+
-- whose human_identity_claim equals the supplied snapshot verbatim, that the audit
|
|
5
|
+
-- table carries NO Portal session/MFA/availability/liveness column (it is a static
|
|
6
|
+
-- enrollment-time claim snapshot), and that NO write-path RPC reads that table — the
|
|
7
|
+
-- audit is never consulted as a live trust signal. Runs as postgres.
|
|
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(11);
|
|
14
|
+
|
|
15
|
+
create temp table _acfx (agent uuid, claim jsonb) on commit drop;
|
|
16
|
+
|
|
17
|
+
do $$
|
|
18
|
+
declare
|
|
19
|
+
s text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
20
|
+
v_agent uuid;
|
|
21
|
+
v_claim jsonb := jsonb_build_object('sub','portal|'||s,'name','Fed '||s,'groups',jsonb_build_array('eng','fed'));
|
|
22
|
+
-- A valid unique 43-char base64url key + its matching (RPC-recomputed) fingerprint.
|
|
23
|
+
v_pub text := rtrim(translate(encode(extensions.digest(convert_to('k-acfx-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
24
|
+
v_fp text;
|
|
25
|
+
begin
|
|
26
|
+
v_fp := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
27
|
+
insert into agent_board.identity_providers
|
|
28
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
29
|
+
values ('https://mock-portal-'||s||'.test', 'message-board', true, true, '{board:send}', '{board:x}', '{agent,human}');
|
|
30
|
+
|
|
31
|
+
select e.agent_id into v_agent
|
|
32
|
+
from agent_board.enroll_federated_identity(
|
|
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;
|
|
35
|
+
|
|
36
|
+
insert into _acfx values (v_agent, v_claim);
|
|
37
|
+
end
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
-- Exactly one audit row, with the verbatim claim snapshot. -------------------------
|
|
41
|
+
select is(
|
|
42
|
+
(select count(*)::int from agent_board.enrollment_federation_audit where agent_id = (select agent from _acfx)),
|
|
43
|
+
1, 'enroll_federated_identity writes exactly one enrollment_federation_audit row');
|
|
44
|
+
select is(
|
|
45
|
+
(select human_identity_claim from agent_board.enrollment_federation_audit where agent_id = (select agent from _acfx)),
|
|
46
|
+
(select claim from _acfx),
|
|
47
|
+
'human_identity_claim equals the supplied snapshot verbatim');
|
|
48
|
+
select is(
|
|
49
|
+
(select granted_scopes from agent_board.enrollment_federation_audit where agent_id = (select agent from _acfx)),
|
|
50
|
+
'{board:send}'::text[],
|
|
51
|
+
'granted_scopes records the in-ceiling requested scopes');
|
|
52
|
+
|
|
53
|
+
-- The audit is a STATIC snapshot: no Portal session/MFA/availability/liveness column.
|
|
54
|
+
select hasnt_column('agent_board', 'enrollment_federation_audit', 'portal_session_id',
|
|
55
|
+
'audit has no portal_session_id column');
|
|
56
|
+
select is(
|
|
57
|
+
(select count(*)::int
|
|
58
|
+
from information_schema.columns
|
|
59
|
+
where table_schema = 'agent_board'
|
|
60
|
+
and table_name = 'enrollment_federation_audit'
|
|
61
|
+
and (column_name ~* '(session|mfa|liveness|availability|reachab|online|heartbeat)')),
|
|
62
|
+
0, 'audit table carries no Portal session/MFA/liveness/availability column (static snapshot only)');
|
|
63
|
+
|
|
64
|
+
-- The audit exists and stores the enrollment-time snapshot, not a live signal. ------
|
|
65
|
+
select has_column('agent_board', 'enrollment_federation_audit', 'human_identity_claim', 'audit stores human_identity_claim');
|
|
66
|
+
select has_column('agent_board', 'enrollment_federation_audit', 'enrolled_at', 'audit stores enrolled_at (enrollment-time)');
|
|
67
|
+
|
|
68
|
+
-- No write-path RPC reads enrollment_federation_audit. Only the enroll RPC (which
|
|
69
|
+
-- WRITES it) may reference the table; nothing on the send/session path reads it. ----
|
|
70
|
+
select is(
|
|
71
|
+
(select count(*)::int from (
|
|
72
|
+
select pg_get_functiondef(p.oid) as def, p.proname
|
|
73
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
74
|
+
where n.nspname = 'agent_board'
|
|
75
|
+
and p.proname <> 'enroll_federated_identity'
|
|
76
|
+
) d
|
|
77
|
+
where lower(d.def) like '%enrollment_federation_audit%'),
|
|
78
|
+
0, 'no agent_board RPC other than enroll_federated_identity references enrollment_federation_audit');
|
|
79
|
+
select is(
|
|
80
|
+
(select count(*)::int from (
|
|
81
|
+
select pg_get_functiondef(p.oid) as def
|
|
82
|
+
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
83
|
+
where n.nspname = 'agent_board'
|
|
84
|
+
and p.proname in (
|
|
85
|
+
'mint_fleet_agent_session', 'post_board_message', 'send_direct_message',
|
|
86
|
+
'send_multicast_message', 'create_tasking_delivery', 'revoke_agent')
|
|
87
|
+
) d
|
|
88
|
+
where lower(d.def) like '%enrollment_federation_audit%'),
|
|
89
|
+
0, 'no send/session/revoke write-path RPC reads the enrollment audit table');
|
|
90
|
+
|
|
91
|
+
-- #6 fix: the authenticated SELECT policy is scoped to the owning principal
|
|
92
|
+
-- (agent_id = current_agent_id()), not `using (true)`. Two enrolled human
|
|
93
|
+
-- principals; principal A must NOT see principal B's PII-bearing audit row.
|
|
94
|
+
create temp table _rls (a_agent uuid, a_user uuid, b_agent uuid) on commit drop;
|
|
95
|
+
do $$
|
|
96
|
+
declare
|
|
97
|
+
s text := replace(substr(gen_random_uuid()::text,1,8),'-','');
|
|
98
|
+
v_a_agent uuid; v_a_user uuid; v_b_agent uuid;
|
|
99
|
+
v_pub_a text := rtrim(translate(encode(extensions.digest(convert_to('k-rls-a-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
100
|
+
v_pub_b text := rtrim(translate(encode(extensions.digest(convert_to('k-rls-b-'||s,'UTF8'),'sha256'),'base64'),'+/','-_'),'=');
|
|
101
|
+
v_fp_a text; v_fp_b text;
|
|
102
|
+
begin
|
|
103
|
+
v_fp_a := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub_a,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
104
|
+
v_fp_b := 'sha256:' || rtrim(translate(encode(extensions.digest(decode(translate(v_pub_b,'-_','+/')||'=','base64'),'sha256'),'base64'),'+/','-_'),'=');
|
|
105
|
+
insert into agent_board.identity_providers
|
|
106
|
+
(issuer, audience, supported, policy_approved, allowed_scopes, allowed_board_scopes, allowed_agent_kinds)
|
|
107
|
+
values ('https://rls-portal-'||s||'.test','message-board',true,true,'{board:send}','{board:x}','{agent,human}');
|
|
108
|
+
select e.agent_id, e.auth_user_id into v_a_agent, v_a_user
|
|
109
|
+
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|a-'||s,
|
|
110
|
+
jsonb_build_object('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;
|
|
111
|
+
select e.agent_id into v_b_agent
|
|
112
|
+
from agent_board.enroll_federated_identity('https://rls-portal-'||s||'.test','message-board','portal|b-'||s,
|
|
113
|
+
jsonb_build_object('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;
|
|
114
|
+
insert into _rls values (v_a_agent, v_a_user, v_b_agent);
|
|
115
|
+
end
|
|
116
|
+
$$;
|
|
117
|
+
grant select on _rls to authenticated;
|
|
118
|
+
|
|
119
|
+
set local role authenticated;
|
|
120
|
+
select set_config('request.jwt.claim.role','authenticated', true);
|
|
121
|
+
select set_config('request.jwt.claim.sub', (select a_user from _rls)::text, true);
|
|
122
|
+
|
|
123
|
+
select is(
|
|
124
|
+
(select count(*)::int from agent_board.enrollment_federation_audit e where e.agent_id = (select b_agent from _rls)),
|
|
125
|
+
0, 'agent A cannot read agent B''s enrollment audit row (RLS scoped to owning principal, not using(true))');
|
|
126
|
+
select is(
|
|
127
|
+
(select count(*)::int from agent_board.enrollment_federation_audit e where e.agent_id = (select a_agent from _rls)),
|
|
128
|
+
1, 'agent A can read its own enrollment audit row');
|
|
129
|
+
|
|
130
|
+
reset role;
|
|
131
|
+
|
|
132
|
+
select finish();
|
|
133
|
+
|
|
134
|
+
rollback;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- MBSESSION SL-1 pgTAP: nonfleet_sessions.identity_class admits fleet_agent_session
|
|
2
|
+
-- (plus the 4 legacy classes), rejects an unknown class, and the TTL pin caps a
|
|
3
|
+
-- fleet_agent_session to <= 900s while legacy classes keep the 3600 default.
|
|
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(10);
|
|
10
|
+
|
|
11
|
+
-- All five valid identity classes are admitted (TTL within the fleet pin bound).
|
|
12
|
+
select lives_ok(
|
|
13
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
14
|
+
values ('fac-eph', 'fac-eph', 'ephemeral_child', now() + interval '1 hour', 900)$$,
|
|
15
|
+
'ephemeral_child identity_class is admitted');
|
|
16
|
+
select lives_ok(
|
|
17
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
18
|
+
values ('fac-hw', 'fac-hw', 'hosted_workload', now() + interval '1 hour', 900)$$,
|
|
19
|
+
'hosted_workload identity_class is admitted');
|
|
20
|
+
select lives_ok(
|
|
21
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
22
|
+
values ('fac-rs', 'fac-rs', 'remote_sandbox', now() + interval '1 hour', 900)$$,
|
|
23
|
+
'remote_sandbox identity_class is admitted');
|
|
24
|
+
select lives_ok(
|
|
25
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
26
|
+
values ('fac-inv', 'fac-inv', 'invited_external', now() + interval '1 hour', 900)$$,
|
|
27
|
+
'invited_external identity_class is admitted');
|
|
28
|
+
select lives_ok(
|
|
29
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
30
|
+
values ('fac-fas', 'fac-fas', 'fleet_agent_session', now() + interval '15 minutes', 900)$$,
|
|
31
|
+
'fleet_agent_session identity_class is admitted (new 5th class)');
|
|
32
|
+
|
|
33
|
+
-- A legacy class still keeps the 3600 default (fleet pin does not apply to it).
|
|
34
|
+
select lives_ok(
|
|
35
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at)
|
|
36
|
+
values ('fac-hw3600', 'fac-hw3600', 'hosted_workload', now() + interval '1 hour')$$,
|
|
37
|
+
'a legacy class row with the 3600 default is unaffected by the fleet TTL pin');
|
|
38
|
+
|
|
39
|
+
-- An unknown identity_class is rejected by the re-added CHECK.
|
|
40
|
+
select throws_ok(
|
|
41
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
42
|
+
values ('fac-bogus', 'fac-bogus', 'not_a_real_class', now() + interval '1 hour', 900)$$,
|
|
43
|
+
'23514', null,
|
|
44
|
+
'an unknown identity_class violates nonfleet_sessions_identity_class_check');
|
|
45
|
+
|
|
46
|
+
-- fleet_agent_session at exactly 900s is allowed; above 900s violates the TTL pin.
|
|
47
|
+
select lives_ok(
|
|
48
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
49
|
+
values ('fac-fas900', 'fac-fas900', 'fleet_agent_session', now() + interval '15 minutes', 900)$$,
|
|
50
|
+
'fleet_agent_session at the 900s boundary is admitted');
|
|
51
|
+
select throws_ok(
|
|
52
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at, max_ttl_seconds)
|
|
53
|
+
values ('fac-fas901', 'fac-fas901', 'fleet_agent_session', now() + interval '15 minutes', 901)$$,
|
|
54
|
+
'23514', null,
|
|
55
|
+
'fleet_agent_session with max_ttl_seconds > 900 violates nonfleet_sessions_fleet_ttl_pin');
|
|
56
|
+
select throws_ok(
|
|
57
|
+
$$insert into agent_board.nonfleet_sessions(id, session_id, identity_class, expires_at)
|
|
58
|
+
values ('fac-fas3600', 'fac-fas3600', 'fleet_agent_session', now() + interval '15 minutes')$$,
|
|
59
|
+
'23514', null,
|
|
60
|
+
'fleet_agent_session cannot inherit the 3600 max_ttl_seconds default');
|
|
61
|
+
|
|
62
|
+
select finish();
|
|
63
|
+
|
|
64
|
+
rollback;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
-- MBSENDAUTHZ SL-2 pgTAP: the non-fleet fail-open in evaluate_signed_write_fleet_policy
|
|
2
|
+
-- is now mode-gated per sender-fleet-scope (fail_open_preserved / observe / deny)
|
|
3
|
+
-- with a tight non-fleet allowlist. Fleet-resolvable senders are unaffected.
|
|
4
|
+
--
|
|
5
|
+
-- Runs as the postgres db role in a service_role context (the function requires
|
|
6
|
+
-- service role); request.jwt.claim.role is left '' so _is_service_role() is true.
|
|
7
|
+
create extension if not exists pgtap with schema extensions;
|
|
8
|
+
set search_path = extensions, public, agent_board;
|
|
9
|
+
|
|
10
|
+
begin;
|
|
11
|
+
|
|
12
|
+
select plan(13);
|
|
13
|
+
|
|
14
|
+
create temp table _fpd_fixture (
|
|
15
|
+
ff_sender uuid,
|
|
16
|
+
ob_sender uuid,
|
|
17
|
+
dn_sender uuid,
|
|
18
|
+
dn2_sender uuid,
|
|
19
|
+
al_sender uuid,
|
|
20
|
+
fleet_sender uuid,
|
|
21
|
+
owner_agent uuid
|
|
22
|
+
) on commit drop;
|
|
23
|
+
|
|
24
|
+
do $$
|
|
25
|
+
declare
|
|
26
|
+
v_ff uuid := gen_random_uuid();
|
|
27
|
+
v_ob uuid := gen_random_uuid();
|
|
28
|
+
v_dn uuid := gen_random_uuid();
|
|
29
|
+
v_dn2 uuid := gen_random_uuid();
|
|
30
|
+
v_al uuid := gen_random_uuid();
|
|
31
|
+
v_fleet uuid := gen_random_uuid();
|
|
32
|
+
v_owner uuid := gen_random_uuid();
|
|
33
|
+
v_domain uuid;
|
|
34
|
+
v_machine uuid;
|
|
35
|
+
begin
|
|
36
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
37
|
+
values
|
|
38
|
+
(v_ff, 'agent', 'fpd-ff', 'FPD FailOpen'),
|
|
39
|
+
(v_ob, 'agent', 'fpd-ob', 'FPD Observe'),
|
|
40
|
+
(v_dn, 'agent', 'fpd-dn', 'FPD Deny'),
|
|
41
|
+
(v_dn2, 'agent', 'fpd-dn2', 'FPD Deny2'),
|
|
42
|
+
(v_al, 'agent', 'fpd-al', 'FPD Allowlisted'),
|
|
43
|
+
(v_fleet, 'agent', 'fpd-fleet', 'FPD Fleet'),
|
|
44
|
+
(v_owner, 'agent', 'fpd-owner', 'FPD Owner');
|
|
45
|
+
|
|
46
|
+
-- A fleet-resolvable sender: active trust domain + machine + agent mapping.
|
|
47
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at, metadata)
|
|
48
|
+
values ('fpd-tailnet', 'FPD Tailnet', 'tailnet', v_owner, 'active', now(), '{"invitationState":"owner_approved"}'::jsonb);
|
|
49
|
+
select id into v_domain from agent_board.fleet_trust_domains where slug = 'fpd-tailnet';
|
|
50
|
+
select machine_id into v_machine
|
|
51
|
+
from agent_board.register_fleet_machine(v_owner, v_domain, 'fpd-machine', 'sha256:fpd-machine', 'portal', '{"redacted":true}'::jsonb);
|
|
52
|
+
insert into agent_board.fleet_machine_agents(machine_id, agent_id, agent_role)
|
|
53
|
+
values (v_machine, v_fleet, 'phase_worker');
|
|
54
|
+
|
|
55
|
+
insert into _fpd_fixture values (v_ff, v_ob, v_dn, v_dn2, v_al, v_fleet, v_owner);
|
|
56
|
+
end
|
|
57
|
+
$$;
|
|
58
|
+
|
|
59
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
60
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
61
|
+
|
|
62
|
+
-- Signature ------------------------------------------------------------------
|
|
63
|
+
select has_function('agent_board', 'evaluate_signed_write_fleet_policy', array['text', 'jsonb', 'jsonb'],
|
|
64
|
+
'evaluate_signed_write_fleet_policy(text,jsonb,jsonb) exists');
|
|
65
|
+
|
|
66
|
+
-- Phase A: no flag row => fail_open_preserved (unchanged behavior) ------------
|
|
67
|
+
select is(
|
|
68
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
69
|
+
'send_direct_message', '{}'::jsonb,
|
|
70
|
+
jsonb_build_object('agent_id', (select ff_sender from _fpd_fixture)::text, 'verification_status', 'verified'))),
|
|
71
|
+
true, 'no flag row: non-fleet sender passes (fail-open preserved)');
|
|
72
|
+
|
|
73
|
+
select is(
|
|
74
|
+
(select count(*)::int from agent_board.send_authz_telemetry
|
|
75
|
+
where kind = 'would_be_deny' and actor_agent_id = (select ff_sender from _fpd_fixture)),
|
|
76
|
+
0, 'fail_open_preserved records no would_be_deny telemetry');
|
|
77
|
+
|
|
78
|
+
-- Phase B: observe => allow AND record exactly one would_be_deny row ----------
|
|
79
|
+
insert into agent_board.fleet_send_enforcement_flags(scope_key, mode) values ('nonfleet', 'observe');
|
|
80
|
+
|
|
81
|
+
select is(
|
|
82
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
83
|
+
'send_direct_message', '{}'::jsonb,
|
|
84
|
+
jsonb_build_object('agent_id', (select ob_sender from _fpd_fixture)::text,
|
|
85
|
+
'signed_write', jsonb_build_object('origin', 'host')))),
|
|
86
|
+
true, 'observe mode: non-fleet sender still passes');
|
|
87
|
+
|
|
88
|
+
select is(
|
|
89
|
+
(select count(*)::int from agent_board.send_authz_telemetry
|
|
90
|
+
where kind = 'would_be_deny' and actor_agent_id = (select ob_sender from _fpd_fixture)),
|
|
91
|
+
1, 'observe mode records exactly one would_be_deny telemetry row');
|
|
92
|
+
|
|
93
|
+
-- Phase C: deny => deny non-allowlisted, admit allowlisted (tight) -----------
|
|
94
|
+
update agent_board.fleet_send_enforcement_flags set mode = 'deny' where scope_key = 'nonfleet';
|
|
95
|
+
insert into agent_board.nonfleet_sender_allowlist(agent_id, reason)
|
|
96
|
+
values ((select al_sender from _fpd_fixture), 'sl2 test: enumerated legitimate flow');
|
|
97
|
+
|
|
98
|
+
select is(
|
|
99
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
100
|
+
'send_direct_message', '{}'::jsonb,
|
|
101
|
+
jsonb_build_object('agent_id', (select dn_sender from _fpd_fixture)::text))),
|
|
102
|
+
false, 'deny mode: non-allowlisted non-fleet sender is denied');
|
|
103
|
+
|
|
104
|
+
select is(
|
|
105
|
+
(select denial_class from agent_board.evaluate_signed_write_fleet_policy(
|
|
106
|
+
'send_direct_message', '{}'::jsonb,
|
|
107
|
+
jsonb_build_object('agent_id', (select dn_sender from _fpd_fixture)::text))),
|
|
108
|
+
'nonfleet_sender_denied', 'deny mode: denial_class is nonfleet_sender_denied');
|
|
109
|
+
|
|
110
|
+
select is(
|
|
111
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
112
|
+
'send_direct_message', '{}'::jsonb,
|
|
113
|
+
jsonb_build_object('agent_id', (select al_sender from _fpd_fixture)::text))),
|
|
114
|
+
true, 'deny mode: allowlisted non-fleet sender is admitted');
|
|
115
|
+
|
|
116
|
+
-- Tight allowlist: a different, non-enumerated non-fleet sender stays denied.
|
|
117
|
+
select is(
|
|
118
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
119
|
+
'send_direct_message', '{}'::jsonb,
|
|
120
|
+
jsonb_build_object('agent_id', (select dn2_sender from _fpd_fixture)::text))),
|
|
121
|
+
false, 'deny mode: allowlist is tight — a non-enumerated sender is still denied');
|
|
122
|
+
|
|
123
|
+
-- Boardless send_direct_message path is gated (scope resolves before any board).
|
|
124
|
+
select is(
|
|
125
|
+
(select denial_class from agent_board.evaluate_signed_write_fleet_policy(
|
|
126
|
+
'send_direct_message', jsonb_build_object('p_recipient_agent_id', gen_random_uuid()::text),
|
|
127
|
+
jsonb_build_object('agent_id', (select dn_sender from _fpd_fixture)::text))),
|
|
128
|
+
'nonfleet_sender_denied', 'deny mode gates the boardless send_direct_message path (scope before board)');
|
|
129
|
+
|
|
130
|
+
-- Fleet-resolvable sender is unaffected by the non-fleet deny flag.
|
|
131
|
+
-- An unhandled operation returns allowed=true for a fleet sender (v_machine_id
|
|
132
|
+
-- not null), proving it never enters the non-fleet branch.
|
|
133
|
+
select is(
|
|
134
|
+
(select allowed from agent_board.evaluate_signed_write_fleet_policy(
|
|
135
|
+
'noop_operation', '{}'::jsonb,
|
|
136
|
+
jsonb_build_object('agent_id', (select fleet_sender from _fpd_fixture)::text, 'verification_status', 'verified'))),
|
|
137
|
+
true, 'fleet-resolvable sender is unaffected by the non-fleet deny flag');
|
|
138
|
+
|
|
139
|
+
-- Grants preserved -----------------------------------------------------------
|
|
140
|
+
select function_privs_are(
|
|
141
|
+
'agent_board', 'evaluate_signed_write_fleet_policy', array['text', 'jsonb', 'jsonb'],
|
|
142
|
+
'authenticated', array[]::text[], 'evaluate_signed_write_fleet_policy execute is revoked from authenticated');
|
|
143
|
+
select function_privs_are(
|
|
144
|
+
'agent_board', 'evaluate_signed_write_fleet_policy', array['text', 'jsonb', 'jsonb'],
|
|
145
|
+
'service_role', array['EXECUTE'], 'evaluate_signed_write_fleet_policy execute is granted to service_role');
|
|
146
|
+
|
|
147
|
+
select finish();
|
|
148
|
+
|
|
149
|
+
rollback;
|
|
@@ -40,8 +40,8 @@ select fk_ok('agent_board', 'fleet_machine_agents', 'signing_key_id', 'agent_boa
|
|
|
40
40
|
select fk_ok('agent_board', 'fleet_receive_policies', 'recipient_agent_id', 'agent_board', 'agents', 'id', 'receive policies reference recipient agents');
|
|
41
41
|
|
|
42
42
|
select ok(
|
|
43
|
-
exists (select 1 from agent_board.schema_version where version
|
|
44
|
-
'schema_version advanced to fleet store version 7'
|
|
43
|
+
exists (select 1 from agent_board.schema_version where version >= 7),
|
|
44
|
+
'schema_version advanced to at least fleet store version 7'
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
select ok(
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
-- MBFED SL-1 / MBPORTALGATE SL-1 pgTAP: identity_providers mirrors workload_providers
|
|
2
|
+
-- (shape + RLS + grants), the real Portal contract row is seeded INERT with the frozen
|
|
3
|
+
-- resource-audience + actor_claim, and enroll_federated_identity is service_role-only
|
|
4
|
+
-- (revoked from authenticated). Runs as postgres.
|
|
5
|
+
create extension if not exists pgtap with schema extensions;
|
|
6
|
+
set search_path = extensions, public, agent_board;
|
|
7
|
+
|
|
8
|
+
begin;
|
|
9
|
+
|
|
10
|
+
select plan(21);
|
|
11
|
+
|
|
12
|
+
-- Shape ------------------------------------------------------------------------
|
|
13
|
+
select has_table('agent_board', 'identity_providers', 'identity_providers table exists');
|
|
14
|
+
select col_is_pk('agent_board', 'identity_providers', 'id', 'id is the primary key');
|
|
15
|
+
select col_is_unique('agent_board', 'identity_providers', array['issuer', 'audience'],
|
|
16
|
+
'unique(issuer, audience) mirrors workload_providers');
|
|
17
|
+
select has_column('agent_board', 'identity_providers', 'policy_approved', 'has policy_approved');
|
|
18
|
+
select col_default_is('agent_board', 'identity_providers', 'policy_approved', 'false',
|
|
19
|
+
'policy_approved defaults false (no provider auto-approved)');
|
|
20
|
+
|
|
21
|
+
-- MBPORTALGATE: actor_claim column (forward-only, default 'act.sub') ------------
|
|
22
|
+
select has_column('agent_board', 'identity_providers', 'actor_claim',
|
|
23
|
+
'MBPORTALGATE adds the actor_claim column (forward-only)');
|
|
24
|
+
select col_default_is('agent_board', 'identity_providers', 'actor_claim', 'act.sub',
|
|
25
|
+
'actor_claim defaults to act.sub (audit-only delegated-actor claim)');
|
|
26
|
+
|
|
27
|
+
-- RLS (deny-anon + authenticated-read-true, exactly like workload_providers) ----
|
|
28
|
+
select ok(
|
|
29
|
+
(select relrowsecurity from pg_class c join pg_namespace n on n.oid=c.relnamespace
|
|
30
|
+
where n.nspname='agent_board' and c.relname='identity_providers'),
|
|
31
|
+
'identity_providers has RLS enabled');
|
|
32
|
+
select is(
|
|
33
|
+
(select count(*)::int from pg_policy pol join pg_class c on c.oid=pol.polrelid
|
|
34
|
+
join pg_namespace n on n.oid=c.relnamespace
|
|
35
|
+
where n.nspname='agent_board' and c.relname='identity_providers'
|
|
36
|
+
and pol.polname='identity_providers_deny_anon'),
|
|
37
|
+
1, 'identity_providers_deny_anon policy exists');
|
|
38
|
+
select is(
|
|
39
|
+
(select pg_get_expr(polqual, polrelid) from pg_policy pol join pg_class c on c.oid=pol.polrelid
|
|
40
|
+
join pg_namespace n on n.oid=c.relnamespace
|
|
41
|
+
where n.nspname='agent_board' and c.relname='identity_providers'
|
|
42
|
+
and pol.polname='identity_providers_authenticated_read'),
|
|
43
|
+
'true', 'authenticated read predicate is true (global registry, fleet-readable)');
|
|
44
|
+
|
|
45
|
+
-- Grants: select->authenticated, select/insert/update->service_role ------------
|
|
46
|
+
select ok(
|
|
47
|
+
has_table_privilege('authenticated', 'agent_board.identity_providers', 'SELECT'),
|
|
48
|
+
'authenticated has SELECT on identity_providers');
|
|
49
|
+
select ok(
|
|
50
|
+
not has_table_privilege('authenticated', 'agent_board.identity_providers', 'INSERT'),
|
|
51
|
+
'authenticated has NO INSERT on identity_providers (mirrors workload_providers)');
|
|
52
|
+
select ok(
|
|
53
|
+
has_table_privilege('service_role', 'agent_board.identity_providers', 'INSERT'),
|
|
54
|
+
'service_role has INSERT on identity_providers');
|
|
55
|
+
select ok(
|
|
56
|
+
has_table_privilege('service_role', 'agent_board.identity_providers', 'UPDATE'),
|
|
57
|
+
'service_role has UPDATE on identity_providers');
|
|
58
|
+
|
|
59
|
+
-- Seed policy: the shipped seed carries NO policy_approved=true row -------------
|
|
60
|
+
select is(
|
|
61
|
+
(select count(*)::int from agent_board.identity_providers where policy_approved),
|
|
62
|
+
0, 'no seeded identity provider ships policy_approved=true (approval is operational)');
|
|
63
|
+
|
|
64
|
+
-- MBPORTALGATE: the REAL Portal row is present and INERT -----------------------
|
|
65
|
+
-- Frozen resource-audience constant, asserted byte-identical (IF-0-MBPORTALGATE-1/2).
|
|
66
|
+
select is(
|
|
67
|
+
(select count(*)::int from agent_board.identity_providers
|
|
68
|
+
where issuer = 'https://portal.consiliency.io'
|
|
69
|
+
and audience = 'https://message-board.consiliency.io'
|
|
70
|
+
and supported
|
|
71
|
+
and not policy_approved
|
|
72
|
+
and subject_claim = 'sub'
|
|
73
|
+
and actor_claim = 'act.sub'),
|
|
74
|
+
1, 'real Portal row present, INERT (policy_approved=false), frozen subject/actor_claim');
|
|
75
|
+
select is(
|
|
76
|
+
(select audience from agent_board.identity_providers
|
|
77
|
+
where issuer = 'https://portal.consiliency.io'),
|
|
78
|
+
'https://message-board.consiliency.io',
|
|
79
|
+
'real Portal audience is byte-identical to the frozen resource constant (RFC 8707)');
|
|
80
|
+
select is(
|
|
81
|
+
(select policy_approved from agent_board.identity_providers
|
|
82
|
+
where issuer = 'https://portal.consiliency.io'
|
|
83
|
+
and audience = 'https://message-board.consiliency.io'),
|
|
84
|
+
false, 'real Portal row ships INERT (policy_approved=false) — approval is operational');
|
|
85
|
+
|
|
86
|
+
-- The pre-existing mock row is untouched (still present, still inert) -----------
|
|
87
|
+
select is(
|
|
88
|
+
(select count(*)::int from agent_board.identity_providers
|
|
89
|
+
where issuer = 'https://mock-portal.test'
|
|
90
|
+
and audience = 'message-board'
|
|
91
|
+
and supported
|
|
92
|
+
and not policy_approved),
|
|
93
|
+
1, 'the mock issuer row is untouched (present, supported, inert)');
|
|
94
|
+
|
|
95
|
+
-- enroll_federated_identity is service_role-only (reworked 13-arg signature) ----
|
|
96
|
+
select function_privs_are(
|
|
97
|
+
'agent_board', 'enroll_federated_identity',
|
|
98
|
+
array['text','text','text','jsonb','text','text','text','text','boolean','text','text','text[]','text[]'],
|
|
99
|
+
'authenticated', array[]::text[],
|
|
100
|
+
'reworked enroll_federated_identity execute is revoked from authenticated');
|
|
101
|
+
|
|
102
|
+
select finish();
|
|
103
|
+
|
|
104
|
+
rollback;
|