@creatornader/agent-bridge 0.2.0 → 0.3.1
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/CHANGELOG.md +86 -4
- package/README.md +567 -55
- package/ROADMAP.md +114 -0
- package/SECURITY.md +90 -0
- package/SKILL.md +68 -2
- package/bin/agent-bridge +2 -2
- package/clients/claude-desktop.json +3 -1
- package/dist/archive.d.ts +163 -0
- package/dist/archive.js +50 -0
- package/dist/archive.js.map +1 -0
- package/dist/bridge-domain-CBX3IVqC.d.ts +153 -0
- package/dist/{bridge-service-CfqMcMAn.d.ts → bridge-service-hX1ZQp4W.d.ts} +15 -5
- package/dist/bridge-store-DPVSCwfs.d.ts +102 -0
- package/dist/bridge.d.ts +76 -17
- package/dist/bridge.js +52 -24
- package/dist/chunk-2DFNOLMO.js +618 -0
- package/dist/chunk-2DFNOLMO.js.map +1 -0
- package/dist/{chunk-KVR4JBRF.js → chunk-2EVLG3YA.js} +2 -2
- package/dist/{chunk-KVR4JBRF.js.map → chunk-2EVLG3YA.js.map} +1 -1
- package/dist/chunk-45BZGB77.js +709 -0
- package/dist/chunk-45BZGB77.js.map +1 -0
- package/dist/chunk-4VB44KJU.js +230 -0
- package/dist/chunk-4VB44KJU.js.map +1 -0
- package/dist/chunk-5GMB3A77.js +1013 -0
- package/dist/chunk-5GMB3A77.js.map +1 -0
- package/dist/chunk-A6W4QALF.js +1258 -0
- package/dist/chunk-A6W4QALF.js.map +1 -0
- package/dist/chunk-BZLMF5O5.js +328 -0
- package/dist/chunk-BZLMF5O5.js.map +1 -0
- package/dist/{chunk-AXL2I7FH.js → chunk-CEXBMOI5.js} +240 -79
- package/dist/chunk-CEXBMOI5.js.map +1 -0
- package/dist/chunk-GADH244D.js +527 -0
- package/dist/chunk-GADH244D.js.map +1 -0
- package/dist/chunk-MBPZXTJA.js +2428 -0
- package/dist/chunk-MBPZXTJA.js.map +1 -0
- package/dist/chunk-NCO7RLBM.js +146 -0
- package/dist/chunk-NCO7RLBM.js.map +1 -0
- package/dist/chunk-Q6PPMXJC.js +1050 -0
- package/dist/chunk-Q6PPMXJC.js.map +1 -0
- package/dist/{chunk-4KAOBC5X.js → chunk-SPSMZOCJ.js} +190 -165
- package/dist/chunk-SPSMZOCJ.js.map +1 -0
- package/dist/chunk-UAHMP6GC.js +54 -0
- package/dist/chunk-UAHMP6GC.js.map +1 -0
- package/dist/{chunk-YD6YJATN.js → chunk-VOQIDTCJ.js} +4 -2
- package/dist/chunk-VOQIDTCJ.js.map +1 -0
- package/dist/chunk-WAI2IYIQ.js +554 -0
- package/dist/chunk-WAI2IYIQ.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +3555 -214
- package/dist/cli.js.map +1 -1
- package/dist/client-config.d.ts +1 -1
- package/dist/client-config.js +1 -1
- package/dist/client-runtime.d.ts +10 -6
- package/dist/client-runtime.js +9 -6
- package/dist/contracts/generate.d.ts +2 -0
- package/dist/contracts/generate.js +247 -0
- package/dist/contracts/generate.js.map +1 -0
- package/dist/dr.d.ts +324 -0
- package/dist/dr.js +124 -0
- package/dist/dr.js.map +1 -0
- package/dist/gateway-CggQ8vTC.d.ts +78 -0
- package/dist/gateway-main.js +190 -8
- package/dist/gateway-main.js.map +1 -1
- package/dist/gateway.d.ts +4 -3
- package/dist/gateway.js +4 -3
- package/dist/index.js +11 -8
- package/dist/index.js.map +1 -1
- package/dist/postgres-bridge-store-Bfd3hRId.d.ts +49 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +13 -8
- package/dist/sqlite.d.ts +94 -12
- package/dist/sqlite.js +7 -4
- package/docs/architecture-v2.md +394 -0
- package/docs/decisions/0001-protocol-layers-and-acknowledgment-semantics.md +85 -0
- package/docs/decisions/0002-canonical-operation-contract-registry.md +46 -0
- package/docs/decisions/0003-host-adapters-and-consumer-instance-keys.md +88 -0
- package/docs/ecosystem.md +86 -0
- package/docs/postmortems/2026-07-08-wrapper-source-drift.md +82 -0
- package/docs/troubleshooting.md +89 -0
- package/llms.txt +107 -6
- package/openapi/agent-bridge-v2.openapi.json +9610 -0
- package/package.json +24 -4
- package/schemas/agent-bridge-v2.capabilities.json +1829 -0
- package/schemas/agent-bridge-v2.mcp.json +2074 -0
- package/schemas/agent-bridge-v2.schema.json +4560 -0
- package/sql/migrations/008_message_projects.sql +36 -0
- package/sql/migrations/009_mailbox_query_indexes.sql +28 -0
- package/sql/migrations/010_publisher_delivery_policy.sql +210 -0
- package/sql/migrations/011_credential_security.sql +875 -0
- package/sql/migrations/012_request_authority.sql +177 -0
- package/sql/migrations/013_row_isolation.sql +495 -0
- package/sql/migrations/014_owner_control_plane.sql +1641 -0
- package/sql/migrations/015_portable_archives.sql +1813 -0
- package/sql/migrations/016_native_dr_fence.sql +1005 -0
- package/bin/CLAUDE.md +0 -11
- package/dist/bridge-domain-BcnBeHOn.d.ts +0 -97
- package/dist/bridge-store-BuGjxTn4.d.ts +0 -61
- package/dist/chunk-4KAOBC5X.js.map +0 -1
- package/dist/chunk-6MAZ6YLZ.js +0 -146
- package/dist/chunk-6MAZ6YLZ.js.map +0 -1
- package/dist/chunk-AXL2I7FH.js.map +0 -1
- package/dist/chunk-BV6W4AJK.js +0 -211
- package/dist/chunk-BV6W4AJK.js.map +0 -1
- package/dist/chunk-CKSKUBDQ.js +0 -153
- package/dist/chunk-CKSKUBDQ.js.map +0 -1
- package/dist/chunk-JOUBDRYX.js +0 -272
- package/dist/chunk-JOUBDRYX.js.map +0 -1
- package/dist/chunk-JTC5YURG.js +0 -49
- package/dist/chunk-JTC5YURG.js.map +0 -1
- package/dist/chunk-O26W5QHS.js +0 -409
- package/dist/chunk-O26W5QHS.js.map +0 -1
- package/dist/chunk-WEULPMUY.js +0 -988
- package/dist/chunk-WEULPMUY.js.map +0 -1
- package/dist/chunk-YD6YJATN.js.map +0 -1
- package/dist/gateway-BULozv3K.d.ts +0 -54
|
@@ -0,0 +1,1641 @@
|
|
|
1
|
+
begin;
|
|
2
|
+
|
|
3
|
+
select pg_advisory_xact_lock(1646705660);
|
|
4
|
+
|
|
5
|
+
do $roles$
|
|
6
|
+
declare
|
|
7
|
+
suffix text := substr(md5(current_database()),1,16);
|
|
8
|
+
role_name text;
|
|
9
|
+
begin
|
|
10
|
+
foreach role_name in array array[
|
|
11
|
+
'agent_bridge_control_owner_' || suffix,
|
|
12
|
+
'agent_bridge_control_operator_' || suffix,
|
|
13
|
+
'agent_bridge_control_auditor_' || suffix
|
|
14
|
+
] loop
|
|
15
|
+
if not exists (select 1 from pg_roles where rolname=role_name) then
|
|
16
|
+
execute format(
|
|
17
|
+
'create role %I nologin nosuperuser nocreatedb nocreaterole noreplication nobypassrls',
|
|
18
|
+
role_name
|
|
19
|
+
);
|
|
20
|
+
end if;
|
|
21
|
+
execute format(
|
|
22
|
+
'alter role %I nologin nosuperuser nocreatedb nocreaterole noreplication nobypassrls',
|
|
23
|
+
role_name
|
|
24
|
+
);
|
|
25
|
+
execute format('grant %I to %I with admin option',role_name,current_user);
|
|
26
|
+
end loop;
|
|
27
|
+
end
|
|
28
|
+
$roles$;
|
|
29
|
+
|
|
30
|
+
create table agent_bridge.control_requests (
|
|
31
|
+
request_id uuid primary key,
|
|
32
|
+
operation text not null,
|
|
33
|
+
fingerprint char(64) not null,
|
|
34
|
+
actor name not null,
|
|
35
|
+
result jsonb not null,
|
|
36
|
+
created_at timestamptz not null default clock_timestamp(),
|
|
37
|
+
constraint control_requests_operation check(operation in ('provision','rotate','revoke')),
|
|
38
|
+
constraint control_requests_fingerprint check(fingerprint ~ '^[0-9a-f]{64}$')
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
create table agent_bridge.control_events (
|
|
42
|
+
sequence bigint generated always as identity primary key,
|
|
43
|
+
event_id uuid not null default gen_random_uuid() unique,
|
|
44
|
+
request_id uuid not null references agent_bridge.control_requests(request_id),
|
|
45
|
+
operation text not null,
|
|
46
|
+
outcome text not null,
|
|
47
|
+
actor name not null,
|
|
48
|
+
workspace_id text,
|
|
49
|
+
principal text,
|
|
50
|
+
credential_id uuid,
|
|
51
|
+
related_credential_id uuid,
|
|
52
|
+
reason_code text,
|
|
53
|
+
created_at timestamptz not null default clock_timestamp(),
|
|
54
|
+
constraint control_events_operation check(operation in ('provision','rotate','revoke')),
|
|
55
|
+
constraint control_events_outcome check(outcome='succeeded')
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
create table agent_bridge.owner_control_attestations (
|
|
59
|
+
name text primary key,
|
|
60
|
+
catalog_definition text not null,
|
|
61
|
+
attested_at timestamptz not null default now(),
|
|
62
|
+
constraint owner_control_attestation_name check(name ~ '^owner-control-v[0-9]+$')
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
create table agent_bridge.control_membership_events (
|
|
66
|
+
sequence bigint generated always as identity primary key,
|
|
67
|
+
request_id uuid not null unique,
|
|
68
|
+
action text not null,
|
|
69
|
+
member_role name not null,
|
|
70
|
+
control_role text not null,
|
|
71
|
+
actor name not null,
|
|
72
|
+
created_at timestamptz not null default clock_timestamp(),
|
|
73
|
+
constraint control_membership_events_action check(action in ('register','revoke')),
|
|
74
|
+
constraint control_membership_events_role check(control_role in ('operator','auditor'))
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
create index control_events_workspace_sequence
|
|
78
|
+
on agent_bridge.control_events(workspace_id,sequence desc);
|
|
79
|
+
create index control_membership_events_member_sequence
|
|
80
|
+
on agent_bridge.control_membership_events(member_role,control_role,sequence desc);
|
|
81
|
+
create index credentials_inventory_global
|
|
82
|
+
on agent_bridge.credentials(
|
|
83
|
+
date_bin('1 millisecond',created_at,'2000-01-01 00:00:00+00'::timestamptz),id
|
|
84
|
+
);
|
|
85
|
+
create index credentials_inventory_workspace
|
|
86
|
+
on agent_bridge.credentials(
|
|
87
|
+
workspace_id,
|
|
88
|
+
date_bin('1 millisecond',created_at,'2000-01-01 00:00:00+00'::timestamptz),id
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
create or replace function agent_bridge.reject_control_ledger_mutation()
|
|
92
|
+
returns trigger language plpgsql set search_path = '' as $$
|
|
93
|
+
begin
|
|
94
|
+
raise exception 'control ledgers are append-only';
|
|
95
|
+
end
|
|
96
|
+
$$;
|
|
97
|
+
|
|
98
|
+
create trigger control_requests_append_only
|
|
99
|
+
before update or delete or truncate on agent_bridge.control_requests
|
|
100
|
+
for each statement execute function agent_bridge.reject_control_ledger_mutation();
|
|
101
|
+
create trigger control_events_append_only
|
|
102
|
+
before update or delete or truncate on agent_bridge.control_events
|
|
103
|
+
for each statement execute function agent_bridge.reject_control_ledger_mutation();
|
|
104
|
+
create trigger owner_control_attestations_append_only
|
|
105
|
+
before update or delete or truncate on agent_bridge.owner_control_attestations
|
|
106
|
+
for each statement execute function agent_bridge.reject_control_ledger_mutation();
|
|
107
|
+
create trigger control_membership_events_append_only
|
|
108
|
+
before update or delete or truncate on agent_bridge.control_membership_events
|
|
109
|
+
for each statement execute function agent_bridge.reject_control_ledger_mutation();
|
|
110
|
+
|
|
111
|
+
alter table agent_bridge.credentials alter column scopes set default '{}'::text[];
|
|
112
|
+
|
|
113
|
+
create or replace function agent_bridge.assert_control_actor(requested_capability text)
|
|
114
|
+
returns void language plpgsql security definer set search_path = '' as $$
|
|
115
|
+
declare
|
|
116
|
+
suffix text := substr(md5(current_database()),1,16);
|
|
117
|
+
schema_owner name;
|
|
118
|
+
operator_role name := ('agent_bridge_control_operator_'||suffix)::name;
|
|
119
|
+
auditor_role name := ('agent_bridge_control_auditor_'||suffix)::name;
|
|
120
|
+
operator_allowed boolean := false;
|
|
121
|
+
auditor_allowed boolean := false;
|
|
122
|
+
begin
|
|
123
|
+
select pg_catalog.pg_get_userbyid(namespace.nspowner)::name into schema_owner
|
|
124
|
+
from pg_catalog.pg_namespace namespace where namespace.nspname='agent_bridge';
|
|
125
|
+
if requested_capability is null or requested_capability not in ('operator','inventory') then
|
|
126
|
+
raise exception 'control capability is invalid';
|
|
127
|
+
end if;
|
|
128
|
+
|
|
129
|
+
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
130
|
+
'control-membership'||chr(31)||session_user||chr(31)||'global',1646705661
|
|
131
|
+
));
|
|
132
|
+
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
133
|
+
'control-membership'||chr(31)||session_user||chr(31)||'operator',1646705661
|
|
134
|
+
));
|
|
135
|
+
if requested_capability='inventory' then
|
|
136
|
+
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
137
|
+
'control-membership'||chr(31)||session_user||chr(31)||'auditor',1646705661
|
|
138
|
+
));
|
|
139
|
+
end if;
|
|
140
|
+
|
|
141
|
+
with latest as (
|
|
142
|
+
select distinct on (event.control_role) event.control_role,event.action
|
|
143
|
+
from agent_bridge.control_membership_events event
|
|
144
|
+
where event.member_role=session_user and event.control_role in ('operator','auditor')
|
|
145
|
+
order by event.control_role,event.sequence desc
|
|
146
|
+
), direct_membership as (
|
|
147
|
+
select granted.rolname control_role,membership.admin_option,
|
|
148
|
+
coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) inherit_option,
|
|
149
|
+
coalesce((to_jsonb(membership)->>'set_option')::boolean,true) set_option,
|
|
150
|
+
grantor.rolname grantor_role
|
|
151
|
+
from pg_catalog.pg_auth_members membership
|
|
152
|
+
join pg_catalog.pg_roles granted on granted.oid=membership.roleid
|
|
153
|
+
join pg_catalog.pg_roles member on member.oid=membership.member
|
|
154
|
+
join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
|
|
155
|
+
where member.rolname=session_user and granted.rolname in (operator_role,auditor_role)
|
|
156
|
+
)
|
|
157
|
+
select
|
|
158
|
+
coalesce(bool_or(membership.control_role=operator_role
|
|
159
|
+
and membership.inherit_option and membership.set_option
|
|
160
|
+
and membership.grantor_role=schema_owner and (
|
|
161
|
+
(session_user=schema_owner and membership.admin_option)
|
|
162
|
+
or (latest.control_role='operator' and latest.action='register'
|
|
163
|
+
and not membership.admin_option)
|
|
164
|
+
)),false),
|
|
165
|
+
coalesce(bool_or(membership.control_role=auditor_role
|
|
166
|
+
and membership.inherit_option and membership.set_option
|
|
167
|
+
and membership.grantor_role=schema_owner and (
|
|
168
|
+
(session_user=schema_owner and membership.admin_option)
|
|
169
|
+
or (latest.control_role='auditor' and latest.action='register'
|
|
170
|
+
and not membership.admin_option)
|
|
171
|
+
)),false)
|
|
172
|
+
into operator_allowed,auditor_allowed
|
|
173
|
+
from direct_membership membership left join latest on latest.control_role=case
|
|
174
|
+
membership.control_role when operator_role then 'operator' else 'auditor' end;
|
|
175
|
+
|
|
176
|
+
if session_user<>schema_owner and (exists (
|
|
177
|
+
select 1 from pg_catalog.pg_roles inherited
|
|
178
|
+
where inherited.rolname<>session_user
|
|
179
|
+
and pg_catalog.pg_has_role(session_user,inherited.rolname,'MEMBER')
|
|
180
|
+
and not (
|
|
181
|
+
(inherited.rolname=operator_role and operator_allowed)
|
|
182
|
+
or (inherited.rolname=auditor_role and auditor_allowed)
|
|
183
|
+
)
|
|
184
|
+
) or exists (
|
|
185
|
+
select 1 from pg_catalog.pg_roles candidate
|
|
186
|
+
where candidate.rolname not in (session_user,schema_owner)
|
|
187
|
+
and not candidate.rolsuper
|
|
188
|
+
and pg_catalog.pg_has_role(candidate.rolname,session_user,'MEMBER')
|
|
189
|
+
) or exists (
|
|
190
|
+
select 1 from pg_catalog.pg_roles actor
|
|
191
|
+
where actor.rolname=session_user and (
|
|
192
|
+
not actor.rolcanlogin or actor.rolsuper or actor.rolcreaterole or actor.rolcreatedb
|
|
193
|
+
or actor.rolreplication or actor.rolbypassrls
|
|
194
|
+
)
|
|
195
|
+
)) then
|
|
196
|
+
raise exception 'control actor has an unsafe membership graph';
|
|
197
|
+
end if;
|
|
198
|
+
|
|
199
|
+
if (requested_capability='operator' and not operator_allowed)
|
|
200
|
+
or (requested_capability='inventory' and not (operator_allowed or auditor_allowed)) then
|
|
201
|
+
raise exception 'control actor is not registered for requested capability';
|
|
202
|
+
end if;
|
|
203
|
+
end
|
|
204
|
+
$$;
|
|
205
|
+
|
|
206
|
+
create or replace function agent_bridge.validate_credential_security()
|
|
207
|
+
returns trigger
|
|
208
|
+
language plpgsql set search_path = '' as $$
|
|
209
|
+
declare
|
|
210
|
+
predecessor_workspace_id text;
|
|
211
|
+
predecessor_agent_id uuid;
|
|
212
|
+
profile_scopes text[];
|
|
213
|
+
lifecycle text := coalesce(current_setting('agent_bridge.lifecycle_authorized',true),'');
|
|
214
|
+
begin
|
|
215
|
+
if tg_op='UPDATE' then
|
|
216
|
+
if old.id is distinct from new.id
|
|
217
|
+
or old.workspace_id is distinct from new.workspace_id
|
|
218
|
+
or old.agent_id is distinct from new.agent_id
|
|
219
|
+
or old.token_hash is distinct from new.token_hash
|
|
220
|
+
or old.scopes is distinct from new.scopes
|
|
221
|
+
or old.scope_set_name is distinct from new.scope_set_name
|
|
222
|
+
or old.label is distinct from new.label
|
|
223
|
+
or old.expires_at is distinct from new.expires_at
|
|
224
|
+
or old.created_at is distinct from new.created_at then
|
|
225
|
+
raise exception 'credential issued state is immutable';
|
|
226
|
+
end if;
|
|
227
|
+
if old.replaces_credential_id is distinct from new.replaces_credential_id then
|
|
228
|
+
raise exception 'credential replacement lineage is immutable';
|
|
229
|
+
end if;
|
|
230
|
+
if old.revoked_at is not null and (
|
|
231
|
+
old.revoked_at is distinct from new.revoked_at
|
|
232
|
+
or old.revoked_by is distinct from new.revoked_by
|
|
233
|
+
or old.revocation_reason is distinct from new.revocation_reason
|
|
234
|
+
) then raise exception 'credential revocation is immutable'; end if;
|
|
235
|
+
if old.revoked_at is null and new.revoked_at is not null and lifecycle<>'revocation' then
|
|
236
|
+
raise exception 'credential revocation requires an owner lifecycle function';
|
|
237
|
+
end if;
|
|
238
|
+
if (old.revoked_by is distinct from new.revoked_by
|
|
239
|
+
or old.revocation_reason is distinct from new.revocation_reason) and lifecycle<>'revocation' then
|
|
240
|
+
raise exception 'credential revocation metadata requires an owner lifecycle function';
|
|
241
|
+
end if;
|
|
242
|
+
if old.expiry_grace_until is distinct from new.expiry_grace_until and lifecycle<>'replacement' then
|
|
243
|
+
raise exception 'credential grace requires an owner lifecycle function';
|
|
244
|
+
end if;
|
|
245
|
+
if old.expiry_grace_until is not null and (
|
|
246
|
+
new.expiry_grace_until is null or new.expiry_grace_until>old.expiry_grace_until
|
|
247
|
+
) then raise exception 'credential grace may only move earlier'; end if;
|
|
248
|
+
end if;
|
|
249
|
+
if tg_op='INSERT' then
|
|
250
|
+
if new.scope_set_name is null then
|
|
251
|
+
if new.scopes<>'{}'::text[] then
|
|
252
|
+
raise exception 'credential scopes must match a named scope set';
|
|
253
|
+
end if;
|
|
254
|
+
else
|
|
255
|
+
select scopes into profile_scopes from agent_bridge.credential_scope_sets
|
|
256
|
+
where name=new.scope_set_name;
|
|
257
|
+
if not found or profile_scopes is distinct from new.scopes then
|
|
258
|
+
raise exception 'credential scopes must match a named scope set';
|
|
259
|
+
end if;
|
|
260
|
+
end if;
|
|
261
|
+
if new.label is not null and (length(new.label) not between 1 and 128 or new.label ~ '[[:cntrl:]]') then
|
|
262
|
+
raise exception 'credential label is invalid';
|
|
263
|
+
end if;
|
|
264
|
+
end if;
|
|
265
|
+
if new.replaces_credential_id is not null then
|
|
266
|
+
select workspace_id,agent_id into predecessor_workspace_id,predecessor_agent_id
|
|
267
|
+
from agent_bridge.credentials where id=new.replaces_credential_id;
|
|
268
|
+
if not found or predecessor_workspace_id is distinct from new.workspace_id
|
|
269
|
+
or predecessor_agent_id is distinct from new.agent_id then
|
|
270
|
+
raise exception 'replacement credentials must use the same workspace and agent';
|
|
271
|
+
end if;
|
|
272
|
+
if tg_op='INSERT' and lifecycle<>'replacement' then
|
|
273
|
+
raise exception 'credential replacement requires an owner lifecycle function';
|
|
274
|
+
end if;
|
|
275
|
+
end if;
|
|
276
|
+
if tg_op='INSERT' and (new.revoked_at is not null or new.revoked_by is not null
|
|
277
|
+
or new.revocation_reason is not null or new.expiry_grace_until is not null) then
|
|
278
|
+
raise exception 'new credentials cannot begin with lifecycle state';
|
|
279
|
+
end if;
|
|
280
|
+
if new.expiry_grace_until is not null and not exists (
|
|
281
|
+
select 1 from agent_bridge.credentials successor where successor.replaces_credential_id=new.id
|
|
282
|
+
) then raise exception 'credential grace requires a successor'; end if;
|
|
283
|
+
return new;
|
|
284
|
+
end
|
|
285
|
+
$$;
|
|
286
|
+
|
|
287
|
+
create or replace function agent_bridge.control_provision(
|
|
288
|
+
requested_request_id uuid, requested_workspace_id text, requested_workspace_name text,
|
|
289
|
+
requested_principal text, requested_display_name text, requested_runtime_type text,
|
|
290
|
+
requested_token_hash char(64), requested_label text, requested_scope_set_name text,
|
|
291
|
+
requested_expires_at timestamptz
|
|
292
|
+
) returns table(workspace_id text,agent_id uuid,credential_id uuid,replayed boolean)
|
|
293
|
+
language plpgsql security definer set search_path = '' as $$
|
|
294
|
+
declare
|
|
295
|
+
canonical_fingerprint char(64);
|
|
296
|
+
prior agent_bridge.control_requests%rowtype;
|
|
297
|
+
provisioned_agent_id uuid;
|
|
298
|
+
provisioned_credential_id uuid;
|
|
299
|
+
canonical_scopes text[];
|
|
300
|
+
existing_workspace_name text;
|
|
301
|
+
begin
|
|
302
|
+
perform agent_bridge.assert_control_actor('operator');
|
|
303
|
+
if requested_request_id is null or requested_workspace_id is null
|
|
304
|
+
or requested_workspace_id<>btrim(requested_workspace_id)
|
|
305
|
+
or length(requested_workspace_id) not between 1 and 128
|
|
306
|
+
or requested_workspace_id ~ '[[:cntrl:]]'
|
|
307
|
+
or requested_workspace_name is null or requested_workspace_name<>btrim(requested_workspace_name)
|
|
308
|
+
or length(requested_workspace_name) not between 1 and 128
|
|
309
|
+
or requested_workspace_name ~ '[[:cntrl:]]'
|
|
310
|
+
or requested_principal is null or requested_principal<>btrim(requested_principal)
|
|
311
|
+
or length(requested_principal) not between 1 and 128
|
|
312
|
+
or requested_principal ~ '[[:cntrl:]]'
|
|
313
|
+
or (requested_display_name is not null and (
|
|
314
|
+
requested_display_name<>btrim(requested_display_name)
|
|
315
|
+
or length(requested_display_name) not between 1 and 128
|
|
316
|
+
or requested_display_name ~ '[[:cntrl:]]'))
|
|
317
|
+
or (requested_runtime_type is not null and (
|
|
318
|
+
requested_runtime_type<>btrim(requested_runtime_type)
|
|
319
|
+
or length(requested_runtime_type) not between 1 and 128
|
|
320
|
+
or requested_runtime_type ~ '[[:cntrl:]]'))
|
|
321
|
+
or (requested_label is not null and (
|
|
322
|
+
requested_label<>btrim(requested_label) or length(requested_label) not between 1 and 128
|
|
323
|
+
or requested_label ~ '[[:cntrl:]]'))
|
|
324
|
+
or requested_scope_set_name is null
|
|
325
|
+
or requested_scope_set_name<>btrim(requested_scope_set_name)
|
|
326
|
+
or length(requested_scope_set_name) not between 1 and 128
|
|
327
|
+
or requested_scope_set_name ~ '[[:cntrl:]]'
|
|
328
|
+
or requested_token_hash is null
|
|
329
|
+
or requested_token_hash::text !~ '^[0-9a-f]{64}$'
|
|
330
|
+
then raise exception 'invalid provisioning request'; end if;
|
|
331
|
+
perform pg_catalog.pg_advisory_xact_lock(
|
|
332
|
+
pg_catalog.hashtextextended(requested_request_id::text,1646705660)
|
|
333
|
+
);
|
|
334
|
+
canonical_fingerprint := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(pg_catalog.jsonb_build_array(
|
|
335
|
+
requested_workspace_id,requested_workspace_name,requested_principal,requested_display_name,
|
|
336
|
+
requested_runtime_type,requested_token_hash::text,requested_label,requested_scope_set_name,
|
|
337
|
+
case when requested_expires_at is null then null else
|
|
338
|
+
trunc(extract(epoch from requested_expires_at)*1000000)::numeric end)::text,'UTF8')),'hex');
|
|
339
|
+
select * into prior from agent_bridge.control_requests where request_id=requested_request_id;
|
|
340
|
+
if found then
|
|
341
|
+
if prior.operation<>'provision' or prior.fingerprint<>canonical_fingerprint then
|
|
342
|
+
raise exception 'request id was already used with different content';
|
|
343
|
+
end if;
|
|
344
|
+
return query select prior.result->>'workspace_id',(prior.result->>'agent_id')::uuid,
|
|
345
|
+
(prior.result->>'credential_id')::uuid,true;
|
|
346
|
+
return;
|
|
347
|
+
end if;
|
|
348
|
+
if requested_expires_at is not null and requested_expires_at<=clock_timestamp() then
|
|
349
|
+
raise exception 'invalid provisioning request';
|
|
350
|
+
end if;
|
|
351
|
+
select scopes into canonical_scopes from agent_bridge.credential_scope_sets
|
|
352
|
+
where name=requested_scope_set_name;
|
|
353
|
+
if not found then raise exception 'unknown credential scope set'; end if;
|
|
354
|
+
insert into agent_bridge.workspaces(id,name) values(requested_workspace_id,requested_workspace_name)
|
|
355
|
+
on conflict(id) do nothing;
|
|
356
|
+
select name into existing_workspace_name from agent_bridge.workspaces
|
|
357
|
+
where id=requested_workspace_id;
|
|
358
|
+
if existing_workspace_name is distinct from requested_workspace_name then
|
|
359
|
+
raise exception 'workspace id already has a different name';
|
|
360
|
+
end if;
|
|
361
|
+
insert into agent_bridge.agents(workspace_id,principal,display_name,runtime_type)
|
|
362
|
+
values(requested_workspace_id,requested_principal,requested_display_name,requested_runtime_type)
|
|
363
|
+
returning id into provisioned_agent_id;
|
|
364
|
+
perform set_config('agent_bridge.lifecycle_authorized','provision',true);
|
|
365
|
+
insert into agent_bridge.credentials(workspace_id,agent_id,token_hash,label,expires_at,scopes,scope_set_name)
|
|
366
|
+
values(requested_workspace_id,provisioned_agent_id,requested_token_hash,requested_label,
|
|
367
|
+
requested_expires_at,canonical_scopes,requested_scope_set_name)
|
|
368
|
+
returning id into provisioned_credential_id;
|
|
369
|
+
perform set_config('agent_bridge.lifecycle_authorized','',true);
|
|
370
|
+
insert into agent_bridge.control_requests(request_id,operation,fingerprint,actor,result)
|
|
371
|
+
values(requested_request_id,'provision',canonical_fingerprint,session_user,
|
|
372
|
+
jsonb_build_object('workspace_id',requested_workspace_id,'agent_id',provisioned_agent_id,
|
|
373
|
+
'credential_id',provisioned_credential_id));
|
|
374
|
+
insert into agent_bridge.control_events(request_id,operation,outcome,actor,workspace_id,principal,credential_id)
|
|
375
|
+
values(requested_request_id,'provision','succeeded',session_user,requested_workspace_id,
|
|
376
|
+
requested_principal,provisioned_credential_id);
|
|
377
|
+
return query select requested_workspace_id,provisioned_agent_id,provisioned_credential_id,false;
|
|
378
|
+
exception when unique_violation then
|
|
379
|
+
select * into prior from agent_bridge.control_requests where request_id=requested_request_id;
|
|
380
|
+
if found and prior.operation='provision' and prior.fingerprint=canonical_fingerprint then
|
|
381
|
+
return query select prior.result->>'workspace_id',(prior.result->>'agent_id')::uuid,
|
|
382
|
+
(prior.result->>'credential_id')::uuid,true;
|
|
383
|
+
return;
|
|
384
|
+
end if;
|
|
385
|
+
raise exception using errcode='23505',message='provisioning request conflicts with existing state';
|
|
386
|
+
end
|
|
387
|
+
$$;
|
|
388
|
+
|
|
389
|
+
create or replace function agent_bridge.control_credential_inventory(
|
|
390
|
+
requested_workspace_id text default null,
|
|
391
|
+
requested_after_created_at timestamptz default null,
|
|
392
|
+
requested_after_credential_id uuid default null,
|
|
393
|
+
requested_limit integer default 100
|
|
394
|
+
)
|
|
395
|
+
returns table(credential_id uuid,workspace_id text,principal text,label text,scopes text[],scope_set_name text,
|
|
396
|
+
expires_at timestamptz,revoked_at timestamptz,revoked_by text,revocation_reason text,
|
|
397
|
+
replaces_credential_id uuid,expiry_grace_until timestamptz,created_at timestamptz,last_used_at timestamptz,
|
|
398
|
+
agent_disabled_at timestamptz,workspace_disabled_at timestamptz)
|
|
399
|
+
language plpgsql stable security definer set search_path = '' as $$
|
|
400
|
+
begin
|
|
401
|
+
perform agent_bridge.assert_control_actor('inventory');
|
|
402
|
+
if requested_limit is null or requested_limit not between 1 and 1000
|
|
403
|
+
or (requested_after_created_at is null)<>(requested_after_credential_id is null)
|
|
404
|
+
or (requested_workspace_id is not null and (
|
|
405
|
+
requested_workspace_id<>btrim(requested_workspace_id)
|
|
406
|
+
or length(requested_workspace_id) not between 1 and 128
|
|
407
|
+
or requested_workspace_id ~ '[[:cntrl:]]')) then
|
|
408
|
+
raise exception 'credential inventory cursor or limit is invalid';
|
|
409
|
+
end if;
|
|
410
|
+
return query select credential.id,credential.workspace_id,agent.principal,credential.label,credential.scopes,
|
|
411
|
+
credential.scope_set_name,credential.expires_at,credential.revoked_at,credential.revoked_by,
|
|
412
|
+
credential.revocation_reason,credential.replaces_credential_id,credential.expiry_grace_until,
|
|
413
|
+
credential.created_at,credential.last_used_at,agent.disabled_at,workspace.disabled_at
|
|
414
|
+
from agent_bridge.credentials credential
|
|
415
|
+
join agent_bridge.agents agent on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
|
|
416
|
+
join agent_bridge.workspaces workspace on workspace.id=credential.workspace_id
|
|
417
|
+
where (requested_workspace_id is null or credential.workspace_id=requested_workspace_id)
|
|
418
|
+
and (requested_after_created_at is null
|
|
419
|
+
or (date_bin('1 millisecond',credential.created_at,
|
|
420
|
+
'2000-01-01 00:00:00+00'::timestamptz),credential.id)>
|
|
421
|
+
(date_bin('1 millisecond',requested_after_created_at,
|
|
422
|
+
'2000-01-01 00:00:00+00'::timestamptz),requested_after_credential_id))
|
|
423
|
+
order by date_bin('1 millisecond',credential.created_at,
|
|
424
|
+
'2000-01-01 00:00:00+00'::timestamptz),credential.id
|
|
425
|
+
limit requested_limit;
|
|
426
|
+
end
|
|
427
|
+
$$;
|
|
428
|
+
|
|
429
|
+
create or replace function agent_bridge.control_rotate_credential(
|
|
430
|
+
requested_request_id uuid, requested_predecessor_id uuid, requested_workspace_id text,
|
|
431
|
+
requested_principal text, requested_token_hash char(64), requested_label text,
|
|
432
|
+
requested_scope_set_name text, requested_expires_at timestamptz, requested_grace_until timestamptz
|
|
433
|
+
) returns table(credential_id uuid,workspace_id text,principal text,replayed boolean)
|
|
434
|
+
language plpgsql security definer set search_path = '' as $$
|
|
435
|
+
declare
|
|
436
|
+
canonical_fingerprint char(64);
|
|
437
|
+
prior agent_bridge.control_requests%rowtype;
|
|
438
|
+
predecessor_workspace_id text;
|
|
439
|
+
predecessor_agent_id uuid;
|
|
440
|
+
predecessor_expires_at timestamptz;
|
|
441
|
+
successor_id uuid;
|
|
442
|
+
canonical_scopes text[];
|
|
443
|
+
principal_value text;
|
|
444
|
+
begin
|
|
445
|
+
perform agent_bridge.assert_control_actor('operator');
|
|
446
|
+
if requested_request_id is null or requested_predecessor_id is null
|
|
447
|
+
or requested_workspace_id is null or requested_workspace_id<>btrim(requested_workspace_id)
|
|
448
|
+
or length(requested_workspace_id) not between 1 and 128
|
|
449
|
+
or requested_workspace_id ~ '[[:cntrl:]]'
|
|
450
|
+
or requested_principal is null or requested_principal<>btrim(requested_principal)
|
|
451
|
+
or length(requested_principal) not between 1 and 128
|
|
452
|
+
or requested_principal ~ '[[:cntrl:]]'
|
|
453
|
+
or requested_token_hash is null
|
|
454
|
+
or requested_token_hash::text !~ '^[0-9a-f]{64}$'
|
|
455
|
+
or (requested_label is not null and (
|
|
456
|
+
requested_label<>btrim(requested_label) or length(requested_label) not between 1 and 128
|
|
457
|
+
or requested_label ~ '[[:cntrl:]]'))
|
|
458
|
+
or requested_scope_set_name is null
|
|
459
|
+
or requested_scope_set_name<>btrim(requested_scope_set_name)
|
|
460
|
+
or length(requested_scope_set_name) not between 1 and 128
|
|
461
|
+
or requested_scope_set_name ~ '[[:cntrl:]]' then
|
|
462
|
+
raise exception 'invalid credential rotation request';
|
|
463
|
+
end if;
|
|
464
|
+
perform pg_catalog.pg_advisory_xact_lock(
|
|
465
|
+
pg_catalog.hashtextextended(requested_request_id::text,1646705660)
|
|
466
|
+
);
|
|
467
|
+
canonical_fingerprint := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(pg_catalog.jsonb_build_array(requested_predecessor_id,
|
|
468
|
+
requested_workspace_id,requested_principal,requested_token_hash::text,requested_label,requested_scope_set_name,
|
|
469
|
+
case when requested_expires_at is null then null else
|
|
470
|
+
trunc(extract(epoch from requested_expires_at)*1000000)::numeric end,
|
|
471
|
+
case when requested_grace_until is null then null else
|
|
472
|
+
trunc(extract(epoch from requested_grace_until)*1000000)::numeric end)::text,'UTF8')),'hex');
|
|
473
|
+
select * into prior from agent_bridge.control_requests where request_id=requested_request_id;
|
|
474
|
+
if found then
|
|
475
|
+
if prior.operation<>'rotate' or prior.fingerprint<>canonical_fingerprint then
|
|
476
|
+
raise exception 'request id was already used with different content'; end if;
|
|
477
|
+
return query select (prior.result->>'credential_id')::uuid,prior.result->>'workspace_id',
|
|
478
|
+
prior.result->>'principal',true;
|
|
479
|
+
return;
|
|
480
|
+
end if;
|
|
481
|
+
select credential.workspace_id,credential.agent_id,credential.expires_at,agent.principal
|
|
482
|
+
into predecessor_workspace_id,predecessor_agent_id,predecessor_expires_at,principal_value
|
|
483
|
+
from agent_bridge.credentials credential join agent_bridge.agents agent
|
|
484
|
+
on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
|
|
485
|
+
join agent_bridge.workspaces workspace on workspace.id=credential.workspace_id
|
|
486
|
+
where credential.id=requested_predecessor_id
|
|
487
|
+
and credential.workspace_id=requested_workspace_id
|
|
488
|
+
and agent.principal=requested_principal
|
|
489
|
+
and credential.revoked_at is null
|
|
490
|
+
and (credential.expires_at is null or credential.expires_at>clock_timestamp())
|
|
491
|
+
and agent.disabled_at is null
|
|
492
|
+
and workspace.disabled_at is null
|
|
493
|
+
and not exists(select 1 from agent_bridge.credentials successor
|
|
494
|
+
where successor.replaces_credential_id=credential.id)
|
|
495
|
+
for update of credential;
|
|
496
|
+
if not found then raise exception 'predecessor credential is not active and replaceable'; end if;
|
|
497
|
+
if (requested_expires_at is not null and requested_expires_at<=clock_timestamp())
|
|
498
|
+
or (requested_grace_until is not null and predecessor_expires_at is not null
|
|
499
|
+
and requested_grace_until>predecessor_expires_at)
|
|
500
|
+
or (requested_grace_until is not null and requested_expires_at is not null
|
|
501
|
+
and requested_grace_until>requested_expires_at)
|
|
502
|
+
or (requested_grace_until is not null and requested_grace_until<=clock_timestamp()) then
|
|
503
|
+
raise exception 'credential grace is invalid'; end if;
|
|
504
|
+
select scopes into canonical_scopes from agent_bridge.credential_scope_sets
|
|
505
|
+
where name=requested_scope_set_name;
|
|
506
|
+
if not found then raise exception 'unknown credential scope set'; end if;
|
|
507
|
+
perform set_config('agent_bridge.lifecycle_authorized','replacement',true);
|
|
508
|
+
insert into agent_bridge.credentials(workspace_id,agent_id,token_hash,label,expires_at,scopes,
|
|
509
|
+
scope_set_name,replaces_credential_id) values(predecessor_workspace_id,predecessor_agent_id,
|
|
510
|
+
requested_token_hash,requested_label,requested_expires_at,canonical_scopes,
|
|
511
|
+
requested_scope_set_name,requested_predecessor_id) returning id into successor_id;
|
|
512
|
+
update agent_bridge.credentials set expiry_grace_until=requested_grace_until
|
|
513
|
+
where id=requested_predecessor_id;
|
|
514
|
+
perform set_config('agent_bridge.lifecycle_authorized','',true);
|
|
515
|
+
insert into agent_bridge.control_requests(request_id,operation,fingerprint,actor,result)
|
|
516
|
+
values(requested_request_id,'rotate',canonical_fingerprint,session_user,
|
|
517
|
+
jsonb_build_object('credential_id',successor_id,'workspace_id',predecessor_workspace_id,
|
|
518
|
+
'principal',principal_value));
|
|
519
|
+
insert into agent_bridge.control_events(request_id,operation,outcome,actor,workspace_id,principal,
|
|
520
|
+
credential_id,related_credential_id) values(requested_request_id,'rotate','succeeded',session_user,
|
|
521
|
+
predecessor_workspace_id,principal_value,successor_id,requested_predecessor_id);
|
|
522
|
+
return query select successor_id,predecessor_workspace_id,principal_value,false;
|
|
523
|
+
exception when unique_violation then
|
|
524
|
+
raise exception using errcode='23505',message='credential rotation conflicts with existing state';
|
|
525
|
+
end
|
|
526
|
+
$$;
|
|
527
|
+
|
|
528
|
+
create or replace function agent_bridge.control_revoke_credential(
|
|
529
|
+
requested_request_id uuid, requested_credential_id uuid, requested_reason_code text
|
|
530
|
+
) returns table(revoked boolean,replayed boolean)
|
|
531
|
+
language plpgsql security definer set search_path = '' as $$
|
|
532
|
+
declare
|
|
533
|
+
canonical_fingerprint char(64);
|
|
534
|
+
prior agent_bridge.control_requests%rowtype;
|
|
535
|
+
workspace_value text;
|
|
536
|
+
principal_value text;
|
|
537
|
+
begin
|
|
538
|
+
perform agent_bridge.assert_control_actor('operator');
|
|
539
|
+
if requested_request_id is null or requested_credential_id is null
|
|
540
|
+
or requested_reason_code is null
|
|
541
|
+
or requested_reason_code not in ('operator_request','rotation','compromise','retired') then
|
|
542
|
+
raise exception 'credential revocation reason is invalid'; end if;
|
|
543
|
+
perform pg_catalog.pg_advisory_xact_lock(
|
|
544
|
+
pg_catalog.hashtextextended(requested_request_id::text,1646705660)
|
|
545
|
+
);
|
|
546
|
+
canonical_fingerprint := pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
547
|
+
pg_catalog.jsonb_build_array(requested_credential_id,requested_reason_code)::text,'UTF8')),'hex');
|
|
548
|
+
select * into prior from agent_bridge.control_requests where request_id=requested_request_id;
|
|
549
|
+
if found then
|
|
550
|
+
if prior.operation<>'revoke' or prior.fingerprint<>canonical_fingerprint then
|
|
551
|
+
raise exception 'request id was already used with different content'; end if;
|
|
552
|
+
return query select (prior.result->>'revoked')::boolean,true; return;
|
|
553
|
+
end if;
|
|
554
|
+
select credential.workspace_id,agent.principal into workspace_value,principal_value
|
|
555
|
+
from agent_bridge.credentials credential join agent_bridge.agents agent
|
|
556
|
+
on agent.id=credential.agent_id and agent.workspace_id=credential.workspace_id
|
|
557
|
+
where credential.id=requested_credential_id for update of credential;
|
|
558
|
+
if not found then raise exception 'credential was not found'; end if;
|
|
559
|
+
perform set_config('agent_bridge.lifecycle_authorized','revocation',true);
|
|
560
|
+
update agent_bridge.credentials set revoked_at=clock_timestamp(),revoked_by=session_user,
|
|
561
|
+
revocation_reason=requested_reason_code where id=requested_credential_id and revoked_at is null;
|
|
562
|
+
if not found then raise exception 'credential is already revoked'; end if;
|
|
563
|
+
perform set_config('agent_bridge.lifecycle_authorized','',true);
|
|
564
|
+
insert into agent_bridge.control_requests(request_id,operation,fingerprint,actor,result)
|
|
565
|
+
values(requested_request_id,'revoke',canonical_fingerprint,session_user,jsonb_build_object('revoked',true));
|
|
566
|
+
insert into agent_bridge.control_events(request_id,operation,outcome,actor,workspace_id,principal,
|
|
567
|
+
credential_id,reason_code) values(requested_request_id,'revoke','succeeded',session_user,
|
|
568
|
+
workspace_value,principal_value,requested_credential_id,requested_reason_code);
|
|
569
|
+
return query select true,false;
|
|
570
|
+
end
|
|
571
|
+
$$;
|
|
572
|
+
|
|
573
|
+
create or replace function agent_bridge.register_control_member(
|
|
574
|
+
requested_request_id uuid, requested_member_role name, requested_control_role text
|
|
575
|
+
) returns table(replayed boolean)
|
|
576
|
+
language plpgsql security definer set search_path = '' as $$
|
|
577
|
+
declare
|
|
578
|
+
suffix text := substr(md5(current_database()),1,16);
|
|
579
|
+
target_role name;
|
|
580
|
+
prior agent_bridge.control_membership_events%rowtype;
|
|
581
|
+
member_record pg_catalog.pg_roles%rowtype;
|
|
582
|
+
begin
|
|
583
|
+
if requested_request_id is null or requested_member_role is null
|
|
584
|
+
or requested_control_role is null
|
|
585
|
+
or requested_control_role not in ('operator','auditor') then
|
|
586
|
+
raise exception 'invalid control membership registration';
|
|
587
|
+
end if;
|
|
588
|
+
target_role := ('agent_bridge_control_'||requested_control_role||'_'||suffix)::name;
|
|
589
|
+
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
590
|
+
'control-membership'||chr(31)||requested_member_role||chr(31)||'global',
|
|
591
|
+
1646705661
|
|
592
|
+
));
|
|
593
|
+
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
594
|
+
'control-membership'||chr(31)||requested_member_role||chr(31)||requested_control_role,
|
|
595
|
+
1646705661
|
|
596
|
+
));
|
|
597
|
+
perform pg_catalog.pg_advisory_xact_lock(
|
|
598
|
+
pg_catalog.hashtextextended(requested_request_id::text,1646705660)
|
|
599
|
+
);
|
|
600
|
+
select * into prior from agent_bridge.control_membership_events
|
|
601
|
+
where request_id=requested_request_id;
|
|
602
|
+
if found then
|
|
603
|
+
if prior.action<>'register' or prior.member_role<>requested_member_role
|
|
604
|
+
or prior.control_role<>requested_control_role then
|
|
605
|
+
raise exception 'request id was already used with different content';
|
|
606
|
+
end if;
|
|
607
|
+
return query select true;
|
|
608
|
+
return;
|
|
609
|
+
end if;
|
|
610
|
+
select * into member_record from pg_catalog.pg_roles where rolname=requested_member_role;
|
|
611
|
+
if not found or not member_record.rolcanlogin or member_record.rolsuper
|
|
612
|
+
or member_record.rolcreaterole or member_record.rolcreatedb
|
|
613
|
+
or member_record.rolreplication or member_record.rolbypassrls
|
|
614
|
+
or requested_member_role in (
|
|
615
|
+
current_user::name,
|
|
616
|
+
('agent_bridge_control_owner_'||suffix)::name,
|
|
617
|
+
('agent_bridge_control_operator_'||suffix)::name,
|
|
618
|
+
('agent_bridge_control_auditor_'||suffix)::name
|
|
619
|
+
) then
|
|
620
|
+
raise exception 'control membership target is not an eligible login';
|
|
621
|
+
end if;
|
|
622
|
+
if exists (
|
|
623
|
+
select 1 from pg_catalog.pg_roles inherited
|
|
624
|
+
where inherited.rolname<>requested_member_role
|
|
625
|
+
and pg_catalog.pg_has_role(requested_member_role,inherited.rolname,'MEMBER')
|
|
626
|
+
and inherited.rolname<>target_role
|
|
627
|
+
and not exists (
|
|
628
|
+
select 1 from (
|
|
629
|
+
select distinct on (event.control_role)
|
|
630
|
+
event.control_role,event.action
|
|
631
|
+
from agent_bridge.control_membership_events event
|
|
632
|
+
where event.member_role=requested_member_role
|
|
633
|
+
order by event.control_role,event.sequence desc
|
|
634
|
+
) latest
|
|
635
|
+
where latest.action='register'
|
|
636
|
+
and inherited.rolname=(
|
|
637
|
+
'agent_bridge_control_'||latest.control_role||'_'||suffix
|
|
638
|
+
)::name
|
|
639
|
+
)
|
|
640
|
+
) or exists (
|
|
641
|
+
select 1 from pg_catalog.pg_roles candidate
|
|
642
|
+
where candidate.rolname not in (requested_member_role,current_user)
|
|
643
|
+
and not candidate.rolsuper
|
|
644
|
+
and pg_catalog.pg_has_role(candidate.rolname,requested_member_role,'MEMBER')
|
|
645
|
+
) then
|
|
646
|
+
raise exception 'control membership target has an unsafe membership graph';
|
|
647
|
+
end if;
|
|
648
|
+
if exists (
|
|
649
|
+
select 1 from pg_catalog.pg_auth_members membership
|
|
650
|
+
join pg_catalog.pg_roles granted on granted.oid=membership.roleid
|
|
651
|
+
join pg_catalog.pg_roles member on member.oid=membership.member
|
|
652
|
+
join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
|
|
653
|
+
where granted.rolname=target_role and member.rolname=requested_member_role
|
|
654
|
+
and (
|
|
655
|
+
membership.admin_option
|
|
656
|
+
or not coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true)
|
|
657
|
+
or not coalesce((to_jsonb(membership)->>'set_option')::boolean,true)
|
|
658
|
+
or grantor.rolname<>current_user
|
|
659
|
+
)
|
|
660
|
+
) then
|
|
661
|
+
raise exception 'control membership target has an unsafe direct grant';
|
|
662
|
+
end if;
|
|
663
|
+
execute format('grant %I to %I',target_role,requested_member_role);
|
|
664
|
+
insert into agent_bridge.control_membership_events(
|
|
665
|
+
request_id,action,member_role,control_role,actor
|
|
666
|
+
) values (
|
|
667
|
+
requested_request_id,'register',requested_member_role,requested_control_role,session_user
|
|
668
|
+
);
|
|
669
|
+
return query select false;
|
|
670
|
+
end
|
|
671
|
+
$$;
|
|
672
|
+
|
|
673
|
+
create or replace function agent_bridge.revoke_control_member(
|
|
674
|
+
requested_request_id uuid, requested_member_role name, requested_control_role text
|
|
675
|
+
) returns table(replayed boolean)
|
|
676
|
+
language plpgsql security definer set search_path = '' as $$
|
|
677
|
+
declare
|
|
678
|
+
suffix text := substr(md5(current_database()),1,16);
|
|
679
|
+
target_role name;
|
|
680
|
+
prior agent_bridge.control_membership_events%rowtype;
|
|
681
|
+
begin
|
|
682
|
+
if requested_request_id is null or requested_member_role is null
|
|
683
|
+
or requested_control_role is null
|
|
684
|
+
or requested_control_role not in ('operator','auditor')
|
|
685
|
+
or requested_member_role in (
|
|
686
|
+
current_user::name,
|
|
687
|
+
('agent_bridge_control_owner_'||suffix)::name,
|
|
688
|
+
('agent_bridge_control_operator_'||suffix)::name,
|
|
689
|
+
('agent_bridge_control_auditor_'||suffix)::name
|
|
690
|
+
) then
|
|
691
|
+
raise exception 'invalid control membership revocation';
|
|
692
|
+
end if;
|
|
693
|
+
target_role := ('agent_bridge_control_'||requested_control_role||'_'||suffix)::name;
|
|
694
|
+
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
695
|
+
'control-membership'||chr(31)||requested_member_role||chr(31)||'global',
|
|
696
|
+
1646705661
|
|
697
|
+
));
|
|
698
|
+
perform pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended(
|
|
699
|
+
'control-membership'||chr(31)||requested_member_role||chr(31)||requested_control_role,
|
|
700
|
+
1646705661
|
|
701
|
+
));
|
|
702
|
+
perform pg_catalog.pg_advisory_xact_lock(
|
|
703
|
+
pg_catalog.hashtextextended(requested_request_id::text,1646705660)
|
|
704
|
+
);
|
|
705
|
+
select * into prior from agent_bridge.control_membership_events
|
|
706
|
+
where request_id=requested_request_id;
|
|
707
|
+
if found then
|
|
708
|
+
if prior.action<>'revoke' or prior.member_role<>requested_member_role
|
|
709
|
+
or prior.control_role<>requested_control_role then
|
|
710
|
+
raise exception 'request id was already used with different content';
|
|
711
|
+
end if;
|
|
712
|
+
return query select true;
|
|
713
|
+
return;
|
|
714
|
+
end if;
|
|
715
|
+
if exists(select 1 from pg_catalog.pg_roles where rolname=requested_member_role) then
|
|
716
|
+
execute format('revoke %I from %I',target_role,requested_member_role);
|
|
717
|
+
end if;
|
|
718
|
+
insert into agent_bridge.control_membership_events(
|
|
719
|
+
request_id,action,member_role,control_role,actor
|
|
720
|
+
) values (
|
|
721
|
+
requested_request_id,'revoke',requested_member_role,requested_control_role,session_user
|
|
722
|
+
);
|
|
723
|
+
return query select false;
|
|
724
|
+
end
|
|
725
|
+
$$;
|
|
726
|
+
|
|
727
|
+
create or replace function agent_bridge.current_request_workspace() returns text
|
|
728
|
+
language sql stable security definer set search_path = '' as $$
|
|
729
|
+
select authority.workspace_id
|
|
730
|
+
from agent_bridge.request_authorities authority
|
|
731
|
+
where pg_catalog.pg_has_role(
|
|
732
|
+
session_user,
|
|
733
|
+
('agent_bridge_runtime_' || substr(pg_catalog.md5(pg_catalog.current_database()),1,16))::name,
|
|
734
|
+
'MEMBER'
|
|
735
|
+
)
|
|
736
|
+
and authority.backend_pid=pg_catalog.pg_backend_pid()
|
|
737
|
+
and authority.transaction_id=pg_catalog.pg_current_xact_id_if_assigned()
|
|
738
|
+
and authority.opened_session_user=session_user
|
|
739
|
+
$$;
|
|
740
|
+
|
|
741
|
+
create or replace function agent_bridge.current_request_principal() returns text
|
|
742
|
+
language sql stable security definer set search_path = '' as $$
|
|
743
|
+
select authority.principal
|
|
744
|
+
from agent_bridge.request_authorities authority
|
|
745
|
+
where pg_catalog.pg_has_role(
|
|
746
|
+
session_user,
|
|
747
|
+
('agent_bridge_runtime_' || substr(pg_catalog.md5(pg_catalog.current_database()),1,16))::name,
|
|
748
|
+
'MEMBER'
|
|
749
|
+
)
|
|
750
|
+
and authority.backend_pid=pg_catalog.pg_backend_pid()
|
|
751
|
+
and authority.transaction_id=pg_catalog.pg_current_xact_id_if_assigned()
|
|
752
|
+
and authority.opened_session_user=session_user
|
|
753
|
+
$$;
|
|
754
|
+
|
|
755
|
+
create or replace function agent_bridge.row_isolation_catalog_definition() returns text
|
|
756
|
+
language sql stable set search_path = '' as $$
|
|
757
|
+
with domain_tables(table_name) as (
|
|
758
|
+
values ('messages'),('receipts'),('deliveries'),('delivery_events'),('agent_instances')
|
|
759
|
+
), catalog_objects(kind,identity,definition) as (
|
|
760
|
+
select 'policy',relation.relname || '.' || policy.polname,
|
|
761
|
+
policy.polcmd::text || ':' || policy.polpermissive::text || ':' ||
|
|
762
|
+
coalesce(pg_catalog.pg_get_expr(policy.polqual,policy.polrelid),'') || ':' ||
|
|
763
|
+
coalesce(pg_catalog.pg_get_expr(policy.polwithcheck,policy.polrelid),'')
|
|
764
|
+
from pg_catalog.pg_policy policy
|
|
765
|
+
join pg_catalog.pg_class relation on relation.oid=policy.polrelid
|
|
766
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
767
|
+
where namespace.nspname='agent_bridge'
|
|
768
|
+
and relation.relname in (select table_name from domain_tables)
|
|
769
|
+
union all
|
|
770
|
+
select 'constraint',constraint_record.conrelid::regclass::text || '.' || constraint_record.conname,
|
|
771
|
+
pg_catalog.pg_get_constraintdef(constraint_record.oid,true)
|
|
772
|
+
from pg_catalog.pg_constraint constraint_record
|
|
773
|
+
join pg_catalog.pg_class constraint_relation on constraint_relation.oid=constraint_record.conrelid
|
|
774
|
+
join pg_catalog.pg_namespace constraint_namespace on constraint_namespace.oid=constraint_relation.relnamespace
|
|
775
|
+
where constraint_namespace.nspname='agent_bridge'
|
|
776
|
+
and constraint_relation.relname in (
|
|
777
|
+
select table_name from domain_tables
|
|
778
|
+
union all select 'row_isolation_attestations'
|
|
779
|
+
)
|
|
780
|
+
union all
|
|
781
|
+
select 'trigger',trigger.tgrelid::regclass::text || '.' || trigger.tgname,
|
|
782
|
+
pg_catalog.pg_get_triggerdef(trigger.oid,true)
|
|
783
|
+
from pg_catalog.pg_trigger trigger
|
|
784
|
+
join pg_catalog.pg_class trigger_relation on trigger_relation.oid=trigger.tgrelid
|
|
785
|
+
join pg_catalog.pg_namespace trigger_namespace on trigger_namespace.oid=trigger_relation.relnamespace
|
|
786
|
+
where not trigger.tgisinternal
|
|
787
|
+
and trigger_namespace.nspname='agent_bridge'
|
|
788
|
+
and trigger_relation.relname in (select table_name from domain_tables)
|
|
789
|
+
union all
|
|
790
|
+
select 'function',procedure.oid::regprocedure::text,
|
|
791
|
+
pg_catalog.pg_get_userbyid(procedure.proowner) || ':' ||
|
|
792
|
+
pg_catalog.pg_get_functiondef(procedure.oid)
|
|
793
|
+
from pg_catalog.pg_proc procedure
|
|
794
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
795
|
+
where namespace.nspname='agent_bridge' and procedure.proname in (
|
|
796
|
+
'current_request_workspace','current_request_principal','record_delivery_event',
|
|
797
|
+
'reject_delivery_identity_mutation','enforce_delivery_actor_role',
|
|
798
|
+
'reject_delivery_event_mutation','row_isolation_catalog_definition'
|
|
799
|
+
)
|
|
800
|
+
)
|
|
801
|
+
select string_agg(kind || E'\x1f' || identity || E'\x1f' || definition,E'\x1e'
|
|
802
|
+
order by kind,identity)
|
|
803
|
+
from catalog_objects
|
|
804
|
+
$$;
|
|
805
|
+
|
|
806
|
+
create or replace function agent_bridge.credential_security_prerequisite_definition()
|
|
807
|
+
returns text language sql stable set search_path = '' set timezone = 'UTC' as $$
|
|
808
|
+
with dependency_relations(relation_name) as (values
|
|
809
|
+
('credentials'),('credential_scope_sets'),('security_events'),
|
|
810
|
+
('rate_limit_policies'),('rate_limit_buckets')
|
|
811
|
+
), protected_functions(function_name) as (values
|
|
812
|
+
('canonicalize_scopes'),('validate_credential_security'),
|
|
813
|
+
('reject_credential_delete'),('reject_scope_set_mutation'),
|
|
814
|
+
('reject_security_event_mutation'),('record_scope_denial'),
|
|
815
|
+
('consume_rate_limit'),('replace_credential'),('revoke_credential'),
|
|
816
|
+
('security_schema_ready')
|
|
817
|
+
), catalog_objects(kind,identity,definition) as (
|
|
818
|
+
select 'relation',relation.relname,
|
|
819
|
+
case when relation.relowner=namespace.nspowner then 'schema-owner' else 'unexpected-owner' end
|
|
820
|
+
||':'||relation.relkind::text||':'||relation.relpersistence::text||':'||
|
|
821
|
+
relation.relrowsecurity::text||':'||relation.relforcerowsecurity::text
|
|
822
|
+
from pg_catalog.pg_class relation
|
|
823
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
824
|
+
where namespace.nspname='agent_bridge'
|
|
825
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
826
|
+
union all
|
|
827
|
+
select 'column',relation.relname||'.'||attribute.attname,
|
|
828
|
+
pg_catalog.format_type(attribute.atttypid,attribute.atttypmod)||':'||
|
|
829
|
+
attribute.attnotnull::text||':'||attribute.attidentity::text||':'||
|
|
830
|
+
attribute.attgenerated::text||':'||coalesce(pg_catalog.pg_get_expr(
|
|
831
|
+
default_record.adbin,default_record.adrelid),'')
|
|
832
|
+
from pg_catalog.pg_attribute attribute
|
|
833
|
+
join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
|
|
834
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
835
|
+
left join pg_catalog.pg_attrdef default_record on default_record.adrelid=attribute.attrelid
|
|
836
|
+
and default_record.adnum=attribute.attnum
|
|
837
|
+
where namespace.nspname='agent_bridge'
|
|
838
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
839
|
+
and attribute.attnum>0 and not attribute.attisdropped
|
|
840
|
+
union all
|
|
841
|
+
select 'constraint',relation.relname||'.'||constraint_record.conname,
|
|
842
|
+
constraint_record.convalidated::text||':'||
|
|
843
|
+
pg_catalog.pg_get_constraintdef(constraint_record.oid,true)
|
|
844
|
+
from pg_catalog.pg_constraint constraint_record
|
|
845
|
+
join pg_catalog.pg_class relation on relation.oid=constraint_record.conrelid
|
|
846
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
847
|
+
where namespace.nspname='agent_bridge'
|
|
848
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
849
|
+
union all
|
|
850
|
+
select 'index',index_relation.relname,
|
|
851
|
+
index_record.indisunique::text||':'||index_record.indisvalid::text||':'||
|
|
852
|
+
index_record.indisready::text||':'||pg_catalog.pg_get_indexdef(index_record.indexrelid)||':'||
|
|
853
|
+
coalesce(pg_catalog.pg_get_expr(index_record.indpred,index_record.indrelid),'')
|
|
854
|
+
from pg_catalog.pg_index index_record
|
|
855
|
+
join pg_catalog.pg_class source_relation on source_relation.oid=index_record.indrelid
|
|
856
|
+
join pg_catalog.pg_class index_relation on index_relation.oid=index_record.indexrelid
|
|
857
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=source_relation.relnamespace
|
|
858
|
+
where namespace.nspname='agent_bridge'
|
|
859
|
+
and source_relation.relname in (select relation_name from dependency_relations)
|
|
860
|
+
union all
|
|
861
|
+
select 'trigger',relation.relname||'.'||trigger.tgname,
|
|
862
|
+
trigger.tgenabled::text||':'||pg_catalog.pg_get_triggerdef(trigger.oid,true)
|
|
863
|
+
from pg_catalog.pg_trigger trigger
|
|
864
|
+
join pg_catalog.pg_class relation on relation.oid=trigger.tgrelid
|
|
865
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
866
|
+
where not trigger.tgisinternal and namespace.nspname='agent_bridge'
|
|
867
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
868
|
+
union all
|
|
869
|
+
select 'function',procedure.oid::regprocedure::text,
|
|
870
|
+
case when procedure.proowner=namespace.nspowner then 'schema-owner' else 'unexpected-owner' end
|
|
871
|
+
||':'||pg_catalog.pg_get_functiondef(procedure.oid)
|
|
872
|
+
from pg_catalog.pg_proc procedure
|
|
873
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
874
|
+
where namespace.nspname='agent_bridge'
|
|
875
|
+
and procedure.proname in (select function_name from protected_functions)
|
|
876
|
+
)
|
|
877
|
+
select string_agg(kind||E'\x1f'||identity||E'\x1f'||definition,E'\x1e'
|
|
878
|
+
order by kind,identity,definition) from catalog_objects
|
|
879
|
+
$$;
|
|
880
|
+
|
|
881
|
+
create or replace function agent_bridge.owner_control_catalog_definition() returns text
|
|
882
|
+
language sql stable set search_path = '' set timezone = 'UTC' as $$
|
|
883
|
+
with names as (select
|
|
884
|
+
('agent_bridge_control_owner_'||substr(md5(current_database()),1,16))::name owner_role,
|
|
885
|
+
('agent_bridge_control_operator_'||substr(md5(current_database()),1,16))::name operator_role,
|
|
886
|
+
('agent_bridge_control_auditor_'||substr(md5(current_database()),1,16))::name auditor_role,
|
|
887
|
+
('agent_bridge_runtime_'||substr(md5(current_database()),1,16))::name runtime_role,
|
|
888
|
+
(select nspowner from pg_catalog.pg_namespace where nspname='agent_bridge') schema_owner
|
|
889
|
+
), control_roles(role_name) as (
|
|
890
|
+
select owner_role from names union all select operator_role from names
|
|
891
|
+
union all select auditor_role from names
|
|
892
|
+
), protected_functions(function_name) as (values
|
|
893
|
+
('reject_control_ledger_mutation'),('validate_credential_security'),('control_provision'),
|
|
894
|
+
('control_credential_inventory'),('control_rotate_credential'),('control_revoke_credential'),
|
|
895
|
+
('owner_control_catalog_definition'),('owner_control_plane_ready'),('canonicalize_scopes'),
|
|
896
|
+
('replace_credential'),('revoke_credential'),('security_schema_ready'),
|
|
897
|
+
('validate_credential_security'),('reject_credential_delete'),
|
|
898
|
+
('reject_scope_set_mutation'),('reject_security_event_mutation'),
|
|
899
|
+
('record_scope_denial'),('consume_rate_limit'),
|
|
900
|
+
('current_request_workspace'),('current_request_principal'),
|
|
901
|
+
('row_isolation_catalog_definition'),('credential_security_prerequisite_definition'),
|
|
902
|
+
('assert_control_actor'),('register_control_member'),('revoke_control_member')
|
|
903
|
+
), dependency_relations(relation_name) as (values
|
|
904
|
+
('control_requests'),('control_events'),('owner_control_attestations'),('workspaces'),
|
|
905
|
+
('control_membership_events'),('agents'),('credentials'),('credential_scope_sets')
|
|
906
|
+
), dependency_columns(relation_name,column_name) as (values
|
|
907
|
+
('control_requests','request_id'),('control_requests','operation'),
|
|
908
|
+
('control_requests','fingerprint'),('control_requests','actor'),
|
|
909
|
+
('control_requests','result'),('control_requests','created_at'),
|
|
910
|
+
('control_events','sequence'),('control_events','event_id'),
|
|
911
|
+
('control_events','request_id'),('control_events','operation'),
|
|
912
|
+
('control_events','outcome'),('control_events','actor'),
|
|
913
|
+
('control_events','workspace_id'),('control_events','principal'),
|
|
914
|
+
('control_events','credential_id'),('control_events','related_credential_id'),
|
|
915
|
+
('control_events','reason_code'),('control_events','created_at'),
|
|
916
|
+
('owner_control_attestations','name'),('owner_control_attestations','catalog_definition'),
|
|
917
|
+
('owner_control_attestations','attested_at'),
|
|
918
|
+
('control_membership_events','sequence'),('control_membership_events','request_id'),
|
|
919
|
+
('control_membership_events','action'),('control_membership_events','member_role'),
|
|
920
|
+
('control_membership_events','control_role'),('control_membership_events','actor'),
|
|
921
|
+
('control_membership_events','created_at'),
|
|
922
|
+
('workspaces','id'),('workspaces','name'),('workspaces','disabled_at'),
|
|
923
|
+
('agents','id'),('agents','workspace_id'),('agents','principal'),
|
|
924
|
+
('agents','display_name'),('agents','runtime_type'),('agents','disabled_at'),
|
|
925
|
+
('credentials','id'),('credentials','workspace_id'),('credentials','agent_id'),
|
|
926
|
+
('credentials','token_hash'),('credentials','label'),('credentials','expires_at'),
|
|
927
|
+
('credentials','revoked_at'),('credentials','created_at'),('credentials','last_used_at'),
|
|
928
|
+
('credentials','scopes'),('credentials','scope_set_name'),
|
|
929
|
+
('credentials','replaces_credential_id'),('credentials','revoked_by'),
|
|
930
|
+
('credentials','revocation_reason'),('credentials','expiry_grace_until'),
|
|
931
|
+
('credential_scope_sets','name'),('credential_scope_sets','scopes')
|
|
932
|
+
), catalog_objects(kind,identity,definition) as (
|
|
933
|
+
select 'role',role.rolname,
|
|
934
|
+
concat_ws(':',role.rolsuper,role.rolinherit,role.rolcreaterole,role.rolcreatedb,
|
|
935
|
+
role.rolcanlogin,role.rolreplication,role.rolbypassrls,role.rolconnlimit,
|
|
936
|
+
coalesce(role.rolvaliduntil::text,''),coalesce(role.rolconfig::text,''))
|
|
937
|
+
from pg_catalog.pg_roles role where role.rolname in (select role_name from control_roles)
|
|
938
|
+
union all
|
|
939
|
+
select 'membership',granted.rolname||'->'||member.rolname,
|
|
940
|
+
grantor.rolname||':'||membership.admin_option::text||':'||
|
|
941
|
+
coalesce(to_jsonb(membership)->>'inherit_option','')||':'||
|
|
942
|
+
coalesce(to_jsonb(membership)->>'set_option','')
|
|
943
|
+
from pg_catalog.pg_auth_members membership
|
|
944
|
+
join pg_catalog.pg_roles granted on granted.oid=membership.roleid
|
|
945
|
+
join pg_catalog.pg_roles member on member.oid=membership.member
|
|
946
|
+
join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
|
|
947
|
+
where member.rolname in (select role_name from control_roles)
|
|
948
|
+
or granted.rolname=(select owner_role from names)
|
|
949
|
+
union all
|
|
950
|
+
select 'schema',namespace.nspname,pg_catalog.pg_get_userbyid(namespace.nspowner)
|
|
951
|
+
from pg_catalog.pg_namespace namespace where namespace.nspname='agent_bridge'
|
|
952
|
+
union all
|
|
953
|
+
select 'schema_acl',namespace.nspname,
|
|
954
|
+
pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
|
|
955
|
+
coalesce(pg_catalog.pg_get_userbyid(privilege.grantee),'PUBLIC')||':'||
|
|
956
|
+
privilege.privilege_type||':'||privilege.is_grantable::text
|
|
957
|
+
from pg_catalog.pg_namespace namespace
|
|
958
|
+
cross join lateral pg_catalog.aclexplode(coalesce(namespace.nspacl,
|
|
959
|
+
pg_catalog.acldefault('n',namespace.nspowner))) privilege
|
|
960
|
+
where namespace.nspname='agent_bridge'
|
|
961
|
+
union all
|
|
962
|
+
select 'relation',relation.oid::regclass::text,
|
|
963
|
+
pg_catalog.pg_get_userbyid(relation.relowner)||':'||relation.relkind::text||':'||
|
|
964
|
+
relation.relpersistence::text||':'||relation.relrowsecurity::text||':'||
|
|
965
|
+
relation.relforcerowsecurity::text
|
|
966
|
+
from pg_catalog.pg_class relation
|
|
967
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
968
|
+
where namespace.nspname='agent_bridge' and relation.relkind in ('r','p','S','v','m','f') and (
|
|
969
|
+
relation.relname in (select relation_name from dependency_relations)
|
|
970
|
+
or relation.relowner=(select oid from pg_catalog.pg_roles where rolname=(select owner_role from names)))
|
|
971
|
+
union all
|
|
972
|
+
select 'relation_acl',relation.oid::regclass::text,
|
|
973
|
+
pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
|
|
974
|
+
coalesce(pg_catalog.pg_get_userbyid(privilege.grantee),'PUBLIC')||':'||
|
|
975
|
+
privilege.privilege_type||':'||privilege.is_grantable::text
|
|
976
|
+
from pg_catalog.pg_class relation
|
|
977
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
978
|
+
cross join lateral pg_catalog.aclexplode(coalesce(relation.relacl,pg_catalog.acldefault(
|
|
979
|
+
case when relation.relkind='S' then 'S'::"char" else 'r'::"char" end,relation.relowner
|
|
980
|
+
))) privilege
|
|
981
|
+
where namespace.nspname='agent_bridge' and relation.relkind in ('r','p','S','v','m','f') and (
|
|
982
|
+
relation.relname in (select relation_name from dependency_relations)
|
|
983
|
+
or relation.relowner=(select oid from pg_catalog.pg_roles where rolname=(select owner_role from names))
|
|
984
|
+
or privilege.grantee in (
|
|
985
|
+
select oid from pg_catalog.pg_roles where rolname in (select role_name from control_roles)
|
|
986
|
+
))
|
|
987
|
+
union all
|
|
988
|
+
select 'index',index_relation.oid::regclass::text,
|
|
989
|
+
index_record.indisunique::text||':'||index_record.indisvalid::text||':'||
|
|
990
|
+
index_record.indisready::text||':'||pg_catalog.pg_get_indexdef(index_record.indexrelid)||':'||
|
|
991
|
+
coalesce(pg_catalog.pg_get_expr(index_record.indpred,index_record.indrelid),'')
|
|
992
|
+
from pg_catalog.pg_index index_record
|
|
993
|
+
join pg_catalog.pg_class source_relation on source_relation.oid=index_record.indrelid
|
|
994
|
+
join pg_catalog.pg_class index_relation on index_relation.oid=index_record.indexrelid
|
|
995
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=source_relation.relnamespace
|
|
996
|
+
where namespace.nspname='agent_bridge'
|
|
997
|
+
and source_relation.relname in (select relation_name from dependency_relations)
|
|
998
|
+
union all
|
|
999
|
+
select 'column',relation.oid::regclass::text||'.'||attribute.attname,
|
|
1000
|
+
pg_catalog.format_type(attribute.atttypid,attribute.atttypmod)||':'||
|
|
1001
|
+
attribute.attnotnull::text||':'||attribute.attidentity::text||':'||
|
|
1002
|
+
attribute.attgenerated::text||':'||coalesce(pg_catalog.pg_get_expr(
|
|
1003
|
+
default_record.adbin,default_record.adrelid),'')
|
|
1004
|
+
from dependency_columns dependency
|
|
1005
|
+
join pg_catalog.pg_namespace namespace on namespace.nspname='agent_bridge'
|
|
1006
|
+
join pg_catalog.pg_class relation on relation.relnamespace=namespace.oid
|
|
1007
|
+
and relation.relname=dependency.relation_name
|
|
1008
|
+
join pg_catalog.pg_attribute attribute on attribute.attrelid=relation.oid
|
|
1009
|
+
and attribute.attname=dependency.column_name and attribute.attnum>0 and not attribute.attisdropped
|
|
1010
|
+
left join pg_catalog.pg_attrdef default_record on default_record.adrelid=attribute.attrelid
|
|
1011
|
+
and default_record.adnum=attribute.attnum
|
|
1012
|
+
union all
|
|
1013
|
+
select 'column_acl',relation.oid::regclass::text||'.'||attribute.attname,
|
|
1014
|
+
pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
|
|
1015
|
+
coalesce(pg_catalog.pg_get_userbyid(privilege.grantee),'PUBLIC')||':'||
|
|
1016
|
+
privilege.privilege_type||':'||privilege.is_grantable::text
|
|
1017
|
+
from pg_catalog.pg_attribute attribute
|
|
1018
|
+
join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
|
|
1019
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1020
|
+
cross join lateral pg_catalog.aclexplode(attribute.attacl) privilege
|
|
1021
|
+
where namespace.nspname='agent_bridge' and attribute.attnum>0 and not attribute.attisdropped
|
|
1022
|
+
and attribute.attacl is not null and (
|
|
1023
|
+
relation.relname in (select relation_name from dependency_relations)
|
|
1024
|
+
or relation.relowner=(select oid from pg_catalog.pg_roles where rolname=(select owner_role from names))
|
|
1025
|
+
or privilege.grantee in (
|
|
1026
|
+
select oid from pg_catalog.pg_roles where rolname in (select role_name from control_roles)
|
|
1027
|
+
))
|
|
1028
|
+
union all
|
|
1029
|
+
select 'constraint',constraint_record.conrelid::regclass::text||'.'||constraint_record.conname,
|
|
1030
|
+
pg_catalog.pg_get_constraintdef(constraint_record.oid,true)
|
|
1031
|
+
from pg_catalog.pg_constraint constraint_record
|
|
1032
|
+
join pg_catalog.pg_class relation on relation.oid=constraint_record.conrelid
|
|
1033
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1034
|
+
where namespace.nspname='agent_bridge'
|
|
1035
|
+
and relation.relname in (select relation_name from dependency_relations)
|
|
1036
|
+
union all
|
|
1037
|
+
select 'trigger',trigger.tgrelid::regclass::text||'.'||trigger.tgname,
|
|
1038
|
+
trigger.tgenabled::text||':'||pg_catalog.pg_get_triggerdef(trigger.oid,true)
|
|
1039
|
+
from pg_catalog.pg_trigger trigger
|
|
1040
|
+
join pg_catalog.pg_class relation on relation.oid=trigger.tgrelid
|
|
1041
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1042
|
+
where not trigger.tgisinternal and namespace.nspname='agent_bridge'
|
|
1043
|
+
and relation.relname in (
|
|
1044
|
+
'control_requests','control_events','owner_control_attestations',
|
|
1045
|
+
'control_membership_events','credentials'
|
|
1046
|
+
)
|
|
1047
|
+
union all
|
|
1048
|
+
select 'function',procedure.oid::regprocedure::text,
|
|
1049
|
+
pg_catalog.pg_get_userbyid(procedure.proowner)||':'||pg_catalog.pg_get_functiondef(procedure.oid)
|
|
1050
|
+
from pg_catalog.pg_proc procedure
|
|
1051
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
1052
|
+
where namespace.nspname='agent_bridge' and (
|
|
1053
|
+
procedure.proname in (select function_name from protected_functions)
|
|
1054
|
+
or procedure.proowner=(select oid from pg_catalog.pg_roles where rolname=(select owner_role from names)))
|
|
1055
|
+
union all
|
|
1056
|
+
select 'function_acl',procedure.oid::regprocedure::text,
|
|
1057
|
+
pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
|
|
1058
|
+
coalesce(pg_catalog.pg_get_userbyid(privilege.grantee),'PUBLIC')||':'||
|
|
1059
|
+
privilege.privilege_type||':'||privilege.is_grantable::text
|
|
1060
|
+
from pg_catalog.pg_proc procedure
|
|
1061
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
1062
|
+
cross join lateral pg_catalog.aclexplode(coalesce(procedure.proacl,
|
|
1063
|
+
pg_catalog.acldefault('f',procedure.proowner))) privilege
|
|
1064
|
+
where namespace.nspname='agent_bridge' and (
|
|
1065
|
+
procedure.proname in (select function_name from protected_functions)
|
|
1066
|
+
or procedure.proowner=(select oid from pg_catalog.pg_roles where rolname=(select owner_role from names))
|
|
1067
|
+
or privilege.grantee in (select oid from pg_catalog.pg_roles where rolname in (select role_name from control_roles))
|
|
1068
|
+
or privilege.grantee=0
|
|
1069
|
+
)
|
|
1070
|
+
union all
|
|
1071
|
+
select 'default_acl',owner.rolname||':'||coalesce(namespace.nspname,'')||':'||
|
|
1072
|
+
default_acl.defaclobjtype::text,
|
|
1073
|
+
pg_catalog.pg_get_userbyid(privilege.grantor)||':'||
|
|
1074
|
+
coalesce(pg_catalog.pg_get_userbyid(privilege.grantee),'PUBLIC')||':'||
|
|
1075
|
+
privilege.privilege_type||':'||privilege.is_grantable::text
|
|
1076
|
+
from pg_catalog.pg_default_acl default_acl
|
|
1077
|
+
join pg_catalog.pg_roles owner on owner.oid=default_acl.defaclrole
|
|
1078
|
+
left join pg_catalog.pg_namespace namespace on namespace.oid=default_acl.defaclnamespace
|
|
1079
|
+
cross join lateral pg_catalog.aclexplode(default_acl.defaclacl) privilege
|
|
1080
|
+
where (namespace.nspname='agent_bridge' or default_acl.defaclnamespace=0)
|
|
1081
|
+
and default_acl.defaclrole in (
|
|
1082
|
+
(select schema_owner from names),
|
|
1083
|
+
(select oid from pg_catalog.pg_roles where rolname=(select owner_role from names))
|
|
1084
|
+
)
|
|
1085
|
+
union all
|
|
1086
|
+
select 'credential_security_prerequisite','v1',
|
|
1087
|
+
agent_bridge.credential_security_prerequisite_definition()
|
|
1088
|
+
)
|
|
1089
|
+
select string_agg(kind||E'\x1f'||identity||E'\x1f'||definition,E'\x1e'
|
|
1090
|
+
order by kind,identity,definition) from catalog_objects
|
|
1091
|
+
$$;
|
|
1092
|
+
|
|
1093
|
+
create or replace function agent_bridge.owner_control_plane_ready()
|
|
1094
|
+
returns boolean language sql stable security definer set search_path = '' as $$
|
|
1095
|
+
with names as (select
|
|
1096
|
+
('agent_bridge_control_owner_'||substr(md5(current_database()),1,16))::name owner_role,
|
|
1097
|
+
('agent_bridge_control_operator_'||substr(md5(current_database()),1,16))::name operator_role,
|
|
1098
|
+
('agent_bridge_control_auditor_'||substr(md5(current_database()),1,16))::name auditor_role,
|
|
1099
|
+
(select pg_catalog.pg_get_userbyid(nspowner)::name from pg_catalog.pg_namespace
|
|
1100
|
+
where nspname='agent_bridge') schema_owner
|
|
1101
|
+
), latest_registry as (
|
|
1102
|
+
select distinct on (event.member_role,event.control_role)
|
|
1103
|
+
event.member_role,event.control_role,event.action
|
|
1104
|
+
from agent_bridge.control_membership_events event
|
|
1105
|
+
order by event.member_role,event.control_role,event.sequence desc
|
|
1106
|
+
), active_registry as (
|
|
1107
|
+
select member_role,control_role from latest_registry where action='register'
|
|
1108
|
+
), expected_memberships as (
|
|
1109
|
+
select names.owner_role granted_role,names.schema_owner member_role,true admin_option
|
|
1110
|
+
from names
|
|
1111
|
+
union all select names.operator_role,names.schema_owner,true from names
|
|
1112
|
+
union all select names.auditor_role,names.schema_owner,true from names
|
|
1113
|
+
union all
|
|
1114
|
+
select case registry.control_role when 'operator' then names.operator_role
|
|
1115
|
+
else names.auditor_role end,registry.member_role,false
|
|
1116
|
+
from active_registry registry cross join names
|
|
1117
|
+
), actual_memberships as (
|
|
1118
|
+
select granted.rolname::name granted_role,member.rolname::name member_role,
|
|
1119
|
+
membership.admin_option,
|
|
1120
|
+
coalesce((to_jsonb(membership)->>'inherit_option')::boolean,true) inherit_option,
|
|
1121
|
+
coalesce((to_jsonb(membership)->>'set_option')::boolean,true) set_option,
|
|
1122
|
+
grantor.rolname::name grantor_role
|
|
1123
|
+
from pg_catalog.pg_auth_members membership
|
|
1124
|
+
join pg_catalog.pg_roles granted on granted.oid=membership.roleid
|
|
1125
|
+
join pg_catalog.pg_roles member on member.oid=membership.member
|
|
1126
|
+
join pg_catalog.pg_roles grantor on grantor.oid=membership.grantor
|
|
1127
|
+
where granted.rolname in (
|
|
1128
|
+
(select owner_role from names),(select operator_role from names),(select auditor_role from names)
|
|
1129
|
+
)
|
|
1130
|
+
), authority_closure as (
|
|
1131
|
+
select control_role.granted_role,candidate.rolname::name member_role
|
|
1132
|
+
from names
|
|
1133
|
+
cross join lateral (values
|
|
1134
|
+
(names.owner_role),(names.operator_role),(names.auditor_role)
|
|
1135
|
+
) control_role(granted_role)
|
|
1136
|
+
cross join pg_catalog.pg_roles candidate
|
|
1137
|
+
where candidate.rolname not in (names.owner_role,names.operator_role,names.auditor_role)
|
|
1138
|
+
and (not candidate.rolsuper or candidate.rolname=names.schema_owner)
|
|
1139
|
+
and pg_catalog.pg_has_role(candidate.rolname,control_role.granted_role,'MEMBER')
|
|
1140
|
+
)
|
|
1141
|
+
select
|
|
1142
|
+
current_setting('server_version_num')::integer/10000=any(array[15,16,17,18])
|
|
1143
|
+
and (select count(*)=1 and bool_and(
|
|
1144
|
+
attestation.catalog_definition=agent_bridge.owner_control_catalog_definition()
|
|
1145
|
+
) from agent_bridge.owner_control_attestations attestation
|
|
1146
|
+
where attestation.name='owner-control-v1')
|
|
1147
|
+
and not exists (
|
|
1148
|
+
(select granted_role,member_role,admin_option from actual_memberships
|
|
1149
|
+
except select granted_role,member_role,admin_option from expected_memberships)
|
|
1150
|
+
union all
|
|
1151
|
+
(select granted_role,member_role,admin_option from expected_memberships
|
|
1152
|
+
except select granted_role,member_role,admin_option from actual_memberships)
|
|
1153
|
+
)
|
|
1154
|
+
and not exists (
|
|
1155
|
+
select 1 from actual_memberships membership,names
|
|
1156
|
+
where not membership.inherit_option or not membership.set_option
|
|
1157
|
+
or membership.grantor_role<>names.schema_owner
|
|
1158
|
+
)
|
|
1159
|
+
and not exists (
|
|
1160
|
+
select 1 from active_registry registry
|
|
1161
|
+
left join pg_catalog.pg_roles role_record on role_record.rolname=registry.member_role
|
|
1162
|
+
where role_record.oid is null or not role_record.rolcanlogin or role_record.rolsuper
|
|
1163
|
+
or role_record.rolcreaterole or role_record.rolcreatedb
|
|
1164
|
+
or role_record.rolreplication or role_record.rolbypassrls
|
|
1165
|
+
)
|
|
1166
|
+
and not exists (
|
|
1167
|
+
(select granted_role,member_role from authority_closure
|
|
1168
|
+
except select granted_role,member_role from expected_memberships)
|
|
1169
|
+
union all
|
|
1170
|
+
(select granted_role,member_role from expected_memberships
|
|
1171
|
+
except select granted_role,member_role from authority_closure)
|
|
1172
|
+
)
|
|
1173
|
+
and not exists (
|
|
1174
|
+
select 1 from active_registry registry
|
|
1175
|
+
join pg_catalog.pg_roles inherited on inherited.rolname<>registry.member_role
|
|
1176
|
+
where pg_catalog.pg_has_role(registry.member_role,inherited.rolname,'MEMBER')
|
|
1177
|
+
and not exists (
|
|
1178
|
+
select 1 from active_registry permitted
|
|
1179
|
+
cross join names
|
|
1180
|
+
where permitted.member_role=registry.member_role
|
|
1181
|
+
and inherited.rolname=case permitted.control_role
|
|
1182
|
+
when 'operator' then names.operator_role else names.auditor_role end
|
|
1183
|
+
)
|
|
1184
|
+
)
|
|
1185
|
+
and not exists (
|
|
1186
|
+
select 1 from active_registry registry
|
|
1187
|
+
cross join pg_catalog.pg_roles candidate
|
|
1188
|
+
cross join names
|
|
1189
|
+
where candidate.rolname not in (registry.member_role,names.schema_owner)
|
|
1190
|
+
and not candidate.rolsuper
|
|
1191
|
+
and pg_catalog.pg_has_role(candidate.rolname,registry.member_role,'MEMBER')
|
|
1192
|
+
)
|
|
1193
|
+
$$;
|
|
1194
|
+
|
|
1195
|
+
revoke all on agent_bridge.control_requests,agent_bridge.control_events,
|
|
1196
|
+
agent_bridge.owner_control_attestations,agent_bridge.control_membership_events from public;
|
|
1197
|
+
revoke all on sequence agent_bridge.control_events_sequence_seq,
|
|
1198
|
+
agent_bridge.control_membership_events_sequence_seq from public;
|
|
1199
|
+
revoke execute on all functions in schema agent_bridge from public;
|
|
1200
|
+
|
|
1201
|
+
do $grants$
|
|
1202
|
+
declare
|
|
1203
|
+
suffix text := substr(md5(current_database()),1,16);
|
|
1204
|
+
owner_role text := 'agent_bridge_control_owner_'||suffix;
|
|
1205
|
+
operator_role text := 'agent_bridge_control_operator_'||suffix;
|
|
1206
|
+
auditor_role text := 'agent_bridge_control_auditor_'||suffix;
|
|
1207
|
+
runtime_role text := 'agent_bridge_runtime_'||suffix;
|
|
1208
|
+
data_owner_role text := 'agent_bridge_data_owner_'||suffix;
|
|
1209
|
+
context_reader_role text := 'agent_bridge_context_reader_'||suffix;
|
|
1210
|
+
event_writer_role text := 'agent_bridge_event_writer_'||suffix;
|
|
1211
|
+
role_name text;
|
|
1212
|
+
granted_role text;
|
|
1213
|
+
member_role text;
|
|
1214
|
+
relation_name text;
|
|
1215
|
+
column_name text;
|
|
1216
|
+
membership_record record;
|
|
1217
|
+
relation_record record;
|
|
1218
|
+
begin
|
|
1219
|
+
execute format('alter table agent_bridge.control_requests owner to %I',owner_role);
|
|
1220
|
+
execute format('alter table agent_bridge.control_events owner to %I',owner_role);
|
|
1221
|
+
execute format('alter table agent_bridge.owner_control_attestations owner to %I',owner_role);
|
|
1222
|
+
execute format('alter table agent_bridge.control_membership_events owner to %I',owner_role);
|
|
1223
|
+
execute format('alter sequence agent_bridge.control_events_sequence_seq owner to %I',owner_role);
|
|
1224
|
+
execute format('alter sequence agent_bridge.control_membership_events_sequence_seq owner to %I',owner_role);
|
|
1225
|
+
foreach role_name in array array[
|
|
1226
|
+
'reject_control_ledger_mutation()',
|
|
1227
|
+
'control_provision(uuid,text,text,text,text,text,character,text,text,timestamp with time zone)',
|
|
1228
|
+
'control_credential_inventory(text,timestamp with time zone,uuid,integer)',
|
|
1229
|
+
'control_rotate_credential(uuid,uuid,text,text,character,text,text,timestamp with time zone,timestamp with time zone)',
|
|
1230
|
+
'control_revoke_credential(uuid,uuid,text)','owner_control_catalog_definition()',
|
|
1231
|
+
'owner_control_plane_ready()'
|
|
1232
|
+
] loop execute format('alter function agent_bridge.%s owner to %I',role_name,owner_role); end loop;
|
|
1233
|
+
|
|
1234
|
+
for membership_record in
|
|
1235
|
+
select granted.rolname granted_name,member.rolname member_name
|
|
1236
|
+
from pg_catalog.pg_auth_members membership
|
|
1237
|
+
join pg_catalog.pg_roles granted on granted.oid=membership.roleid
|
|
1238
|
+
join pg_catalog.pg_roles member on member.oid=membership.member
|
|
1239
|
+
where member.rolname in (owner_role,operator_role,auditor_role)
|
|
1240
|
+
loop
|
|
1241
|
+
execute format('revoke %I from %I',membership_record.granted_name,membership_record.member_name);
|
|
1242
|
+
end loop;
|
|
1243
|
+
for membership_record in
|
|
1244
|
+
select granted.rolname granted_name,member.rolname member_name
|
|
1245
|
+
from pg_catalog.pg_auth_members membership
|
|
1246
|
+
join pg_catalog.pg_roles granted on granted.oid=membership.roleid
|
|
1247
|
+
join pg_catalog.pg_roles member on member.oid=membership.member
|
|
1248
|
+
where granted.rolname in (owner_role,operator_role,auditor_role)
|
|
1249
|
+
and member.rolname<>current_user
|
|
1250
|
+
loop
|
|
1251
|
+
execute format('revoke %I from %I',membership_record.granted_name,membership_record.member_name);
|
|
1252
|
+
end loop;
|
|
1253
|
+
|
|
1254
|
+
foreach role_name in array array[owner_role,operator_role,auditor_role] loop
|
|
1255
|
+
execute format('revoke all on schema agent_bridge from %I',role_name);
|
|
1256
|
+
for relation_record in
|
|
1257
|
+
select relation.relname,relation.relkind from pg_catalog.pg_class relation
|
|
1258
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1259
|
+
where namespace.nspname='agent_bridge' and relation.relkind in ('r','p','v','S','m','f')
|
|
1260
|
+
loop
|
|
1261
|
+
if relation_record.relkind='S' then
|
|
1262
|
+
execute format('revoke all on sequence agent_bridge.%I from %I',relation_record.relname,role_name);
|
|
1263
|
+
else
|
|
1264
|
+
execute format('revoke all on table agent_bridge.%I from %I',relation_record.relname,role_name);
|
|
1265
|
+
for column_name in select attribute.attname from pg_catalog.pg_attribute attribute
|
|
1266
|
+
join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
|
|
1267
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1268
|
+
where namespace.nspname='agent_bridge' and relation.relname=relation_record.relname
|
|
1269
|
+
and attribute.attnum>0 and not attribute.attisdropped loop
|
|
1270
|
+
execute format('revoke select(%I),insert(%I),update(%I),references(%I) on agent_bridge.%I from %I',
|
|
1271
|
+
column_name,column_name,column_name,column_name,relation_record.relname,role_name);
|
|
1272
|
+
end loop;
|
|
1273
|
+
end if;
|
|
1274
|
+
end loop;
|
|
1275
|
+
execute format('revoke execute on all functions in schema agent_bridge from %I',role_name);
|
|
1276
|
+
end loop;
|
|
1277
|
+
|
|
1278
|
+
foreach role_name in array array[operator_role,auditor_role,runtime_role,data_owner_role,
|
|
1279
|
+
context_reader_role,event_writer_role] loop
|
|
1280
|
+
execute format('revoke all on agent_bridge.control_requests,agent_bridge.control_events,agent_bridge.owner_control_attestations,agent_bridge.control_membership_events from %I',role_name);
|
|
1281
|
+
execute format('revoke all on sequence agent_bridge.control_events_sequence_seq,agent_bridge.control_membership_events_sequence_seq from %I',role_name);
|
|
1282
|
+
end loop;
|
|
1283
|
+
foreach role_name in array array[runtime_role,data_owner_role,context_reader_role,event_writer_role] loop
|
|
1284
|
+
execute format('revoke execute on function agent_bridge.control_provision(uuid,text,text,text,text,text,character,text,text,timestamp with time zone),agent_bridge.control_credential_inventory(text,timestamp with time zone,uuid,integer),agent_bridge.control_rotate_credential(uuid,uuid,text,text,character,text,text,timestamp with time zone,timestamp with time zone),agent_bridge.control_revoke_credential(uuid,uuid,text),agent_bridge.owner_control_catalog_definition(),agent_bridge.owner_control_plane_ready() from %I',role_name);
|
|
1285
|
+
end loop;
|
|
1286
|
+
|
|
1287
|
+
execute format('grant usage on schema agent_bridge to %I,%I,%I',owner_role,operator_role,auditor_role);
|
|
1288
|
+
execute format('grant select,insert,update(request_id) on agent_bridge.control_requests to %I',owner_role);
|
|
1289
|
+
execute format('grant insert on agent_bridge.control_events to %I',owner_role);
|
|
1290
|
+
execute format('grant select on agent_bridge.owner_control_attestations to %I',owner_role);
|
|
1291
|
+
execute format('grant select,insert on agent_bridge.control_membership_events to %I',owner_role);
|
|
1292
|
+
execute format('grant usage on sequence agent_bridge.control_events_sequence_seq to %I',owner_role);
|
|
1293
|
+
execute format('grant usage on sequence agent_bridge.control_membership_events_sequence_seq to %I',owner_role);
|
|
1294
|
+
execute format('grant select(id,name,disabled_at),insert(id,name) on agent_bridge.workspaces to %I',owner_role);
|
|
1295
|
+
execute format('grant select(id,workspace_id,principal,disabled_at),insert(workspace_id,principal,display_name,runtime_type) on agent_bridge.agents to %I',owner_role);
|
|
1296
|
+
execute format('grant select(id,workspace_id,agent_id,label,expires_at,revoked_at,created_at,last_used_at,scopes,scope_set_name,replaces_credential_id,revoked_by,revocation_reason,expiry_grace_until) on agent_bridge.credentials to %I',owner_role);
|
|
1297
|
+
execute format('grant insert(workspace_id,agent_id,token_hash,label,expires_at,scopes,scope_set_name,replaces_credential_id) on agent_bridge.credentials to %I',owner_role);
|
|
1298
|
+
execute format('grant update(expiry_grace_until,revoked_at,revoked_by,revocation_reason) on agent_bridge.credentials to %I',owner_role);
|
|
1299
|
+
execute format('grant select(name,scopes) on agent_bridge.credential_scope_sets to %I',owner_role);
|
|
1300
|
+
execute format('grant execute on function agent_bridge.canonicalize_scopes(text[]) to %I',owner_role);
|
|
1301
|
+
execute format('grant execute on function agent_bridge.assert_control_actor(text) to %I',owner_role);
|
|
1302
|
+
execute format('grant execute on function agent_bridge.credential_security_prerequisite_definition() to %I',owner_role);
|
|
1303
|
+
execute format('grant execute on function agent_bridge.owner_control_catalog_definition() to %I',owner_role);
|
|
1304
|
+
execute format('grant execute on function agent_bridge.control_provision(uuid,text,text,text,text,text,character,text,text,timestamp with time zone) to %I',operator_role);
|
|
1305
|
+
execute format('grant execute on function agent_bridge.control_rotate_credential(uuid,uuid,text,text,character,text,text,timestamp with time zone,timestamp with time zone) to %I',operator_role);
|
|
1306
|
+
execute format('grant execute on function agent_bridge.control_revoke_credential(uuid,uuid,text) to %I',operator_role);
|
|
1307
|
+
execute format('grant execute on function agent_bridge.control_credential_inventory(text,timestamp with time zone,uuid,integer) to %I,%I',operator_role,auditor_role);
|
|
1308
|
+
execute format('grant execute on function agent_bridge.owner_control_plane_ready() to %I',runtime_role);
|
|
1309
|
+
execute format('revoke execute on function agent_bridge.replace_credential(uuid,character,text[],text,text,timestamp with time zone,timestamp with time zone,text,uuid) from %I,%I,%I',operator_role,auditor_role,runtime_role);
|
|
1310
|
+
execute format('revoke execute on function agent_bridge.revoke_credential(uuid,text,text,uuid) from %I,%I,%I',operator_role,auditor_role,runtime_role);
|
|
1311
|
+
foreach role_name in array array['anon','authenticated'] loop
|
|
1312
|
+
if exists(select 1 from pg_roles where rolname=role_name) then
|
|
1313
|
+
execute format('revoke all on agent_bridge.control_requests,agent_bridge.control_events,agent_bridge.owner_control_attestations,agent_bridge.control_membership_events from %I',role_name);
|
|
1314
|
+
execute format('revoke all on sequence agent_bridge.control_events_sequence_seq,agent_bridge.control_membership_events_sequence_seq from %I',role_name);
|
|
1315
|
+
execute format('revoke execute on function agent_bridge.control_provision(uuid,text,text,text,text,text,character,text,text,timestamp with time zone),agent_bridge.control_credential_inventory(text,timestamp with time zone,uuid,integer),agent_bridge.control_rotate_credential(uuid,uuid,text,text,character,text,text,timestamp with time zone,timestamp with time zone),agent_bridge.control_revoke_credential(uuid,uuid,text),agent_bridge.owner_control_catalog_definition(),agent_bridge.owner_control_plane_ready() from %I',role_name);
|
|
1316
|
+
end if;
|
|
1317
|
+
end loop;
|
|
1318
|
+
foreach role_name in array array[current_user,owner_role] loop
|
|
1319
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on tables from public',role_name);
|
|
1320
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on sequences from public',role_name);
|
|
1321
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on functions from public',role_name);
|
|
1322
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on types from public',role_name);
|
|
1323
|
+
foreach member_role in array array['anon','authenticated'] loop
|
|
1324
|
+
if exists(select 1 from pg_catalog.pg_roles where rolname=member_role) then
|
|
1325
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on tables from %I',role_name,member_role);
|
|
1326
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on sequences from %I',role_name,member_role);
|
|
1327
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on functions from %I',role_name,member_role);
|
|
1328
|
+
execute format('alter default privileges for role %I in schema agent_bridge revoke all on types from %I',role_name,member_role);
|
|
1329
|
+
end if;
|
|
1330
|
+
end loop;
|
|
1331
|
+
end loop;
|
|
1332
|
+
end
|
|
1333
|
+
$grants$;
|
|
1334
|
+
|
|
1335
|
+
do $preflight$
|
|
1336
|
+
declare
|
|
1337
|
+
suffix text := substr(md5(current_database()),1,16);
|
|
1338
|
+
owner_role name := ('agent_bridge_control_owner_'||suffix)::name;
|
|
1339
|
+
operator_role name := ('agent_bridge_control_operator_'||suffix)::name;
|
|
1340
|
+
auditor_role name := ('agent_bridge_control_auditor_'||suffix)::name;
|
|
1341
|
+
expected_count integer;
|
|
1342
|
+
actual_security_digest text;
|
|
1343
|
+
expected_security_digest text;
|
|
1344
|
+
begin
|
|
1345
|
+
select pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(
|
|
1346
|
+
agent_bridge.credential_security_prerequisite_definition(),'UTF8')),'hex')
|
|
1347
|
+
into actual_security_digest;
|
|
1348
|
+
expected_security_digest := case
|
|
1349
|
+
when current_setting('server_version_num')::integer/10000=15
|
|
1350
|
+
then '9db61d6bb63f753041bae021cfa006bc497fa0d3d12ea8636c747d095ff675cf'
|
|
1351
|
+
when current_setting('server_version_num')::integer/10000=16
|
|
1352
|
+
then '9db61d6bb63f753041bae021cfa006bc497fa0d3d12ea8636c747d095ff675cf'
|
|
1353
|
+
when current_setting('server_version_num')::integer/10000=17
|
|
1354
|
+
then '9db61d6bb63f753041bae021cfa006bc497fa0d3d12ea8636c747d095ff675cf'
|
|
1355
|
+
when current_setting('server_version_num')::integer/10000=18
|
|
1356
|
+
then 'cfb571d4d6d5a00ec5d46fcf3ec0f18403ebd2f2bf020302c5feb7701164fcf9'
|
|
1357
|
+
else null
|
|
1358
|
+
end;
|
|
1359
|
+
if expected_security_digest is null or actual_security_digest<>expected_security_digest then
|
|
1360
|
+
raise exception 'owner control preflight rejected credential security definition drift'
|
|
1361
|
+
using detail='observed prerequisite digest: '||actual_security_digest;
|
|
1362
|
+
end if;
|
|
1363
|
+
if not agent_bridge.security_schema_ready() then
|
|
1364
|
+
raise exception 'owner control preflight rejected credential security drift';
|
|
1365
|
+
end if;
|
|
1366
|
+
if not exists (
|
|
1367
|
+
select 1 from agent_bridge.row_isolation_attestations attestation
|
|
1368
|
+
where attestation.name='domain-v1'
|
|
1369
|
+
and attestation.catalog_definition=agent_bridge.row_isolation_catalog_definition()
|
|
1370
|
+
) then
|
|
1371
|
+
raise exception 'owner control preflight rejected row isolation drift';
|
|
1372
|
+
end if;
|
|
1373
|
+
|
|
1374
|
+
if exists (
|
|
1375
|
+
select 1 from pg_catalog.pg_namespace namespace
|
|
1376
|
+
cross join lateral pg_catalog.aclexplode(coalesce(
|
|
1377
|
+
namespace.nspacl,pg_catalog.acldefault('n',namespace.nspowner)
|
|
1378
|
+
)) access
|
|
1379
|
+
left join pg_catalog.pg_roles grantee on grantee.oid=access.grantee
|
|
1380
|
+
where namespace.nspname='agent_bridge' and (
|
|
1381
|
+
access.grantee=0 or grantee.rolname not in (
|
|
1382
|
+
current_user,owner_role,operator_role,auditor_role,
|
|
1383
|
+
'agent_bridge_runtime_'||suffix,'agent_bridge_data_owner_'||suffix,
|
|
1384
|
+
'agent_bridge_context_reader_'||suffix,'agent_bridge_event_writer_'||suffix
|
|
1385
|
+
)
|
|
1386
|
+
)
|
|
1387
|
+
) then
|
|
1388
|
+
raise exception 'owner control preflight rejected schema privilege drift';
|
|
1389
|
+
end if;
|
|
1390
|
+
|
|
1391
|
+
if exists (
|
|
1392
|
+
select 1 from pg_catalog.pg_class relation
|
|
1393
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1394
|
+
cross join lateral pg_catalog.aclexplode(coalesce(relation.relacl,pg_catalog.acldefault(
|
|
1395
|
+
case when relation.relkind='S' then 'S'::"char" else 'r'::"char" end,relation.relowner
|
|
1396
|
+
))) access
|
|
1397
|
+
where namespace.nspname='agent_bridge'
|
|
1398
|
+
and relation.relname in (
|
|
1399
|
+
'control_requests','control_events','owner_control_attestations',
|
|
1400
|
+
'control_membership_events','control_events_sequence_seq',
|
|
1401
|
+
'control_membership_events_sequence_seq','workspaces','agents','credentials',
|
|
1402
|
+
'credential_scope_sets'
|
|
1403
|
+
)
|
|
1404
|
+
and access.grantee not in (
|
|
1405
|
+
relation.relowner,(select oid from pg_catalog.pg_roles where rolname=owner_role)
|
|
1406
|
+
)
|
|
1407
|
+
) then
|
|
1408
|
+
raise exception 'owner control preflight rejected relation privilege drift';
|
|
1409
|
+
end if;
|
|
1410
|
+
|
|
1411
|
+
if exists (
|
|
1412
|
+
select 1 from pg_catalog.pg_attribute attribute
|
|
1413
|
+
join pg_catalog.pg_class relation on relation.oid=attribute.attrelid
|
|
1414
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1415
|
+
cross join lateral pg_catalog.aclexplode(attribute.attacl) access
|
|
1416
|
+
where namespace.nspname='agent_bridge' and attribute.attacl is not null
|
|
1417
|
+
and relation.relname in (
|
|
1418
|
+
'control_requests','control_events','owner_control_attestations',
|
|
1419
|
+
'control_membership_events','workspaces','agents','credentials','credential_scope_sets'
|
|
1420
|
+
)
|
|
1421
|
+
and access.grantee<>(select oid from pg_catalog.pg_roles where rolname=owner_role)
|
|
1422
|
+
) then
|
|
1423
|
+
raise exception 'owner control preflight rejected column privilege drift';
|
|
1424
|
+
end if;
|
|
1425
|
+
|
|
1426
|
+
if exists (
|
|
1427
|
+
select 1 from pg_catalog.pg_proc procedure
|
|
1428
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
1429
|
+
cross join lateral pg_catalog.aclexplode(coalesce(
|
|
1430
|
+
procedure.proacl,pg_catalog.acldefault('f',procedure.proowner)
|
|
1431
|
+
)) access
|
|
1432
|
+
left join pg_catalog.pg_roles grantee on grantee.oid=access.grantee
|
|
1433
|
+
where namespace.nspname='agent_bridge' and procedure.proname in (
|
|
1434
|
+
'canonicalize_scopes','validate_credential_security','reject_credential_delete',
|
|
1435
|
+
'reject_scope_set_mutation','reject_security_event_mutation','record_scope_denial',
|
|
1436
|
+
'consume_rate_limit','replace_credential','revoke_credential','security_schema_ready',
|
|
1437
|
+
'current_request_workspace','current_request_principal',
|
|
1438
|
+
'row_isolation_catalog_definition','credential_security_prerequisite_definition',
|
|
1439
|
+
'assert_control_actor',
|
|
1440
|
+
'reject_control_ledger_mutation','control_provision','control_credential_inventory',
|
|
1441
|
+
'control_rotate_credential','control_revoke_credential','register_control_member',
|
|
1442
|
+
'revoke_control_member','owner_control_catalog_definition','owner_control_plane_ready'
|
|
1443
|
+
) and (access.grantee=0 or grantee.rolname not in (
|
|
1444
|
+
current_user,owner_role,operator_role,auditor_role,
|
|
1445
|
+
'agent_bridge_runtime_'||suffix,'agent_bridge_data_owner_'||suffix,
|
|
1446
|
+
'agent_bridge_context_reader_'||suffix,'agent_bridge_event_writer_'||suffix
|
|
1447
|
+
))
|
|
1448
|
+
) then
|
|
1449
|
+
raise exception 'owner control preflight rejected function privilege drift';
|
|
1450
|
+
end if;
|
|
1451
|
+
|
|
1452
|
+
if exists (
|
|
1453
|
+
select 1 from pg_catalog.pg_default_acl default_acl
|
|
1454
|
+
left join pg_catalog.pg_namespace namespace on namespace.oid=default_acl.defaclnamespace
|
|
1455
|
+
cross join lateral pg_catalog.aclexplode(default_acl.defaclacl) access
|
|
1456
|
+
where default_acl.defaclrole in (
|
|
1457
|
+
current_user::regrole::oid,
|
|
1458
|
+
(select oid from pg_catalog.pg_roles where rolname=owner_role)
|
|
1459
|
+
) and (namespace.nspname='agent_bridge' or default_acl.defaclnamespace=0)
|
|
1460
|
+
and access.grantee<>default_acl.defaclrole
|
|
1461
|
+
) then
|
|
1462
|
+
raise exception 'owner control preflight rejected default privilege drift';
|
|
1463
|
+
end if;
|
|
1464
|
+
|
|
1465
|
+
if exists (
|
|
1466
|
+
select 1 from (values
|
|
1467
|
+
('workspaces','r','postgres'),('agents','r','postgres'),
|
|
1468
|
+
('credentials','r','postgres'),('credential_scope_sets','r','postgres'),
|
|
1469
|
+
('control_requests','r','owner'),('control_events','r','owner'),
|
|
1470
|
+
('owner_control_attestations','r','owner'),
|
|
1471
|
+
('control_membership_events','r','owner'),
|
|
1472
|
+
('control_events_sequence_seq','S','owner'),
|
|
1473
|
+
('control_membership_events_sequence_seq','S','owner')
|
|
1474
|
+
) expected(relation_name,relation_kind,owner_kind)
|
|
1475
|
+
left join pg_catalog.pg_namespace namespace on namespace.nspname='agent_bridge'
|
|
1476
|
+
left join pg_catalog.pg_class relation on relation.relnamespace=namespace.oid
|
|
1477
|
+
and relation.relname=expected.relation_name
|
|
1478
|
+
where relation.oid is null or relation.relkind::text<>expected.relation_kind
|
|
1479
|
+
or relation.relpersistence<>'p' or relation.relrowsecurity or relation.relforcerowsecurity
|
|
1480
|
+
or relation.relowner<>case expected.owner_kind when 'owner'
|
|
1481
|
+
then (select oid from pg_catalog.pg_roles where rolname=owner_role)
|
|
1482
|
+
else current_user::regrole::oid end
|
|
1483
|
+
) then
|
|
1484
|
+
raise exception 'owner control preflight rejected relation ownership or security drift';
|
|
1485
|
+
end if;
|
|
1486
|
+
|
|
1487
|
+
if exists (
|
|
1488
|
+
select 1 from pg_catalog.pg_class relation
|
|
1489
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=relation.relnamespace
|
|
1490
|
+
where namespace.nspname='agent_bridge'
|
|
1491
|
+
and relation.relowner in (
|
|
1492
|
+
select oid from pg_catalog.pg_roles where rolname in (owner_role,operator_role,auditor_role)
|
|
1493
|
+
) and not (
|
|
1494
|
+
relation.relowner=(select oid from pg_catalog.pg_roles where rolname=owner_role)
|
|
1495
|
+
and (
|
|
1496
|
+
(relation.relkind='r' and relation.relname in (
|
|
1497
|
+
'control_requests','control_events','owner_control_attestations',
|
|
1498
|
+
'control_membership_events'))
|
|
1499
|
+
or (relation.relkind='S' and relation.relname in (
|
|
1500
|
+
'control_events_sequence_seq','control_membership_events_sequence_seq'))
|
|
1501
|
+
or (relation.relkind='i' and exists (
|
|
1502
|
+
select 1 from pg_catalog.pg_index index_record
|
|
1503
|
+
join pg_catalog.pg_class source_relation on source_relation.oid=index_record.indrelid
|
|
1504
|
+
where index_record.indexrelid=relation.oid
|
|
1505
|
+
and source_relation.relname in (
|
|
1506
|
+
'control_requests','control_events','owner_control_attestations',
|
|
1507
|
+
'control_membership_events')
|
|
1508
|
+
))
|
|
1509
|
+
)
|
|
1510
|
+
)
|
|
1511
|
+
) then
|
|
1512
|
+
raise exception 'owner control preflight rejected unexpected control-owned relation';
|
|
1513
|
+
end if;
|
|
1514
|
+
|
|
1515
|
+
if exists (
|
|
1516
|
+
select 1 from (values
|
|
1517
|
+
('agent_bridge.workspaces'::regclass,'workspaces_pkey','p','PRIMARY KEY (id)'),
|
|
1518
|
+
('agent_bridge.agents'::regclass,'agents_pkey','p','PRIMARY KEY (id)'),
|
|
1519
|
+
('agent_bridge.agents'::regclass,'agents_workspace_id_fkey','f',
|
|
1520
|
+
'FOREIGN KEY (workspace_id) REFERENCES agent_bridge.workspaces(id)'),
|
|
1521
|
+
('agent_bridge.agents'::regclass,'agents_workspace_id_id_key','u',
|
|
1522
|
+
'UNIQUE (workspace_id, id)'),
|
|
1523
|
+
('agent_bridge.agents'::regclass,'agents_workspace_id_principal_key','u',
|
|
1524
|
+
'UNIQUE (workspace_id, principal)')
|
|
1525
|
+
) expected(relation_id,constraint_name,constraint_type,constraint_definition)
|
|
1526
|
+
left join pg_catalog.pg_constraint constraint_record
|
|
1527
|
+
on constraint_record.conrelid=expected.relation_id
|
|
1528
|
+
and constraint_record.conname=expected.constraint_name
|
|
1529
|
+
where constraint_record.oid is null or not constraint_record.convalidated
|
|
1530
|
+
or constraint_record.contype::text<>expected.constraint_type
|
|
1531
|
+
or pg_catalog.pg_get_constraintdef(constraint_record.oid,true)
|
|
1532
|
+
<>expected.constraint_definition
|
|
1533
|
+
) then
|
|
1534
|
+
raise exception 'owner control preflight rejected workspace or agent constraint drift';
|
|
1535
|
+
end if;
|
|
1536
|
+
|
|
1537
|
+
if exists (
|
|
1538
|
+
select 1 from (values
|
|
1539
|
+
('agent_bridge.agents_workspace_id_principal_key'::regclass,
|
|
1540
|
+
'CREATE UNIQUE INDEX agents_workspace_id_principal_key ON agent_bridge.agents USING btree (workspace_id, principal)',
|
|
1541
|
+
''),
|
|
1542
|
+
('agent_bridge.credentials_replacement_lineage'::regclass,
|
|
1543
|
+
'CREATE UNIQUE INDEX credentials_replacement_lineage ON agent_bridge.credentials USING btree (replaces_credential_id) WHERE (replaces_credential_id IS NOT NULL)',
|
|
1544
|
+
'(replaces_credential_id IS NOT NULL)')
|
|
1545
|
+
) expected(index_id,index_definition,index_predicate)
|
|
1546
|
+
left join pg_catalog.pg_index index_record on index_record.indexrelid=expected.index_id
|
|
1547
|
+
where index_record.indexrelid is null or not index_record.indisunique
|
|
1548
|
+
or not index_record.indisvalid or not index_record.indisready
|
|
1549
|
+
or pg_catalog.pg_get_indexdef(index_record.indexrelid)<>expected.index_definition
|
|
1550
|
+
or coalesce(pg_catalog.pg_get_expr(index_record.indpred,index_record.indrelid),'')
|
|
1551
|
+
<>expected.index_predicate
|
|
1552
|
+
) then
|
|
1553
|
+
raise exception 'owner control preflight rejected critical unique index drift';
|
|
1554
|
+
end if;
|
|
1555
|
+
|
|
1556
|
+
select count(*) into expected_count from (values
|
|
1557
|
+
('agent_bridge.reject_control_ledger_mutation()','owner',false,'v',false),
|
|
1558
|
+
('agent_bridge.control_provision(uuid,text,text,text,text,text,character,text,text,timestamp with time zone)','owner',true,'v',false),
|
|
1559
|
+
('agent_bridge.control_credential_inventory(text,timestamp with time zone,uuid,integer)','owner',true,'s',false),
|
|
1560
|
+
('agent_bridge.control_rotate_credential(uuid,uuid,text,text,character,text,text,timestamp with time zone,timestamp with time zone)','owner',true,'v',false),
|
|
1561
|
+
('agent_bridge.control_revoke_credential(uuid,uuid,text)','owner',true,'v',false),
|
|
1562
|
+
('agent_bridge.owner_control_catalog_definition()','owner',false,'s',true),
|
|
1563
|
+
('agent_bridge.owner_control_plane_ready()','owner',true,'s',false),
|
|
1564
|
+
('agent_bridge.register_control_member(uuid,name,text)','migration',true,'v',false),
|
|
1565
|
+
('agent_bridge.revoke_control_member(uuid,name,text)','migration',true,'v',false),
|
|
1566
|
+
('agent_bridge.assert_control_actor(text)','migration',true,'v',false),
|
|
1567
|
+
('agent_bridge.credential_security_prerequisite_definition()','migration',false,'s',true),
|
|
1568
|
+
('agent_bridge.row_isolation_catalog_definition()','migration',false,'s',false),
|
|
1569
|
+
('agent_bridge.validate_credential_security()','migration',false,'v',false),
|
|
1570
|
+
('agent_bridge.canonicalize_scopes(text[])','migration',false,'i',false),
|
|
1571
|
+
('agent_bridge.replace_credential(uuid,character,text[],text,text,timestamp with time zone,timestamp with time zone,text,uuid)','migration',true,'v',false),
|
|
1572
|
+
('agent_bridge.revoke_credential(uuid,text,text,uuid)','migration',true,'v',false)
|
|
1573
|
+
) expected(signature,owner_kind,security_definer,volatility,utc_config)
|
|
1574
|
+
left join pg_catalog.pg_proc procedure
|
|
1575
|
+
on procedure.oid=pg_catalog.to_regprocedure(expected.signature)
|
|
1576
|
+
where procedure.oid is null
|
|
1577
|
+
or procedure.proowner<>case expected.owner_kind when 'owner'
|
|
1578
|
+
then (select oid from pg_catalog.pg_roles where rolname=owner_role)
|
|
1579
|
+
else current_user::regrole::oid end
|
|
1580
|
+
or procedure.prosecdef<>expected.security_definer
|
|
1581
|
+
or procedure.provolatile::text<>expected.volatility
|
|
1582
|
+
or not procedure.proconfig @> array['search_path=""']::text[]
|
|
1583
|
+
or (expected.utc_config and not procedure.proconfig @> array['TimeZone=UTC']::text[])
|
|
1584
|
+
or (not expected.utc_config and cardinality(procedure.proconfig)<>1)
|
|
1585
|
+
or (expected.utc_config and cardinality(procedure.proconfig)<>2);
|
|
1586
|
+
if expected_count<>0 then
|
|
1587
|
+
raise exception 'owner control preflight rejected protected function drift';
|
|
1588
|
+
end if;
|
|
1589
|
+
|
|
1590
|
+
if exists (
|
|
1591
|
+
select 1 from pg_catalog.pg_proc procedure
|
|
1592
|
+
join pg_catalog.pg_namespace namespace on namespace.oid=procedure.pronamespace
|
|
1593
|
+
where namespace.nspname='agent_bridge'
|
|
1594
|
+
and procedure.proowner in (
|
|
1595
|
+
select oid from pg_catalog.pg_roles where rolname in (owner_role,operator_role,auditor_role)
|
|
1596
|
+
) and not (
|
|
1597
|
+
procedure.proowner=(select oid from pg_catalog.pg_roles where rolname=owner_role)
|
|
1598
|
+
and procedure.oid in (
|
|
1599
|
+
'agent_bridge.reject_control_ledger_mutation()'::regprocedure,
|
|
1600
|
+
'agent_bridge.control_provision(uuid,text,text,text,text,text,character,text,text,timestamp with time zone)'::regprocedure,
|
|
1601
|
+
'agent_bridge.control_credential_inventory(text,timestamp with time zone,uuid,integer)'::regprocedure,
|
|
1602
|
+
'agent_bridge.control_rotate_credential(uuid,uuid,text,text,character,text,text,timestamp with time zone,timestamp with time zone)'::regprocedure,
|
|
1603
|
+
'agent_bridge.control_revoke_credential(uuid,uuid,text)'::regprocedure,
|
|
1604
|
+
'agent_bridge.owner_control_catalog_definition()'::regprocedure,
|
|
1605
|
+
'agent_bridge.owner_control_plane_ready()'::regprocedure
|
|
1606
|
+
)
|
|
1607
|
+
)
|
|
1608
|
+
) then
|
|
1609
|
+
raise exception 'owner control preflight rejected unexpected control-owned function';
|
|
1610
|
+
end if;
|
|
1611
|
+
|
|
1612
|
+
if exists (
|
|
1613
|
+
select 1 from (values
|
|
1614
|
+
('agent_bridge.control_requests'::regclass,'control_requests_append_only',
|
|
1615
|
+
'CREATE TRIGGER control_requests_append_only BEFORE DELETE OR UPDATE OR TRUNCATE ON agent_bridge.control_requests FOR EACH STATEMENT EXECUTE FUNCTION agent_bridge.reject_control_ledger_mutation()'),
|
|
1616
|
+
('agent_bridge.control_events'::regclass,'control_events_append_only',
|
|
1617
|
+
'CREATE TRIGGER control_events_append_only BEFORE DELETE OR UPDATE OR TRUNCATE ON agent_bridge.control_events FOR EACH STATEMENT EXECUTE FUNCTION agent_bridge.reject_control_ledger_mutation()'),
|
|
1618
|
+
('agent_bridge.owner_control_attestations'::regclass,'owner_control_attestations_append_only',
|
|
1619
|
+
'CREATE TRIGGER owner_control_attestations_append_only BEFORE DELETE OR UPDATE OR TRUNCATE ON agent_bridge.owner_control_attestations FOR EACH STATEMENT EXECUTE FUNCTION agent_bridge.reject_control_ledger_mutation()'),
|
|
1620
|
+
('agent_bridge.control_membership_events'::regclass,'control_membership_events_append_only',
|
|
1621
|
+
'CREATE TRIGGER control_membership_events_append_only BEFORE DELETE OR UPDATE OR TRUNCATE ON agent_bridge.control_membership_events FOR EACH STATEMENT EXECUTE FUNCTION agent_bridge.reject_control_ledger_mutation()'),
|
|
1622
|
+
('agent_bridge.credentials'::regclass,'credentials_validate_security',
|
|
1623
|
+
'CREATE TRIGGER credentials_validate_security BEFORE INSERT OR UPDATE ON agent_bridge.credentials FOR EACH ROW EXECUTE FUNCTION agent_bridge.validate_credential_security()')
|
|
1624
|
+
) expected(relation_id,trigger_name,trigger_definition)
|
|
1625
|
+
left join pg_catalog.pg_trigger trigger on trigger.tgrelid=expected.relation_id
|
|
1626
|
+
and trigger.tgname=expected.trigger_name and not trigger.tgisinternal
|
|
1627
|
+
where trigger.oid is null or trigger.tgenabled<>'O'
|
|
1628
|
+
or pg_catalog.pg_get_triggerdef(trigger.oid,true)<>expected.trigger_definition
|
|
1629
|
+
) then
|
|
1630
|
+
raise exception 'owner control preflight rejected protected trigger drift';
|
|
1631
|
+
end if;
|
|
1632
|
+
end
|
|
1633
|
+
$preflight$;
|
|
1634
|
+
|
|
1635
|
+
insert into agent_bridge.owner_control_attestations(name,catalog_definition)
|
|
1636
|
+
values('owner-control-v1',agent_bridge.owner_control_catalog_definition());
|
|
1637
|
+
|
|
1638
|
+
insert into agent_bridge.schema_migrations(version,name,checksum)
|
|
1639
|
+
values(14,'owner_control_plane','__AGENT_BRIDGE_MIGRATION_CHECKSUM__');
|
|
1640
|
+
|
|
1641
|
+
commit;
|