@consiliency/agent-board-schema 1.12.0 → 1.23.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 +160 -0
- package/migrations/20260731000100_agent_board_agentsdir_column_scope.sql +88 -0
- package/migrations/20260801000100_agent_board_leaveidem_terminal_noop.sql +317 -0
- package/migrations/20260802000100_agent_board_rolereset_reentry_clamp.sql +237 -0
- package/migrations/20260803000100_agent_board_rowid_total_order.sql +1201 -0
- package/migrations/20260804000100_agent_board_hygiene_actor_binding_rls.sql +229 -0
- package/migrations/20260806000100_agent_board_consent_substrate.sql +339 -0
- package/migrations/20260807000100_agent_board_consent_emission.sql +619 -0
- package/migrations/20260808000100_agent_board_rejectsem.sql +145 -0
- package/migrations/20260809000100_agent_board_consent_gating.sql +492 -0
- package/migrations/20260810000100_agent_board_consent_locking.sql +485 -0
- package/migrations/20260811000100_agent_board_retention_metadata_revoke.sql +23 -0
- package/migrations/20260812000100_agent_board_pubexec_class_revoke.sql +54 -0
- package/migrations/20260813000100_agent_board_invitepolicy_block.sql +420 -0
- package/migrations/20260814000100_agent_board_obsgate_thread_post_policy.sql +239 -0
- package/migrations/20260815000100_agent_board_obsgate2_authoring_gates.sql +287 -0
- package/migrations/20260816000100_agent_board_obsgate3_task_lifecycle.sql +118 -0
- package/migrations/20260817000100_agent_board_obsgate4_claims_lifecycle.sql +250 -0
- package/migrations/20260818000100_agent_board_fleet_machines_rls_correlation.sql +43 -0
- package/migrations/20260819000100_agent_board_resolve_noop_retire.sql +166 -0
- package/migrations/20260820000100_agent_board_roomdeleg_lane_b.sql +206 -0
- package/migrations/20260821000100_agent_board_claimgate_claimkey.sql +372 -0
- package/migrations/20260822000100_agent_board_mint_harden.sql +710 -0
- package/migrations/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql +207 -0
- package/migrations/20260823000100_agent_board_fleet_machine_hijack.sql +106 -0
- package/migrations/20260824000100_agent_board_claimgate_shadow.sql +443 -0
- package/migrations/20260825000100_agent_board_claimgate_enforce.sql +660 -0
- package/migrations/20260826000100_agent_board_fleet_machine_reactivation.sql +514 -0
- package/migrations/20260828000100_agent_board_delegation_cap_ceiling.sql +474 -0
- package/migrations/20260829000100_agent_board_fleet_machine_reclaim.sql +432 -0
- package/migrations/20260830000100_agent_board_delegation_sole_writer.sql +41 -0
- package/migrations/20260831000100_agent_board_fleet_domain_reclaim.sql +389 -0
- package/migrations/20260901000100_agent_board_atomic_fleet_onboarding.sql +1496 -0
- package/migrations/20260901000110_agent_board_fleet_onboarding_ceiling_lock.sql +604 -0
- package/migrations/20260901000120_agent_board_human_revocation_evidence_completeness.sql +78 -0
- package/migrations/20260901000130_agent_board_fedsession_human_revocation_gate.sql +74 -0
- package/migrations/20260901000140_agent_board_fedsession_board_owner_lock_order.sql +102 -0
- package/migrations/20260902000100_agent_board_outbox_wake_ordering.sql +184 -0
- package/migrations/20260903000100_agent_board_enroll_identity_board_owner_lock_order.sql +604 -0
- package/migrations/20260903000200_agent_board_fedsession_provider_absence_gate.sql +177 -0
- package/migrations/20260904000100_agent_board_wake_recovery_surface.sql +155 -0
- package/package.json +1 -1
- package/schema-manifest.json +363 -3
- package/src/tooling.test.mjs +113 -2
- package/tests/agent_board_actorspoof.test.sql +349 -0
- package/tests/agent_board_agentsdir.test.sql +154 -0
- package/tests/agent_board_atomic_fleet_onboarding.test.sql +623 -0
- package/tests/agent_board_atomic_fleet_onboarding_backfill.test.sql +142 -0
- package/tests/agent_board_atomic_fleet_onboarding_runtime.test.sql +1250 -0
- package/tests/agent_board_authbound_boundary.test.sql +9 -1
- package/tests/agent_board_claimgate_enforce.test.sql +464 -0
- package/tests/agent_board_claimgate_predicate.test.sql +360 -0
- package/tests/agent_board_claimkey.test.sql +229 -0
- package/tests/agent_board_consent_backfill.test.sql +48 -0
- package/tests/agent_board_consent_emission.test.sql +601 -0
- package/tests/agent_board_consent_gating.test.sql +489 -0
- package/tests/agent_board_consent_substrate.test.sql +720 -0
- package/tests/agent_board_delegation_cap_ceiling.test.sql +303 -0
- package/tests/agent_board_delegation_enforcement.test.sql +55 -6
- package/tests/agent_board_delegation_policies.test.sql +107 -1
- package/tests/agent_board_enroll_federated_identity.test.sql +3 -3
- package/tests/agent_board_enroll_pop_binding.test.sql +13 -3
- package/tests/agent_board_enrollment_audit_content.test.sql +11 -7
- package/tests/agent_board_federated_lock_order_class.test.sql +390 -0
- package/tests/agent_board_fedsession.test.sql +51 -5
- package/tests/agent_board_fedsession_provider_absence_gate.test.sql +170 -0
- package/tests/agent_board_fleet_domain_reclaim.test.sql +419 -0
- package/tests/agent_board_fleet_machine_hijack.test.sql +190 -0
- package/tests/agent_board_fleet_machine_reactivation.test.sql +426 -0
- package/tests/agent_board_fleet_machine_reclaim.test.sql +509 -0
- package/tests/agent_board_fleet_machines_rls.test.sql +79 -0
- package/tests/agent_board_fleet_policy.test.sql +2 -2
- package/tests/agent_board_grants_hygiene.test.sql +408 -0
- package/tests/agent_board_identity_providers.test.sql +9 -0
- package/tests/agent_board_inbox_reads.test.sql +45 -31
- package/tests/agent_board_inbox_rpcs.test.sql +44 -34
- package/tests/agent_board_invitepolicy.test.sql +205 -0
- package/tests/agent_board_leaveidem.test.sql +194 -0
- package/tests/agent_board_mint_fleet_agent_session.test.sql +10 -10
- package/tests/agent_board_mint_harden.test.sql +303 -0
- package/tests/agent_board_mint_harden_backfill.test.sql +95 -0
- package/tests/agent_board_obsgate.test.sql +178 -0
- package/tests/agent_board_obsgate2.test.sql +153 -0
- package/tests/agent_board_obsgate3.test.sql +170 -0
- package/tests/agent_board_obsgate4.test.sql +196 -0
- package/tests/agent_board_outbox_wake_ordering.test.sql +452 -0
- package/tests/agent_board_portal_offline_invariant.test.sql +14 -3
- package/tests/agent_board_pubexec_class.test.sql +53 -0
- package/tests/agent_board_rejectsem.test.sql +271 -0
- package/tests/agent_board_resigning_refresh.test.sql +1 -1
- package/tests/agent_board_resolve_noop_retire.test.sql +71 -0
- package/tests/agent_board_retention_metadata_acl.test.sql +53 -0
- package/tests/agent_board_revoke_agent_subtree.test.sql +3 -3
- package/tests/agent_board_rolereset.test.sql +351 -0
- package/tests/agent_board_roomdeleg.test.sql +5 -3
- package/tests/agent_board_roomdeleg_lane_b.test.sql +144 -0
- package/tests/agent_board_roomlink.test.sql +27 -11
- package/tests/agent_board_rowid.test.sql +1274 -0
- package/tests/agent_board_signature_rls_reads.test.sql +3 -3
- package/tests/agent_board_signed_write_gateway.test.sql +3 -3
- package/tests/agent_board_signing_challenge_pending_cap.test.sql +2 -2
- package/tests/agent_board_signing_challenge_reaper.test.sql +2 -2
- package/tests/agent_board_signing_proof_rpcs.test.sql +4 -4
- package/tests/agent_board_signing_store.test.sql +6 -6
- package/tests/agent_board_wake_recovery.test.sql +756 -0
- package/tests/fixtures/isolation-substrate.sql +93 -0
- package/tests/fixtures/test-prereqs.sql +33 -0
- package/tests/isolation/expected/atomic_fedsession_fleet_lock_order.out +79 -0
- package/tests/isolation/expected/atomic_fedsession_human_revoke.out +87 -0
- package/tests/isolation/expected/atomic_fleet_authority_races.out +469 -0
- package/tests/isolation/expected/atomic_fleet_binding_revoke.out +103 -0
- package/tests/isolation/expected/atomic_fleet_board_archive.out +117 -0
- package/tests/isolation/expected/atomic_fleet_human_fresh_revoke.out +136 -0
- package/tests/isolation/expected/atomic_fleet_identity_disposition.out +61 -0
- package/tests/isolation/expected/atomic_fleet_lifecycle_races.out +1479 -0
- package/tests/isolation/expected/atomic_fleet_onboarding.out +75 -0
- package/tests/isolation/expected/atomic_fleet_subtree_lock_order.out +60 -0
- package/tests/isolation/expected/ceiling_setter_lock_before_upsert.out +95 -0
- package/tests/isolation/expected/claimgate_ownerless_quota.out +36 -0
- package/tests/isolation/expected/claimkey_renew_race.out +38 -0
- package/tests/isolation/expected/consent_approval_lock.out +43 -0
- package/tests/isolation/expected/consent_emit_actual_state.out +37 -0
- package/tests/isolation/expected/domain_reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/enroll_identity_board_owner_lock_order.out +55 -0
- package/tests/isolation/expected/fedsession_provider_absent_owner_lock.out +37 -0
- package/tests/isolation/expected/onboarding_ceiling_lock_order.out +123 -0
- package/tests/isolation/expected/outbox_wake_ordering.out +86 -0
- package/tests/isolation/expected/reactivate_revoke_race.out +80 -0
- package/tests/isolation/expected/reclaim_revoke_race.out +40 -0
- package/tests/isolation/expected/wake_recovery_late_commit.out +89 -0
- package/tests/isolation/specs/atomic_fedsession_fleet_lock_order.spec +117 -0
- package/tests/isolation/specs/atomic_fedsession_human_revoke.spec +66 -0
- package/tests/isolation/specs/atomic_fleet_authority_races.spec +235 -0
- package/tests/isolation/specs/atomic_fleet_binding_revoke.spec +127 -0
- package/tests/isolation/specs/atomic_fleet_board_archive.spec +134 -0
- package/tests/isolation/specs/atomic_fleet_human_fresh_revoke.spec +142 -0
- package/tests/isolation/specs/atomic_fleet_identity_disposition.spec +118 -0
- package/tests/isolation/specs/atomic_fleet_lifecycle_races.spec +244 -0
- package/tests/isolation/specs/atomic_fleet_onboarding.spec +106 -0
- package/tests/isolation/specs/atomic_fleet_subtree_lock_order.spec +147 -0
- package/tests/isolation/specs/ceiling_setter_lock_before_upsert.spec +117 -0
- package/tests/isolation/specs/claimgate_ownerless_quota.spec +77 -0
- package/tests/isolation/specs/claimkey_renew_race.spec +67 -0
- package/tests/isolation/specs/consent_approval_lock.spec +98 -0
- package/tests/isolation/specs/consent_emit_actual_state.spec +79 -0
- package/tests/isolation/specs/domain_reclaim_revoke_race.spec +88 -0
- package/tests/isolation/specs/enroll_identity_board_owner_lock_order.spec +117 -0
- package/tests/isolation/specs/fedsession_provider_absent_owner_lock.spec +63 -0
- package/tests/isolation/specs/onboarding_ceiling_lock_order.spec +183 -0
- package/tests/isolation/specs/outbox_wake_ordering.spec +202 -0
- package/tests/isolation/specs/reactivate_revoke_race.spec +115 -0
- package/tests/isolation/specs/reclaim_revoke_race.spec +94 -0
- package/tests/isolation/specs/wake_recovery_late_commit.spec +207 -0
- package/tests/seeds/20260806000100_agent_board_consent_substrate.sql.pre.sql +19 -0
- package/tests/seeds/20260822000200_agent_board_mint_harden_fingerprint_backfill.sql.pre.sql +39 -0
- package/tests/seeds/20260901000100_agent_board_atomic_fleet_onboarding.sql.pre.sql +110 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
-- #141 register_fleet_machine fleet-ownership hijack.
|
|
2
|
+
--
|
|
3
|
+
-- machine_fingerprint is globally unique, so a fingerprint conflict can target ANY owner's
|
|
4
|
+
-- machine. The fix gates the `on conflict do update` with `where owner_agent_id = <resolved
|
|
5
|
+
-- actor>` and raises 42501 when the conflicting row belongs to a different owner. These
|
|
6
|
+
-- assertions pin: (a) a cross-owner re-register RAISES and leaves the victim row untouched,
|
|
7
|
+
-- (b) a same-owner re-register still updates descriptive fields, and (c) a same-owner
|
|
8
|
+
-- re-register does NOT resurrect a deliberately revoked machine.
|
|
9
|
+
--
|
|
10
|
+
-- Tests run as session_user=postgres => _is_service_role() is true => _resolved_actor_agent_id
|
|
11
|
+
-- returns the supplied p_owner_agent_id verbatim, so owner A and owner B are addressed
|
|
12
|
+
-- directly. (The authenticated-actor path -- p_owner must match current_agent_id() -- is
|
|
13
|
+
-- already covered by agent_board_fleet_policy.test.sql's impersonation block.)
|
|
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(14);
|
|
21
|
+
|
|
22
|
+
select has_function(
|
|
23
|
+
'agent_board', 'register_fleet_machine',
|
|
24
|
+
array['uuid', 'uuid', 'text', 'text', 'text', 'jsonb'],
|
|
25
|
+
'register_fleet_machine exists'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
create temp table _hijack_fx (
|
|
29
|
+
owner_a uuid,
|
|
30
|
+
owner_b uuid,
|
|
31
|
+
domain_a uuid,
|
|
32
|
+
domain_b uuid,
|
|
33
|
+
machine uuid
|
|
34
|
+
) on commit drop;
|
|
35
|
+
|
|
36
|
+
grant all on table _hijack_fx to authenticated, service_role;
|
|
37
|
+
|
|
38
|
+
do $$
|
|
39
|
+
declare
|
|
40
|
+
v_owner_a uuid := gen_random_uuid();
|
|
41
|
+
v_owner_b uuid := gen_random_uuid();
|
|
42
|
+
v_domain_a uuid;
|
|
43
|
+
v_domain_b uuid;
|
|
44
|
+
v_machine uuid;
|
|
45
|
+
begin
|
|
46
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
47
|
+
values
|
|
48
|
+
(v_owner_a, 'agent', 'fleet-hijack-owner-a', 'Fleet Hijack Owner A'),
|
|
49
|
+
(v_owner_b, 'agent', 'fleet-hijack-owner-b', 'Fleet Hijack Owner B');
|
|
50
|
+
|
|
51
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
52
|
+
values
|
|
53
|
+
('fleet-hijack-domain-a', 'Hijack Domain A', 'tailnet', v_owner_a, 'active', now()),
|
|
54
|
+
('fleet-hijack-domain-b', 'Hijack Domain B', 'external', v_owner_b, 'active', now());
|
|
55
|
+
|
|
56
|
+
select id into v_domain_a from agent_board.fleet_trust_domains where slug = 'fleet-hijack-domain-a';
|
|
57
|
+
select id into v_domain_b from agent_board.fleet_trust_domains where slug = 'fleet-hijack-domain-b';
|
|
58
|
+
|
|
59
|
+
-- Owner A registers the machine that owner B will try to steal.
|
|
60
|
+
select machine_id
|
|
61
|
+
into v_machine
|
|
62
|
+
from agent_board.register_fleet_machine(
|
|
63
|
+
v_owner_a, v_domain_a, 'owner-a-claw', 'sha256:fleet-hijack-fixture', 'portal', '{"redacted":true}'::jsonb
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
insert into _hijack_fx(owner_a, owner_b, domain_a, domain_b, machine)
|
|
67
|
+
values (v_owner_a, v_owner_b, v_domain_a, v_domain_b, v_machine);
|
|
68
|
+
end
|
|
69
|
+
$$;
|
|
70
|
+
|
|
71
|
+
-- (1) Cross-owner re-register of an existing fingerprint RAISES 42501.
|
|
72
|
+
-- Mutation pin: neutering the `where owner_agent_id = v_actor_agent_id` gate on the
|
|
73
|
+
-- do-update (e.g. `where true`) lets the cross-owner update proceed and this THROWS_OK
|
|
74
|
+
-- flips to `not ok` (the call lives instead of raising).
|
|
75
|
+
select throws_ok(
|
|
76
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
77
|
+
(select owner_b from _hijack_fx),
|
|
78
|
+
(select domain_b from _hijack_fx),
|
|
79
|
+
'owner-b-steal',
|
|
80
|
+
'sha256:fleet-hijack-fixture',
|
|
81
|
+
'portal',
|
|
82
|
+
'{"redacted":true}'::jsonb
|
|
83
|
+
) $$,
|
|
84
|
+
'42501',
|
|
85
|
+
NULL,
|
|
86
|
+
'cross-owner re-register of an existing fingerprint raises 42501'
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
-- (2)(3) The victim row is untouched by the failed hijack: owner and lifecycle unchanged.
|
|
90
|
+
-- Mutation pin: additionally restoring `owner_agent_id = excluded.owner_agent_id` to the
|
|
91
|
+
-- do-update (the original stealing upsert) flips assertion (2) -- the owner becomes owner_b.
|
|
92
|
+
select is(
|
|
93
|
+
(select owner_agent_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
94
|
+
(select owner_a from _hijack_fx),
|
|
95
|
+
'failed hijack leaves owner_agent_id unchanged (still owner A)'
|
|
96
|
+
);
|
|
97
|
+
select is(
|
|
98
|
+
(select lifecycle_state from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
99
|
+
'active',
|
|
100
|
+
'failed hijack leaves lifecycle_state unchanged (still active)'
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
-- (4)(5) No cross-owner WRITE at all: the descriptive fields the attacker supplied
|
|
104
|
+
-- (label, trust domain) are also untouched. My do-update writes machine_label and
|
|
105
|
+
-- trust_domain_id unconditionally once the owner-gate opens, so neutering that gate
|
|
106
|
+
-- (mutation 1) flips these directly -- pinning the no-cross-owner-write property, not
|
|
107
|
+
-- just the raise.
|
|
108
|
+
select is(
|
|
109
|
+
(select machine_label from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
110
|
+
'owner-a-claw',
|
|
111
|
+
'failed hijack leaves machine_label unchanged (attacker label not written)'
|
|
112
|
+
);
|
|
113
|
+
select is(
|
|
114
|
+
(select trust_domain_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
115
|
+
(select domain_a from _hijack_fx),
|
|
116
|
+
'failed hijack leaves trust_domain_id unchanged (attacker domain not written)'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
-- (6) Same-owner re-register still succeeds and updates descriptive fields.
|
|
120
|
+
select lives_ok(
|
|
121
|
+
$$ create temp table _hijack_rereg as
|
|
122
|
+
select * from agent_board.register_fleet_machine(
|
|
123
|
+
(select owner_a from _hijack_fx),
|
|
124
|
+
(select domain_a from _hijack_fx),
|
|
125
|
+
'owner-a-claw-renamed',
|
|
126
|
+
'sha256:fleet-hijack-fixture',
|
|
127
|
+
'portal',
|
|
128
|
+
'{"redacted":true}'::jsonb
|
|
129
|
+
) $$,
|
|
130
|
+
'same-owner re-register succeeds'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
-- (7)(8)(9) It is the SAME row, the label updated, and the owner is still owner A.
|
|
134
|
+
select is(
|
|
135
|
+
(select machine_id from _hijack_rereg),
|
|
136
|
+
(select machine from _hijack_fx),
|
|
137
|
+
'same-owner re-register updates the existing row (machine_id unchanged)'
|
|
138
|
+
);
|
|
139
|
+
select is(
|
|
140
|
+
(select machine_label from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
141
|
+
'owner-a-claw-renamed',
|
|
142
|
+
'same-owner re-register updates the machine label'
|
|
143
|
+
);
|
|
144
|
+
select is(
|
|
145
|
+
(select owner_agent_id from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
146
|
+
(select owner_a from _hijack_fx),
|
|
147
|
+
'same-owner re-register keeps owner_agent_id = owner A'
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
-- (10) Owner A revokes their own machine.
|
|
151
|
+
select lives_ok(
|
|
152
|
+
$$ select * from agent_board.revoke_fleet_subject(
|
|
153
|
+
'machine',
|
|
154
|
+
(select machine from _hijack_fx),
|
|
155
|
+
(select owner_a from _hijack_fx),
|
|
156
|
+
'test revocation',
|
|
157
|
+
'{"redacted":true}'::jsonb
|
|
158
|
+
) $$,
|
|
159
|
+
'owner A revokes their machine'
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
-- (11) A same-owner re-register of the now-revoked machine still succeeds ...
|
|
163
|
+
select lives_ok(
|
|
164
|
+
$$ select * from agent_board.register_fleet_machine(
|
|
165
|
+
(select owner_a from _hijack_fx),
|
|
166
|
+
(select domain_a from _hijack_fx),
|
|
167
|
+
'owner-a-claw-reregistered',
|
|
168
|
+
'sha256:fleet-hijack-fixture',
|
|
169
|
+
'portal',
|
|
170
|
+
'{"redacted":true}'::jsonb
|
|
171
|
+
) $$,
|
|
172
|
+
'same-owner re-register of a revoked machine succeeds'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
-- (13)(14) ... but does NOT resurrect it: lifecycle_state and revoked_at are preserved.
|
|
176
|
+
-- Mutation pin: restoring `lifecycle_state = 'active', revoked_at = null` to the do-update
|
|
177
|
+
-- (the original un-revoke) flips both assertions.
|
|
178
|
+
select is(
|
|
179
|
+
(select lifecycle_state from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
180
|
+
'revoked',
|
|
181
|
+
're-register does not resurrect a revoked machine (lifecycle_state stays revoked)'
|
|
182
|
+
);
|
|
183
|
+
select ok(
|
|
184
|
+
(select revoked_at is not null from agent_board.fleet_machines where machine_fingerprint = 'sha256:fleet-hijack-fixture'),
|
|
185
|
+
're-register does not clear revoked_at of a revoked machine'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
select * from finish();
|
|
189
|
+
|
|
190
|
+
rollback;
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
-- #146 operator machine reactivation — supersede-based un-revoke.
|
|
2
|
+
--
|
|
3
|
+
-- reactivate_fleet_machine (service_role only) stamps fleet_machines.reactivated_at and
|
|
4
|
+
-- clears revoked_at/lifecycle so machine-revocation READERS ignore machine events
|
|
5
|
+
-- at-or-before that supersession point. The readers are: evaluate_fleet_sender_policy and
|
|
6
|
+
-- the v_fleet_machine_status read model. revoke_fleet_subject (the WRITER) advances the
|
|
7
|
+
-- event's effective_at to now() on re-revocation so the append-only ledger stays
|
|
8
|
+
-- authoritative (a re-revocation after reactivation supersedes reactivated_at).
|
|
9
|
+
--
|
|
10
|
+
-- These assertions pin:
|
|
11
|
+
-- * revoke → policy denies; reactivate → the SAME sender passes (authority restored).
|
|
12
|
+
-- * a re-revocation via the REAL writer denies again AND advances the ledger's effective_at
|
|
13
|
+
-- past reactivated_at (supersession is prior-only; ledger stays authoritative — #146 panel
|
|
14
|
+
-- MUST-FIX 1). Because now() is transaction-frozen in pgTAP, the re-revoke fixture ages
|
|
15
|
+
-- the original revocation (event -2h) and the reactivation (marker -1h) so the real
|
|
16
|
+
-- writer's now() re-revocation lands strictly after the supersession point.
|
|
17
|
+
-- * reactivation supersedes ONLY the machine subject: a revoked trust_domain or a revoked
|
|
18
|
+
-- sender agent stays denied after machine reactivation.
|
|
19
|
+
-- * reactivation is operator-only: an authenticated caller (even the owner) is refused.
|
|
20
|
+
-- * reactivating a non-revoked machine raises (nothing to reactivate).
|
|
21
|
+
-- * v_fleet_machine_status.machine_revoked honors reactivated_at (#146 panel MUST-FIX 2).
|
|
22
|
+
-- * rule #1 CLASS: every catalog-enumerated machine-subject revocation reader honors
|
|
23
|
+
-- reactivated_at (a new non-conforming reader names itself); the WRITER exemption is
|
|
24
|
+
-- asserted positively (it bumps effective_at=now()).
|
|
25
|
+
--
|
|
26
|
+
-- Tests run as session_user=postgres ⇒ _is_service_role() is true; the authz assertion flips
|
|
27
|
+
-- to role=authenticated (jwt claims) to exercise the body guard.
|
|
28
|
+
--
|
|
29
|
+
-- Mutation pins (rule #2):
|
|
30
|
+
-- * drop the machine-clause reactivated_at guard in the policy → (8) fails.
|
|
31
|
+
-- * revert revoke_fleet_subject's UPDATE branch (remove `effective_at = now()`) → (12) fails
|
|
32
|
+
-- (the aged event stays before reactivated_at → ledger not advanced).
|
|
33
|
+
-- * remove the policy's lifecycle_state='revoked' guard → (11) STILL passes (the advanced
|
|
34
|
+
-- event clause catches the re-revocation): 2-of-2 depth.
|
|
35
|
+
-- * remove reactivate's _is_service_role() body guard → (17) fails.
|
|
36
|
+
-- * add the reactivated_at guard to the trust_domain clause → (14) fails; to the agent
|
|
37
|
+
-- clause → (16) fails.
|
|
38
|
+
-- * revert v_fleet_machine_status.machine_revoked to bare event-existence → (19) fails.
|
|
39
|
+
-- * add any machine-subject reader that omits reactivated_at → (21) fails and names it.
|
|
40
|
+
|
|
41
|
+
create extension if not exists pgtap with schema extensions;
|
|
42
|
+
set search_path = extensions, public, agent_board;
|
|
43
|
+
|
|
44
|
+
begin;
|
|
45
|
+
|
|
46
|
+
select plan(22);
|
|
47
|
+
|
|
48
|
+
-- (1)(2) Shape.
|
|
49
|
+
select has_function(
|
|
50
|
+
'agent_board', 'reactivate_fleet_machine',
|
|
51
|
+
array['uuid', 'uuid', 'text'],
|
|
52
|
+
'reactivate_fleet_machine exists'
|
|
53
|
+
);
|
|
54
|
+
select has_column(
|
|
55
|
+
'agent_board', 'fleet_machines', 'reactivated_at',
|
|
56
|
+
'fleet_machines.reactivated_at supersession marker exists'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
create temp table _react_fx (
|
|
60
|
+
recipient uuid,
|
|
61
|
+
owner uuid, -- machine owner (registers machines); also the trust-domain owner
|
|
62
|
+
a1 uuid, a2 uuid, a3 uuid, a4 uuid, a5 uuid, a6 uuid, -- sender agents
|
|
63
|
+
m1 uuid, m2 uuid, m3 uuid, m4 uuid, m5 uuid, m6 uuid, -- machines
|
|
64
|
+
domain_main uuid,
|
|
65
|
+
domain_revoked uuid,
|
|
66
|
+
owner_user_id uuid
|
|
67
|
+
) on commit drop;
|
|
68
|
+
|
|
69
|
+
grant all on table _react_fx to authenticated, service_role;
|
|
70
|
+
|
|
71
|
+
do $$
|
|
72
|
+
declare
|
|
73
|
+
v_recipient uuid := gen_random_uuid();
|
|
74
|
+
v_owner uuid := gen_random_uuid();
|
|
75
|
+
v_a1 uuid := gen_random_uuid();
|
|
76
|
+
v_a2 uuid := gen_random_uuid();
|
|
77
|
+
v_a3 uuid := gen_random_uuid();
|
|
78
|
+
v_a4 uuid := gen_random_uuid();
|
|
79
|
+
v_a5 uuid := gen_random_uuid();
|
|
80
|
+
v_a6 uuid := gen_random_uuid();
|
|
81
|
+
v_domain_main uuid;
|
|
82
|
+
v_domain_revoked uuid;
|
|
83
|
+
v_m1 uuid; v_m2 uuid; v_m3 uuid; v_m4 uuid; v_m5 uuid; v_m6 uuid;
|
|
84
|
+
begin
|
|
85
|
+
insert into agent_board.agents(id, agent_type, handle, display_name)
|
|
86
|
+
values
|
|
87
|
+
(v_recipient, 'agent', 'react-recipient', 'React Recipient'),
|
|
88
|
+
(v_owner, 'agent', 'react-owner', 'React Owner'),
|
|
89
|
+
(v_a1, 'agent', 'react-sender-1', 'React Sender 1'),
|
|
90
|
+
(v_a2, 'agent', 'react-sender-2', 'React Sender 2'),
|
|
91
|
+
(v_a3, 'agent', 'react-sender-3', 'React Sender 3'),
|
|
92
|
+
(v_a4, 'agent', 'react-sender-4', 'React Sender 4'),
|
|
93
|
+
(v_a5, 'agent', 'react-sender-5', 'React Sender 5'),
|
|
94
|
+
(v_a6, 'agent', 'react-sender-6', 'React Sender 6');
|
|
95
|
+
|
|
96
|
+
insert into agent_board.fleet_trust_domains(slug, display_name, domain_kind, owner_agent_id, lifecycle_state, approved_at)
|
|
97
|
+
values
|
|
98
|
+
('react-domain-main', 'React Domain Main', 'tailnet', v_owner, 'active', now()),
|
|
99
|
+
('react-domain-revoked', 'React Domain Revoked', 'tailnet', v_owner, 'active', now());
|
|
100
|
+
|
|
101
|
+
select id into v_domain_main from agent_board.fleet_trust_domains where slug = 'react-domain-main';
|
|
102
|
+
select id into v_domain_revoked from agent_board.fleet_trust_domains where slug = 'react-domain-revoked';
|
|
103
|
+
|
|
104
|
+
select machine_id into v_m1 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m1', 'sha256:react-m1', 'portal', '{"redacted":true}'::jsonb);
|
|
105
|
+
select machine_id into v_m2 from agent_board.register_fleet_machine(v_owner, v_domain_revoked, 'react-m2', 'sha256:react-m2', 'portal', '{"redacted":true}'::jsonb);
|
|
106
|
+
select machine_id into v_m3 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m3', 'sha256:react-m3', 'portal', '{"redacted":true}'::jsonb);
|
|
107
|
+
select machine_id into v_m4 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m4', 'sha256:react-m4', 'portal', '{"redacted":true}'::jsonb);
|
|
108
|
+
select machine_id into v_m5 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m5', 'sha256:react-m5', 'portal', '{"redacted":true}'::jsonb);
|
|
109
|
+
select machine_id into v_m6 from agent_board.register_fleet_machine(v_owner, v_domain_main, 'react-m6', 'sha256:react-m6', 'portal', '{"redacted":true}'::jsonb);
|
|
110
|
+
|
|
111
|
+
insert into agent_board.fleet_machine_agents(machine_id, agent_id, agent_role)
|
|
112
|
+
values
|
|
113
|
+
(v_m1, v_a1, 'phase_worker'),
|
|
114
|
+
(v_m2, v_a2, 'phase_worker'),
|
|
115
|
+
(v_m3, v_a3, 'phase_worker'),
|
|
116
|
+
(v_m4, v_a4, 'phase_worker'),
|
|
117
|
+
(v_m5, v_a5, 'phase_worker'),
|
|
118
|
+
(v_m6, v_a6, 'phase_worker');
|
|
119
|
+
|
|
120
|
+
-- Allow-by-default receive policy for the recipient: a non-revoked bound machine passes,
|
|
121
|
+
-- so the ONLY lever these assertions exercise is the machine-revocation state.
|
|
122
|
+
perform agent_board.set_fleet_receive_policy(v_recipient, 'react allow-by-default', 'allow', false, null, null, '{"senderPolicy":"open"}'::jsonb);
|
|
123
|
+
|
|
124
|
+
insert into _react_fx(recipient, owner, a1, a2, a3, a4, a5, a6, m1, m2, m3, m4, m5, m6, domain_main, domain_revoked)
|
|
125
|
+
values (v_recipient, v_owner, v_a1, v_a2, v_a3, v_a4, v_a5, v_a6, v_m1, v_m2, v_m3, v_m4, v_m5, v_m6, v_domain_main, v_domain_revoked);
|
|
126
|
+
end
|
|
127
|
+
$$;
|
|
128
|
+
|
|
129
|
+
------------------------------------------------------------------------------------------
|
|
130
|
+
-- CORE (m1): revoke → deny; reactivate → the SAME sender passes.
|
|
131
|
+
------------------------------------------------------------------------------------------
|
|
132
|
+
do $$
|
|
133
|
+
begin
|
|
134
|
+
perform agent_board.revoke_fleet_subject('machine', (select m1 from _react_fx), (select owner from _react_fx), 'core revoke', '{"redacted":true}'::jsonb);
|
|
135
|
+
end
|
|
136
|
+
$$;
|
|
137
|
+
|
|
138
|
+
-- (3) Revoked machine is denied.
|
|
139
|
+
select is(
|
|
140
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
141
|
+
(select a1 from _react_fx), (select recipient from _react_fx), (select m1 from _react_fx), null, 'send_direct_message', null, false)),
|
|
142
|
+
'machine_revoked',
|
|
143
|
+
'core: revoked machine is denied before reactivation'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
-- (4) Operator reactivation succeeds (allow-side lives_ok + separate state reads, #113).
|
|
147
|
+
select lives_ok(
|
|
148
|
+
$$ create temp table _react_m1 as
|
|
149
|
+
select * from agent_board.reactivate_fleet_machine(
|
|
150
|
+
(select m1 from _react_fx), (select owner from _react_fx), 'operator brings m1 back') $$,
|
|
151
|
+
'core: service_role reactivation of m1 succeeds'
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
-- (5)(6)(7) Row is reconciled: reactivated_at stamped, lifecycle active, revoked_at cleared.
|
|
155
|
+
select ok(
|
|
156
|
+
(select reactivated_at is not null from agent_board.fleet_machines where id = (select m1 from _react_fx)),
|
|
157
|
+
'core: reactivation stamps reactivated_at'
|
|
158
|
+
);
|
|
159
|
+
select is(
|
|
160
|
+
(select lifecycle_state from agent_board.fleet_machines where id = (select m1 from _react_fx)),
|
|
161
|
+
'active',
|
|
162
|
+
'core: reactivation sets lifecycle_state=active'
|
|
163
|
+
);
|
|
164
|
+
select ok(
|
|
165
|
+
(select revoked_at is null from agent_board.fleet_machines where id = (select m1 from _react_fx)),
|
|
166
|
+
'core: reactivation clears revoked_at'
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
-- (8) The SAME sender now passes. MUTATION PIN: dropping the reactivated_at guard on the
|
|
170
|
+
-- machine clause makes the prior machine event match again → this flips to `not ok`.
|
|
171
|
+
select is(
|
|
172
|
+
(select allowed from agent_board.evaluate_fleet_sender_policy(
|
|
173
|
+
(select a1 from _react_fx), (select recipient from _react_fx), (select m1 from _react_fx), null, 'send_direct_message', null, false)),
|
|
174
|
+
true,
|
|
175
|
+
'core: reactivation restores sender authority (same sender now passes)'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
------------------------------------------------------------------------------------------
|
|
179
|
+
-- LEDGER AUTHORITY (m6): a re-revocation via the REAL writer denies again AND advances the
|
|
180
|
+
-- append-only ledger past reactivated_at (#146 panel MUST-FIX 1). now() is transaction-frozen,
|
|
181
|
+
-- so the fixture ages the original revocation and the reactivation to create a real gap.
|
|
182
|
+
------------------------------------------------------------------------------------------
|
|
183
|
+
do $$
|
|
184
|
+
begin
|
|
185
|
+
perform agent_board.revoke_fleet_subject('machine', (select m6 from _react_fx), (select owner from _react_fx), 'm6 initial revoke', '{"redacted":true}'::jsonb);
|
|
186
|
+
perform * from agent_board.reactivate_fleet_machine((select m6 from _react_fx), (select owner from _react_fx), 'm6 reactivate');
|
|
187
|
+
-- Simulate elapsed time (frozen now()): original revocation 2h ago, reactivation 1h ago.
|
|
188
|
+
-- Only fleet_machines.reactivated_at (a mutable marker) and the existing ledger row's
|
|
189
|
+
-- timestamp are aged; the RE-REVOCATION below uses the real writer, unmodified.
|
|
190
|
+
update agent_board.fleet_revocation_events
|
|
191
|
+
set effective_at = now() - interval '2 hours'
|
|
192
|
+
where subject_kind = 'machine' and subject_id = (select m6 from _react_fx);
|
|
193
|
+
update agent_board.fleet_machines
|
|
194
|
+
set reactivated_at = now() - interval '1 hour'
|
|
195
|
+
where id = (select m6 from _react_fx);
|
|
196
|
+
end
|
|
197
|
+
$$;
|
|
198
|
+
|
|
199
|
+
-- (9) After the aged reactivation, the aged prior revocation is superseded → sender passes.
|
|
200
|
+
select is(
|
|
201
|
+
(select allowed from agent_board.evaluate_fleet_sender_policy(
|
|
202
|
+
(select a6 from _react_fx), (select recipient from _react_fx), (select m6 from _react_fx), null, 'send_direct_message', null, false)),
|
|
203
|
+
true,
|
|
204
|
+
'ledger: reactivation holds over a real gap (aged prior revocation is superseded)'
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
-- (10) Re-revoke via the REAL writer.
|
|
208
|
+
select lives_ok(
|
|
209
|
+
$$ select * from agent_board.revoke_fleet_subject('machine', (select m6 from _react_fx), (select owner from _react_fx), 'm6 re-revoke', '{"redacted":true}'::jsonb) $$,
|
|
210
|
+
'ledger: re-revoking a reactivated machine via the real writer succeeds'
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
-- (11) Denied again. MUTATION PIN: with the writer fix the ledger event now post-dates
|
|
214
|
+
-- reactivated_at, so this denial survives removing the policy's lifecycle_state guard
|
|
215
|
+
-- (the event clause catches it — 2-of-2 depth).
|
|
216
|
+
select is(
|
|
217
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
218
|
+
(select a6 from _react_fx), (select recipient from _react_fx), (select m6 from _react_fx), null, 'send_direct_message', null, false)),
|
|
219
|
+
'machine_revoked',
|
|
220
|
+
'ledger: reactivated-then-revoked machine is denied again'
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
-- (12) MUST-FIX 1 PIN: the real writer advanced the append-only ledger past the supersession
|
|
224
|
+
-- point. Reverting revoke_fleet_subject's UPDATE branch (no `effective_at = now()`) leaves the
|
|
225
|
+
-- event at -2h < reactivated_at (-1h) → this flips to `not ok`.
|
|
226
|
+
select ok(
|
|
227
|
+
(
|
|
228
|
+
(select effective_at from agent_board.fleet_revocation_events
|
|
229
|
+
where subject_kind = 'machine' and subject_id = (select m6 from _react_fx))
|
|
230
|
+
> (select reactivated_at from agent_board.fleet_machines where id = (select m6 from _react_fx))
|
|
231
|
+
),
|
|
232
|
+
'ledger: re-revocation advances the append-only event effective_at past reactivated_at (ledger stays authoritative)'
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
------------------------------------------------------------------------------------------
|
|
236
|
+
-- DOMAIN (m2): reactivating the machine does NOT un-revoke its trust domain.
|
|
237
|
+
------------------------------------------------------------------------------------------
|
|
238
|
+
do $$
|
|
239
|
+
begin
|
|
240
|
+
perform agent_board.revoke_fleet_subject('machine', (select m2 from _react_fx), (select owner from _react_fx), 'domain-case machine revoke', '{"redacted":true}'::jsonb);
|
|
241
|
+
perform agent_board.revoke_fleet_subject('trust_domain', (select domain_revoked from _react_fx), (select owner from _react_fx), 'domain revoke', '{"redacted":true}'::jsonb);
|
|
242
|
+
end
|
|
243
|
+
$$;
|
|
244
|
+
|
|
245
|
+
-- (13) Reactivate the machine only.
|
|
246
|
+
select lives_ok(
|
|
247
|
+
$$ select * from agent_board.reactivate_fleet_machine((select m2 from _react_fx), (select owner from _react_fx), 'reactivate m2 in revoked domain') $$,
|
|
248
|
+
'domain: reactivating m2 (whose domain is revoked) succeeds'
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
-- (14) Still denied: the trust_domain clause is unchanged by machine reactivation.
|
|
252
|
+
-- MUTATION PIN: adding the reactivated_at guard to the trust_domain clause flips this.
|
|
253
|
+
select is(
|
|
254
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
255
|
+
(select a2 from _react_fx), (select recipient from _react_fx), (select m2 from _react_fx), null, 'send_direct_message', null, false)),
|
|
256
|
+
'machine_revoked',
|
|
257
|
+
'domain: machine reactivation does NOT un-revoke a revoked trust domain'
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
------------------------------------------------------------------------------------------
|
|
261
|
+
-- AGENT (m3): reactivating the machine does NOT un-revoke the sender agent.
|
|
262
|
+
------------------------------------------------------------------------------------------
|
|
263
|
+
do $$
|
|
264
|
+
begin
|
|
265
|
+
perform agent_board.revoke_fleet_subject('machine', (select m3 from _react_fx), (select owner from _react_fx), 'agent-case machine revoke', '{"redacted":true}'::jsonb);
|
|
266
|
+
perform agent_board.revoke_fleet_subject('agent', (select a3 from _react_fx), (select a3 from _react_fx), 'agent revoke', '{"redacted":true}'::jsonb);
|
|
267
|
+
end
|
|
268
|
+
$$;
|
|
269
|
+
|
|
270
|
+
-- (15) Reactivate the machine only.
|
|
271
|
+
select lives_ok(
|
|
272
|
+
$$ select * from agent_board.reactivate_fleet_machine((select m3 from _react_fx), (select owner from _react_fx), 'reactivate m3 with revoked agent') $$,
|
|
273
|
+
'agent: reactivating m3 (whose sender agent is revoked) succeeds'
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
-- (16) Still denied: the agent clause is unchanged by machine reactivation.
|
|
277
|
+
-- MUTATION PIN: adding the reactivated_at guard to the agent clause flips this.
|
|
278
|
+
select is(
|
|
279
|
+
(select denial_class from agent_board.evaluate_fleet_sender_policy(
|
|
280
|
+
(select a3 from _react_fx), (select recipient from _react_fx), (select m3 from _react_fx), null, 'send_direct_message', null, false)),
|
|
281
|
+
'machine_revoked',
|
|
282
|
+
'agent: machine reactivation does NOT un-revoke a revoked sender agent'
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
------------------------------------------------------------------------------------------
|
|
286
|
+
-- AUTHZ (m4): operator-only. An authenticated caller — even the machine owner — is refused.
|
|
287
|
+
------------------------------------------------------------------------------------------
|
|
288
|
+
do $$
|
|
289
|
+
begin
|
|
290
|
+
perform agent_board.revoke_fleet_subject('machine', (select m4 from _react_fx), (select owner from _react_fx), 'authz-case revoke', '{"redacted":true}'::jsonb);
|
|
291
|
+
end
|
|
292
|
+
$$;
|
|
293
|
+
|
|
294
|
+
-- Map the owner agent to an auth user so the authenticated path resolves a principal.
|
|
295
|
+
do $$
|
|
296
|
+
declare
|
|
297
|
+
v_user_id uuid;
|
|
298
|
+
v_suffix text := replace(substr(gen_random_uuid()::text, 1, 8), '-', '');
|
|
299
|
+
begin
|
|
300
|
+
v_user_id := public.ensure_github_user(
|
|
301
|
+
'react-owner-' || v_suffix,
|
|
302
|
+
'react-owner-' || v_suffix || '@example.test',
|
|
303
|
+
'react-owner-' || v_suffix,
|
|
304
|
+
'React Owner ' || v_suffix
|
|
305
|
+
);
|
|
306
|
+
update agent_board.agents set auth_user_id = v_user_id where id = (select owner from _react_fx);
|
|
307
|
+
update _react_fx set owner_user_id = v_user_id;
|
|
308
|
+
end
|
|
309
|
+
$$;
|
|
310
|
+
|
|
311
|
+
create temp table _react_authz (denied boolean) on commit drop;
|
|
312
|
+
grant all on table _react_authz to authenticated, service_role;
|
|
313
|
+
|
|
314
|
+
-- Isolate the BODY guard from the grant: temporarily grant EXECUTE to authenticated so a
|
|
315
|
+
-- call reaching the function body is refused by _is_service_role(), not the ACL. (Rolled
|
|
316
|
+
-- back with the test transaction.) This pins the "callable by authenticated" mutation to the
|
|
317
|
+
-- guard itself, independent of the service_role-only grant restated in the migration.
|
|
318
|
+
grant execute on function agent_board.reactivate_fleet_machine(uuid, uuid, text) to authenticated;
|
|
319
|
+
|
|
320
|
+
set local role authenticated;
|
|
321
|
+
select set_config('request.jwt.claim.role', 'authenticated', true);
|
|
322
|
+
select set_config('request.jwt.claim.sub', (select owner_user_id::text from _react_fx), true);
|
|
323
|
+
|
|
324
|
+
do $$
|
|
325
|
+
begin
|
|
326
|
+
begin
|
|
327
|
+
perform * from agent_board.reactivate_fleet_machine(
|
|
328
|
+
(select m4 from _react_fx), (select owner from _react_fx), 'authenticated owner attempt');
|
|
329
|
+
insert into _react_authz values (false); -- reached completion → NOT denied
|
|
330
|
+
exception
|
|
331
|
+
when sqlstate '42501' then insert into _react_authz values (true);
|
|
332
|
+
end;
|
|
333
|
+
end
|
|
334
|
+
$$;
|
|
335
|
+
|
|
336
|
+
reset role;
|
|
337
|
+
select set_config('request.jwt.claim.role', '', true);
|
|
338
|
+
select set_config('request.jwt.claim.sub', '', true);
|
|
339
|
+
revoke execute on function agent_board.reactivate_fleet_machine(uuid, uuid, text) from authenticated;
|
|
340
|
+
|
|
341
|
+
-- (17) MUTATION PIN: removing the _is_service_role() body guard lets the authenticated call
|
|
342
|
+
-- complete → denied=false → this flips to `not ok`.
|
|
343
|
+
select ok(
|
|
344
|
+
(select denied from _react_authz),
|
|
345
|
+
'authz: authenticated caller (even the owner) cannot reactivate (42501)'
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
------------------------------------------------------------------------------------------
|
|
349
|
+
-- NON-REVOKED (m5): reactivating a machine that is not revoked raises (nothing to do).
|
|
350
|
+
------------------------------------------------------------------------------------------
|
|
351
|
+
-- (18)
|
|
352
|
+
select throws_ok(
|
|
353
|
+
$$ select * from agent_board.reactivate_fleet_machine((select m5 from _react_fx), (select owner from _react_fx), 'reactivate a live machine') $$,
|
|
354
|
+
'22023',
|
|
355
|
+
NULL,
|
|
356
|
+
'non-revoked: reactivating a machine that is not revoked raises'
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
------------------------------------------------------------------------------------------
|
|
360
|
+
-- READ MODEL (MUST-FIX 2): v_fleet_machine_status.machine_revoked honors reactivated_at.
|
|
361
|
+
------------------------------------------------------------------------------------------
|
|
362
|
+
-- (19) MUTATION PIN: reverting machine_revoked to bare event-existence flips this — m1 was
|
|
363
|
+
-- reactivated, so its lingering (superseded) event must NOT read as revoked.
|
|
364
|
+
select is(
|
|
365
|
+
(select machine_revoked from agent_board.v_fleet_machine_status where machine_id = (select m1 from _react_fx)),
|
|
366
|
+
false,
|
|
367
|
+
'view: a reactivated machine reports machine_revoked=false (agrees with the policy)'
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
-- (20) Sanity: a genuinely revoked-and-not-reactivated machine still reads revoked.
|
|
371
|
+
select is(
|
|
372
|
+
(select machine_revoked from agent_board.v_fleet_machine_status where machine_id = (select m4 from _react_fx)),
|
|
373
|
+
true,
|
|
374
|
+
'view: a revoked (never reactivated) machine still reports machine_revoked=true'
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
------------------------------------------------------------------------------------------
|
|
378
|
+
-- CLASS (rule #1): catalog-derived — every machine-subject revocation READER honors
|
|
379
|
+
-- reactivated_at; the WRITER exemption is asserted positively.
|
|
380
|
+
------------------------------------------------------------------------------------------
|
|
381
|
+
-- (21) Enumerate readers (functions + views) whose definition tests a machine-subject
|
|
382
|
+
-- revocation event (`fre.subject_kind = 'machine'`) but does NOT reference reactivated_at.
|
|
383
|
+
-- Expected: none. A new non-conforming reader appears here BY NAME (rule #1: offenders name
|
|
384
|
+
-- themselves). Aggregates are excluded (pg_get_functiondef errors on them; prokind='f').
|
|
385
|
+
select is(
|
|
386
|
+
(
|
|
387
|
+
select string_agg(offender, ', ' order by offender)
|
|
388
|
+
from (
|
|
389
|
+
select p.proname as offender
|
|
390
|
+
from pg_proc p
|
|
391
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
392
|
+
where n.nspname = 'agent_board'
|
|
393
|
+
and p.prokind = 'f'
|
|
394
|
+
and pg_get_functiondef(p.oid) ~ 'fre\.subject_kind = ''machine'''
|
|
395
|
+
and pg_get_functiondef(p.oid) not ilike '%reactivated_at%'
|
|
396
|
+
union all
|
|
397
|
+
select c.relname
|
|
398
|
+
from pg_class c
|
|
399
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
400
|
+
where n.nspname = 'agent_board'
|
|
401
|
+
and c.relkind = 'v'
|
|
402
|
+
and pg_get_viewdef(c.oid) ~ 'fre\.subject_kind = ''machine'''
|
|
403
|
+
and pg_get_viewdef(c.oid) not ilike '%reactivated_at%'
|
|
404
|
+
) offenders
|
|
405
|
+
),
|
|
406
|
+
null,
|
|
407
|
+
'class: every machine-subject revocation reader honors reactivated_at (offenders name themselves)'
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
-- (22) Positive exemption (rule #1): revoke_fleet_subject is the machine-revocation WRITER,
|
|
411
|
+
-- not a state reader — its exemption claim is that it advances the ledger's effective_at.
|
|
412
|
+
select ok(
|
|
413
|
+
(
|
|
414
|
+
select pg_get_functiondef(p.oid) ~ 'effective_at = now\(\)'
|
|
415
|
+
from pg_proc p
|
|
416
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
417
|
+
where n.nspname = 'agent_board'
|
|
418
|
+
and p.prokind = 'f'
|
|
419
|
+
and p.proname = 'revoke_fleet_subject'
|
|
420
|
+
),
|
|
421
|
+
'class exemption: revoke_fleet_subject (the writer) advances effective_at=now() on re-revocation'
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
select * from finish();
|
|
425
|
+
|
|
426
|
+
rollback;
|