@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,259 @@
|
|
|
1
|
+
-- An unarchive never seals rows edited while their key was archived (T134; docs/design/0.15-plan.md, "Unarchives restore what the log sealed"; the spec: formal/SealedObjects.tla's RestoreFromLog, RestorePinned, EndWhileArchived). An archived key is no object and its row has no seal, so nothing checked its rows until its unarchive sealed whatever they held: a version, a reference row or a source a database writer edited meanwhile. Now the vault holds an unarchive to the object its key's log last sealed (`prepare`), and `app_change` holds that seal to the log's newest sealed one under the fence (`p_restores`). And the end of a reference an archived key's kept object stands on is logged on that object (`change_input`: ended, then deleted again), so an unarchive restores it ended, never live. It replaces two functions only 0.15's change path calls, behind the switch; no release calls `app_change`, whose new argument replaces the old function.
|
|
2
|
+
DROP FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea);
|
|
3
|
+
--> statement-breakpoint
|
|
4
|
+
-- `app_change` as 0029 made it, but for an unarchive's `p_restores`: the seal of the object its key's log last sealed, which must still be its newest sealed one.
|
|
5
|
+
CREATE FUNCTION app_change(
|
|
6
|
+
p_kind text, p_id text, p_request text, p_at bigint, p_actor text, p_payload text, p_seal_from bytea, p_seal bytea, p_mac bytea, p_restores bytea
|
|
7
|
+
) RETURNS bigint
|
|
8
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
|
|
9
|
+
DECLARE
|
|
10
|
+
op jsonb := p_payload::jsonb;
|
|
11
|
+
change text := op ->> 'op';
|
|
12
|
+
target uuid := public.log_object(p_kind, p_id);
|
|
13
|
+
n bigint;
|
|
14
|
+
done integer;
|
|
15
|
+
held bytea;
|
|
16
|
+
-- An issue of a credential this very transaction wrote (its row's xmin is ours: not a savepoint's, nor a row another committed).
|
|
17
|
+
-- Never null, whatever is missing (a 0.14 row's call carries no payload, so no op): else the fence.
|
|
18
|
+
fresh boolean := coalesce(change = 'credential.issue' AND p_kind = 'credential'
|
|
19
|
+
AND EXISTS (SELECT 1 FROM public.credentials c WHERE c.id = p_id::uuid AND c.xmin = pg_current_xact_id()::xid), false);
|
|
20
|
+
BEGIN
|
|
21
|
+
PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
|
|
22
|
+
-- The issue lock shared (IssueOwnLock); and, but for a fresh issue, the fence exclusive after it (IssueBeforeFence).
|
|
23
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 1);
|
|
24
|
+
IF NOT fresh THEN PERFORM pg_advisory_xact_lock(1668244070, 0); END IF;
|
|
25
|
+
held := CASE p_kind
|
|
26
|
+
WHEN 'credential' THEN (SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid)
|
|
27
|
+
WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
|
|
28
|
+
WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
|
|
29
|
+
END;
|
|
30
|
+
IF p_request IS NULL THEN
|
|
31
|
+
IF held IS NOT NULL OR EXISTS (SELECT 1 FROM public.log_changes c WHERE c.object = target) THEN
|
|
32
|
+
RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
|
|
33
|
+
END IF;
|
|
34
|
+
RETURN 0;
|
|
35
|
+
END IF;
|
|
36
|
+
SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
|
|
37
|
+
IF FOUND THEN
|
|
38
|
+
IF NOT EXISTS (
|
|
39
|
+
SELECT 1 FROM public.log_changes c
|
|
40
|
+
WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
|
|
41
|
+
) THEN
|
|
42
|
+
RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
|
|
43
|
+
END IF;
|
|
44
|
+
RETURN n;
|
|
45
|
+
END IF;
|
|
46
|
+
IF NOT ((p_kind = 'credential' AND change IN ('credential.issue', 'credential.revoke', 'credential.renew', 'credential.scope'))
|
|
47
|
+
OR (p_kind = 'secret' AND change IN ('secret.write', 'secret.delete'))
|
|
48
|
+
OR (p_kind = 'environment' AND change IN ('environment.create', 'environment.rename', 'environment.archive', 'project.archive', 'environment.delete'))) THEN
|
|
49
|
+
RAISE EXCEPTION 'the app makes no change % to a %', coalesce(change, 'without an op'), p_kind USING ERRCODE = 'invalid_parameter_value';
|
|
50
|
+
END IF;
|
|
51
|
+
IF op -> 'ref' ->> 'kind' IS DISTINCT FROM p_kind OR op -> 'ref' ->> 'id' IS DISTINCT FROM p_id THEN
|
|
52
|
+
RAISE EXCEPTION 'a change names the object it is to: %, %', p_kind, p_id USING ERRCODE = 'invalid_parameter_value';
|
|
53
|
+
END IF;
|
|
54
|
+
-- ChangeChecksSeal.
|
|
55
|
+
IF held IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1) THEN
|
|
56
|
+
RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
|
|
57
|
+
END IF;
|
|
58
|
+
-- RestorePinned: an unarchive, a key's object made again from none, restores the one its log last sealed, still so.
|
|
59
|
+
IF p_restores IS NOT NULL THEN
|
|
60
|
+
IF p_kind <> 'secret' OR change <> 'secret.write' OR p_seal_from IS NOT NULL THEN
|
|
61
|
+
RAISE EXCEPTION 'only a key''s object made again from none restores one' USING ERRCODE = 'invalid_parameter_value';
|
|
62
|
+
END IF;
|
|
63
|
+
IF p_restores IS DISTINCT FROM (SELECT c.seal FROM public.log_changes c WHERE c.object = target AND c.seal IS NOT NULL ORDER BY c.number DESC LIMIT 1) THEN
|
|
64
|
+
RAISE EXCEPTION 'the secret object an unarchive restores is not its newest sealed one: stale' USING ERRCODE = 'data_corrupted';
|
|
65
|
+
END IF;
|
|
66
|
+
END IF;
|
|
67
|
+
-- ChangeCAS.
|
|
68
|
+
CASE p_kind
|
|
69
|
+
WHEN 'credential' THEN
|
|
70
|
+
UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
71
|
+
WHEN 'secret' THEN
|
|
72
|
+
UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
73
|
+
WHEN 'environment' THEN
|
|
74
|
+
UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
75
|
+
END CASE;
|
|
76
|
+
GET DIAGNOSTICS done = ROW_COUNT;
|
|
77
|
+
IF done = 0 THEN RETURN NULL; END IF;
|
|
78
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
79
|
+
VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
|
|
80
|
+
RETURNING number INTO n;
|
|
81
|
+
RETURN n;
|
|
82
|
+
END
|
|
83
|
+
$$;
|
|
84
|
+
--> statement-breakpoint
|
|
85
|
+
REVOKE ALL ON FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea, bytea) FROM PUBLIC;
|
|
86
|
+
--> statement-breakpoint
|
|
87
|
+
GRANT EXECUTE ON FUNCTION app_change(text, text, text, bigint, text, text, bytea, bytea, bytea, bytea) TO coffre_app;
|
|
88
|
+
--> statement-breakpoint
|
|
89
|
+
-- `change_input` as 0029 made it, but for an archived key whose kept object stands on a reference that ends: that object written ended from no seal (its row's end already written by its id), then deleted again (`secret.delete`).
|
|
90
|
+
CREATE OR REPLACE FUNCTION change_input(
|
|
91
|
+
p_kind text, p_id text, p_request text, p_at bigint, p_actor text, p_payload text, p_value text, p_seal_from bytea, p_seal bytea, p_mac bytea
|
|
92
|
+
) RETURNS bigint
|
|
93
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET jit = off AS $$
|
|
94
|
+
DECLARE
|
|
95
|
+
op jsonb := p_payload::jsonb;
|
|
96
|
+
v jsonb := p_value::jsonb;
|
|
97
|
+
change text := op ->> 'op';
|
|
98
|
+
target uuid := public.log_object(p_kind, p_id);
|
|
99
|
+
n bigint;
|
|
100
|
+
done integer;
|
|
101
|
+
member text;
|
|
102
|
+
held bytea;
|
|
103
|
+
newest bytea;
|
|
104
|
+
config jsonb;
|
|
105
|
+
BEGIN
|
|
106
|
+
-- Its locks in the order 0.14's writers take theirs, so that none waits on the other in a circle until 0.16 (row,
|
|
107
|
+
-- head, fence: Migration015.tla's RowFirst, ChangeHeadFirst): the member's queue (MEMBER_QUEUE, 0x636f666d) and row
|
|
108
|
+
-- first, a principal's own or a credential's (0.14's vault locks them before the head, lockMembers; a change made
|
|
109
|
+
-- inside a 0.14 decision holds them already), then 0.14's head (its app takes it before its rows), then the fence
|
|
110
|
+
-- (0.14's appends take it after the head, audit_log_fence). The head held, no 0.14 write lands between this change
|
|
111
|
+
-- and its entry.
|
|
112
|
+
member := CASE p_kind
|
|
113
|
+
WHEN 'principal' THEN p_id
|
|
114
|
+
WHEN 'credential' THEN coalesce(
|
|
115
|
+
(SELECT c.principal FROM public.credentials c WHERE c.id = p_id::uuid),
|
|
116
|
+
(SELECT b.principal FROM public.service_bindings b WHERE b.id = p_id::uuid))
|
|
117
|
+
END;
|
|
118
|
+
IF member IS NOT NULL THEN
|
|
119
|
+
PERFORM pg_advisory_xact_lock(1668245101, hashtext(m.principal)) FROM public.vault_members m WHERE m.principal = member FOR NO KEY UPDATE;
|
|
120
|
+
END IF;
|
|
121
|
+
PERFORM 1 FROM public.audit_chain_head WHERE only_row FOR UPDATE;
|
|
122
|
+
-- ChangeLocks: exclusive, so that it waits for every use in flight, and every use after it waits for its commit.
|
|
123
|
+
PERFORM pg_advisory_xact_lock(1668244070, 0);
|
|
124
|
+
SELECT c.number INTO n FROM public.log_changes c WHERE c.request_id = p_request;
|
|
125
|
+
IF FOUND THEN
|
|
126
|
+
IF NOT EXISTS (
|
|
127
|
+
SELECT 1 FROM public.log_changes c
|
|
128
|
+
WHERE c.request_id = p_request AND c.object = target AND c.payload = p_payload AND c.seal IS NOT DISTINCT FROM p_seal
|
|
129
|
+
) THEN
|
|
130
|
+
RAISE EXCEPTION 'request % names another change', p_request USING ERRCODE = 'unique_violation';
|
|
131
|
+
END IF;
|
|
132
|
+
RETURN n;
|
|
133
|
+
END IF;
|
|
134
|
+
IF NOT (change = 'genesis'
|
|
135
|
+
OR (p_kind = 'principal' AND change IN ('member.add', 'member.role', 'member.remove', 'member.restore', 'access.grant', 'access.revoke'))
|
|
136
|
+
OR (p_kind = 'environment' AND change IN ('environment.protect', 'environment.rename', 'environment.archive'))
|
|
137
|
+
OR (p_kind = 'secret' AND change IN ('secret.write', 'secret.delete'))
|
|
138
|
+
OR (p_kind = 'roles' AND change = 'role.define')
|
|
139
|
+
OR (p_kind = 'policy' AND change = 'bulk.limit')) THEN
|
|
140
|
+
RAISE EXCEPTION 'this schema knows no change % to a %', coalesce(change, 'without an op'), p_kind USING ERRCODE = 'invalid_parameter_value';
|
|
141
|
+
END IF;
|
|
142
|
+
-- The payload names its object (`ref`), so that a replay knows what each change is to: the column holds only its
|
|
143
|
+
-- key. The cutover's genesis may leave it out, its value naming it.
|
|
144
|
+
IF (change <> 'genesis' OR op ? 'ref') AND (op -> 'ref' ->> 'kind' IS DISTINCT FROM p_kind OR op -> 'ref' ->> 'id' IS DISTINCT FROM p_id) THEN
|
|
145
|
+
RAISE EXCEPTION 'a change names the object it is to: %, %', p_kind, p_id USING ERRCODE = 'invalid_parameter_value';
|
|
146
|
+
END IF;
|
|
147
|
+
-- A value is the object as this function writes it: an environment's, which a genesis may leave out to seal the
|
|
148
|
+
-- row as it is; a key's, the end of its reference (`secret.write`, the one change of a key the vault makes), and
|
|
149
|
+
-- none at its genesis; the roles' and the policy's, but at their genesis, whose payload holds it. The others' rows
|
|
150
|
+
-- are written by whoever keeps them, and carry none.
|
|
151
|
+
IF (CASE
|
|
152
|
+
WHEN p_kind = 'environment' THEN change <> 'genesis' AND v IS NULL
|
|
153
|
+
WHEN p_kind = 'secret' THEN (change = 'secret.write') = (v IS NULL)
|
|
154
|
+
WHEN p_kind IN ('roles', 'policy') THEN (change = 'genesis') = (v IS NOT NULL)
|
|
155
|
+
ELSE v IS NOT NULL
|
|
156
|
+
END) THEN
|
|
157
|
+
RAISE EXCEPTION 'a % change % carries the object it leaves exactly when this function writes it', p_kind, change USING ERRCODE = 'invalid_parameter_value';
|
|
158
|
+
END IF;
|
|
159
|
+
-- ChangeChecksSeal: the row's seal is its object's newest change's, or the object has neither yet (one probe, as
|
|
160
|
+
-- a use makes). A row deleted outright, or a seal no change wrote, is stale too.
|
|
161
|
+
held := CASE p_kind
|
|
162
|
+
WHEN 'principal' THEN (SELECT m.seal FROM public.vault_members m WHERE m.principal = p_id)
|
|
163
|
+
WHEN 'credential' THEN coalesce(
|
|
164
|
+
(SELECT c.seal FROM public.credentials c WHERE c.id = p_id::uuid),
|
|
165
|
+
(SELECT b.seal FROM public.service_bindings b WHERE b.id = p_id::uuid))
|
|
166
|
+
WHEN 'environment' THEN (SELECT e.seal FROM public.environments e WHERE e.id = p_id::uuid)
|
|
167
|
+
WHEN 'secret' THEN (SELECT x.seal FROM public.secrets x WHERE x.id = p_id::uuid)
|
|
168
|
+
WHEN 'policy' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'policy')
|
|
169
|
+
WHEN 'roles' THEN (SELECT g.seal FROM public.log_config g WHERE g.name = 'roles')
|
|
170
|
+
END;
|
|
171
|
+
newest := (SELECT c.seal FROM public.log_changes c WHERE c.object = target ORDER BY c.number DESC LIMIT 1);
|
|
172
|
+
IF held IS DISTINCT FROM newest THEN
|
|
173
|
+
RAISE EXCEPTION 'the % object''s row is not its newest change''s: stale', p_kind USING ERRCODE = 'data_corrupted';
|
|
174
|
+
END IF;
|
|
175
|
+
-- ChangeCAS: each write moves the row only from the seal the vault read.
|
|
176
|
+
CASE p_kind
|
|
177
|
+
WHEN 'principal' THEN
|
|
178
|
+
UPDATE public.vault_members SET seal = p_seal WHERE principal = p_id AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
179
|
+
WHEN 'credential' THEN
|
|
180
|
+
-- A token's row, or a trust binding's: one kind of object, ids that never meet.
|
|
181
|
+
UPDATE public.credentials SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
182
|
+
GET DIAGNOSTICS done = ROW_COUNT;
|
|
183
|
+
IF done = 0 THEN
|
|
184
|
+
UPDATE public.service_bindings SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
185
|
+
END IF;
|
|
186
|
+
WHEN 'environment' THEN
|
|
187
|
+
IF v IS NULL THEN
|
|
188
|
+
UPDATE public.environments SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
189
|
+
ELSE
|
|
190
|
+
-- A deleted environment's row keeps its tombstone, `dev~deleted-…` (@coffre/core/schemas's isTombstone),
|
|
191
|
+
-- and its object the slug before it (objects.ts, environmentObject): an environment deleted stays so,
|
|
192
|
+
-- its slug is never written again, and none is deleted here; an object's slug holds no tombstone.
|
|
193
|
+
UPDATE public.environments e
|
|
194
|
+
SET slug = CASE WHEN strpos(e.slug, '~') > 0 THEN e.slug ELSE v ->> 'slug' END,
|
|
195
|
+
protected = (v ->> 'protected')::boolean,
|
|
196
|
+
archived_at = CASE WHEN (v ->> 'archived')::boolean THEN coalesce(e.archived_at, to_timestamp(p_at / 1000.0)) END,
|
|
197
|
+
seal = p_seal
|
|
198
|
+
WHERE e.id = p_id::uuid AND e.seal IS NOT DISTINCT FROM p_seal_from
|
|
199
|
+
AND v ->> 'kind' = 'environment' AND v ->> 'id' = p_id
|
|
200
|
+
AND e.project_id = (v ->> 'projectId')::uuid
|
|
201
|
+
AND e.parent_id IS NOT DISTINCT FROM (v ->> 'parentId')::uuid
|
|
202
|
+
AND e.owner IS NOT DISTINCT FROM v ->> 'owner'
|
|
203
|
+
AND (strpos(e.slug, '~') > 0) = (v ->> 'deleted')::boolean
|
|
204
|
+
-- Its project's archival is the project's row's, which the app writes, and none once deleted (objects.ts): held,
|
|
205
|
+
-- never written here.
|
|
206
|
+
AND (v ->> 'projectArchived')::boolean = (strpos(e.slug, '~') = 0 AND EXISTS (SELECT 1 FROM public.projects p WHERE p.id = e.project_id AND p.archived_at IS NOT NULL))
|
|
207
|
+
AND strpos(v ->> 'slug', '~') = 0
|
|
208
|
+
AND (NOT (v ->> 'deleted')::boolean OR split_part(e.slug, '~', 1) = v ->> 'slug');
|
|
209
|
+
END IF;
|
|
210
|
+
WHEN 'secret' THEN
|
|
211
|
+
IF v IS NULL THEN
|
|
212
|
+
UPDATE public.secrets SET seal = p_seal WHERE id = p_id::uuid AND seal IS NOT DISTINCT FROM p_seal_from;
|
|
213
|
+
ELSE
|
|
214
|
+
-- A reference's end, in the vault's decision that appends 0.14's `reference.end` (EndInRow, EndWithEntry:
|
|
215
|
+
-- formal/SealedObjects.tla): the holder, a key with no version of its own, left with no source. Live, from
|
|
216
|
+
-- its seal, its newest reference row ended. That row must stand: its seal, which the check above held to
|
|
217
|
+
-- the newest change, says the holder reads its source; a row ended or gone under it is not the log's.
|
|
218
|
+
-- Archived, from none (EndWhileArchived): the object its unarchive would restore, ended, then deleted
|
|
219
|
+
-- again by the next call (`secret.delete`); its row was ended by its id (`end_reference`).
|
|
220
|
+
UPDATE public.secrets x SET seal = p_seal
|
|
221
|
+
WHERE x.id = p_id::uuid AND x.seal IS NOT DISTINCT FROM p_seal_from AND (x.archived_at IS NULL) = (p_seal_from IS NOT NULL)
|
|
222
|
+
AND x.current_version_id IS NULL
|
|
223
|
+
AND v ->> 'kind' = 'secret' AND v ->> 'id' = p_id AND x.project_id = (v ->> 'projectId')::uuid AND x.environment_id = (v ->> 'environmentId')::uuid
|
|
224
|
+
AND v ->> 'version' = '' AND v ->> 'nature' = 'secret' AND v -> 'key' = 'null'::jsonb AND v -> 'digest' = 'null'::jsonb AND v -> 'source' = 'null'::jsonb
|
|
225
|
+
AND v -> 'reference' = 'null'::jsonb;
|
|
226
|
+
GET DIAGNOSTICS done = ROW_COUNT;
|
|
227
|
+
IF done > 0 AND p_seal_from IS NOT NULL THEN
|
|
228
|
+
UPDATE public.secret_references y SET ended_at = to_timestamp(p_at / 1000.0)
|
|
229
|
+
WHERE y.id = (SELECT z.id FROM public.secret_references z WHERE z.secret_id = p_id::uuid ORDER BY z.created_seq DESC LIMIT 1) AND y.ended_at IS NULL;
|
|
230
|
+
GET DIAGNOSTICS done = ROW_COUNT;
|
|
231
|
+
IF done = 0 THEN
|
|
232
|
+
RAISE EXCEPTION 'the secret object''s reference has no standing row: stale' USING ERRCODE = 'data_corrupted';
|
|
233
|
+
END IF;
|
|
234
|
+
END IF;
|
|
235
|
+
END IF;
|
|
236
|
+
WHEN 'policy', 'roles' THEN
|
|
237
|
+
config := CASE change WHEN 'genesis' THEN op -> 'value' ELSE v END;
|
|
238
|
+
IF p_id <> 'instance' OR config IS NULL OR jsonb_typeof(config) <> 'object' OR (change = 'genesis' AND p_seal_from IS NOT NULL) THEN
|
|
239
|
+
RAISE EXCEPTION 'the % object is one, made once from its value and changed from its seal', p_kind USING ERRCODE = 'invalid_parameter_value';
|
|
240
|
+
END IF;
|
|
241
|
+
-- Made by its first change (genesis, or the first role defined, the first cap set); moved after from its seal.
|
|
242
|
+
IF p_seal_from IS NULL THEN
|
|
243
|
+
INSERT INTO public.log_config (name, value, seal) VALUES (p_kind, config::text, p_seal) ON CONFLICT (name) DO NOTHING;
|
|
244
|
+
ELSE
|
|
245
|
+
UPDATE public.log_config SET value = config::text, seal = p_seal WHERE log_config.name = p_kind AND seal = p_seal_from;
|
|
246
|
+
END IF;
|
|
247
|
+
ELSE
|
|
248
|
+
RAISE EXCEPTION 'no object kind %', p_kind USING ERRCODE = 'invalid_parameter_value';
|
|
249
|
+
END CASE;
|
|
250
|
+
-- A row gone, a row whose seal is no longer the one read, an environment that is not what the value says it
|
|
251
|
+
-- stays, or an object made already: nothing was done.
|
|
252
|
+
GET DIAGNOSTICS done = ROW_COUNT;
|
|
253
|
+
IF done = 0 THEN RETURN NULL; END IF;
|
|
254
|
+
INSERT INTO public.log_changes (number, at, request_id, object, actor, payload, seal, mac)
|
|
255
|
+
VALUES (nextval('public.log_number'), p_at, p_request, target, p_actor, p_payload, p_seal, p_mac)
|
|
256
|
+
RETURNING number INTO n;
|
|
257
|
+
RETURN n;
|
|
258
|
+
END
|
|
259
|
+
$$;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
-- The app's reads before a use, in one call (T135; docs/design/0.15-plan.md, "As built: T135"): a request's credential, the newest policy change, the place it reads and its keys' rows, each what the app's own statements read (`credentialRow`, `resolvePath`, `environmentSecrets` in packages/server/src/db/queries.ts), as one JSON value. A function, so that each backend plans its statements once: planned anew for every request, the app's three statements cost 2.35 ms of Postgres CPU a request on a 2 vCPU Postgres, their plans most of it; this, 0.5 ms (measured, ibid.). One snapshot (STABLE): the place and the keys are read with the credential. It decides nothing: the app checks each row as it checks its own statements' (the MAC, what is live, what is inherited). Only 0.15's switch calls it: 0.14's code serves on it as before.
|
|
2
|
+
--
|
|
3
|
+
-- Planned once per backend (`force_generic_plan`), as the hot login's statements are: the plpgsql default plans each call again while a custom plan looks cheaper, which here was every call. Every read is a probe of a unique key, or an environment's keys by their index: `enable_seqscan = off` keeps a generic plan from reading a small table whole, whose pages grow with every row the instance gains (R48; proxy-use-cost.test.ts). `jit = off`, as the hot path's `use_credential` has it: a generic plan's estimate, a disabled scan's above all, can pass `jit_above_cost`, and compiling it costs a hundred times the read.
|
|
4
|
+
--
|
|
5
|
+
-- Each part is asked for or not: `p_token_hash` the credential (null: none); `p_project` and `p_environment` the place, by slugs, standing ones only (null: none); `p_keys` its keys, those it names, each once (null: every one). A personal config (`dev@me`), a key's own path and a tombstone are the app's own statements' to read; so are a fork's keys, and none is read for a token that names no credential. `bytea_output` pinned: the app reads each bytea as `\x`-hex.
|
|
6
|
+
CREATE FUNCTION app_read(p_token_hash bytea, p_project text, p_environment text, p_keys text[]) RETURNS text
|
|
7
|
+
LANGUAGE plpgsql STABLE SET search_path = pg_catalog, pg_temp SET plan_cache_mode = force_generic_plan SET enable_seqscan = off SET jit = off SET bytea_output = 'hex' AS $$
|
|
8
|
+
DECLARE
|
|
9
|
+
v_credential json;
|
|
10
|
+
v_identity json;
|
|
11
|
+
v_policy text;
|
|
12
|
+
v_project json;
|
|
13
|
+
v_environment json;
|
|
14
|
+
v_parent text;
|
|
15
|
+
v_project_id uuid;
|
|
16
|
+
v_environment_id uuid;
|
|
17
|
+
v_parent_id uuid;
|
|
18
|
+
v_live boolean;
|
|
19
|
+
v_rows json;
|
|
20
|
+
BEGIN
|
|
21
|
+
SELECT to_json(c), to_json(i), pol.seq::text, to_json(p), to_json(e), par.slug, p.id, e.id, e.parent_id, p.archived_at IS NULL AND e.archived_at IS NULL
|
|
22
|
+
INTO v_credential, v_identity, v_policy, v_project, v_environment, v_parent, v_project_id, v_environment_id, v_parent_id, v_live
|
|
23
|
+
FROM (SELECT 1) one
|
|
24
|
+
LEFT JOIN public.credentials c ON c.token_hash = p_token_hash
|
|
25
|
+
LEFT JOIN public.identities i ON i.id = c.identity_id
|
|
26
|
+
-- The newest policy change (`newestSeqQuery`), read with the credential, as `credentialRow` reads it.
|
|
27
|
+
LEFT JOIN LATERAL (
|
|
28
|
+
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
|
|
29
|
+
) pol ON p_token_hash IS NOT NULL
|
|
30
|
+
LEFT JOIN public.projects p ON p.slug = p_project AND p.slug NOT LIKE '%~%'
|
|
31
|
+
LEFT JOIN public.environments e ON e.project_id = p.id AND e.slug = p_environment AND e.slug NOT LIKE '%~%'
|
|
32
|
+
LEFT JOIN public.environments par ON par.id = e.parent_id;
|
|
33
|
+
-- Its keys as `environmentSecrets` reads them, each its row and its current version, in key order: none of an
|
|
34
|
+
-- archived project's or environment's. The named ones (distinct: the app's to say) one probe each, by the key's
|
|
35
|
+
-- index and the version's; two statements, as one plan serves only one shape. Not read (null) for a token that
|
|
36
|
+
-- names no credential, which is refused on that alone, so that an unknown one costs its probe and no more; nor of
|
|
37
|
+
-- a fork, whose layers the app reads together, in one snapshot (`readableValues`).
|
|
38
|
+
IF v_environment_id IS NOT NULL AND v_live AND (p_token_hash IS NULL OR v_credential IS NOT NULL) AND v_parent_id IS NULL THEN
|
|
39
|
+
IF p_keys IS NULL THEN
|
|
40
|
+
SELECT json_agg(json_build_object('secret', to_json(s), 'version', to_json(v)) ORDER BY s.key) INTO v_rows
|
|
41
|
+
FROM public.secrets s LEFT JOIN public.secret_versions v ON v.id = s.current_version_id
|
|
42
|
+
WHERE s.project_id = v_project_id AND s.environment_id = v_environment_id;
|
|
43
|
+
ELSE
|
|
44
|
+
SELECT json_agg(json_build_object('secret', to_json(s), 'version', to_json(v)) ORDER BY s.key) INTO v_rows
|
|
45
|
+
FROM unnest(p_keys) k (key)
|
|
46
|
+
-- LIMIT 1, a key being unique in its environment, keeps the lateral a probe by `k.key`: flattened, the planner
|
|
47
|
+
-- estimates an environment's keys as one row, of so many environments, and reads them all to join (review r4).
|
|
48
|
+
CROSS JOIN LATERAL (
|
|
49
|
+
SELECT * FROM public.secrets x WHERE x.project_id = v_project_id AND x.environment_id = v_environment_id AND x.key = k.key LIMIT 1
|
|
50
|
+
) s
|
|
51
|
+
LEFT JOIN LATERAL (SELECT * FROM public.secret_versions y WHERE y.id = s.current_version_id) v ON true;
|
|
52
|
+
END IF;
|
|
53
|
+
END IF;
|
|
54
|
+
RETURN json_build_object(
|
|
55
|
+
'credential', v_credential, 'identity', v_identity, 'policy', v_policy,
|
|
56
|
+
'project', v_project, 'environment', v_environment, 'parent', v_parent,
|
|
57
|
+
'secrets', CASE WHEN v_environment_id IS NULL OR (p_token_hash IS NOT NULL AND v_credential IS NULL) OR v_parent_id IS NOT NULL THEN NULL
|
|
58
|
+
WHEN v_live THEN coalesce(v_rows, '[]') ELSE '[]' END
|
|
59
|
+
)::text;
|
|
60
|
+
END;
|
|
61
|
+
$$;
|
|
62
|
+
--> statement-breakpoint
|
|
63
|
+
REVOKE ALL ON FUNCTION app_read(bytea, text, text, text[]) FROM PUBLIC;
|
|
64
|
+
--> statement-breakpoint
|
|
65
|
+
GRANT EXECUTE ON FUNCTION app_read(bytea, text, text, text[]) TO coffre_app;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- An index for the reads of an environment's live keys that do not name its project (T152): the places tree's counts in every `me`, `liveKeysIn`, `distinctSecretCounts`. `secrets_lookup_idx` leads with the project, so such a read walked it end to end, or the table, every project's keys and every archived one in it. Partial, so that the archived keys an environment piles up are no part of it: a read of the live ones costs what they do. It only adds an index: 0.14's code serves on it as before.
|
|
2
|
+
--
|
|
3
|
+
-- A plain CREATE INDEX, not CONCURRENTLY, which cannot run in the transaction the migrator wraps a migration in: it blocks writes to `secrets` while it builds, a table of tens of thousands of rows in well under a second.
|
|
4
|
+
CREATE INDEX "secrets_live_idx" ON "secrets" USING btree ("environment_id") WHERE "archived_at" IS NULL;
|