@coffre/db 0.14.4 → 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 -596
- package/dist/log.js +224 -10
- package/dist/migrate.d.ts +43 -3
- package/dist/migrate.js +149 -29
- package/dist/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- 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/0021_snapshot.json +3334 -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 +189 -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-z8GcN_Xu.d.ts → schema-DgoPqqsI.d.ts} +1329 -9
- package/dist/schema-sqlite.d.ts +1308 -8
- package/dist/schema-sqlite.js +156 -20
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +261 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +229 -22
- 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 +244 -11
- package/src/migrate.ts +157 -30
- package/src/migrations/postgres/0021_prune_vault_releases.sql +19 -0
- 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/0021_snapshot.json +3334 -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 +189 -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 +200 -9
- package/src/schema.ts +294 -8
- package/src/verify-runtime.ts +18 -29
- package/LICENSE +0 -21
- package/dist/database-CFhTnnzM.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,268 @@
|
|
|
1
|
+
-- 0.15's cutover from 0.14, finished (docs/design/0.15-plan.md, "As built: PR 9b"; formal/Migration015.tla). It adds a column 0.14 never writes, a trigger, and replaces three functions only 0.15's vault calls.
|
|
2
|
+
--
|
|
3
|
+
-- Once the cutover is there, 0.14's code writes nothing (OldWritesRefused, OldWritesBeforeAnchor). An isolate of 0.14 may still serve after 0.15 cut over: its uses go on, its writes would leave rows no change of 0.15's seals, `stale` to every use (the plan's "late 0.14 write"). Each entry a writer under 0.15's log appends carries `log015`, which no MAC covers and 0.14 never sets; the trigger refuses an allowed entry without it, but a read's, a use's or the log's own (`audit.*`: its beats, checkpoints, findings and the losses an Owner accepts; the alarms), once the cutover's change is in `log_changes`. The append fails, and with it the transaction that wrote its rows: the old isolate answers an error, and its client asks again of 0.15. Every 0.14 append holds 0.14's head, which the cutover holds to its commit, so an append either commits before the cutover or sees it. Sign-ins are refused too (`sign_in`, `token.exchange`, `device.approve`): a credential 0.14 makes after the cutover is one 0.15 never sealed. Before the cutover it costs a 0.14 write one probe of `log_changes_object_idx`; a 0.15 entry passes it by its WHEN. An owner who drops the trigger lets a late write land, its object `stale`; a row put back over it is still refused by the use's probe of 0.14's newest access entry (UsesSeeOldWrites).
|
|
4
|
+
ALTER TABLE "audit_log" ADD COLUMN "log015" boolean;
|
|
5
|
+
--> statement-breakpoint
|
|
6
|
+
CREATE FUNCTION log_refuse_old_write() RETURNS trigger
|
|
7
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
|
|
8
|
+
BEGIN
|
|
9
|
+
IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('log', 'cutover')) THEN
|
|
10
|
+
RAISE EXCEPTION '0.15''s log is cut over from 0.14: 0.14''s % is refused, as every write of 0.14''s after it (an isolate of the earlier release still serving: ask again)', NEW.action
|
|
11
|
+
USING ERRCODE = 'object_not_in_prerequisite_state';
|
|
12
|
+
END IF;
|
|
13
|
+
RETURN NEW;
|
|
14
|
+
END
|
|
15
|
+
$$;
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
REVOKE ALL ON FUNCTION log_refuse_old_write() FROM PUBLIC;
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
CREATE TRIGGER audit_log_refuse_old BEFORE INSERT ON audit_log FOR EACH ROW
|
|
20
|
+
WHEN (NEW.decision = 'allow' AND NEW.log015 IS NULL AND NEW.action NOT LIKE 'audit.%' AND NEW.action NOT IN (
|
|
21
|
+
'credential.use', 'token.use', 'secret.read', 'secret.use', 'secret.list', 'secret.history', 'secret.version', 'missing.list', 'personal.list',
|
|
22
|
+
'roles.list', 'roles.preview', 'mcp.read', 'mcp.view', 'key.check', 'key.intent', 'workload.alert', 'vault.tampered'
|
|
23
|
+
))
|
|
24
|
+
EXECUTE FUNCTION log_refuse_old_write();
|
|
25
|
+
--> statement-breakpoint
|
|
26
|
+
-- A revoke as 0034 made it (its comment says how), but for 0.14's head, taken first (EntryLessHead, Migration015EntryLess): it writes no 0.14 entry, and 0.14's use of the credential, on an isolate still serving, checks its row under the head, then commits. Without the head, the revoke commits and answers between the two, and that use goes on. So: 0.14's head, the row, the fence, as the app's writes take them (AppHeadFirst). Until 0.16 removes 0.14's path.
|
|
27
|
+
CREATE OR REPLACE FUNCTION revoke_credential(
|
|
28
|
+
p_request text, p_at bigint, p_actor text, p_payload text, p_mac bytea, p_seal_from bytea, p_seal bytea,
|
|
29
|
+
p_id uuid, p_mac_from bytea, p_auth_mac bytea, p_revoked_at bigint, p_revoked_by text,
|
|
30
|
+
p_policy boolean, p_policy_seq bigint, p_protected uuid[], p_slugs text[]
|
|
31
|
+
) RETURNS json
|
|
32
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
|
|
33
|
+
DECLARE
|
|
34
|
+
n bigint;
|
|
35
|
+
target uuid := public.log_object('credential', p_id::text);
|
|
36
|
+
BEGIN
|
|
37
|
+
IF p_seal_from IS NULL OR p_seal IS NULL THEN
|
|
38
|
+
RAISE EXCEPTION 'the vault revokes a credential the log holds, from its seal' USING ERRCODE = 'invalid_parameter_value';
|
|
39
|
+
END IF;
|
|
40
|
+
PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
|
|
41
|
+
PERFORM 1 FROM public.credentials c
|
|
42
|
+
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
|
|
43
|
+
FOR UPDATE;
|
|
44
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'moved'); END IF;
|
|
45
|
+
PERFORM pg_advisory_xact_lock(1668244070, 0);
|
|
46
|
+
-- ChangeChecksSeal: a row the log sealed once, put back since, is not its newest change's.
|
|
47
|
+
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
|
|
48
|
+
RETURN json_build_object('state', 'stale');
|
|
49
|
+
END IF;
|
|
50
|
+
IF p_policy AND (
|
|
51
|
+
(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)
|
|
52
|
+
IS DISTINCT FROM p_policy_seq
|
|
53
|
+
-- Each protected environment's slug as it stands, none once it or its project is deleted (a tombstone), as `policyNow` reads them.
|
|
54
|
+
OR EXISTS (
|
|
55
|
+
SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
|
|
56
|
+
WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
|
|
57
|
+
WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
|
|
58
|
+
)
|
|
59
|
+
) THEN
|
|
60
|
+
RETURN json_build_object('state', 'policy');
|
|
61
|
+
END IF;
|
|
62
|
+
UPDATE public.credentials
|
|
63
|
+
SET revoked_at = timestamptz 'epoch' + p_revoked_at * interval '1 millisecond', revoked_by = p_revoked_by, auth_mac = p_auth_mac, seal = p_seal
|
|
64
|
+
WHERE id = p_id;
|
|
65
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
66
|
+
VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
|
|
67
|
+
RETURNING number INTO n;
|
|
68
|
+
RETURN json_build_object('number', n::text);
|
|
69
|
+
END
|
|
70
|
+
$$;
|
|
71
|
+
--> statement-breakpoint
|
|
72
|
+
-- A change of bindings as 0040 made it (its comment says how), but for 0.14's head when it ends any, for the revokes it writes with no 0.14 entry, as `revoke_credential` above: after the members' rows (0.14's vault takes a member's row before the head) and before any row of the app's (the credential it came in on, the bindings, their credentials), as 0.14's app takes the head before its rows. A bind that ends nothing takes none.
|
|
73
|
+
CREATE OR REPLACE FUNCTION change_bindings(
|
|
74
|
+
p_at bigint, p_actor text,
|
|
75
|
+
p_make boolean, p_principal text, p_generation integer, p_principal_seq bigint, p_actor_generation integer, p_actor_seq bigint,
|
|
76
|
+
p_via uuid, p_via_seal bytea, p_via_end bigint, p_connection uuid, p_connection_generation integer,
|
|
77
|
+
p_policy_seq bigint, p_protected uuid[], p_slugs text[], p_app_key text, p_max integer,
|
|
78
|
+
p_live uuid[], p_read_keys uuid[], p_read_names text[],
|
|
79
|
+
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, p_origin text,
|
|
80
|
+
p_label text, p_created_by text, p_created_at bigint, p_seal bytea,
|
|
81
|
+
p_ends uuid[], p_end_mac_from bytea[], p_end_seal_from bytea[], p_end_auth_mac bytea[], p_end_seal bytea[],
|
|
82
|
+
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[],
|
|
83
|
+
p_revoked_at bigint, p_revoked_by text,
|
|
84
|
+
p_change_request text[], p_change_object uuid[], p_change_payload text[], p_change_seal bytea[], p_change_mac bytea[]
|
|
85
|
+
) RETURNS json
|
|
86
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '1s' AS $$
|
|
87
|
+
DECLARE
|
|
88
|
+
n bigint;
|
|
89
|
+
-- A value, not `clock_timestamp()` in a predicate: volatile there, it bounds no index.
|
|
90
|
+
now_ts timestamptz := clock_timestamp();
|
|
91
|
+
ended timestamptz := timestamptz 'epoch' + p_revoked_at * interval '1 millisecond';
|
|
92
|
+
BEGIN
|
|
93
|
+
IF p_make = (p_id IS NULL) OR (NOT p_make AND cardinality(p_ends) = 0) OR cardinality(p_creds) <> cardinality(p_cred_binding)
|
|
94
|
+
OR p_make = (p_live IS NULL) OR cardinality(p_read_keys) <> cardinality(p_read_names)
|
|
95
|
+
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)
|
|
96
|
+
+ (SELECT count(*) FROM unnest(p_cred_seal) AS s WHERE s IS NOT NULL) THEN
|
|
97
|
+
RAISE EXCEPTION 'a change of bindings makes one, ends some, or both, with a change for each object' USING ERRCODE = 'invalid_parameter_value';
|
|
98
|
+
END IF;
|
|
99
|
+
IF p_make THEN
|
|
100
|
+
-- IssuerLocked, IssuerCAS, as `issue_token`, by principal: the account's row first (`token:` sorts before `user:`),
|
|
101
|
+
-- its update lock, so that two binds of it take turns over its count of bindings (a sign-in through one of its
|
|
102
|
+
-- bindings waits for the statement too); then the maker's, shared. Then both compared.
|
|
103
|
+
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;
|
|
104
|
+
PERFORM 1 FROM public.vault_members m WHERE m.principal = p_principal FOR NO KEY UPDATE;
|
|
105
|
+
PERFORM 1 FROM public.vault_members m WHERE m.principal = p_actor FOR SHARE;
|
|
106
|
+
-- 0.14's head, when it ends any, after the members' rows and before any row of the app's (row, head, issue lock, fence).
|
|
107
|
+
IF cardinality(p_ends) > 0 THEN PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE; END IF;
|
|
108
|
+
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)
|
|
109
|
+
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
|
|
110
|
+
RETURN json_build_object('state', 'moved');
|
|
111
|
+
END IF;
|
|
112
|
+
IF p_via IS NOT NULL THEN
|
|
113
|
+
PERFORM 1 FROM public.credentials c WHERE c.id = p_via FOR SHARE;
|
|
114
|
+
IF NOT EXISTS (SELECT 1 FROM public.credentials c
|
|
115
|
+
WHERE c.id = p_via AND c.principal = p_actor AND c.revoked_at IS NULL AND c.seal = p_via_seal
|
|
116
|
+
AND floor(extract(epoch FROM least(c.expires_at, coalesce(c.not_after, c.expires_at))) * 1000)::bigint = p_via_end)
|
|
117
|
+
-- The clock once the locks are had, not when the statement began: one that ended while it waited has ended.
|
|
118
|
+
OR p_via_end <= (extract(epoch FROM clock_timestamp()) * 1000)::bigint
|
|
119
|
+
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
|
|
120
|
+
RETURN json_build_object('state', 'ended');
|
|
121
|
+
END IF;
|
|
122
|
+
END IF;
|
|
123
|
+
-- The MCP connection it came through, still connected at the caller's generation (as `credentialStill`): shared,
|
|
124
|
+
-- so a disconnect waits for this, or is seen (review r3).
|
|
125
|
+
IF p_connection IS NOT NULL THEN
|
|
126
|
+
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;
|
|
127
|
+
IF NOT FOUND THEN RETURN json_build_object('state', 'ended'); END IF;
|
|
128
|
+
END IF;
|
|
129
|
+
-- An id the log holds already is no binding to make, even once its row is gone: its object stands (review r3).
|
|
130
|
+
IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
|
|
131
|
+
RETURN json_build_object('state', 'stale');
|
|
132
|
+
END IF;
|
|
133
|
+
END IF;
|
|
134
|
+
-- An unbind alone: 0.14's head first, as above.
|
|
135
|
+
IF NOT p_make THEN PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE; END IF;
|
|
136
|
+
-- The bindings it ends, then the credentials they issued: each row locked, by id, then held to its MAC and seal as
|
|
137
|
+
-- read. Every live credential they issued must be named, so one that a sign-in made meanwhile is read again (`moved`),
|
|
138
|
+
-- never left live.
|
|
139
|
+
PERFORM 1 FROM public.service_bindings b WHERE b.id = ANY(p_ends) ORDER BY b.id FOR UPDATE;
|
|
140
|
+
IF (SELECT count(*) FROM unnest(p_ends, p_end_mac_from, p_end_seal_from) AS e(id, mac, seal)
|
|
141
|
+
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)
|
|
142
|
+
<> cardinality(p_ends) THEN
|
|
143
|
+
RETURN json_build_object('state', 'moved');
|
|
144
|
+
END IF;
|
|
145
|
+
-- A credential 0.14 made, or one written around the vault, has no seal: ended with its binding all the same, its row
|
|
146
|
+
-- revoked with no change, the log holding no object of it.
|
|
147
|
+
PERFORM 1 FROM public.credentials c WHERE c.id = ANY(p_creds) ORDER BY c.id FOR UPDATE;
|
|
148
|
+
IF (SELECT count(*) FROM unnest(p_creds, p_cred_binding, p_cred_mac_from, p_cred_seal_from) AS x(id, binding, mac, seal)
|
|
149
|
+
JOIN public.credentials c ON c.id = x.id AND x.binding = ANY(p_ends) AND c.created_by = 'binding:' || x.binding::text
|
|
150
|
+
AND c.revoked_at IS NULL AND c.auth_mac = x.mac AND c.seal IS NOT DISTINCT FROM x.seal)
|
|
151
|
+
<> cardinality(p_creds)
|
|
152
|
+
OR EXISTS (SELECT 1 FROM unnest(p_ends) AS e(id) JOIN public.credentials c
|
|
153
|
+
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
|
|
154
|
+
WHERE NOT (c.id = ANY(p_creds))) THEN
|
|
155
|
+
RETURN json_build_object('state', 'moved');
|
|
156
|
+
END IF;
|
|
157
|
+
IF p_make THEN
|
|
158
|
+
-- The account's live bindings as the app read them, which it weighed this one beside (an issuer's keys moved, the
|
|
159
|
+
-- bindings it replaces): another made or ended since is read again. Under the account's row's lock, which every
|
|
160
|
+
-- bind takes, so two binds take turns. At most `p_max`, those it ends left out.
|
|
161
|
+
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)
|
|
162
|
+
IS DISTINCT FROM ARRAY(SELECT l FROM unnest(p_live) AS l ORDER BY l) THEN
|
|
163
|
+
RETURN json_build_object('state', 'moved');
|
|
164
|
+
END IF;
|
|
165
|
+
IF cardinality(p_live) - (SELECT count(*) FROM unnest(p_live) AS l WHERE l = ANY(p_ends)) >= p_max THEN
|
|
166
|
+
RETURN json_build_object('state', 'full');
|
|
167
|
+
END IF;
|
|
168
|
+
END IF;
|
|
169
|
+
-- IssuePolicyHeld: the issue lock, shared; then, under it, the policy and the slugs this change was weighed on, and the numbers.
|
|
170
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
|
|
171
|
+
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)
|
|
172
|
+
IS DISTINCT FROM p_policy_seq
|
|
173
|
+
OR EXISTS (
|
|
174
|
+
SELECT 1 FROM unnest(p_protected, p_slugs) AS p(id, slug)
|
|
175
|
+
WHERE (SELECT e.slug FROM public.environments e JOIN public.projects q ON q.id = e.project_id
|
|
176
|
+
WHERE e.id = p.id AND strpos(e.slug, '~') = 0 AND strpos(q.slug, '~') = 0) IS DISTINCT FROM p.slug
|
|
177
|
+
) THEN
|
|
178
|
+
RETURN json_build_object('state', 'policy');
|
|
179
|
+
END IF;
|
|
180
|
+
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
|
|
181
|
+
RETURN json_build_object('state', 'retired');
|
|
182
|
+
END IF;
|
|
183
|
+
-- What it ends is a use's input: the fence exclusive (RevokeTakesFence), after the issue lock (PolicyIssueFirst), for
|
|
184
|
+
-- the statement's work only. A bind that ends nothing takes none.
|
|
185
|
+
IF cardinality(p_ends) > 0 THEN PERFORM pg_advisory_xact_lock(1668244070, 0); END IF;
|
|
186
|
+
-- The keys a binding it makes reads, by the names they were asked by: still those names (a rename meanwhile names
|
|
187
|
+
-- another). Compared, not locked (a key's writer takes the fence before its row: a share lock here, held into the
|
|
188
|
+
-- fence, would be a cycle; review r3), and once the fence is had, which a rename takes too: one that committed while
|
|
189
|
+
-- this waited is seen (review r4). With no fence, one that commits after this is a rename after the bind.
|
|
190
|
+
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
|
|
191
|
+
RETURN json_build_object('state', 'moved');
|
|
192
|
+
END IF;
|
|
193
|
+
-- ChangeChecksSeal: each row it ends is its object's newest change's; one put back is stale. A row with no seal is one
|
|
194
|
+
-- 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
|
|
195
|
+
-- log's binding is never ended with no change of it (review r2).
|
|
196
|
+
IF EXISTS (SELECT 1 FROM unnest(p_ends, p_end_seal_from) AS e(id, seal)
|
|
197
|
+
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))
|
|
198
|
+
OR EXISTS (SELECT 1 FROM unnest(p_creds, p_cred_seal_from) AS x(id, seal)
|
|
199
|
+
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
|
|
200
|
+
RETURN json_build_object('state', 'stale');
|
|
201
|
+
END IF;
|
|
202
|
+
IF p_make THEN
|
|
203
|
+
-- Again, now that no lock is left to wait for: a make of the same id that committed meanwhile, its row deleted since,
|
|
204
|
+
-- is seen; one not yet committed holds the id's key, which this insert then waits for and fails on (review r4).
|
|
205
|
+
IF EXISTS (SELECT 1 FROM public.log_changes l WHERE l.object = public.log_object('binding', p_id::text)) THEN
|
|
206
|
+
RETURN json_build_object('state', 'stale');
|
|
207
|
+
END IF;
|
|
208
|
+
INSERT INTO public.service_bindings (id, auth_mac, principal, generation, profile, issuer, jwks_uri, jwks, claims, scope, policy, origin, label, created_by, created_at, seal)
|
|
209
|
+
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_origin, p_label, p_created_by,
|
|
210
|
+
timestamptz 'epoch' + p_created_at * interval '1 millisecond', p_seal);
|
|
211
|
+
END IF;
|
|
212
|
+
UPDATE public.service_bindings b SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = e.mac, seal = e.seal
|
|
213
|
+
FROM unnest(p_ends, p_end_auth_mac, p_end_seal) AS e(id, mac, seal) WHERE b.id = e.id;
|
|
214
|
+
UPDATE public.credentials c SET revoked_at = ended, revoked_by = p_revoked_by, auth_mac = x.mac, seal = x.seal
|
|
215
|
+
FROM unnest(p_creds, p_cred_auth_mac, p_cred_seal) AS x(id, mac, seal) WHERE c.id = x.id;
|
|
216
|
+
-- Each object's change, numbered in the order named: the binding made, those ended, their credentials.
|
|
217
|
+
FOR i IN 1 .. cardinality(p_change_request) LOOP
|
|
218
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
219
|
+
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])
|
|
220
|
+
RETURNING number INTO n;
|
|
221
|
+
END LOOP;
|
|
222
|
+
-- Only rows the log never held, ended: no change, nothing to seal.
|
|
223
|
+
RETURN json_build_object('number', coalesce(n::text, '0'));
|
|
224
|
+
END
|
|
225
|
+
$$;
|
|
226
|
+
--> statement-breakpoint
|
|
227
|
+
-- A binding's genesis as 0041 made it, but for its scope, held to the object too (PR 9a's review, P3): a row whose scope moved past the cutover's read, under its locks, is not sealed (`moved`: the cutover reads again), rather than sealed to an object every use of it then refuses. A scope is canonical JSON in its object and any JSON in its row: compared as jsonb. One the vault cannot read as a scope is no binding's of 0.14 (`planCutover`), never sealed.
|
|
228
|
+
CREATE OR REPLACE FUNCTION seal_binding(p_id uuid, p_request text, p_at bigint, p_actor text, p_payload text, p_seal bytea, p_mac bytea) RETURNS bigint
|
|
229
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
|
|
230
|
+
DECLARE
|
|
231
|
+
op jsonb := p_payload::jsonb;
|
|
232
|
+
target uuid := public.log_object('binding', p_id::text);
|
|
233
|
+
n bigint;
|
|
234
|
+
done integer;
|
|
235
|
+
BEGIN
|
|
236
|
+
IF op ->> 'op' IS DISTINCT FROM 'genesis' OR op #>> '{ref,kind}' IS DISTINCT FROM 'binding' OR op #>> '{ref,id}' IS DISTINCT FROM p_id::text
|
|
237
|
+
OR op #>> '{value,kind}' IS DISTINCT FROM 'binding' OR op #>> '{value,id}' IS DISTINCT FROM p_id::text OR op #>> '{value,origin}' IS NULL OR p_seal IS NULL THEN
|
|
238
|
+
RAISE EXCEPTION 'not a binding''s genesis: %', p_payload USING ERRCODE = 'invalid_parameter_value';
|
|
239
|
+
END IF;
|
|
240
|
+
PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
|
|
241
|
+
PERFORM pg_advisory_xact_lock(1668244070, 0);
|
|
242
|
+
SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
|
|
243
|
+
IF FOUND THEN
|
|
244
|
+
IF NOT EXISTS (SELECT 1 FROM public.log_changes c WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal = p_seal) THEN
|
|
245
|
+
RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
|
|
246
|
+
END IF;
|
|
247
|
+
RETURN n;
|
|
248
|
+
END IF;
|
|
249
|
+
IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = public.log_object('log', 'cutover')) THEN
|
|
250
|
+
RAISE EXCEPTION 'the log is cut over from 0.14 already: a binding is the vault''s to make' USING ERRCODE = 'invalid_parameter_value';
|
|
251
|
+
END IF;
|
|
252
|
+
IF EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = target) THEN
|
|
253
|
+
RETURN NULL;
|
|
254
|
+
END IF;
|
|
255
|
+
UPDATE public.service_bindings b SET seal = p_seal, origin = op #>> '{value,origin}'
|
|
256
|
+
WHERE b.id = p_id AND b.seal IS NULL AND b.revoked_at IS NULL AND b.origin IS NULL
|
|
257
|
+
AND b.principal = op #>> '{value,principal}' AND b.generation = (op #>> '{value,generation}')::integer
|
|
258
|
+
AND b.profile = op #>> '{value,profile}' AND b.issuer = op #>> '{value,issuer}' AND b.jwks_uri = op #>> '{value,jwksUri}'
|
|
259
|
+
AND b.claims = op #>> '{value,claims}' AND b.policy IS NOT DISTINCT FROM op #>> '{value,policy}'
|
|
260
|
+
AND (CASE WHEN b.scope IS NULL THEN 'null'::jsonb WHEN pg_input_is_valid(b.scope, 'jsonb') THEN b.scope::jsonb END) = op #> '{value,scope}';
|
|
261
|
+
GET DIAGNOSTICS done = ROW_COUNT;
|
|
262
|
+
IF done = 0 THEN RETURN NULL; END IF;
|
|
263
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
264
|
+
VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
|
|
265
|
+
RETURNING number INTO n;
|
|
266
|
+
RETURN n;
|
|
267
|
+
END
|
|
268
|
+
$$;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
-- B2 (T167): a request's several keys in one statement, where each had a call of its own (docs/design/0.15-plan.md,
|
|
2
|
+
-- "As built: T167"; formal/Batching.tla). Each key's use is `use_credential`'s, by ids, as 0046 has it, made in the
|
|
3
|
+
-- request's order in this one transaction: the same reads, the same entry, the same row, so that every guarantee of a
|
|
4
|
+
-- use holds for each key, and the batch adds only what Batching.tla holds of several uses together.
|
|
5
|
+
--
|
|
6
|
+
-- - One fence span: the fence (shared) is taken by the first key's use and kept to the commit, so no change is numbered
|
|
7
|
+
-- among the batch's entries, and every key is decided on the same changes.
|
|
8
|
+
-- - One principal's lock, its strongest first (`p_lock`, StrongestFirst): a batch whose keys take the principal's lock
|
|
9
|
+
-- (the vault's second call, its keys decided) takes it exclusive, after the fence, before its first key. Each key's
|
|
10
|
+
-- use then takes it again as it needs, which its own transaction already holds. Shared for one key then exclusive for
|
|
11
|
+
-- the next, two batches of one principal's would each wait for the other (Postgres ends one after deadlock_timeout).
|
|
12
|
+
-- - Each key counts the cap's set as its own statements read it, the batch's earlier keys included (CountsOwnBatch):
|
|
13
|
+
-- inside the transaction, each sees what the ones before it wrote.
|
|
14
|
+
-- - Nothing else: a key that would take the lock in a first call (`p_decided` false) answers `decide` and writes and
|
|
15
|
+
-- locks nothing, as alone; whether there is a second call is the vault's, once it has every key's rows
|
|
16
|
+
-- (AllDecidedFirst).
|
|
17
|
+
--
|
|
18
|
+
-- Its answer is a JSON array of `use_credential`'s answers, in the request's order. Any key's raise (past the deadline
|
|
19
|
+
-- by Postgres's clock, a lock not had within the hot login's lock_timeout, a replayed request id) rolls the whole batch
|
|
20
|
+
-- back: nothing of it is written.
|
|
21
|
+
CREATE FUNCTION use_batch(
|
|
22
|
+
p_credential uuid, p_secrets uuid[], p_requests bigint[], p_at bigint, p_purpose smallint, p_places uuid[], p_macs bytea[], p_deadline bigint,
|
|
23
|
+
p_covers bigint[], p_cover_ats bigint[], p_cover_requests bigint[], p_knowns bigint[], p_known_macs bytea[], p_cap_counts integer[], p_cap_windows bigint[],
|
|
24
|
+
p_decided boolean[], p_way smallint, p_principal text, p_lock text
|
|
25
|
+
) RETURNS json
|
|
26
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET bytea_output = 'hex' SET jit = off AS $$
|
|
27
|
+
DECLARE
|
|
28
|
+
n integer := coalesce(cardinality(p_secrets), 0);
|
|
29
|
+
answers json[] := '{}';
|
|
30
|
+
BEGIN
|
|
31
|
+
IF n = 0
|
|
32
|
+
OR cardinality(p_requests) IS DISTINCT FROM n OR cardinality(p_places) IS DISTINCT FROM n OR cardinality(p_macs) IS DISTINCT FROM n
|
|
33
|
+
OR cardinality(p_covers) IS DISTINCT FROM n OR cardinality(p_cover_ats) IS DISTINCT FROM n OR cardinality(p_cover_requests) IS DISTINCT FROM n
|
|
34
|
+
OR cardinality(p_knowns) IS DISTINCT FROM n OR cardinality(p_known_macs) IS DISTINCT FROM n
|
|
35
|
+
OR cardinality(p_cap_counts) IS DISTINCT FROM n OR cardinality(p_cap_windows) IS DISTINCT FROM n OR cardinality(p_decided) IS DISTINCT FROM n THEN
|
|
36
|
+
RAISE EXCEPTION 'a batch names a key at least, and each of its lists one entry a key' USING ERRCODE = 'invalid_parameter_value';
|
|
37
|
+
END IF;
|
|
38
|
+
IF p_lock IS NOT NULL THEN
|
|
39
|
+
-- The fence first, as every use takes it, then the principal's lock, exclusive, before any key (StrongestFirst).
|
|
40
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 0);
|
|
41
|
+
PERFORM pg_advisory_xact_lock(1668244066, hashtext(p_lock));
|
|
42
|
+
END IF;
|
|
43
|
+
FOR k IN 1 .. n LOOP
|
|
44
|
+
answers := answers || public.use_credential(
|
|
45
|
+
p_credential, p_secrets[k], p_requests[k], p_at, p_purpose, p_places[k], p_macs[k], p_deadline,
|
|
46
|
+
p_covers[k], p_cover_ats[k], p_cover_requests[k], p_knowns[k], p_known_macs[k], p_cap_counts[k], p_cap_windows[k],
|
|
47
|
+
NULL, NULL, NULL, NULL,
|
|
48
|
+
coalesce(p_decided[k], false),
|
|
49
|
+
false, NULL, NULL, NULL, NULL,
|
|
50
|
+
p_way, p_principal
|
|
51
|
+
);
|
|
52
|
+
END LOOP;
|
|
53
|
+
RETURN array_to_json(answers);
|
|
54
|
+
END
|
|
55
|
+
$$;
|
|
56
|
+
--> statement-breakpoint
|
|
57
|
+
REVOKE ALL ON FUNCTION use_batch(uuid, uuid[], bigint[], bigint, smallint, uuid[], bytea[], bigint, bigint[], bigint[], bigint[], bigint[], bytea[], integer[], bigint[], boolean[], smallint, text, text) FROM PUBLIC;
|
|
58
|
+
--> statement-breakpoint
|
|
59
|
+
GRANT EXECUTE ON FUNCTION use_batch(uuid, uuid[], bigint[], bigint, smallint, uuid[], bytea[], bigint, bigint[], bigint[], bigint[], bigint[], bytea[], integer[], bigint[], boolean[], smallint, text, text) TO coffre_vault;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
-- The audit pages list 0.15's reads from a sealed projection of its uses (T166; docs/design/0.15-plan.md, "As built: T166"; formal/Reads.tla). With 0.15's log a read writes no 0.14 `secret.read` entry: its use, the slim row of R53, is the record, which names neither whose read it was nor where, and whose MAC cannot cover the number the sequence gives it. So the vault's projector, apart from the checkpointer, writes a row of `log_reads` for each use a signed pair seals: its account, its key's place, the fork it was read in, through a reference the source and its place, and the version read, every one from the sealed objects at the use's number, under a MAC of the vault's over all of it, the number included. The audit pages list them on the whole log, and by member (`log_reads_principal_idx`); a key's or an environment's page lists none in 0.15 (D160). `log_reads_cursor` says how far the projector has gone; the pages say so when it is behind. Nothing of this is on a use's path. Additive: the previous release reads nothing here.
|
|
2
|
+
--> statement-breakpoint
|
|
3
|
+
CREATE TABLE "log_reads" (
|
|
4
|
+
"number" bigint NOT NULL,
|
|
5
|
+
"at" bigint NOT NULL,
|
|
6
|
+
"credential" uuid NOT NULL,
|
|
7
|
+
"purpose" smallint NOT NULL,
|
|
8
|
+
"principal" text NOT NULL,
|
|
9
|
+
"secret" uuid NOT NULL,
|
|
10
|
+
"environment" uuid NOT NULL,
|
|
11
|
+
"project" uuid NOT NULL,
|
|
12
|
+
"place" uuid,
|
|
13
|
+
"source" uuid,
|
|
14
|
+
"source_environment" uuid,
|
|
15
|
+
"source_project" uuid,
|
|
16
|
+
"version" uuid,
|
|
17
|
+
"mac" "bytea" NOT NULL,
|
|
18
|
+
CONSTRAINT "log_reads_mac_check" CHECK (octet_length("log_reads"."mac") = 16)
|
|
19
|
+
) PARTITION BY RANGE ("at");
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
CREATE TABLE "log_read_days" (
|
|
22
|
+
"principal" text NOT NULL,
|
|
23
|
+
"day" integer NOT NULL,
|
|
24
|
+
CONSTRAINT "log_read_days_pkey" PRIMARY KEY("principal","day")
|
|
25
|
+
);
|
|
26
|
+
--> statement-breakpoint
|
|
27
|
+
CREATE TABLE "log_reads_cursor" (
|
|
28
|
+
"only_row" boolean PRIMARY KEY DEFAULT true NOT NULL,
|
|
29
|
+
"through" bigint NOT NULL,
|
|
30
|
+
"pair" bigint NOT NULL,
|
|
31
|
+
"at" bigint NOT NULL,
|
|
32
|
+
"mac" "bytea",
|
|
33
|
+
CONSTRAINT "log_reads_cursor_only_row_check" CHECK ("log_reads_cursor"."only_row"),
|
|
34
|
+
CONSTRAINT "log_reads_cursor_mac_check" CHECK ("log_reads_cursor"."mac" IS NULL OR octet_length("log_reads_cursor"."mac") = 16)
|
|
35
|
+
);
|
|
36
|
+
--> statement-breakpoint
|
|
37
|
+
-- A use's projection once, by its time and number (a unique index of a partitioned table holds its partition key).
|
|
38
|
+
CREATE UNIQUE INDEX "log_reads_number_idx" ON "log_reads" USING btree ("at","number");--> statement-breakpoint
|
|
39
|
+
-- A member's reads, within a day: `log_read_days` says which days hold any, so that a page of theirs reads those days
|
|
40
|
+
-- alone, never a probe of each day since (an index of a partitioned table is each day's; review r6: a member's page
|
|
41
|
+
-- read 26 buffers, then 125 with 100 newer days of others' reads).
|
|
42
|
+
CREATE INDEX "log_reads_principal_idx" ON "log_reads" USING btree ("principal","at","number");--> statement-breakpoint
|
|
43
|
+
INSERT INTO "log_reads_cursor" ("only_row", "through", "pair", "at") VALUES (true, 0, 0, 0);--> statement-breakpoint
|
|
44
|
+
-- A day of reads, made as make_day makes one of uses, and beside it.
|
|
45
|
+
CREATE FUNCTION make_reads_day(p_day date) RETURNS text
|
|
46
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
|
|
47
|
+
DECLARE
|
|
48
|
+
lo bigint := (p_day - date '1970-01-01')::bigint * 86400000;
|
|
49
|
+
parent text;
|
|
50
|
+
name text;
|
|
51
|
+
BEGIN
|
|
52
|
+
FOREACH parent IN ARRAY ARRAY['log_reads'] LOOP
|
|
53
|
+
name := parent || '_' || to_char(p_day, 'YYYYMMDD');
|
|
54
|
+
IF to_regclass('public.' || quote_ident(name)) IS NOT NULL THEN
|
|
55
|
+
IF NOT EXISTS (
|
|
56
|
+
SELECT 1 FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
|
|
57
|
+
WHERE i.inhparent = to_regclass('public.' || parent) AND c.oid = to_regclass('public.' || quote_ident(name))
|
|
58
|
+
AND pg_get_expr(c.relpartbound, c.oid) = format('FOR VALUES FROM (%L) TO (%L)', lo, lo + 86400000)
|
|
59
|
+
) THEN
|
|
60
|
+
RAISE EXCEPTION '% exists, and is not the % of %', name, parent, p_day;
|
|
61
|
+
END IF;
|
|
62
|
+
CONTINUE;
|
|
63
|
+
END IF;
|
|
64
|
+
BEGIN
|
|
65
|
+
EXECUTE format('CREATE TABLE public.%I PARTITION OF public.%I FOR VALUES FROM (%s) TO (%s)', name, parent, lo, lo + 86400000);
|
|
66
|
+
-- pg_dump reads each partition as a table of its own.
|
|
67
|
+
EXECUTE format('GRANT SELECT ON public.%I TO coffre_backup', name);
|
|
68
|
+
EXCEPTION WHEN duplicate_table THEN
|
|
69
|
+
-- Another call made it meanwhile, and committed.
|
|
70
|
+
NULL;
|
|
71
|
+
END;
|
|
72
|
+
END LOOP;
|
|
73
|
+
RETURN to_char(p_day, 'YYYYMMDD');
|
|
74
|
+
END
|
|
75
|
+
$$;--> statement-breakpoint
|
|
76
|
+
-- A day of reads dropped, once its uses are gone from the log (retention detached their day): what is projected of them
|
|
77
|
+
-- goes with them, before the uses' partition is dropped, so that the day is still found (by its uses' table) until both
|
|
78
|
+
-- are gone, and a drop that gave up is tried again (review r10). While the uses' day is the log's, nothing: 'kept'.
|
|
79
|
+
CREATE FUNCTION drop_reads_day(p_day date) RETURNS text
|
|
80
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off SET lock_timeout = '100ms' AS $$
|
|
81
|
+
DECLARE
|
|
82
|
+
parent text;
|
|
83
|
+
name text;
|
|
84
|
+
BEGIN
|
|
85
|
+
IF EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhparent = to_regclass('public.log_uses')
|
|
86
|
+
AND i.inhrelid = to_regclass('public.' || quote_ident('log_uses_' || to_char(p_day, 'YYYYMMDD')))) THEN
|
|
87
|
+
RETURN 'kept';
|
|
88
|
+
END IF;
|
|
89
|
+
FOREACH parent IN ARRAY ARRAY['log_reads'] LOOP
|
|
90
|
+
name := parent || '_' || to_char(p_day, 'YYYYMMDD');
|
|
91
|
+
IF to_regclass('public.' || quote_ident(name)) IS NOT NULL THEN
|
|
92
|
+
IF NOT EXISTS (SELECT 1 FROM pg_inherits i WHERE i.inhrelid = to_regclass('public.' || quote_ident(name)) AND i.inhparent = to_regclass('public.' || parent)) THEN
|
|
93
|
+
RAISE EXCEPTION '% is not a day of %', name, parent;
|
|
94
|
+
END IF;
|
|
95
|
+
EXECUTE format('DROP TABLE public.%I', name);
|
|
96
|
+
END IF;
|
|
97
|
+
END LOOP;
|
|
98
|
+
DELETE FROM public.log_read_days WHERE day = p_day - date '1970-01-01';
|
|
99
|
+
RETURN 'dropped';
|
|
100
|
+
END
|
|
101
|
+
$$;--> statement-breakpoint
|
|
102
|
+
-- The projector's write: reads, each once (by its time and number), the projector's row put back over any other at its number (a row planted ahead would otherwise make it skip one: formal/Reads.tla, ReplaceOnConflict; two projectors write the same bytes), each member's days of them (`log_read_days`), and the cursor moved on to `p_through`, never back, with the signed pair at or below it whose floor the next read takes (`p_pair`) and its MAC (CursorMac), in one call, after the rows (IdempotentByNumber, CursorAfterRows). One projector's call at a time, the lock taken before any statement reads. None past the newest signed pair's number (ProjectSealedOnly): the projector holds the pair's signatures itself before it asks.
|
|
103
|
+
-- Only from where the chunk started (`p_from`, FromChecked): a projector another overtook, whose reads retention may since have dropped with their day, writes nothing, nor makes their day again: whether it wrote (review r11: 600 retired reads came back). The days its reads need, made here, once it may write.
|
|
104
|
+
CREATE FUNCTION project_reads(p_reads jsonb, p_from bigint, p_through bigint, p_pair bigint, p_at bigint, p_mac bytea) RETURNS boolean
|
|
105
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
|
|
106
|
+
DECLARE
|
|
107
|
+
sealed bigint;
|
|
108
|
+
stands bigint;
|
|
109
|
+
BEGIN
|
|
110
|
+
PERFORM pg_advisory_xact_lock(1668244070, 166);
|
|
111
|
+
sealed := (SELECT max(k.upto) FROM public.log_checkpoints k);
|
|
112
|
+
IF sealed IS NULL OR p_through > sealed THEN
|
|
113
|
+
RAISE EXCEPTION 'reads through % are past the newest pair, %', p_through, sealed USING ERRCODE = 'invalid_parameter_value';
|
|
114
|
+
END IF;
|
|
115
|
+
-- The cursor is the migration's one row: gone, nothing is projected, and the pages say so (review r6).
|
|
116
|
+
stands := (SELECT c.through FROM public.log_reads_cursor c WHERE c.only_row);
|
|
117
|
+
IF stands IS NULL THEN
|
|
118
|
+
RAISE EXCEPTION 'the reads cursor is gone' USING ERRCODE = 'data_corrupted';
|
|
119
|
+
END IF;
|
|
120
|
+
IF stands <> p_from THEN
|
|
121
|
+
RETURN false;
|
|
122
|
+
END IF;
|
|
123
|
+
IF EXISTS (SELECT 1 FROM jsonb_array_elements(p_reads) r WHERE (r ->> 'number')::bigint > p_through) THEN
|
|
124
|
+
RAISE EXCEPTION 'a read past % is no read it projects', p_through USING ERRCODE = 'invalid_parameter_value';
|
|
125
|
+
END IF;
|
|
126
|
+
-- Each read's day there to hold it: uses of a log older than the days made ahead (review r4).
|
|
127
|
+
PERFORM public.make_reads_day(d) FROM (SELECT DISTINCT date '1970-01-01' + ((x ->> 'at')::bigint / 86400000)::integer AS d FROM jsonb_array_elements(p_reads) x) days
|
|
128
|
+
WHERE to_regclass('public.' || quote_ident('log_reads_' || to_char(d, 'YYYYMMDD'))) IS NULL;
|
|
129
|
+
INSERT INTO public.log_reads (number, at, credential, purpose, principal, secret, environment, project, place, source, source_environment, source_project, version, mac)
|
|
130
|
+
SELECT (x ->> 'number')::bigint, (x ->> 'at')::bigint, (x ->> 'credential')::uuid, (x ->> 'purpose')::smallint, x ->> 'principal', (x ->> 'secret')::uuid,
|
|
131
|
+
(x ->> 'environment')::uuid, (x ->> 'project')::uuid, (x ->> 'place')::uuid, (x ->> 'source')::uuid, (x ->> 'sourceEnvironment')::uuid,
|
|
132
|
+
(x ->> 'sourceProject')::uuid, (x ->> 'version')::uuid, decode(x ->> 'mac', 'hex')
|
|
133
|
+
FROM jsonb_array_elements(p_reads) x
|
|
134
|
+
ON CONFLICT (at, number) DO UPDATE SET credential = excluded.credential, purpose = excluded.purpose, principal = excluded.principal, secret = excluded.secret,
|
|
135
|
+
environment = excluded.environment, project = excluded.project, place = excluded.place, source = excluded.source, source_environment = excluded.source_environment,
|
|
136
|
+
source_project = excluded.source_project, version = excluded.version, mac = excluded.mac;
|
|
137
|
+
INSERT INTO public.log_read_days (principal, day)
|
|
138
|
+
SELECT DISTINCT x ->> 'principal', ((x ->> 'at')::bigint / 86400000)::integer FROM jsonb_array_elements(p_reads) x
|
|
139
|
+
ON CONFLICT DO NOTHING;
|
|
140
|
+
UPDATE public.log_reads_cursor SET through = p_through, pair = p_pair, at = p_at, mac = p_mac WHERE only_row AND through < p_through;
|
|
141
|
+
RETURN true;
|
|
142
|
+
END
|
|
143
|
+
$$;--> statement-breakpoint
|
|
144
|
+
-- A member's reads, newest first, below a point, at most `p_take`: their days that hold any, newest first, each day's index read in order until the page is full. However many days the log holds, a page reads its member's days and no other (review r6); weighing the empty days made ahead, the planner would otherwise read a busy member's day whole and sort it (measured, T166).
|
|
145
|
+
CREATE FUNCTION log_reads_by(p_principal text, p_at bigint, p_number bigint, p_take integer)
|
|
146
|
+
RETURNS TABLE (at bigint, number bigint)
|
|
147
|
+
LANGUAGE plpgsql STABLE SET search_path = pg_catalog, pg_temp SET jit = off SET enable_sort = off SET enable_bitmapscan = off AS $$
|
|
148
|
+
DECLARE
|
|
149
|
+
d integer;
|
|
150
|
+
left_over integer := p_take;
|
|
151
|
+
found_now integer;
|
|
152
|
+
BEGIN
|
|
153
|
+
FOR d IN SELECT k.day FROM public.log_read_days k WHERE k.principal = p_principal AND k.day <= LEAST(p_at / 86400000, 2147483647) ORDER BY k.day DESC LOOP
|
|
154
|
+
RETURN QUERY SELECT r.at, r.number FROM public.log_reads r
|
|
155
|
+
WHERE r.principal = p_principal AND r.at >= d::bigint * 86400000 AND r.at < (d::bigint + 1) * 86400000 AND (r.at, r.number) < (p_at, p_number)
|
|
156
|
+
ORDER BY r.at DESC, r.number DESC LIMIT left_over;
|
|
157
|
+
GET DIAGNOSTICS found_now = ROW_COUNT;
|
|
158
|
+
left_over := left_over - found_now;
|
|
159
|
+
EXIT WHEN left_over <= 0;
|
|
160
|
+
END LOOP;
|
|
161
|
+
END
|
|
162
|
+
$$;--> statement-breakpoint
|
|
163
|
+
REVOKE ALL ON FUNCTION make_reads_day(date), drop_reads_day(date), project_reads(jsonb, bigint, bigint, bigint, bigint, bytea),
|
|
164
|
+
log_reads_by(text, bigint, bigint, integer) FROM PUBLIC;--> statement-breakpoint
|
|
165
|
+
GRANT EXECUTE ON FUNCTION make_reads_day(date), drop_reads_day(date), project_reads(jsonb, bigint, bigint, bigint, bigint, bytea) TO coffre_vault;--> statement-breakpoint
|
|
166
|
+
GRANT EXECUTE ON FUNCTION log_reads_by(text, bigint, bigint, integer) TO coffre_app;--> statement-breakpoint
|
|
167
|
+
GRANT SELECT ON log_reads, log_read_days, log_reads_cursor TO coffre_vault, coffre_app, coffre_backup;--> statement-breakpoint
|
|
168
|
+
-- The pairs' places and times, for the pages to say how far behind the projection is: public, as the notes are.
|
|
169
|
+
GRANT SELECT ON log_checkpoints TO coffre_app;--> statement-breakpoint
|
|
170
|
+
-- Today's, yesterday's and the next three days, as 0035 made the uses' (the projector makes more as it goes).
|
|
171
|
+
SELECT make_reads_day((now() AT TIME ZONE 'UTC')::date + d) FROM generate_series(-1, 3) AS d;
|