@coffre/db 0.14.5 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/change-records-_fKvahvs.js +55 -0
- package/dist/connect.d.ts +22 -10
- package/dist/connect.js +74 -12
- package/dist/database-CVteLhp1.d.ts +410 -0
- package/dist/{database-BadNeeN-.js → database-D55ENPX-.js} +46 -25
- package/dist/dialect.d.ts +4 -3
- package/dist/dialect.js +14 -5
- package/dist/grants.d.ts +4 -4
- package/dist/grants.js +4 -4
- package/dist/hyperdrive.d.ts +48 -23
- package/dist/hyperdrive.js +168 -91
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/log-DaD7fyUq.d.ts +722 -0
- package/dist/log.d.ts +2 -622
- package/dist/log.js +178 -13
- package/dist/migrate.d.ts +43 -3
- package/dist/migrate.js +149 -29
- package/dist/migrations/postgres/0022_log_015.sql +672 -0
- package/dist/migrations/postgres/0023_change_path.sql +153 -0
- package/dist/migrations/postgres/0024_use_015.sql +556 -0
- package/dist/migrations/postgres/0025_app_change.sql +221 -0
- package/dist/migrations/postgres/0026_issue_lock.sql +106 -0
- package/dist/migrations/postgres/0027_retention.sql +118 -0
- package/dist/migrations/postgres/0028_use_stalls.sql +594 -0
- package/dist/migrations/postgres/0029_use_objects.sql +887 -0
- package/dist/migrations/postgres/0030_unarchive_restores.sql +259 -0
- package/dist/migrations/postgres/0031_app_read.sql +65 -0
- package/dist/migrations/postgres/0032_secrets_live_index.sql +4 -0
- package/dist/migrations/postgres/0033_use_generations.sql +582 -0
- package/dist/migrations/postgres/0034_vault_credentials.sql +153 -0
- package/dist/migrations/postgres/0035_first_days.sql +3 -0
- package/dist/migrations/postgres/0036_vault_issues.sql +91 -0
- package/dist/migrations/postgres/0037_vault_bindings.sql +271 -0
- package/dist/migrations/postgres/0040_binding_origin.sql +159 -0
- package/dist/migrations/postgres/0041_cutover.sql +132 -0
- package/dist/migrations/postgres/0042_sign_in_index.sql +7 -0
- package/dist/migrations/postgres/0043_issue_revoke_index.sql +4 -0
- package/dist/migrations/postgres/0044_use_by_name.sql +705 -0
- package/dist/migrations/postgres/0045_decide_first.sql +878 -0
- package/dist/migrations/postgres/0046_way_in_uses.sql +1149 -0
- package/dist/migrations/postgres/0047_cutover_finish.sql +268 -0
- package/dist/migrations/postgres/0048_use_batch.sql +59 -0
- package/dist/migrations/postgres/0049_reads.sql +171 -0
- package/dist/migrations/postgres/meta/0022_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0023_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0024_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0025_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0026_snapshot.json +3831 -0
- package/dist/migrations/postgres/meta/0027_snapshot.json +3853 -0
- package/dist/migrations/postgres/meta/0028_snapshot.json +3853 -0
- package/dist/migrations/postgres/meta/0029_snapshot.json +3859 -0
- package/dist/migrations/postgres/meta/0030_snapshot.json +3859 -0
- package/dist/migrations/postgres/meta/0031_snapshot.json +3859 -0
- package/dist/migrations/postgres/meta/0032_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0033_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0034_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0035_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0036_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0037_snapshot.json +3875 -0
- package/dist/migrations/postgres/meta/0038_snapshot.json +3881 -0
- package/dist/migrations/postgres/meta/0039_snapshot.json +3881 -0
- package/dist/migrations/postgres/meta/0040_snapshot.json +3931 -0
- package/dist/migrations/postgres/meta/0041_snapshot.json +3981 -0
- package/dist/migrations/postgres/meta/0042_snapshot.json +3981 -0
- package/dist/migrations/postgres/meta/0043_snapshot.json +3981 -0
- package/dist/migrations/postgres/meta/0044_snapshot.json +3991 -0
- package/dist/migrations/postgres/meta/0045_snapshot.json +3997 -0
- package/dist/migrations/postgres/meta/0046_snapshot.json +3997 -0
- package/dist/migrations/postgres/meta/0047_snapshot.json +4234 -0
- package/dist/migrations/postgres/meta/_journal.json +182 -0
- package/dist/migrations/sqlite/0018_log_015.sql +84 -0
- package/dist/migrations/sqlite/0019_retention.sql +2 -0
- package/dist/migrations/sqlite/0020_ended_references.sql +1 -0
- package/dist/migrations/sqlite/0021_secrets_live_index.sql +1 -0
- package/dist/migrations/sqlite/0022_binding_origin.sql +1 -0
- package/dist/migrations/sqlite/0023_sign_in_index.sql +2 -0
- package/dist/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
- package/dist/migrations/sqlite/0025_way_in_uses.sql +42 -0
- package/dist/migrations/sqlite/0026_cutover_finish.sql +11 -0
- package/dist/migrations/sqlite/0027_reads.sql +38 -0
- package/dist/migrations/sqlite/meta/0018_snapshot.json +3529 -0
- package/dist/migrations/sqlite/meta/0019_snapshot.json +3538 -0
- package/dist/migrations/sqlite/meta/0020_snapshot.json +3545 -0
- package/dist/migrations/sqlite/meta/0021_snapshot.json +3553 -0
- package/dist/migrations/sqlite/meta/0022_snapshot.json +3560 -0
- package/dist/migrations/sqlite/meta/0023_snapshot.json +3579 -0
- package/dist/migrations/sqlite/meta/0024_snapshot.json +3598 -0
- package/dist/migrations/sqlite/meta/0025_snapshot.json +3609 -0
- package/dist/migrations/sqlite/meta/0026_snapshot.json +3616 -0
- package/dist/migrations/sqlite/meta/0027_snapshot.json +3834 -0
- package/dist/migrations/sqlite/meta/_journal.json +70 -0
- package/dist/platform-Y3gICK6n.d.ts +58 -0
- package/dist/portable.d.ts +2 -2
- package/dist/postgres-BI3xTnyw.js +1718 -0
- package/dist/postgres-platform-node.d.ts +14 -0
- package/dist/postgres-platform-node.js +21 -0
- package/dist/postgres-platform-workerd.d.ts +61 -0
- package/dist/postgres-platform-workerd.js +163 -0
- package/dist/{schema-BfQEBqFv.d.ts → schema-DgoPqqsI.d.ts} +1326 -7
- package/dist/schema-sqlite.d.ts +1306 -6
- package/dist/schema-sqlite.js +154 -18
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +254 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +226 -20
- package/package.json +13 -5
- package/src/active-subject.ts +9 -0
- package/src/change-records.ts +38 -0
- package/src/connect.ts +67 -13
- package/src/database.ts +67 -51
- package/src/dialect.ts +17 -8
- package/src/grants.ts +3 -3
- package/src/hyperdrive.ts +181 -104
- package/src/log.ts +196 -15
- package/src/migrate.ts +157 -30
- package/src/migrations/postgres/0022_log_015.sql +672 -0
- package/src/migrations/postgres/0023_change_path.sql +153 -0
- package/src/migrations/postgres/0024_use_015.sql +556 -0
- package/src/migrations/postgres/0025_app_change.sql +221 -0
- package/src/migrations/postgres/0026_issue_lock.sql +106 -0
- package/src/migrations/postgres/0027_retention.sql +118 -0
- package/src/migrations/postgres/0028_use_stalls.sql +594 -0
- package/src/migrations/postgres/0029_use_objects.sql +887 -0
- package/src/migrations/postgres/0030_unarchive_restores.sql +259 -0
- package/src/migrations/postgres/0031_app_read.sql +65 -0
- package/src/migrations/postgres/0032_secrets_live_index.sql +4 -0
- package/src/migrations/postgres/0033_use_generations.sql +582 -0
- package/src/migrations/postgres/0034_vault_credentials.sql +153 -0
- package/src/migrations/postgres/0035_first_days.sql +3 -0
- package/src/migrations/postgres/0036_vault_issues.sql +91 -0
- package/src/migrations/postgres/0037_vault_bindings.sql +271 -0
- package/src/migrations/postgres/0040_binding_origin.sql +159 -0
- package/src/migrations/postgres/0041_cutover.sql +132 -0
- package/src/migrations/postgres/0042_sign_in_index.sql +7 -0
- package/src/migrations/postgres/0043_issue_revoke_index.sql +4 -0
- package/src/migrations/postgres/0044_use_by_name.sql +705 -0
- package/src/migrations/postgres/0045_decide_first.sql +878 -0
- package/src/migrations/postgres/0046_way_in_uses.sql +1149 -0
- package/src/migrations/postgres/0047_cutover_finish.sql +268 -0
- package/src/migrations/postgres/0048_use_batch.sql +59 -0
- package/src/migrations/postgres/0049_reads.sql +171 -0
- package/src/migrations/postgres/meta/0022_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0023_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0024_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0025_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0026_snapshot.json +3831 -0
- package/src/migrations/postgres/meta/0027_snapshot.json +3853 -0
- package/src/migrations/postgres/meta/0028_snapshot.json +3853 -0
- package/src/migrations/postgres/meta/0029_snapshot.json +3859 -0
- package/src/migrations/postgres/meta/0030_snapshot.json +3859 -0
- package/src/migrations/postgres/meta/0031_snapshot.json +3859 -0
- package/src/migrations/postgres/meta/0032_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0033_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0034_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0035_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0036_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0037_snapshot.json +3875 -0
- package/src/migrations/postgres/meta/0038_snapshot.json +3881 -0
- package/src/migrations/postgres/meta/0039_snapshot.json +3881 -0
- package/src/migrations/postgres/meta/0040_snapshot.json +3931 -0
- package/src/migrations/postgres/meta/0041_snapshot.json +3981 -0
- package/src/migrations/postgres/meta/0042_snapshot.json +3981 -0
- package/src/migrations/postgres/meta/0043_snapshot.json +3981 -0
- package/src/migrations/postgres/meta/0044_snapshot.json +3991 -0
- package/src/migrations/postgres/meta/0045_snapshot.json +3997 -0
- package/src/migrations/postgres/meta/0046_snapshot.json +3997 -0
- package/src/migrations/postgres/meta/0047_snapshot.json +4234 -0
- package/src/migrations/postgres/meta/_journal.json +182 -0
- package/src/migrations/sqlite/0018_log_015.sql +84 -0
- package/src/migrations/sqlite/0019_retention.sql +2 -0
- package/src/migrations/sqlite/0020_ended_references.sql +1 -0
- package/src/migrations/sqlite/0021_secrets_live_index.sql +1 -0
- package/src/migrations/sqlite/0022_binding_origin.sql +1 -0
- package/src/migrations/sqlite/0023_sign_in_index.sql +2 -0
- package/src/migrations/sqlite/0024_issue_revoke_index.sql +2 -0
- package/src/migrations/sqlite/0025_way_in_uses.sql +42 -0
- package/src/migrations/sqlite/0026_cutover_finish.sql +11 -0
- package/src/migrations/sqlite/0027_reads.sql +38 -0
- package/src/migrations/sqlite/meta/0018_snapshot.json +3529 -0
- package/src/migrations/sqlite/meta/0019_snapshot.json +3538 -0
- package/src/migrations/sqlite/meta/0020_snapshot.json +3545 -0
- package/src/migrations/sqlite/meta/0021_snapshot.json +3553 -0
- package/src/migrations/sqlite/meta/0022_snapshot.json +3560 -0
- package/src/migrations/sqlite/meta/0023_snapshot.json +3579 -0
- package/src/migrations/sqlite/meta/0024_snapshot.json +3598 -0
- package/src/migrations/sqlite/meta/0025_snapshot.json +3609 -0
- package/src/migrations/sqlite/meta/0026_snapshot.json +3616 -0
- package/src/migrations/sqlite/meta/0027_snapshot.json +3834 -0
- package/src/migrations/sqlite/meta/_journal.json +70 -0
- package/src/postgres-js/README.md +183 -0
- package/src/postgres-js/UNLICENSE +24 -0
- package/src/postgres-js/bytes.ts +89 -0
- package/src/postgres-js/connection.ts +882 -0
- package/src/postgres-js/errors.ts +84 -0
- package/src/postgres-js/index.ts +516 -0
- package/src/postgres-js/platform.node.ts +29 -0
- package/src/postgres-js/platform.ts +60 -0
- package/src/postgres-js/platform.workerd.ts +225 -0
- package/src/postgres-js/query.ts +123 -0
- package/src/postgres-js/queue.ts +42 -0
- package/src/postgres-js/result.ts +46 -0
- package/src/postgres-js/types.ts +52 -0
- package/src/postgres.ts +375 -26
- package/src/schema-version.ts +2 -2
- package/src/schema.sqlite.ts +198 -7
- package/src/schema.ts +291 -6
- package/src/verify-runtime.ts +18 -29
- package/LICENSE +0 -21
- package/dist/database-wq1AVRAP.d.ts +0 -73
- package/dist/postgres-Bmar8y0q.js +0 -29
- package/dist/relations-BfR3JjT1.js +0 -38
- package/src/relations.ts +0 -40
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
-- The vault makes and ends credentials itself, each in one statement (T149, D146; docs/design/credential-lifecycle.md; formal/Lifecycle.tla). A sign-in through a trust binding (a CI run's, a workload's) and a revoke were the app's transactions under 0.14's head, with `prepare` sealing whatever row the app proposed: a sign-in was 8 statements, 6 holding the head every app write waits for, and a burst of them queued there (3.5 s at p99 for 30 at once). Here each is one call of the vault's, one statement and one round trip: the vault weighs the binding, the account, the rate, the token spent and the times on the rows as they are under their locks, writes the credential and its seal, and numbers its change, inside Postgres. It only adds: two functions only 0.15's vault calls, behind the switch.
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
-- A sign-in through binding `p_binding`, as the vault decided it from the binding's columns it names (`p_principal`, `p_generation`, `p_binding_profile`, `p_binding_scope`, `p_binding_policy`): it goes ahead only while they are the row's, under its lock. Its answer, JSON: `{"number": "<n>"}`, the change's number, or `{"state": …}` with nothing written:
|
|
4
|
+
-- - `unbound`: the account is not active at the binding's generation, or the binding has ended, has a tombstone, or is not as named;
|
|
5
|
+
-- - `frozen`: a manager froze the account or the binding (`p_frozen`, a workload's sign-in);
|
|
6
|
+
-- - `busy`: the binding made `p_rate` credentials in the last `p_window` ms (a lock not had within `lock_timeout` raises `lock_not_available`, busy too);
|
|
7
|
+
-- - `expired`: the proof's own times are past (`p_valid_until`), or the credential's end is past, or further than `p_longest` ms (and a minute, for the Worker's clock) from the database's clock;
|
|
8
|
+
-- - `replayed`: the ID token, by its signing input's hash (`p_spent`), was spent already;
|
|
9
|
+
-- - `retired`: the app signed the row (its `auth_mac`) under a key it has rotated from (`p_app_key`, its log key's id), which `key.rotate` or `key.reseal` names, as `appendEntries` refuses one (KeyRetired). A rotation committing while this runs leaves a row its new key refuses: dead, never live.
|
|
10
|
+
-- Locks, in the order every writer takes them (0026): the account's row shared (MemberLocked: its removal waits, or is seen), the binding's exclusive (BindingLocked: its rate, and an unbind, whose update takes it before its sweep), then the issue lock shared, its number under it (IssueOffFence: never the fence, so no use waits on a sign-in, R58). Nobody can name the credential before the commit hands its token out. No 0.14 head, and no 0.14 entry: the change is the record (D147).
|
|
11
|
+
CREATE FUNCTION sign_in(
|
|
12
|
+
p_request text, p_at bigint, p_actor text, p_payload text, p_mac bytea, p_seal bytea,
|
|
13
|
+
p_binding uuid, p_principal text, p_generation integer, p_binding_profile text, p_binding_scope text, p_binding_policy text,
|
|
14
|
+
p_spent bytea, p_valid_until bigint, p_frozen boolean, p_rate integer, p_window bigint, p_longest bigint,
|
|
15
|
+
p_id uuid, p_kind text, p_auth_mac bytea, p_token_hash bytea, p_token_hint text, p_label text, p_created_by text,
|
|
16
|
+
p_scope text, p_places text, p_expires bigint, p_not_after bigint, p_jkt text, p_app_key text
|
|
17
|
+
) RETURNS json
|
|
18
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
|
|
19
|
+
DECLARE
|
|
20
|
+
n bigint;
|
|
21
|
+
held record;
|
|
22
|
+
now_ms bigint;
|
|
23
|
+
since timestamptz;
|
|
24
|
+
target uuid := public.log_object('credential', p_id::text);
|
|
25
|
+
BEGIN
|
|
26
|
+
IF p_created_by IS DISTINCT FROM 'binding:' || p_binding::text OR p_kind NOT IN ('service', 'workload') THEN
|
|
27
|
+
RAISE EXCEPTION 'a sign-in makes a credential of its binding''s, a service''s or a workload''s' USING ERRCODE = 'invalid_parameter_value';
|
|
28
|
+
END IF;
|
|
29
|
+
-- A lock not had within `lock_timeout` raises `lock_not_available`, which the vault answers as busy: no subtransaction to catch it here.
|
|
30
|
+
-- The account's row, locked, then held to its newest change's seal in a statement of its own, which reads what committed
|
|
31
|
+
-- while it waited: a recheck under the lock sees the row's new version beside the snapshot's older changes, and would
|
|
32
|
+
-- refuse an account a grant just changed. A row put back is none; 0.14's has neither seal nor change (before the cutover).
|
|
33
|
+
SELECT m.seal INTO held FROM public.vault_members m
|
|
34
|
+
WHERE m.principal = p_principal AND m.status = 'active' AND m.generation = p_generation
|
|
35
|
+
FOR SHARE;
|
|
36
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'unbound'); END IF;
|
|
37
|
+
IF held.seal IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = public.log_object('principal', p_principal) ORDER BY c.number DESC LIMIT 1) THEN
|
|
38
|
+
RETURN json_build_object('state', 'unbound');
|
|
39
|
+
END IF;
|
|
40
|
+
PERFORM 1 FROM public.service_bindings b
|
|
41
|
+
WHERE b.id = p_binding AND b.principal = p_principal AND b.generation = p_generation AND b.revoked_at IS NULL AND b.profile = p_binding_profile
|
|
42
|
+
AND b.scope IS NOT DISTINCT FROM p_binding_scope AND b.policy IS NOT DISTINCT FROM p_binding_policy
|
|
43
|
+
FOR UPDATE;
|
|
44
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'unbound'); END IF;
|
|
45
|
+
-- The app's key, not one it rotated from (audit_log_subject_idx).
|
|
46
|
+
IF EXISTS (SELECT 1 FROM public.audit_log a WHERE a.author = 'app' AND a.subject_principal = p_app_key AND a.action IN ('key.reseal', 'key.rotate')) THEN
|
|
47
|
+
RETURN json_build_object('state', 'retired');
|
|
48
|
+
END IF;
|
|
49
|
+
-- Its tombstone: a binding put back after its unbind is none (audit_log_unbind_idx).
|
|
50
|
+
IF EXISTS (SELECT 1 FROM public.audit_log a WHERE a.author = 'app' AND a.action = 'token.unbind' AND a.decision = 'allow'
|
|
51
|
+
AND (a.metadata::jsonb ->> 'bindingId') = p_binding::text) THEN
|
|
52
|
+
RETURN json_build_object('state', 'unbound');
|
|
53
|
+
END IF;
|
|
54
|
+
-- A freeze of the whole instance, the account or the binding: its newest freeze or lift (audit_log_freeze_idx).
|
|
55
|
+
IF p_frozen AND EXISTS (
|
|
56
|
+
SELECT 1 FROM unnest(ARRAY['instance', p_principal, 'binding:' || p_binding::text]) t(target)
|
|
57
|
+
WHERE (SELECT a.action FROM public.audit_log a
|
|
58
|
+
WHERE a.author = 'app' AND a.action IN ('workload.freeze', 'workload.unfreeze') AND a.decision = 'allow'
|
|
59
|
+
AND (a.metadata::jsonb ->> 'target') = t.target
|
|
60
|
+
ORDER BY a.seq DESC LIMIT 1) = 'workload.freeze'
|
|
61
|
+
) THEN
|
|
62
|
+
RETURN json_build_object('state', 'frozen');
|
|
63
|
+
END IF;
|
|
64
|
+
-- Its rate, under the binding's row (credentials_issued_by_idx: the last window's, never the history). Its start a
|
|
65
|
+
-- value, not `clock_timestamp()`, which is volatile: compared with that, the index bounds nothing, and the window's
|
|
66
|
+
-- count would read the binding's every credential.
|
|
67
|
+
since := clock_timestamp() - p_window * interval '1 millisecond';
|
|
68
|
+
IF (SELECT count(*) FROM (
|
|
69
|
+
SELECT 1 FROM public.credentials c
|
|
70
|
+
WHERE c.principal = p_principal AND c.created_by = p_created_by AND c.created_at > since
|
|
71
|
+
LIMIT p_rate
|
|
72
|
+
) z) >= p_rate THEN
|
|
73
|
+
RETURN json_build_object('state', 'busy');
|
|
74
|
+
END IF;
|
|
75
|
+
now_ms := (extract(epoch FROM clock_timestamp()) * 1000)::bigint;
|
|
76
|
+
IF p_valid_until <= now_ms OR p_expires <= now_ms OR p_expires > now_ms + p_longest + 60000 OR (p_not_after IS NOT NULL AND (p_not_after <= now_ms OR p_expires > p_not_after)) THEN
|
|
77
|
+
RETURN json_build_object('state', 'expired');
|
|
78
|
+
END IF;
|
|
79
|
+
-- The token spent (SpentChecked): the primary key decides between two runs presenting it at once.
|
|
80
|
+
IF p_spent IS NOT NULL THEN
|
|
81
|
+
INSERT INTO public.consumed_tokens (hash) VALUES (p_spent) ON CONFLICT DO NOTHING;
|
|
82
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'replayed'); END IF;
|
|
83
|
+
END IF;
|
|
84
|
+
INSERT INTO public.credentials (id, kind, auth_mac, token_hash, token_hint, generation, principal, identity_id, scope, places, label, created_by,
|
|
85
|
+
expires_at, revoked_at, jkt, not_after, seal)
|
|
86
|
+
VALUES (p_id, p_kind, p_auth_mac, p_token_hash, p_token_hint, p_generation, p_principal, NULL, p_scope, p_places, p_label, p_created_by,
|
|
87
|
+
timestamptz 'epoch' + p_expires * interval '1 millisecond', NULL, p_jkt,
|
|
88
|
+
CASE WHEN p_not_after IS NULL THEN NULL ELSE timestamptz 'epoch' + p_not_after * interval '1 millisecond' END, p_seal);
|
|
89
|
+
UPDATE public.service_bindings SET last_used_at = clock_timestamp() WHERE id = p_binding;
|
|
90
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
|
|
91
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
92
|
+
VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
|
|
93
|
+
RETURNING number INTO n;
|
|
94
|
+
RETURN json_build_object('number', n::text);
|
|
95
|
+
END
|
|
96
|
+
$$;
|
|
97
|
+
--> statement-breakpoint
|
|
98
|
+
-- A revoke of credential `p_id`, from the row as the vault sealed its object (`p_seal_from`) and as the app signed it (`p_mac_from`), to revoked, with the app's MAC over the revoked row (`p_mac`) and the vault's seal over its object (`p_seal`). Only a row the log holds: one 0.14 made, with no seal, the app revokes as 0.14 does, with its entry. Its answer, JSON: `{"number": "<n>"}`, or `{"state": …}` with nothing written:
|
|
99
|
+
-- - `moved`: the row is not as named, or revoked already;
|
|
100
|
+
-- - `stale`: its seal is not its object's newest change's (ChangeChecksSeal), a row put back;
|
|
101
|
+
-- - `policy`: the caller's right to revoke it was weighed on a policy (`p_policy_seq`, the newest `policy.change` then, and the slugs of the environments it protected, `p_protected`, `p_slugs`) that has changed since (RevokePolicyHeld), as `policyStill` refuses under 0.14's head. Read once the fence is held: every policy change and rename takes it before it commits (0.14's entry, through `audit_log_fence`; 0.15's, through its change), so one that commits before is seen, and one after waits.
|
|
102
|
+
-- Locks: the row's, then the fence exclusive (RevokeTakesFence), held only for the statement's work: a use in flight is numbered before, one after reads the row revoked. It issues nothing, and takes no issue lock.
|
|
103
|
+
CREATE FUNCTION revoke_credential(
|
|
104
|
+
p_request text, p_at bigint, p_actor text, p_payload text, p_mac bytea, p_seal_from bytea, p_seal bytea,
|
|
105
|
+
p_id uuid, p_mac_from bytea, p_auth_mac bytea, p_revoked_at bigint, p_revoked_by text,
|
|
106
|
+
p_policy boolean, p_policy_seq bigint, p_protected uuid[], p_slugs text[]
|
|
107
|
+
) RETURNS json
|
|
108
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
|
|
109
|
+
DECLARE
|
|
110
|
+
n bigint;
|
|
111
|
+
target uuid := public.log_object('credential', p_id::text);
|
|
112
|
+
BEGIN
|
|
113
|
+
IF p_seal_from IS NULL OR p_seal IS NULL THEN
|
|
114
|
+
RAISE EXCEPTION 'the vault revokes a credential the log holds, from its seal' USING ERRCODE = 'invalid_parameter_value';
|
|
115
|
+
END IF;
|
|
116
|
+
PERFORM 1 FROM public.credentials c
|
|
117
|
+
WHERE c.id = p_id AND c.revoked_at IS NULL AND c.auth_mac = p_mac_from AND c.seal IS NOT DISTINCT FROM p_seal_from
|
|
118
|
+
FOR UPDATE;
|
|
119
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'moved'); END IF;
|
|
120
|
+
PERFORM pg_advisory_xact_lock(1668244070, 0);
|
|
121
|
+
-- ChangeChecksSeal: a row the log sealed once, put back since, is not its newest change's.
|
|
122
|
+
IF p_seal_from IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1) THEN
|
|
123
|
+
RETURN json_build_object('state', 'stale');
|
|
124
|
+
END IF;
|
|
125
|
+
IF p_policy AND (
|
|
126
|
+
(SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.action = 'policy.change' AND a.decision = 'allow' ORDER BY a.seq DESC LIMIT 1)
|
|
127
|
+
IS DISTINCT FROM p_policy_seq
|
|
128
|
+
-- Each protected environment's slug as it stands, none once it or its project is deleted (a tombstone), as `policyNow` reads them.
|
|
129
|
+
OR EXISTS (
|
|
130
|
+
SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
|
|
131
|
+
WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
|
|
132
|
+
WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
|
|
133
|
+
)
|
|
134
|
+
) THEN
|
|
135
|
+
RETURN json_build_object('state', 'policy');
|
|
136
|
+
END IF;
|
|
137
|
+
UPDATE public.credentials
|
|
138
|
+
SET revoked_at = timestamptz 'epoch' + p_revoked_at * interval '1 millisecond', revoked_by = p_revoked_by, auth_mac = p_auth_mac, seal = p_seal
|
|
139
|
+
WHERE id = p_id;
|
|
140
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
141
|
+
VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
|
|
142
|
+
RETURNING number INTO n;
|
|
143
|
+
RETURN json_build_object('number', n::text);
|
|
144
|
+
END
|
|
145
|
+
$$;
|
|
146
|
+
--> statement-breakpoint
|
|
147
|
+
REVOKE ALL ON FUNCTION sign_in(text, bigint, text, text, bytea, bytea, uuid, text, integer, text, text, text, bytea, bigint, boolean, integer, bigint, bigint, uuid, text, bytea, bytea, text, text, text, text, text, bigint, bigint, text, text) FROM PUBLIC;
|
|
148
|
+
--> statement-breakpoint
|
|
149
|
+
GRANT EXECUTE ON FUNCTION sign_in(text, bigint, text, text, bytea, bytea, uuid, text, integer, text, text, text, bytea, bigint, boolean, integer, bigint, bigint, uuid, text, bytea, bytea, text, text, text, text, text, bigint, bigint, text, text) TO coffre_vault;
|
|
150
|
+
--> statement-breakpoint
|
|
151
|
+
REVOKE ALL ON FUNCTION revoke_credential(text, bigint, text, text, bytea, bytea, bytea, uuid, bytea, bytea, bigint, text, boolean, bigint, uuid[], text[]) FROM PUBLIC;
|
|
152
|
+
--> statement-breakpoint
|
|
153
|
+
GRANT EXECUTE ON FUNCTION revoke_credential(text, bigint, text, text, bytea, bytea, bytea, uuid, bytea, bytea, bigint, text, boolean, bigint, uuid[], text[]) TO coffre_vault;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
-- The days of uses a new database starts with (T143): 0.15's uses go in a partition of their day (0022), made ahead by the checkpointer once it runs (`daysAround`, packages/vault/src/log-checkpointer.ts), and until it has, a use at a time no day holds fails ("no partition of relation log_uses found for row"). A deployment's pipeline migrates before it deploys, so this is the first thing there is: yesterday's (the fence's floor starts three seconds before the migration, which may be just before midnight), today's and the next three, the same days `daysAround` makes. A checkpointer down for longer than the end of today + 3 leaves the days it stopped at (its alarm says so). It only adds partitions, which the previous release's code never touches; and runs once, as the one migration that has it, so later releases' days are the checkpointer's.
|
|
2
|
+
-- Integer offsets from the UTC date, not a series of timestamps: `generate_series` over dates is timezone-aware, and a session zone whose DST jump falls at midnight shifts its values past the last bound.
|
|
3
|
+
SELECT make_day((now() AT TIME ZONE 'UTC')::date + d) FROM generate_series(-1, 3) AS d;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
-- The vault issues tokens itself (T149, PR 2; docs/design/credential-lifecycle.md; formal/Lifecycle.tla's issue): a service account's token, a person's personal token, a handle. They were the app's transactions under 0.14's head, `prepare` sealing whatever row the app proposed, so the vault weighed no right of the issuer's. Now the vault reads the issuer, the account and the credential the request came in on, and decides with `@coffre/core/access`'s rules whether the issuer may give the scope (R15, R32); this statement then writes the token only while what it read is still so, under those rows' locks (IssuerLocked, IssuerCAS). Nothing is held between the read and this statement. It only adds: two functions only 0.15's vault calls, behind the switch.
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
-- A token issued by `p_actor`, on credential `p_via` (null for none), to `p_principal` at generation `p_generation`, as the vault decided it on the members' rows at `p_actor_seq` and `p_principal_seq` (their `access_seq`: every change of what a member holds moves it), the credential at its seal `p_via_seal` and its end `p_via_end`, and the policy at `p_policy_seq` with the slugs of the environments it protected and of those the token's scope names (`p_protected`, `p_slugs`: a scoped role matches by slug). Its answer, JSON: `{"number": "<n>"}`, or `{"state": …}` with nothing written:
|
|
4
|
+
-- - `moved`: a member is not active as read, at its generation, or its access moved since (asked again);
|
|
5
|
+
-- - `ended`: the credential it came in on is revoked, past its end or not at the end the vault read, another's, not sealed as read, or not its object's newest change's;
|
|
6
|
+
-- - `policy`: the policy moved since, or one of those environments' slugs (as `policyStill`);
|
|
7
|
+
-- - `retired`: the app signed the row under a key it rotated from (as `sign_in`);
|
|
8
|
+
-- - `expired`: the token's end is past.
|
|
9
|
+
-- Locks, in the order every writer takes them (0026): the members' rows shared, by principal as `lockMembers` takes them (a demotion or a removal waits for the issue, or the issue sees it), then the credential's row shared (a revoke of it waits, or the issue sees it), each compared in a statement of its own once had (a recheck under the lock would mix the new row with the snapshot's older changes), then the issue lock shared, the policy compared and the number taken under it (IssueOffFence: never the fence, so no use waits on an issue, R58). The policy is compared under the issue lock, shared, which a policy change takes exclusive (IssuePolicyHeld; review r3): ordered, though without the fence. No 0.14 head, no 0.14 entry: the change records the issue (D147).
|
|
10
|
+
CREATE FUNCTION issue_token(
|
|
11
|
+
p_request text, p_at bigint, p_actor text, p_payload text, p_mac bytea, p_seal bytea,
|
|
12
|
+
p_principal text, p_generation integer, p_principal_seq bigint, p_actor_generation integer, p_actor_seq bigint,
|
|
13
|
+
p_via uuid, p_via_seal bytea, p_via_end bigint,
|
|
14
|
+
p_policy_seq bigint, p_protected uuid[], p_slugs text[], p_app_key text,
|
|
15
|
+
p_id uuid, p_kind text, p_auth_mac bytea, p_token_hash bytea, p_token_hint text, p_label text, p_created_by text,
|
|
16
|
+
p_scope text, p_places text, p_expires bigint
|
|
17
|
+
) RETURNS json
|
|
18
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
|
|
19
|
+
DECLARE
|
|
20
|
+
n bigint;
|
|
21
|
+
BEGIN
|
|
22
|
+
IF p_kind NOT IN ('service', 'personal', 'handle') OR (p_kind = 'service') = (p_principal = p_actor) THEN
|
|
23
|
+
RAISE EXCEPTION 'the vault issues a service account a token, or a person one of their own' USING ERRCODE = 'invalid_parameter_value';
|
|
24
|
+
END IF;
|
|
25
|
+
-- IssuerLocked: the members' rows, then the credential's, shared; a lock not had within `lock_timeout` raises `lock_not_available`, busy.
|
|
26
|
+
PERFORM 1 FROM public.vault_members m WHERE m.principal IN (p_actor, p_principal) ORDER BY m.principal FOR SHARE;
|
|
27
|
+
-- IssuerCAS, once the locks are had.
|
|
28
|
+
IF NOT EXISTS (SELECT 1 FROM public.vault_members m WHERE m.principal = p_actor AND m.status = 'active' AND m.generation = p_actor_generation AND m.access_seq = p_actor_seq)
|
|
29
|
+
OR NOT EXISTS (SELECT 1 FROM public.vault_members m WHERE m.principal = p_principal AND m.status = 'active' AND m.generation = p_generation AND m.access_seq = p_principal_seq) THEN
|
|
30
|
+
RETURN json_build_object('state', 'moved');
|
|
31
|
+
END IF;
|
|
32
|
+
IF p_via IS NOT NULL THEN
|
|
33
|
+
PERFORM 1 FROM public.credentials c WHERE c.id = p_via FOR SHARE;
|
|
34
|
+
-- Its end as the vault authenticated it (`p_via_end`, the earlier of its expiry and its hard end, which its seal
|
|
35
|
+
-- covers): the columns still say so, and it is still ahead. A seal compares bytes, never the columns it was made of.
|
|
36
|
+
IF NOT EXISTS (SELECT 1 FROM public.credentials c
|
|
37
|
+
WHERE c.id = p_via AND c.principal = p_actor AND c.revoked_at IS NULL AND c.seal = p_via_seal
|
|
38
|
+
AND floor(extract(epoch FROM least(c.expires_at, coalesce(c.not_after, c.expires_at))) * 1000)::bigint = p_via_end)
|
|
39
|
+
OR p_via_end <= (extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
|
40
|
+
OR p_via_seal IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = public.log_object('credential', p_via::text) ORDER BY c.number DESC LIMIT 1) THEN
|
|
41
|
+
RETURN json_build_object('state', 'ended');
|
|
42
|
+
END IF;
|
|
43
|
+
END IF;
|
|
44
|
+
-- IssuePolicyHeld: the issue lock shared before the policy is compared, and the number under it. A policy change, or a
|
|
45
|
+
-- write that moves a protected environment's slug, takes it exclusive before the fence (`holdIssues`): it commits
|
|
46
|
+
-- before this compares, and is seen, or waits for this issue's commit.
|
|
47
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
|
|
48
|
+
IF (SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.action = 'policy.change' AND a.decision = 'allow' ORDER BY a.seq DESC LIMIT 1)
|
|
49
|
+
IS DISTINCT FROM p_policy_seq
|
|
50
|
+
OR EXISTS (
|
|
51
|
+
SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
|
|
52
|
+
WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
|
|
53
|
+
WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
|
|
54
|
+
) THEN
|
|
55
|
+
RETURN json_build_object('state', 'policy');
|
|
56
|
+
END IF;
|
|
57
|
+
IF EXISTS (SELECT 1 FROM public.audit_log a WHERE a.author = 'app' AND a.subject_principal = p_app_key AND a.action IN ('key.reseal', 'key.rotate')) THEN
|
|
58
|
+
RETURN json_build_object('state', 'retired');
|
|
59
|
+
END IF;
|
|
60
|
+
IF p_expires <= (extract(epoch FROM clock_timestamp()) * 1000)::bigint THEN
|
|
61
|
+
RETURN json_build_object('state', 'expired');
|
|
62
|
+
END IF;
|
|
63
|
+
INSERT INTO public.credentials (id, kind, auth_mac, token_hash, token_hint, generation, principal, identity_id, scope, places, label, created_by,
|
|
64
|
+
expires_at, revoked_at, seal)
|
|
65
|
+
VALUES (p_id, p_kind, p_auth_mac, p_token_hash, p_token_hint, p_generation, p_principal, NULL, p_scope, p_places, p_label, p_created_by,
|
|
66
|
+
timestamptz 'epoch' + p_expires * interval '1 millisecond', NULL, p_seal);
|
|
67
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
68
|
+
VALUES (nextval('public.log_number'), p_at, p_request, public.log_object('credential', p_id::text), p_actor, p_payload, p_seal, p_mac)
|
|
69
|
+
RETURNING number INTO n;
|
|
70
|
+
RETURN json_build_object('number', n::text);
|
|
71
|
+
END
|
|
72
|
+
$$;
|
|
73
|
+
--> statement-breakpoint
|
|
74
|
+
-- The credential an issue came in on, as the vault weighs it: its object's columns, its seal, and its object's newest change's seal; no row for none. The vault's login reads no credential otherwise (vault-guarantees.sql), and this answers neither its token's hash nor its MAC.
|
|
75
|
+
CREATE FUNCTION issuing_credential(p_id uuid) RETURNS TABLE (
|
|
76
|
+
id uuid, principal text, kind text, scope text, revoked boolean, expires_at bigint, not_after bigint, generation integer, seal bytea, newest bytea
|
|
77
|
+
)
|
|
78
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
|
|
79
|
+
SELECT c.id, c.principal, c.kind, c.scope, c.revoked_at IS NOT NULL, floor(extract(epoch FROM c.expires_at) * 1000)::bigint,
|
|
80
|
+
floor(extract(epoch FROM c.not_after) * 1000)::bigint, c.generation, c.seal,
|
|
81
|
+
(SELECT l.seal FROM public.log_changes l WHERE l.object = public.log_object('credential', p_id::text) ORDER BY l.number DESC LIMIT 1)
|
|
82
|
+
FROM public.credentials c WHERE c.id = p_id
|
|
83
|
+
$$;
|
|
84
|
+
--> statement-breakpoint
|
|
85
|
+
REVOKE ALL ON FUNCTION issuing_credential(uuid) FROM PUBLIC;
|
|
86
|
+
--> statement-breakpoint
|
|
87
|
+
GRANT EXECUTE ON FUNCTION issuing_credential(uuid) TO coffre_vault;
|
|
88
|
+
--> statement-breakpoint
|
|
89
|
+
REVOKE ALL ON FUNCTION issue_token(text, bigint, text, text, bytea, bytea, text, integer, bigint, integer, bigint, uuid, bytea, bigint, bigint, uuid[], text[], text, uuid, text, bytea, bytea, text, text, text, text, text, bigint) FROM PUBLIC;
|
|
90
|
+
--> statement-breakpoint
|
|
91
|
+
GRANT EXECUTE ON FUNCTION issue_token(text, bigint, text, text, bytea, bytea, text, integer, bigint, integer, bigint, uuid, bytea, bigint, bigint, uuid[], text[], text, uuid, text, bytea, bytea, text, text, text, text, text, bigint) TO coffre_vault;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
-- A service's trust bindings are the vault's to make and end (T149, PR 3a; D148; docs/design/credential-lifecycle.md, section 4; formal/Lifecycle.tla's BindingSealed). A binding was the app's row, vouched for by its MAC alone, so the app's code, turned hostile, could write one that trusts its own key and sign in through it: it holds APP_KEY. Now the vault weighs who makes one, as it weighs a service token's issue, and seals it as a change of the log (`change_bindings`). A sign-in takes only a binding whose seal is the vault's and its object's newest change's: `sign_in` again, given the binding's every column and its seal. This drops 0034's `sign_in`, which only 0.15's vault calls, and that vault is unreleased; it adds a function only that vault calls.
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
-- A sign-in as in 0034, through a binding held to the columns and seal the vault checked (BindingSealed), and that seal held to its object's newest change. A binding's end is that change, so 0.14's tombstone (`token.unbind`) is no longer read. Locks as in 0034.
|
|
4
|
+
DROP FUNCTION sign_in(text, bigint, text, text, bytea, bytea, uuid, text, integer, text, text, text, bytea, bigint, boolean, integer, bigint, bigint, uuid, text, bytea, bytea, text, text, text, text, text, bigint, bigint, text, text);
|
|
5
|
+
--> statement-breakpoint
|
|
6
|
+
CREATE FUNCTION sign_in(
|
|
7
|
+
p_request text, p_at bigint, p_actor text, p_payload text, p_mac bytea, p_seal bytea,
|
|
8
|
+
p_binding uuid, p_principal text, p_generation integer, p_binding_profile text, p_binding_scope text, p_binding_policy text,
|
|
9
|
+
p_binding_issuer text, p_binding_jwks_uri text, p_binding_claims text, p_binding_seal bytea,
|
|
10
|
+
p_spent bytea, p_valid_until bigint, p_frozen boolean, p_rate integer, p_window bigint, p_longest bigint,
|
|
11
|
+
p_id uuid, p_kind text, p_auth_mac bytea, p_token_hash bytea, p_token_hint text, p_label text, p_created_by text,
|
|
12
|
+
p_scope text, p_places text, p_expires bigint, p_not_after bigint, p_jkt text, p_app_key text
|
|
13
|
+
) RETURNS json
|
|
14
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
|
|
15
|
+
DECLARE
|
|
16
|
+
n bigint;
|
|
17
|
+
held record;
|
|
18
|
+
now_ms bigint;
|
|
19
|
+
since timestamptz;
|
|
20
|
+
target uuid := public.log_object('credential', p_id::text);
|
|
21
|
+
BEGIN
|
|
22
|
+
IF p_created_by IS DISTINCT FROM 'binding:' || p_binding::text OR p_kind NOT IN ('service', 'workload') THEN
|
|
23
|
+
RAISE EXCEPTION 'a sign-in makes a credential of its binding''s, a service''s or a workload''s' USING ERRCODE = 'invalid_parameter_value';
|
|
24
|
+
END IF;
|
|
25
|
+
-- A lock not had within `lock_timeout` raises `lock_not_available`, which the vault answers as busy: no subtransaction to catch it here.
|
|
26
|
+
-- The account's row, locked, then held to its newest change's seal in a statement of its own, which reads what committed
|
|
27
|
+
-- while it waited: a recheck under the lock sees the row's new version beside the snapshot's older changes, and would
|
|
28
|
+
-- refuse an account a grant just changed. A row put back is none; 0.14's has neither seal nor change (before the cutover).
|
|
29
|
+
SELECT m.seal INTO held FROM public.vault_members m
|
|
30
|
+
WHERE m.principal = p_principal AND m.status = 'active' AND m.generation = p_generation
|
|
31
|
+
FOR SHARE;
|
|
32
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'unbound'); END IF;
|
|
33
|
+
IF held.seal IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = public.log_object('principal', p_principal) ORDER BY c.number DESC LIMIT 1) THEN
|
|
34
|
+
RETURN json_build_object('state', 'unbound');
|
|
35
|
+
END IF;
|
|
36
|
+
-- The binding as the vault checked its seal (BindingSealed): its every column its object takes, and that seal; then, in
|
|
37
|
+
-- a statement of its own once locked, that seal its object's newest change's. A row put back after its end, or edited
|
|
38
|
+
-- around the vault, is none.
|
|
39
|
+
PERFORM 1 FROM public.service_bindings b
|
|
40
|
+
WHERE b.id = p_binding AND b.principal = p_principal AND b.generation = p_generation AND b.revoked_at IS NULL AND b.profile = p_binding_profile
|
|
41
|
+
AND b.scope IS NOT DISTINCT FROM p_binding_scope AND b.policy IS NOT DISTINCT FROM p_binding_policy
|
|
42
|
+
AND b.issuer = p_binding_issuer AND b.jwks_uri = p_binding_jwks_uri AND b.claims = p_binding_claims AND b.seal = p_binding_seal
|
|
43
|
+
FOR UPDATE;
|
|
44
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'unbound'); END IF;
|
|
45
|
+
IF p_binding_seal IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = public.log_object('binding', p_binding::text) ORDER BY c.number DESC LIMIT 1) THEN
|
|
46
|
+
RETURN json_build_object('state', 'unbound');
|
|
47
|
+
END IF;
|
|
48
|
+
-- The app's key, not one it rotated from (audit_log_subject_idx).
|
|
49
|
+
IF EXISTS (SELECT 1 FROM public.audit_log a WHERE a.author = 'app' AND a.subject_principal = p_app_key AND a.action IN ('key.reseal', 'key.rotate')) THEN
|
|
50
|
+
RETURN json_build_object('state', 'retired');
|
|
51
|
+
END IF;
|
|
52
|
+
-- A freeze of the whole instance, the account or the binding: its newest freeze or lift (audit_log_freeze_idx).
|
|
53
|
+
IF p_frozen AND EXISTS (
|
|
54
|
+
SELECT 1 FROM unnest(ARRAY['instance', p_principal, 'binding:' || p_binding::text]) t(target)
|
|
55
|
+
WHERE (SELECT a.action FROM public.audit_log a
|
|
56
|
+
WHERE a.author = 'app' AND a.action IN ('workload.freeze', 'workload.unfreeze') AND a.decision = 'allow'
|
|
57
|
+
AND (a.metadata::jsonb ->> 'target') = t.target
|
|
58
|
+
ORDER BY a.seq DESC LIMIT 1) = 'workload.freeze'
|
|
59
|
+
) THEN
|
|
60
|
+
RETURN json_build_object('state', 'frozen');
|
|
61
|
+
END IF;
|
|
62
|
+
-- Its rate, under the binding's row (credentials_issued_by_idx: the last window's, never the history). Its start a
|
|
63
|
+
-- value, not `clock_timestamp()`, which is volatile: compared with that, the index bounds nothing, and the window's
|
|
64
|
+
-- count would read the binding's every credential.
|
|
65
|
+
since := clock_timestamp() - p_window * interval '1 millisecond';
|
|
66
|
+
IF (SELECT count(*) FROM (
|
|
67
|
+
SELECT 1 FROM public.credentials c
|
|
68
|
+
WHERE c.principal = p_principal AND c.created_by = p_created_by AND c.created_at > since
|
|
69
|
+
LIMIT p_rate
|
|
70
|
+
) z) >= p_rate THEN
|
|
71
|
+
RETURN json_build_object('state', 'busy');
|
|
72
|
+
END IF;
|
|
73
|
+
now_ms := (extract(epoch FROM clock_timestamp()) * 1000)::bigint;
|
|
74
|
+
IF p_valid_until <= now_ms OR p_expires <= now_ms OR p_expires > now_ms + p_longest + 60000 OR (p_not_after IS NOT NULL AND (p_not_after <= now_ms OR p_expires > p_not_after)) THEN
|
|
75
|
+
RETURN json_build_object('state', 'expired');
|
|
76
|
+
END IF;
|
|
77
|
+
-- The token spent (SpentChecked): the primary key decides between two runs presenting it at once.
|
|
78
|
+
IF p_spent IS NOT NULL THEN
|
|
79
|
+
INSERT INTO public.consumed_tokens (hash) VALUES (p_spent) ON CONFLICT DO NOTHING;
|
|
80
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'replayed'); END IF;
|
|
81
|
+
END IF;
|
|
82
|
+
INSERT INTO public.credentials (id, kind, auth_mac, token_hash, token_hint, generation, principal, identity_id, scope, places, label, created_by,
|
|
83
|
+
expires_at, revoked_at, jkt, not_after, seal)
|
|
84
|
+
VALUES (p_id, p_kind, p_auth_mac, p_token_hash, p_token_hint, p_generation, p_principal, NULL, p_scope, p_places, p_label, p_created_by,
|
|
85
|
+
timestamptz 'epoch' + p_expires * interval '1 millisecond', NULL, p_jkt,
|
|
86
|
+
CASE WHEN p_not_after IS NULL THEN NULL ELSE timestamptz 'epoch' + p_not_after * interval '1 millisecond' END, p_seal);
|
|
87
|
+
UPDATE public.service_bindings SET last_used_at = clock_timestamp() WHERE id = p_binding;
|
|
88
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
|
|
89
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
90
|
+
VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
|
|
91
|
+
RETURNING number INTO n;
|
|
92
|
+
RETURN json_build_object('number', n::text);
|
|
93
|
+
END
|
|
94
|
+
$$;
|
|
95
|
+
--> statement-breakpoint
|
|
96
|
+
-- A change of service `p_principal`'s bindings, as the vault decided it. It does either or both of these:
|
|
97
|
+
-- - Makes a binding (`p_make`: its row, `p_id` to `p_seal`), weighed on its maker `p_actor` at `p_actor_seq`, the account at `p_principal_seq` (their `access_seq`), the credential `p_via` at its seal and end, and the policy at `p_policy_seq` with its slugs.
|
|
98
|
+
-- - Ends bindings (`p_ends`, moving each from the MAC and seal read to `p_end_auth_mac` and `p_end_seal`), and with them every live credential each issued (`p_creds`, likewise).
|
|
99
|
+
-- Each object's change is in `p_change_*`, in order.
|
|
100
|
+
-- Its answer, JSON: `{"number": "<n>"}`, the last change's number, or `{"state": …}` with nothing written:
|
|
101
|
+
-- - `moved`: a member, a binding or a credential is not as read, or a live credential that a binding it ends issued is not named (asked again);
|
|
102
|
+
-- - `ended`: the credential it came in on is not as read, or past its end;
|
|
103
|
+
-- - `policy`: the policy moved since, or one of its environments' slugs;
|
|
104
|
+
-- - `retired`: the app signed the rows under a key it rotated from;
|
|
105
|
+
-- - `stale`: a row it ends is not its object's newest change's (one put back);
|
|
106
|
+
-- - `full`: the account holds `p_max` live bindings, besides those it ends.
|
|
107
|
+
-- Locks, in the order every writer takes them:
|
|
108
|
+
-- 1. the account's row, its update lock (two binds take turns over its count), then the maker's, shared (IssuerLocked);
|
|
109
|
+
-- 2. the credential it came in on, shared;
|
|
110
|
+
-- 3. the MCP connection a bind came through, shared; the bindings it ends and their credentials, exclusive (BindingLocked: a sign-in through one waits, or is seen);
|
|
111
|
+
-- 4. the issue lock, shared (IssuePolicyHeld, IssueOffFence);
|
|
112
|
+
-- 5. when it ends any, the fence, exclusive (RevokeTakesFence), after the issue lock (PolicyIssueFirst).
|
|
113
|
+
-- No 0.14 head and no 0.14 entry: its changes record it (D147).
|
|
114
|
+
CREATE FUNCTION change_bindings(
|
|
115
|
+
p_at bigint, p_actor text,
|
|
116
|
+
p_make boolean, p_principal text, p_generation integer, p_principal_seq bigint, p_actor_generation integer, p_actor_seq bigint,
|
|
117
|
+
p_via uuid, p_via_seal bytea, p_via_end bigint, p_connection uuid, p_connection_generation integer,
|
|
118
|
+
p_policy_seq bigint, p_protected uuid[], p_slugs text[], p_app_key text, p_max integer,
|
|
119
|
+
p_live uuid[], p_read_keys uuid[], p_read_names text[],
|
|
120
|
+
p_id uuid, p_auth_mac bytea, p_profile text, p_issuer text, p_jwks_uri text, p_jwks text, p_claims text, p_scope text, p_policy text,
|
|
121
|
+
p_label text, p_created_by text, p_created_at bigint, p_seal bytea,
|
|
122
|
+
p_ends uuid[], p_end_mac_from bytea[], p_end_seal_from bytea[], p_end_auth_mac bytea[], p_end_seal bytea[],
|
|
123
|
+
p_creds uuid[], p_cred_binding uuid[], p_cred_mac_from bytea[], p_cred_seal_from bytea[], p_cred_auth_mac bytea[], p_cred_seal bytea[],
|
|
124
|
+
p_revoked_at bigint, p_revoked_by text,
|
|
125
|
+
p_change_request text[], p_change_object uuid[], p_change_payload text[], p_change_seal bytea[], p_change_mac bytea[]
|
|
126
|
+
) RETURNS json
|
|
127
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
|
|
128
|
+
DECLARE
|
|
129
|
+
n bigint;
|
|
130
|
+
-- A value, not `clock_timestamp()` in a predicate: volatile there, it bounds no index.
|
|
131
|
+
now_ts timestamptz := clock_timestamp();
|
|
132
|
+
ended timestamptz := timestamptz 'epoch' + p_revoked_at * interval '1 millisecond';
|
|
133
|
+
BEGIN
|
|
134
|
+
IF p_make = (p_id IS NULL) OR (NOT p_make AND cardinality(p_ends) = 0) OR cardinality(p_creds) <> cardinality(p_cred_binding)
|
|
135
|
+
OR p_make = (p_live IS NULL) OR cardinality(p_read_keys) <> cardinality(p_read_names)
|
|
136
|
+
OR cardinality(p_change_request) <> (CASE WHEN p_make THEN 1 ELSE 0 END) + (SELECT count(*) FROM unnest(p_end_seal) AS s WHERE s IS NOT NULL)
|
|
137
|
+
+ (SELECT count(*) FROM unnest(p_cred_seal) AS s WHERE s IS NOT NULL) THEN
|
|
138
|
+
RAISE EXCEPTION 'a change of bindings makes one, ends some, or both, with a change for each object' USING ERRCODE = 'invalid_parameter_value';
|
|
139
|
+
END IF;
|
|
140
|
+
IF p_make THEN
|
|
141
|
+
-- IssuerLocked, IssuerCAS, as `issue_token`, by principal: the account's row first (`token:` sorts before `user:`),
|
|
142
|
+
-- its update lock, so that two binds of it take turns over its count of bindings (a sign-in through one of its
|
|
143
|
+
-- bindings waits for the statement too); then the maker's, shared. Then both compared.
|
|
144
|
+
IF p_principal >= p_actor THEN RAISE EXCEPTION 'a binding is a service account''s, made by a person' USING ERRCODE = 'invalid_parameter_value'; END IF;
|
|
145
|
+
PERFORM 1 FROM public.vault_members m WHERE m.principal = p_principal FOR NO KEY UPDATE;
|
|
146
|
+
PERFORM 1 FROM public.vault_members m WHERE m.principal = p_actor FOR SHARE;
|
|
147
|
+
IF NOT EXISTS (SELECT 1 FROM public.vault_members m WHERE m.principal = p_actor AND m.status = 'active' AND m.generation = p_actor_generation AND m.access_seq = p_actor_seq)
|
|
148
|
+
OR NOT EXISTS (SELECT 1 FROM public.vault_members m WHERE m.principal = p_principal AND m.status = 'active' AND m.generation = p_generation AND m.access_seq = p_principal_seq) THEN
|
|
149
|
+
RETURN json_build_object('state', 'moved');
|
|
150
|
+
END IF;
|
|
151
|
+
IF p_via IS NOT NULL THEN
|
|
152
|
+
PERFORM 1 FROM public.credentials c WHERE c.id = p_via FOR SHARE;
|
|
153
|
+
IF NOT EXISTS (SELECT 1 FROM public.credentials c
|
|
154
|
+
WHERE c.id = p_via AND c.principal = p_actor AND c.revoked_at IS NULL AND c.seal = p_via_seal
|
|
155
|
+
AND floor(extract(epoch FROM least(c.expires_at, coalesce(c.not_after, c.expires_at))) * 1000)::bigint = p_via_end)
|
|
156
|
+
-- The clock once the locks are had, not when the statement began: one that ended while it waited has ended.
|
|
157
|
+
OR p_via_end <= (extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
|
158
|
+
OR p_via_seal IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = public.log_object('credential', p_via::text) ORDER BY c.number DESC LIMIT 1) THEN
|
|
159
|
+
RETURN json_build_object('state', 'ended');
|
|
160
|
+
END IF;
|
|
161
|
+
END IF;
|
|
162
|
+
-- The MCP connection it came through, still connected at the caller's generation (as `credentialStill`): shared,
|
|
163
|
+
-- so a disconnect waits for this, or is seen (review r3).
|
|
164
|
+
IF p_connection IS NOT NULL THEN
|
|
165
|
+
PERFORM 1 FROM public.mcp_connections k WHERE k.id = p_connection AND k.revoked_at IS NULL AND k.generation = p_connection_generation FOR SHARE;
|
|
166
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'ended'); END IF;
|
|
167
|
+
END IF;
|
|
168
|
+
-- An id the log holds already is no binding to make, even once its row is gone: its object stands (review r3).
|
|
169
|
+
IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
|
|
170
|
+
RETURN json_build_object('state', 'stale');
|
|
171
|
+
END IF;
|
|
172
|
+
END IF;
|
|
173
|
+
-- The bindings it ends, then the credentials they issued: each row locked, by id, then held to its MAC and seal as
|
|
174
|
+
-- read. Every live credential they issued must be named, so one that a sign-in made meanwhile is read again (`moved`),
|
|
175
|
+
-- never left live.
|
|
176
|
+
PERFORM 1 FROM public.service_bindings b WHERE b.id = ANY(p_ends) ORDER BY b.id FOR UPDATE;
|
|
177
|
+
IF (SELECT count(*) FROM unnest(p_ends, p_end_mac_from, p_end_seal_from) AS e(id, mac, seal)
|
|
178
|
+
JOIN public.service_bindings b ON b.id = e.id AND b.principal = p_principal AND b.revoked_at IS NULL AND b.auth_mac = e.mac AND b.seal IS NOT DISTINCT FROM e.seal)
|
|
179
|
+
<> cardinality(p_ends) THEN
|
|
180
|
+
RETURN json_build_object('state', 'moved');
|
|
181
|
+
END IF;
|
|
182
|
+
-- A credential 0.14 made, or one written around the vault, has no seal: ended with its binding all the same, its row
|
|
183
|
+
-- revoked with no change, the log holding no object of it.
|
|
184
|
+
PERFORM 1 FROM public.credentials c WHERE c.id = ANY(p_creds) ORDER BY c.id FOR UPDATE;
|
|
185
|
+
IF (SELECT count(*) FROM unnest(p_creds, p_cred_binding, p_cred_mac_from, p_cred_seal_from) AS x(id, binding, mac, seal)
|
|
186
|
+
JOIN public.credentials c ON c.id = x.id AND x.binding = ANY(p_ends) AND c.created_by = 'binding:' || x.binding::text
|
|
187
|
+
AND c.revoked_at IS NULL AND c.auth_mac = x.mac AND c.seal IS NOT DISTINCT FROM x.seal)
|
|
188
|
+
<> cardinality(p_creds)
|
|
189
|
+
OR EXISTS (SELECT 1 FROM unnest(p_ends) AS e(id) JOIN public.credentials c
|
|
190
|
+
ON c.principal = p_principal AND c.created_by = 'binding:' || e.id::text AND c.revoked_at IS NULL AND c.expires_at > now_ts
|
|
191
|
+
WHERE NOT (c.id = ANY(p_creds))) THEN
|
|
192
|
+
RETURN json_build_object('state', 'moved');
|
|
193
|
+
END IF;
|
|
194
|
+
IF p_make THEN
|
|
195
|
+
-- The account's live bindings as the app read them, which it weighed this one beside (an issuer's keys moved, the
|
|
196
|
+
-- bindings it replaces): another made or ended since is read again. Under the account's row's lock, which every
|
|
197
|
+
-- bind takes, so two binds take turns. At most `p_max`, those it ends left out.
|
|
198
|
+
IF ARRAY(SELECT b.id FROM public.service_bindings b WHERE b.principal = p_principal AND b.generation = p_generation AND b.revoked_at IS NULL ORDER BY b.id)
|
|
199
|
+
IS DISTINCT FROM ARRAY(SELECT l FROM unnest(p_live) AS l ORDER BY l) THEN
|
|
200
|
+
RETURN json_build_object('state', 'moved');
|
|
201
|
+
END IF;
|
|
202
|
+
IF cardinality(p_live) - (SELECT count(*) FROM unnest(p_live) AS l WHERE l = ANY(p_ends)) >= p_max THEN
|
|
203
|
+
RETURN json_build_object('state', 'full');
|
|
204
|
+
END IF;
|
|
205
|
+
END IF;
|
|
206
|
+
-- IssuePolicyHeld: the issue lock, shared; then, under it, the policy and the slugs this change was weighed on, and the numbers.
|
|
207
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
|
|
208
|
+
IF (SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.action = 'policy.change' AND a.decision = 'allow' ORDER BY a.seq DESC LIMIT 1)
|
|
209
|
+
IS DISTINCT FROM p_policy_seq
|
|
210
|
+
OR EXISTS (
|
|
211
|
+
SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
|
|
212
|
+
WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
|
|
213
|
+
WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
|
|
214
|
+
) THEN
|
|
215
|
+
RETURN json_build_object('state', 'policy');
|
|
216
|
+
END IF;
|
|
217
|
+
IF EXISTS (SELECT 1 FROM public.audit_log a WHERE a.author = 'app' AND a.subject_principal = p_app_key AND a.action IN ('key.reseal', 'key.rotate')) THEN
|
|
218
|
+
RETURN json_build_object('state', 'retired');
|
|
219
|
+
END IF;
|
|
220
|
+
-- What it ends is a use's input: the fence exclusive (RevokeTakesFence), after the issue lock (PolicyIssueFirst), for
|
|
221
|
+
-- the statement's work only. A bind that ends nothing takes none.
|
|
222
|
+
IF cardinality(p_ends) > 0 THEN PERFORM pg_advisory_xact_lock(1668244070, 0); END IF;
|
|
223
|
+
-- The keys a binding it makes reads, by the names they were asked by: still those names (a rename meanwhile names
|
|
224
|
+
-- another). Compared, not locked (a key's writer takes the fence before its row: a share lock here, held into the
|
|
225
|
+
-- fence, would be a cycle; review r3), and once the fence is had, which a rename takes too: one that committed while
|
|
226
|
+
-- this waited is seen (review r4). With no fence, one that commits after this is a rename after the bind.
|
|
227
|
+
IF p_make AND (SELECT count(*) FROM unnest(p_read_keys, p_read_names) AS r(id, name) JOIN public.secrets x ON x.id = r.id AND x.key = r.name) <> cardinality(p_read_keys) THEN
|
|
228
|
+
RETURN json_build_object('state', 'moved');
|
|
229
|
+
END IF;
|
|
230
|
+
-- ChangeChecksSeal: each row it ends is its object's newest change's; one put back is stale. A row with no seal is one
|
|
231
|
+
-- the log never held, or none at all once a seal it had was cleared around the vault: that one is stale too, so the
|
|
232
|
+
-- log's binding is never ended with no change of it (review r2).
|
|
233
|
+
IF EXISTS (SELECT 1 FROM unnest(p_ends, p_end_seal_from) AS e(id, seal)
|
|
234
|
+
WHERE e.seal IS DISTINCT FROM (SELECT l.seal FROM public.log_changes l WHERE l.object = public.log_object('binding', e.id::text) ORDER BY l.number DESC LIMIT 1))
|
|
235
|
+
OR EXISTS (SELECT 1 FROM unnest(p_creds, p_cred_seal_from) AS x(id, seal)
|
|
236
|
+
WHERE x.seal IS DISTINCT FROM (SELECT l.seal FROM public.log_changes l WHERE l.object = public.log_object('credential', x.id::text) ORDER BY l.number DESC LIMIT 1)) THEN
|
|
237
|
+
RETURN json_build_object('state', 'stale');
|
|
238
|
+
END IF;
|
|
239
|
+
IF p_make THEN
|
|
240
|
+
-- Again, now that no lock is left to wait for: a make of the same id that committed meanwhile, its row deleted since,
|
|
241
|
+
-- is seen; one not yet committed holds the id's key, which this insert then waits for and fails on (review r4).
|
|
242
|
+
IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
|
|
243
|
+
RETURN json_build_object('state', 'stale');
|
|
244
|
+
END IF;
|
|
245
|
+
INSERT INTO public.service_bindings (id, auth_mac, principal, generation, profile, issuer, jwks_uri, jwks, claims, scope, policy, label, created_by, created_at, seal)
|
|
246
|
+
VALUES (p_id, p_auth_mac, p_principal, p_generation, p_profile, p_issuer, p_jwks_uri, p_jwks, p_claims, p_scope, p_policy, p_label, p_created_by,
|
|
247
|
+
timestamptz 'epoch' + p_created_at * interval '1 millisecond', p_seal);
|
|
248
|
+
END IF;
|
|
249
|
+
UPDATE public.service_bindings b SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = e.mac, seal = e.seal
|
|
250
|
+
FROM unnest(p_ends, p_end_auth_mac, p_end_seal) AS e(id, mac, seal) WHERE b.id = e.id;
|
|
251
|
+
UPDATE public.credentials c SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = x.mac, seal = x.seal
|
|
252
|
+
FROM unnest(p_creds, p_cred_auth_mac, p_cred_seal) AS x(id, mac, seal) WHERE c.id = x.id;
|
|
253
|
+
-- Each object's change, numbered in the order named: the binding made, those ended, their credentials.
|
|
254
|
+
FOR i IN 1 .. cardinality(p_change_request) LOOP
|
|
255
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
256
|
+
VALUES (nextval('public.log_number'), p_at, p_change_request[i], p_change_object[i], p_actor, p_change_payload[i], p_change_seal[i], p_change_mac[i])
|
|
257
|
+
RETURNING number INTO n;
|
|
258
|
+
END LOOP;
|
|
259
|
+
-- Only rows the log never held, ended: no change, nothing to seal.
|
|
260
|
+
RETURN json_build_object('number', coalesce(n::text, '0'));
|
|
261
|
+
END
|
|
262
|
+
$$;
|
|
263
|
+
|
|
264
|
+
--> statement-breakpoint
|
|
265
|
+
REVOKE ALL ON FUNCTION sign_in(text, bigint, text, text, bytea, bytea, uuid, text, integer, text, text, text, text, text, text, bytea, bytea, bigint, boolean, integer, bigint, bigint, uuid, text, bytea, bytea, text, text, text, text, text, bigint, bigint, text, text) FROM PUBLIC;
|
|
266
|
+
--> statement-breakpoint
|
|
267
|
+
GRANT EXECUTE ON FUNCTION sign_in(text, bigint, text, text, bytea, bytea, uuid, text, integer, text, text, text, text, text, text, bytea, bytea, bigint, boolean, integer, bigint, bigint, uuid, text, bytea, bytea, text, text, text, text, text, bigint, bigint, text, text) TO coffre_vault;
|
|
268
|
+
--> statement-breakpoint
|
|
269
|
+
REVOKE ALL ON FUNCTION change_bindings(bigint, text, boolean, text, integer, bigint, integer, bigint, uuid, bytea, bigint, uuid, integer, bigint, uuid[], text[], text, integer, uuid[], uuid[], text[], uuid, bytea, text, text, text, text, text, text, text, text, text, bigint, bytea, uuid[], bytea[], bytea[], bytea[], bytea[], uuid[], uuid[], bytea[], bytea[], bytea[], bytea[], bigint, text, text[], uuid[], text[], bytea[], bytea[]) FROM PUBLIC;
|
|
270
|
+
--> statement-breakpoint
|
|
271
|
+
GRANT EXECUTE ON FUNCTION change_bindings(bigint, text, boolean, text, integer, bigint, integer, bigint, uuid, bytea, bigint, uuid, integer, bigint, uuid[], text[], text, integer, uuid[], uuid[], text[], uuid, bytea, text, text, text, text, text, text, text, text, text, bigint, bytea, uuid[], bytea[], bytea[], bytea[], bytea[], uuid[], uuid[], bytea[], bytea[], bytea[], bytea[], bigint, text, text[], uuid[], text[], bytea[], bytea[]) TO coffre_vault;
|