@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,878 @@
|
|
|
1
|
+
-- 0.15's use by name reads what its request names, by its names, and takes its principal's lock only for a use the vault decided, in the call it makes once it has (T158, D153; docs/design/0.15-plan.md, "As built: T158" and "As built: D153"; formal/Probing.tla, ResolveByName, DecideBeforeLock, DecideEachLock; formal/HotPathOne.tla, NoLockWithoutStanding).
|
|
2
|
+
-- A named call (`p_token_hash`) reads, first and under the fence, the credential its token's hash names, the place its slugs name (a handle's own, `p_handle`), as `use_resolve` reads it, and the key its name names there; every row the call reads is one of those, whatever ids the vault passed. Those ids, which the vault's per-isolate map held and MACed the entry over, only let the call write the use's entry and take the lock, when they are the ones its names name now; otherwise it answers `moved`, writing and locking nothing, with the names' rows and ids (`n_*`), on which the vault decides. So a refusal, whatever its reason (no standing, no such key, a reference ended, a row put back), is decided in this one call, whatever the map held, and the app answers every one alike (D153). A token no credential has answers `unknown`; a place that is none, archived, a fork's or a personal config's, `nowhere`.
|
|
3
|
+
-- A use of a sealed key that would enter the cap's set, or move on in it, took its principal's lock before anything was decided: someone who may not use the key waited for it for a key that exists, and never for one that does not (reviews r1 to r4 of #412). Now a named use takes that lock only in the call it makes once the vault has decided the use, key and all (`p_decided`): otherwise the call writes and locks nothing and answers its rows as `decide`, which the vault decides on. No decision is kept for a later call to take the lock on (review r1 of D153: one kept, its rows' digest blind to a version written beside it, let a refused use wait for the lock).
|
|
4
|
+
-- `use_credential` as 0044 made it (its comments, 0044's, 0033's, 0029's, 0028's and 0024's say how), but for those and its warm-up's mark (`coffre.use_planned`). A use by ids (no name) is as before: the app asked the vault about its caller first. It only adds: 0.15's hot path alone calls it, behind the switch, and its signature grows, so it is made anew, as 0044's was.
|
|
5
|
+
-- A named call reads what its request names itself (D153): `use_resolve`, 0044's read of the ids before a use, which no release called, goes.
|
|
6
|
+
-- A use by ids decides before that lock too (review r2 of D153): its call answers `decide` as a named one does, and the vault makes it, decided, in a second; refused, nothing is written, and the app logs the request. So no call waits on the lock for a use the vault refuses, whichever path asked.
|
|
7
|
+
-- A named call whose key reads an archived place (a reference's source's) answers `archived`, writing and locking nothing, as 0044's `unnamed` did; the vault refuses it, by name, as every refusal of the key (review r4 of D153), where 0044's vault handed it to the app's path.
|
|
8
|
+
-- A handle's places are the credential's, read before the credential is checked: parsed without raising, one that is no single place resolves to none (`nowhere`), and the app's check answers the credential (review r2 of D153). The places a stable URL does not mean (`p_unless`, by slugs) are judged against the slugs the call reads, never the map's.
|
|
9
|
+
-- A named call holds its token's credential to what the app's config accepts first (`p_signin`, `p_issuers`; reviews r6 and r7 of D153): coffre's own sign-in, and for a browser session a sign-in provider the deployment configures, as the app's own check has it. One the app would refuse, as a token no credential has, answers `unknown` before the place, the key or any lock is read or taken; so the app's 401 never follows work that hangs on the key.
|
|
10
|
+
-- A named call decides its caller's standing at the place first, from rows no key decides (the credential, its account and grants, the roles, the place), and reads the key only for a caller who stands (D150 fallback D, W268; docs/design/0.15-plan.md, "As built: W268"; formal/Probing.tla, StandingFirst, NoKeyBeforeStanding): `use_stands`, `namedStanding`'s rule (decide.ts) on the rows as read, whose seals only the vault checks after. A caller who does not stand there (revoked, lapsed, no grant, a Contributor asking to read, a credential ended at the use's `at`) is answered as for a key the place does not hold, by the same statement, whatever the request names and whatever the isolate's map and covers hold: no key's name probed, no key's row, reference or source read, nothing written or locked. So what a refusal costs inside its call hangs on no key, beside its answer and its calls.
|
|
11
|
+
DROP FUNCTION use_resolve(bytea, text, text, text[]);
|
|
12
|
+
--> statement-breakpoint
|
|
13
|
+
ALTER TYPE use_row ADD ATTRIBUTE n_credential uuid;
|
|
14
|
+
--> statement-breakpoint
|
|
15
|
+
ALTER TYPE use_row ADD ATTRIBUTE n_secret uuid;
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
ALTER TYPE use_row ADD ATTRIBUTE n_place uuid;
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
ALTER TYPE use_row ADD ATTRIBUTE n_project text;
|
|
20
|
+
--> statement-breakpoint
|
|
21
|
+
ALTER TYPE use_row ADD ATTRIBUTE n_environment text;
|
|
22
|
+
--> statement-breakpoint
|
|
23
|
+
ALTER TYPE use_row ADD ATTRIBUTE n_standing boolean;
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
-- `admits` (@coffre/core/access): whether a scope's filter takes in a project's id or an environment's slug.
|
|
26
|
+
CREATE FUNCTION use_admits(p_filter jsonb, p_value text) RETURNS boolean
|
|
27
|
+
LANGUAGE sql IMMUTABLE PARALLEL SAFE AS $$
|
|
28
|
+
SELECT CASE WHEN p_filter = '"all"'::jsonb THEN true
|
|
29
|
+
WHEN p_value IS NULL OR jsonb_typeof(p_filter) IS DISTINCT FROM 'object' THEN false
|
|
30
|
+
WHEN p_filter ? 'only' THEN coalesce(p_filter -> 'only' @> to_jsonb(p_value), false)
|
|
31
|
+
WHEN p_filter ? 'except' THEN NOT coalesce(p_filter -> 'except' @> to_jsonb(p_value), false)
|
|
32
|
+
ELSE false END
|
|
33
|
+
$$;
|
|
34
|
+
--> statement-breakpoint
|
|
35
|
+
-- Whether the caller of a named use stands at the place it names, for its purpose (W268): `namedStanding` (decide.ts),
|
|
36
|
+
-- `standingRefusal` before the seals, on the rows as read. The credential: not revoked, not ended at the use's `at`, of
|
|
37
|
+
-- its account's generation, the account active (`credentialEnded`, `readerOf`; `vault_members`' check holds its
|
|
38
|
+
-- principal to `PRINCIPAL`). Then `allows` (@coffre/core/access) for `secret.use` (the purpose `use`, its low byte) or
|
|
39
|
+
-- `secret.read` (any other) at the place, a live environment of its own (`nowhere` otherwise, before this): within the
|
|
40
|
+
-- credential's scope, if it has one; by the instance role inside its scope (a person's, as `storedRole` reads it; a
|
|
41
|
+
-- service's is `member`, everywhere), or by a live grant on the place or its project; each role as the roles' edits
|
|
42
|
+
-- define it over `p_roles`, the ones coffre ships as the vault passes them (`GATE_ROLES`, decide.ts: one source), in a
|
|
43
|
+
-- protected place a person's protected column. It refuses only what it reads whole as refused: a value it cannot parse
|
|
44
|
+
-- (a scope or the roles' edits that are no JSON) stands here, and the vault's rule, which reads it as nothing and checks
|
|
45
|
+
-- its seal besides, refuses it after; so a refusal here is always the rule's (`use-stands.test.ts` holds the two alike
|
|
46
|
+
-- on what a release writes). plpgsql, so that its statement's plan is kept per session, as `use_credential`'s are (a
|
|
47
|
+
-- `LANGUAGE sql` function's is made anew at each call on Postgres 16).
|
|
48
|
+
CREATE FUNCTION use_stands(p_credential uuid, p_place uuid, p_purpose smallint, p_at bigint, p_roles jsonb) RETURNS boolean
|
|
49
|
+
LANGUAGE plpgsql STABLE PARALLEL SAFE AS $$
|
|
50
|
+
BEGIN
|
|
51
|
+
RETURN coalesce((
|
|
52
|
+
SELECT k.live AND (k.c_scope IS NULL OR EXISTS (
|
|
53
|
+
SELECT 1 FROM jsonb_array_elements(k.c_scope) g
|
|
54
|
+
WHERE g.value ->> 'projectId' = k.e_project::text AND (g.value ->> 'environmentId' IS NULL OR g.value ->> 'environmentId' = k.e_id::text)
|
|
55
|
+
AND coalesce(CASE WHEN g.value ? 'permissions' THEN g.value -> 'permissions' -> CASE WHEN k.guarded THEN 'protected' ELSE 'normal' END
|
|
56
|
+
ELSE p_roles -> 'roles' -> (g.value ->> 'role') -> CASE WHEN k.guarded THEN 'protected' ELSE 'permissions' END END @> k.wanted, false)))
|
|
57
|
+
AND ((public.use_admits(k.m_scope -> 'projects', k.e_project::text) AND public.use_admits(k.m_scope -> 'environments', k.e_slug)
|
|
58
|
+
AND coalesce(coalesce(k.r_value -> 'instanceRoles' -> k.role, p_roles -> 'instanceRoles' -> k.role) -> CASE WHEN k.guarded THEN 'protected' ELSE 'permissions' END @> k.wanted, false))
|
|
59
|
+
OR EXISTS (
|
|
60
|
+
SELECT 1 FROM public.vault_grants g LEFT JOIN public.environments ge ON ge.id = g.environment_id
|
|
61
|
+
WHERE g.principal = k.principal AND (g.expires_at IS NULL OR g.expires_at > p_at)
|
|
62
|
+
AND coalesce(g.project_id, ge.project_id) = k.e_project AND (g.environment_id IS NULL OR g.environment_id = k.e_id)
|
|
63
|
+
AND coalesce(coalesce(k.r_value -> 'roles' -> g.role, p_roles -> 'roles' -> g.role) -> CASE WHEN k.guarded THEN 'protected' ELSE 'permissions' END @> k.wanted, false)))
|
|
64
|
+
FROM (
|
|
65
|
+
-- Each row by its key alone, what ends the credential weighed here: a condition on `revoked_at` would
|
|
66
|
+
-- let a generic plan take the live credentials' partial index and read them all (R48).
|
|
67
|
+
SELECT c.revoked_at IS NULL AND m.status = 'active' AND m.generation = c.generation
|
|
68
|
+
AND coalesce(p_at < floor(extract(epoch FROM least(c.expires_at, c.not_after)) * 1000)::bigint, true) AS live,
|
|
69
|
+
c.principal, e.id AS e_id, e.project_id AS e_project, e.slug AS e_slug,
|
|
70
|
+
c.principal LIKE 'user:%' AND coalesce(e.protected, false) AS guarded,
|
|
71
|
+
to_jsonb(CASE WHEN p_purpose % 256 = 4 THEN 'secret.use' ELSE 'secret.read' END) AS wanted,
|
|
72
|
+
-- A scope that is no list stands as none: the rule reads it as nothing, and refuses.
|
|
73
|
+
CASE WHEN c.scope IS NOT NULL AND pg_input_is_valid(c.scope, 'jsonb') AND jsonb_typeof(c.scope::jsonb) = 'array' THEN c.scope::jsonb END AS c_scope,
|
|
74
|
+
CASE WHEN r.value IS NOT NULL AND pg_input_is_valid(r.value, 'jsonb') THEN r.value::jsonb END AS r_value,
|
|
75
|
+
-- `storedRole` (@coffre/db/grants) for a person: their role, and its scope; one that does not parse, everywhere.
|
|
76
|
+
sr.role,
|
|
77
|
+
CASE WHEN sr.role = 'member' OR m.scope IS NULL OR NOT pg_input_is_valid(m.scope, 'jsonb') THEN '{"projects": "all", "environments": "all"}'::jsonb
|
|
78
|
+
ELSE m.scope::jsonb END AS m_scope
|
|
79
|
+
FROM public.credentials c
|
|
80
|
+
JOIN public.vault_members m ON m.principal = c.principal
|
|
81
|
+
JOIN public.environments e ON e.id = p_place
|
|
82
|
+
LEFT JOIN public.log_config r ON r.name = 'roles'
|
|
83
|
+
CROSS JOIN LATERAL (
|
|
84
|
+
SELECT CASE WHEN c.principal NOT LIKE 'user:%' THEN 'member'
|
|
85
|
+
WHEN m.role IS NULL THEN CASE WHEN m.owner THEN 'admin' ELSE 'member' END
|
|
86
|
+
WHEN p_roles -> 'instanceRoles' ? m.role THEN m.role
|
|
87
|
+
ELSE 'member' END AS role
|
|
88
|
+
) sr
|
|
89
|
+
WHERE c.id = p_credential
|
|
90
|
+
) k
|
|
91
|
+
), false);
|
|
92
|
+
END
|
|
93
|
+
$$;
|
|
94
|
+
--> statement-breakpoint
|
|
95
|
+
REVOKE ALL ON FUNCTION use_admits(jsonb, text) FROM PUBLIC;
|
|
96
|
+
--> statement-breakpoint
|
|
97
|
+
REVOKE ALL ON FUNCTION use_stands(uuid, uuid, smallint, bigint, jsonb) FROM PUBLIC;
|
|
98
|
+
--> statement-breakpoint
|
|
99
|
+
DROP FUNCTION use_credential(uuid, uuid, bigint, bigint, smallint, uuid, bytea, bigint, bigint, bigint, bigint, bigint, bytea, integer, bigint, bytea, text, text, text);
|
|
100
|
+
--> statement-breakpoint
|
|
101
|
+
CREATE FUNCTION use_credential(
|
|
102
|
+
p_credential uuid, p_secret uuid, p_request bigint, p_at bigint, p_purpose smallint, p_place uuid, p_mac bytea, p_deadline bigint,
|
|
103
|
+
p_cover bigint DEFAULT NULL, p_cover_at bigint DEFAULT NULL, p_cover_request bigint DEFAULT NULL, p_known bigint DEFAULT NULL, p_known_mac bytea DEFAULT NULL,
|
|
104
|
+
p_cap_count integer DEFAULT NULL, p_cap_window bigint DEFAULT NULL,
|
|
105
|
+
p_token_hash bytea DEFAULT NULL, p_project text DEFAULT NULL, p_environment text DEFAULT NULL, p_key text DEFAULT NULL,
|
|
106
|
+
p_decided boolean DEFAULT false,
|
|
107
|
+
p_handle boolean DEFAULT false,
|
|
108
|
+
p_unless text[] DEFAULT NULL,
|
|
109
|
+
p_signin boolean DEFAULT NULL,
|
|
110
|
+
p_issuers text[] DEFAULT NULL,
|
|
111
|
+
p_roles jsonb DEFAULT NULL
|
|
112
|
+
) RETURNS json
|
|
113
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp SET bytea_output = 'hex' SET jit = off AS $$
|
|
114
|
+
DECLARE
|
|
115
|
+
r public.use_row;
|
|
116
|
+
-- The principal's lock held: exclusive (`locked`), to enter its key in the cap's set; shared (`moving`), to move it on.
|
|
117
|
+
locked boolean := false;
|
|
118
|
+
moving boolean := false;
|
|
119
|
+
probe boolean := p_known IS NULL OR p_cap_count IS NULL OR p_cap_window IS NULL;
|
|
120
|
+
-- A use through a place, or of a key with no value of its own (a reference), reads every input; the others, most
|
|
121
|
+
-- uses, the plain shape: no place, no source, none of their joins.
|
|
122
|
+
whole boolean := p_place IS NOT NULL;
|
|
123
|
+
-- The cached forms' refresh and period, from the cap the isolate passes; the probing forms work theirs out from the cap they read.
|
|
124
|
+
c_refresh bigint := greatest(1, least(60000, p_cap_window / 10));
|
|
125
|
+
c_period bigint := p_at - ((p_at % c_refresh) + c_refresh) % c_refresh;
|
|
126
|
+
-- Whether this session has yet to plan the four statements (T131), and the form the warm-up is at.
|
|
127
|
+
dry boolean := current_setting('coffre.use_planned', true) IS DISTINCT FROM '0045';
|
|
128
|
+
form integer := 0;
|
|
129
|
+
first_probe boolean;
|
|
130
|
+
first_whole boolean;
|
|
131
|
+
-- The period this call writes, if it writes the cap's row, and the lock that elects the one use that moves it.
|
|
132
|
+
b_refresh bigint;
|
|
133
|
+
b_period bigint;
|
|
134
|
+
b_lock bigint;
|
|
135
|
+
b_round integer := 0;
|
|
136
|
+
-- What the call reads by: the ids passed, or, for a named call, those its names name now (D153), read once under
|
|
137
|
+
-- the fence; whether they are the ids passed (`v_right`), and what the names name if not a live place (`v_named`).
|
|
138
|
+
v_credential uuid := p_credential;
|
|
139
|
+
v_secret uuid := p_secret;
|
|
140
|
+
v_place uuid := p_place;
|
|
141
|
+
v_right boolean := true;
|
|
142
|
+
v_named text;
|
|
143
|
+
v_accepted boolean;
|
|
144
|
+
v_np uuid;
|
|
145
|
+
v_np_project uuid;
|
|
146
|
+
v_places text;
|
|
147
|
+
v_bound uuid;
|
|
148
|
+
v_live boolean;
|
|
149
|
+
v_project text;
|
|
150
|
+
v_environment text;
|
|
151
|
+
resolved boolean := p_token_hash IS NULL;
|
|
152
|
+
-- Whether a named call's caller stands at the place its names name, for its purpose (`use_stands`, W268): only then
|
|
153
|
+
-- does it read the key.
|
|
154
|
+
v_stands boolean;
|
|
155
|
+
BEGIN
|
|
156
|
+
IF p_deadline IS NULL OR p_deadline > p_at + public.deadline_max() THEN r.state := 'late'; RETURN to_json(r); END IF;
|
|
157
|
+
-- Its Worker's clock ahead by over a minute (0022 says why): checked here at no cost, and again in the statement.
|
|
158
|
+
IF p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN r.state := 'ahead'; RETURN to_json(r); END IF;
|
|
159
|
+
IF dry THEN
|
|
160
|
+
-- Each form first runs dry, outside the fence, so that its plan is made before this call holds the fence.
|
|
161
|
+
first_probe := probe;
|
|
162
|
+
first_whole := whole;
|
|
163
|
+
probe := true;
|
|
164
|
+
whole := false;
|
|
165
|
+
ELSE
|
|
166
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 0);
|
|
167
|
+
END IF;
|
|
168
|
+
LOOP
|
|
169
|
+
IF NOT dry AND NOT resolved THEN
|
|
170
|
+
-- What the request names, by its names, under the fence, which no change moves (as `use_resolve` reads it):
|
|
171
|
+
-- the credential of its token's hash; the place of its slugs, or a handle's one place; its key there by name.
|
|
172
|
+
-- Each by an index, its plan generic (the hot login's `force_generic_plan`): a handle's place by its id, a
|
|
173
|
+
-- named one by its slugs, never one plan for both, which no index serves (review r1 of D153). The dry run
|
|
174
|
+
-- skips this block: these statements, the gate's among them, are planned on a session's first named call,
|
|
175
|
+
-- under the fence, which hangs on nothing a request names.
|
|
176
|
+
-- The credential first, held to what the app's config accepts (`p_signin`, `p_issuers`; review r6 and r7 of
|
|
177
|
+
-- D153; Probing.tla, AcceptedFirst): the deployment signs in through coffre's own sign-in, and a browser
|
|
178
|
+
-- session's identity names a provider it configures, as the app's own check has it (`#liveCredential`). One it
|
|
179
|
+
-- would refuse is answered as a token no credential has, `unknown`, here: no place, no key, no lock read or
|
|
180
|
+
-- taken, whatever the request names.
|
|
181
|
+
SELECT c.id, c.places,
|
|
182
|
+
coalesce(p_signin, false) AND (c.kind <> 'browser' OR (x.provider || ':' || x.issuer_hash) = ANY (coalesce(p_issuers, '{}')))
|
|
183
|
+
INTO v_credential, v_places, v_accepted
|
|
184
|
+
FROM public.credentials c LEFT JOIN public.identities x ON x.id = c.identity_id
|
|
185
|
+
WHERE c.token_hash = p_token_hash;
|
|
186
|
+
IF v_credential IS NULL OR NOT coalesce(v_accepted, false) THEN r.state := 'unknown'; RETURN to_json(r); END IF;
|
|
187
|
+
IF p_handle THEN
|
|
188
|
+
-- Unchecked yet, so parsed without raising, each step in turn (a CASE's order, which AND does not promise).
|
|
189
|
+
v_bound := CASE WHEN NOT coalesce(pg_input_is_valid(v_places, 'json'), false) THEN NULL
|
|
190
|
+
WHEN json_typeof(v_places::json) IS DISTINCT FROM 'array' OR json_array_length(v_places::json) <> 1 THEN NULL
|
|
191
|
+
WHEN json_typeof(v_places::json -> 0) IS DISTINCT FROM 'object' THEN NULL
|
|
192
|
+
WHEN coalesce(v_places::json -> 0 ->> 'personal', 'false') <> 'false' THEN NULL
|
|
193
|
+
WHEN NOT coalesce(pg_input_is_valid(v_places::json -> 0 ->> 'environmentId', 'uuid'), false) THEN NULL
|
|
194
|
+
ELSE (v_places::json -> 0 ->> 'environmentId')::uuid END;
|
|
195
|
+
SELECT e.id, e.project_id, q.slug, e.slug, e.archived_at IS NULL AND q.archived_at IS NULL AND e.parent_id IS NULL AND e.owner IS NULL
|
|
196
|
+
INTO v_np, v_np_project, v_project, v_environment, v_live
|
|
197
|
+
FROM public.environments e JOIN public.projects q ON q.id = e.project_id
|
|
198
|
+
WHERE e.id = v_bound AND e.slug NOT LIKE '%~%' AND q.slug NOT LIKE '%~%';
|
|
199
|
+
ELSE
|
|
200
|
+
SELECT e.id, e.project_id, q.slug, e.slug, e.archived_at IS NULL AND q.archived_at IS NULL AND e.parent_id IS NULL AND e.owner IS NULL
|
|
201
|
+
INTO v_np, v_np_project, v_project, v_environment, v_live
|
|
202
|
+
FROM public.projects q JOIN public.environments e ON e.project_id = q.id AND e.slug = p_environment
|
|
203
|
+
WHERE q.slug = p_project AND q.slug NOT LIKE '%~%' AND e.slug NOT LIKE '%~%';
|
|
204
|
+
END IF;
|
|
205
|
+
-- A handle's place the stable URL does not mean, by the slugs read here: the app's, as a place that is none.
|
|
206
|
+
v_named := CASE WHEN v_np IS NULL OR NOT v_live OR (p_handle AND (v_project || '/' || v_environment) = ANY (coalesce(p_unless, '{}'))) THEN 'nowhere' END;
|
|
207
|
+
-- The caller's standing at the place, for the request's purpose, from rows no key decides (W268; Probing.tla,
|
|
208
|
+
-- StandingFirst): its key's name is read only for one who stands there. Without the roles coffre ships, which
|
|
209
|
+
-- every named call of the vault's passes, it stands: never refused here where the vault's rule allows.
|
|
210
|
+
v_stands := CASE WHEN v_named IS NOT NULL THEN false WHEN p_roles IS NULL THEN true
|
|
211
|
+
ELSE public.use_stands(v_credential, v_np, p_purpose, p_at, p_roles) END;
|
|
212
|
+
IF p_key IS NOT NULL AND v_stands THEN
|
|
213
|
+
SELECT x.id INTO v_secret FROM public.secrets x
|
|
214
|
+
WHERE x.project_id = v_np_project AND x.environment_id = v_np AND x.key = p_key AND x.archived_at IS NULL LIMIT 1;
|
|
215
|
+
ELSE
|
|
216
|
+
v_secret := NULL;
|
|
217
|
+
END IF;
|
|
218
|
+
-- A key named is read by its id; one the place does not hold, through the place (the whole form, e1).
|
|
219
|
+
v_place := CASE WHEN v_secret IS NULL THEN v_np END;
|
|
220
|
+
v_right := coalesce(v_credential = p_credential, false) AND v_secret IS NOT DISTINCT FROM p_secret AND v_place IS NOT DISTINCT FROM p_place;
|
|
221
|
+
IF NOT v_stands THEN
|
|
222
|
+
-- No standing, or no live place: answered on the place alone, as for a key it does not hold, probing,
|
|
223
|
+
-- whatever the isolate passed: no cover, no seals (NoKeyBeforeStanding). Nothing is written or locked,
|
|
224
|
+
-- as for any key the place does not hold.
|
|
225
|
+
p_cover := NULL;
|
|
226
|
+
probe := true;
|
|
227
|
+
END IF;
|
|
228
|
+
whole := v_place IS NOT NULL;
|
|
229
|
+
resolved := true;
|
|
230
|
+
END IF;
|
|
231
|
+
IF probe AND NOT whole THEN
|
|
232
|
+
WITH i AS MATERIALIZED (
|
|
233
|
+
SELECT n.number AS newest, n.mac AS newest_mac, coalesce(n.number = p_known AND n.mac = p_known_mac, false) AS cached,
|
|
234
|
+
f.at_floor, (lpol.payload::jsonb #>> '{value,bulk,count}')::integer AS cap_count, (lpol.payload::jsonb #>> '{value,bulk,windowMs}')::bigint AS cap_window,
|
|
235
|
+
lpol.number AS lpol_number, lpol.seal AS p_last,
|
|
236
|
+
c.principal AS p, c.kind AS c_kind, c.scope AS c_scope, c.revoked_at IS NOT NULL AS c_revoked,
|
|
237
|
+
floor(extract(epoch FROM least(c.expires_at, c.not_after)) * 1000)::bigint AS c_expires, c.seal AS c_seal,
|
|
238
|
+
m.status AS m_status, m.owner AS m_owner, m.role AS m_role, m.scope AS m_scope, m.seal AS m_seal, c.generation AS c_generation, m.generation AS m_generation,
|
|
239
|
+
-- What the request named, for the names it checks, and the rows the app checks, when it names them (0044).
|
|
240
|
+
c.token_hash AS c_token_hash,
|
|
241
|
+
CASE WHEN p_token_hash IS NOT NULL THEN to_json(c) END AS c_row, CASE WHEN p_token_hash IS NOT NULL THEN to_json(s) END AS s_row,
|
|
242
|
+
CASE WHEN p_token_hash IS NOT NULL AND s.current_version_id IS NOT NULL THEN to_json(sv) END AS sv_row,
|
|
243
|
+
cfg.r_value, cfg.r_seal, cfg.p_value, cfg.p_seal,
|
|
244
|
+
s.project_id AS s_project, s.environment_id AS s_environment, s.current_version_id AS s_version, NULL::uuid AS s_source, NULL::uuid AS s_reference, s.seal AS s_seal,
|
|
245
|
+
NULL::uuid AS x_project, NULL::uuid AS x_environment, NULL::uuid AS x_version, NULL::uuid AS x_source, NULL::uuid AS x_reference, NULL::bytea AS x_seal,
|
|
246
|
+
sv.envelope_version, sv.ciphertext, sv.iv, sv.auth_tag, sv.wrapped_dek, sv.kek_provider, sv.kek_id, sv.kek_version, sv.sensitive, sv.plaintext IS NOT NULL AS v_plain, sv.plaintext AS v_plaintext,
|
|
247
|
+
NULL::uuid AS e1_id, NULL::uuid AS e1_project, NULL::text AS e1_slug, NULL::text AS e1_owner, NULL::uuid AS e1_parent, NULL::boolean AS e1_protected, NULL::boolean AS e1_archived, NULL::boolean AS e1_project_archived, NULL::bytea AS e1_seal,
|
|
248
|
+
s.environment_id AS e2_id, e2.project_id AS e2_project, e2.slug AS e2_slug, e2.owner AS e2_owner, e2.parent_id AS e2_parent, e2.protected AS e2_protected, e2.archived_at IS NOT NULL AS e2_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e2.project_id) AS e2_project_archived, e2.seal AS e2_seal,
|
|
249
|
+
NULL::uuid AS e3_id, NULL::uuid AS e3_project, NULL::text AS e3_slug, NULL::text AS e3_owner, NULL::uuid AS e3_parent, NULL::boolean AS e3_protected, NULL::boolean AS e3_archived, NULL::boolean AS e3_project_archived, NULL::bytea AS e3_seal,
|
|
250
|
+
CASE WHEN e2.owner IS NOT NULL THEN e2.parent_id END AS e4_id, e4.project_id AS e4_project, e4.slug AS e4_slug, e4.owner AS e4_owner, e4.parent_id AS e4_parent, e4.protected AS e4_protected, e4.archived_at IS NOT NULL AS e4_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e4.project_id) AS e4_project_archived, e4.seal AS e4_seal,
|
|
251
|
+
b.last AS bulk_last,
|
|
252
|
+
-- Each input's object key, computed once: a key in a probe's own condition is computed again for every row it reads (0022, decision 3).
|
|
253
|
+
public.log_object('credential', v_credential::text) AS k_credential,
|
|
254
|
+
public.log_object('principal', c.principal) AS k_member,
|
|
255
|
+
public.log_object('secret', v_secret::text) AS k_secret,
|
|
256
|
+
public.log_object('environment', s.environment_id::text) AS k_e2,
|
|
257
|
+
public.log_object('environment', (CASE WHEN e2.owner IS NOT NULL THEN e2.parent_id END)::text) AS k_e4
|
|
258
|
+
FROM (SELECT 1) one
|
|
259
|
+
LEFT JOIN LATERAL (SELECT x.number, x.mac FROM public.log_changes x ORDER BY x.number DESC LIMIT 1) n ON true
|
|
260
|
+
LEFT JOIN public.log_fence f ON f.only_row
|
|
261
|
+
-- The roles and the policy, both rows in one read of their one-page table.
|
|
262
|
+
LEFT JOIN LATERAL (
|
|
263
|
+
SELECT max(g.value) FILTER (WHERE g.name = 'roles') AS r_value, (array_agg(g.seal) FILTER (WHERE g.name = 'roles'))[1] AS r_seal,
|
|
264
|
+
max(g.value) FILTER (WHERE g.name = 'policy') AS p_value, (array_agg(g.seal) FILTER (WHERE g.name = 'policy'))[1] AS p_seal
|
|
265
|
+
FROM public.log_config g
|
|
266
|
+
) cfg ON true
|
|
267
|
+
-- The cap as the policy's newest change says it (CapSealed): every policy change logs the whole policy as its `value`.
|
|
268
|
+
LEFT JOIN LATERAL (
|
|
269
|
+
SELECT x.number, x.seal, x.payload FROM public.log_changes x WHERE x.object = public.log_object('policy', 'instance') ORDER BY x.number DESC LIMIT 1
|
|
270
|
+
) lpol ON true
|
|
271
|
+
LEFT JOIN public.credentials c ON c.id = v_credential
|
|
272
|
+
LEFT JOIN public.vault_members m ON m.principal = c.principal
|
|
273
|
+
-- An archived key is gone (`secret.delete`): no object.
|
|
274
|
+
LEFT JOIN public.secrets s ON s.id = v_secret AND s.archived_at IS NULL
|
|
275
|
+
-- The key's own version, its own environment, and that environment's parent when it is a personal config.
|
|
276
|
+
LEFT JOIN public.secret_versions sv ON sv.id = s.current_version_id
|
|
277
|
+
LEFT JOIN public.environments e2 ON e2.id = s.environment_id
|
|
278
|
+
LEFT JOIN public.environments e4 ON e4.id = e2.parent_id AND e2.owner IS NOT NULL
|
|
279
|
+
LEFT JOIN public.log_bulk b ON b.principal = c.principal AND b.secret = v_secret
|
|
280
|
+
WHERE NOT dry
|
|
281
|
+
),
|
|
282
|
+
v AS MATERIALIZED (
|
|
283
|
+
SELECT i.*, cap.refresh, cap.period,
|
|
284
|
+
CASE WHEN i.s_project IS NOT NULL AND i.s_version IS NULL THEN 'whole'
|
|
285
|
+
-- What no key decides comes first (T158; Probing.tla, AdmissionBeforeKey; review d3 of #416): the
|
|
286
|
+
-- floor, the deadline, the clock, the cap's config; then what the names decide, which the vault
|
|
287
|
+
-- answers on the caller's standing first.
|
|
288
|
+
WHEN i.at_floor IS NULL OR p_at < i.at_floor OR (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN 'late'
|
|
289
|
+
WHEN p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN 'ahead'
|
|
290
|
+
WHEN i.cap_count IS NULL OR i.cap_window IS NULL THEN 'unconfigured'
|
|
291
|
+
-- What the names name, read by them: no credential of that token, no live place of those names.
|
|
292
|
+
WHEN v_named IS NOT NULL THEN v_named
|
|
293
|
+
-- The ids the vault passed are not the ones the names name now (D153): the names' rows are read,
|
|
294
|
+
-- and nothing is written or locked.
|
|
295
|
+
WHEN NOT v_right THEN 'moved'
|
|
296
|
+
WHEN p_token_hash IS NOT NULL AND (coalesce(i.e1_archived, false) OR coalesce(i.e2_archived, false) OR coalesce(i.e3_archived, false)
|
|
297
|
+
OR coalesce(i.e1_project_archived, false) OR coalesce(i.e2_project_archived, false) OR coalesce(i.e3_project_archived, false)) THEN 'archived'
|
|
298
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND NOT coalesce(i.bulk_last > p_at - i.cap_window - cap.refresh, false) THEN 'lock'
|
|
299
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT moving AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND i.bulk_last < cap.period THEN 'share' END AS state,
|
|
300
|
+
greatest(lc.number, lm.number, lr.number, i.lpol_number, ls.number, l2.number, l4.number) AS inputs,
|
|
301
|
+
lc.seal AS c_last, lm.seal AS m_last, (lm.payload::jsonb ->> 'old')::bigint AS m_old, lr.seal AS r_last, ls.seal AS s_last, NULL::bytea AS x_last,
|
|
302
|
+
NULL::bytea AS e1_last, l2.seal AS e2_last, NULL::bytea AS e3_last, l4.seal AS e4_last,
|
|
303
|
+
coalesce(i.bulk_last > p_at - i.cap_window - cap.refresh, false) AS is_in,
|
|
304
|
+
-- The other keys in the set, up to the cap: read only under the principal's lock, for a key not in it.
|
|
305
|
+
CASE WHEN locked AND i.p IS NOT NULL AND NOT coalesce(i.bulk_last > p_at - i.cap_window - cap.refresh, false) THEN (
|
|
306
|
+
SELECT count(*)::integer FROM (
|
|
307
|
+
SELECT 1 FROM public.log_bulk o
|
|
308
|
+
WHERE o.principal = i.p AND o.secret <> v_secret AND o.last > p_at - i.cap_window - cap.refresh - public.deadline_max() - 60000
|
|
309
|
+
LIMIT i.cap_count
|
|
310
|
+
) z
|
|
311
|
+
) ELSE 0 END AS n_others,
|
|
312
|
+
-- A repeat rides its cover only while every part of this holds: never on a key whose bulk row is missing (CoverFresh).
|
|
313
|
+
coalesce(p_cover IS NOT NULL AND i.bulk_last >= cap.period AND p_cover > coalesce(greatest(lc.number, lm.number, lr.number, i.lpol_number, ls.number, l2.number, l4.number), 0)
|
|
314
|
+
AND EXISTS (SELECT 1 FROM public.log_uses u WHERE u.at = p_cover_at AND u.request_id = p_cover_request AND u.number = p_cover
|
|
315
|
+
AND u.credential = p_credential AND u.secret = p_secret AND u.purpose = p_purpose AND u.place IS NOT DISTINCT FROM p_place), false) AS is_covered
|
|
316
|
+
FROM i
|
|
317
|
+
CROSS JOIN LATERAL (SELECT greatest(1, least(60000, i.cap_window / 10)) AS refresh) r0
|
|
318
|
+
CROSS JOIN LATERAL (SELECT r0.refresh, p_at - ((p_at % r0.refresh) + r0.refresh) % r0.refresh AS period) cap
|
|
319
|
+
-- Each input's newest change: one index-only probe each (log_changes_object_idx holds the seal), the principal's
|
|
320
|
+
-- with its payload, which names the 0.14 entry it accounts for.
|
|
321
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_credential ORDER BY x.number DESC LIMIT 1) lc ON true
|
|
322
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal, x.payload FROM public.log_changes x WHERE x.object = i.k_member ORDER BY x.number DESC LIMIT 1) lm ON i.k_member IS NOT NULL
|
|
323
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = public.log_object('roles', 'instance') ORDER BY x.number DESC LIMIT 1) lr ON true
|
|
324
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_secret ORDER BY x.number DESC LIMIT 1) ls ON true
|
|
325
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_e2 ORDER BY x.number DESC LIMIT 1) l2 ON i.k_e2 IS NOT NULL
|
|
326
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_e4 ORDER BY x.number DESC LIMIT 1) l4 ON i.k_e4 IS NOT NULL
|
|
327
|
+
),
|
|
328
|
+
w AS (
|
|
329
|
+
INSERT INTO public.log_uses (number, at, request_id, credential, secret, purpose, mac, place)
|
|
330
|
+
SELECT nextval('public.log_number'), p_at, p_request, p_credential, p_secret, p_purpose, p_mac, p_place
|
|
331
|
+
FROM v WHERE v.state IS NULL AND NOT v.is_covered AND NOT (p_token_hash IS NOT NULL AND (v.s_project IS NULL OR coalesce(v.v_plain, false)))
|
|
332
|
+
RETURNING log_uses.number
|
|
333
|
+
)
|
|
334
|
+
SELECT v.state, (SELECT w.number FROM w), CASE WHEN v.state IS NULL AND v.is_covered THEN p_cover END, v.newest, v.newest_mac, v.inputs, true,
|
|
335
|
+
v.p, v.c_kind, v.c_scope, v.c_revoked, v.c_expires, v.c_seal, v.c_last,
|
|
336
|
+
v.m_status, v.m_owner, v.m_role, v.m_scope, v.m_seal, v.m_last, v.m_old,
|
|
337
|
+
(SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.subject_principal = v.p AND a.decision = 'allow'
|
|
338
|
+
AND a.action IN ('member.add', 'member.restore', 'member.owner', 'member.role', 'member.remove', 'access.grant', 'access.revoke') ORDER BY a.seq DESC LIMIT 1),
|
|
339
|
+
gr.g_project, gr.g_environment, gr.g_slug, gr.g_role, gr.g_expires,
|
|
340
|
+
v.r_value, v.r_seal, v.r_last,
|
|
341
|
+
v.p_value, v.p_seal, v.p_last, v.cap_count, v.cap_window,
|
|
342
|
+
v.s_project, v.s_environment, v.s_version, v.s_source, v.s_seal, v.s_last,
|
|
343
|
+
v.x_project, v.x_environment, v.x_version, v.x_source, v.x_seal, v.x_last,
|
|
344
|
+
v.envelope_version, v.ciphertext, v.iv, v.auth_tag, v.wrapped_dek, v.kek_provider, v.kek_id, v.kek_version, v.sensitive, v.v_plain,
|
|
345
|
+
v.e1_id, v.e1_project, v.e1_slug, v.e1_owner, v.e1_parent, v.e1_protected, v.e1_archived, v.e1_seal, v.e1_last,
|
|
346
|
+
v.e2_id, v.e2_project, v.e2_slug, v.e2_owner, v.e2_parent, v.e2_protected, v.e2_archived, v.e2_seal, v.e2_last,
|
|
347
|
+
v.e3_id, v.e3_project, v.e3_slug, v.e3_owner, v.e3_parent, v.e3_protected, v.e3_archived, v.e3_seal, v.e3_last,
|
|
348
|
+
v.e4_id, v.e4_project, v.e4_slug, v.e4_owner, v.e4_parent, v.e4_protected, v.e4_archived, v.e4_seal, v.e4_last,
|
|
349
|
+
v.is_in, v.n_others, v.v_plaintext, v.e1_project_archived, v.e2_project_archived, v.e3_project_archived, v.e4_project_archived, v.s_reference, v.x_reference, v.c_generation, v.m_generation, v.c_token_hash,
|
|
350
|
+
CASE WHEN p_token_hash IS NOT NULL THEN json_build_object(
|
|
351
|
+
'now', CURRENT_TIMESTAMP, 'credential', v.c_row, 'identity', (SELECT to_json(x) FROM public.identities x WHERE x.id = (v.c_row ->> 'identity_id')::uuid),
|
|
352
|
+
'policy', (SELECT a.seq::text 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),
|
|
353
|
+
'project', (SELECT to_json(q) FROM public.projects q WHERE q.id = coalesce(v.e1_project, v.e2_project)),
|
|
354
|
+
'environment', (SELECT to_json(e) FROM public.environments e WHERE e.id = coalesce(v.e1_id, v.e2_id)), 'parent', NULL,
|
|
355
|
+
'secrets', CASE WHEN v.s_row IS NULL THEN '[]'::json ELSE json_build_array(json_build_object('secret', v.s_row, 'version', v.sv_row)) END) END,
|
|
356
|
+
NULL::bytea
|
|
357
|
+
INTO r
|
|
358
|
+
FROM v
|
|
359
|
+
-- The principal's grants as five lists, one entry per grant, lapsed ones included; an environment's grant with its project, which its row finds through `environments`.
|
|
360
|
+
LEFT JOIN LATERAL (
|
|
361
|
+
SELECT array_agg(coalesce(g.project_id, ge.project_id)) AS g_project, array_agg(g.environment_id) AS g_environment,
|
|
362
|
+
array_agg(g.environment_slug) AS g_slug, array_agg(g.role) AS g_role, array_agg(g.expires_at) AS g_expires
|
|
363
|
+
FROM public.vault_grants g LEFT JOIN public.environments ge ON ge.id = g.environment_id
|
|
364
|
+
WHERE g.principal = v.p
|
|
365
|
+
) gr ON v.m_status IS NOT NULL;
|
|
366
|
+
ELSIF probe THEN
|
|
367
|
+
WITH i AS MATERIALIZED (
|
|
368
|
+
SELECT n.number AS newest, n.mac AS newest_mac, coalesce(n.number = p_known AND n.mac = p_known_mac, false) AS cached,
|
|
369
|
+
f.at_floor, (lpol.payload::jsonb #>> '{value,bulk,count}')::integer AS cap_count, (lpol.payload::jsonb #>> '{value,bulk,windowMs}')::bigint AS cap_window,
|
|
370
|
+
lpol.number AS lpol_number, lpol.seal AS p_last,
|
|
371
|
+
c.principal AS p, c.kind AS c_kind, c.scope AS c_scope, c.revoked_at IS NOT NULL AS c_revoked,
|
|
372
|
+
floor(extract(epoch FROM least(c.expires_at, c.not_after)) * 1000)::bigint AS c_expires, c.seal AS c_seal,
|
|
373
|
+
m.status AS m_status, m.owner AS m_owner, m.role AS m_role, m.scope AS m_scope, m.seal AS m_seal, c.generation AS c_generation, m.generation AS m_generation,
|
|
374
|
+
-- What the request named, for the names it checks, and the rows the app checks, when it names them (0044).
|
|
375
|
+
c.token_hash AS c_token_hash,
|
|
376
|
+
CASE WHEN p_token_hash IS NOT NULL THEN to_json(c) END AS c_row, CASE WHEN p_token_hash IS NOT NULL THEN to_json(s) END AS s_row,
|
|
377
|
+
CASE WHEN p_token_hash IS NOT NULL AND s.current_version_id IS NOT NULL THEN to_json(sv) END AS sv_row,
|
|
378
|
+
cfg.r_value, cfg.r_seal, cfg.p_value, cfg.p_seal,
|
|
379
|
+
s.project_id AS s_project, s.environment_id AS s_environment, s.current_version_id AS s_version, ref.source_secret_id AS s_source, ref.reference_id AS s_reference, s.seal AS s_seal,
|
|
380
|
+
src.project_id AS x_project, src.environment_id AS x_environment, src.current_version_id AS x_version, xref.source_secret_id AS x_source, xref.reference_id AS x_reference, src.seal AS x_seal,
|
|
381
|
+
sv.envelope_version, sv.ciphertext, sv.iv, sv.auth_tag, sv.wrapped_dek, sv.kek_provider, sv.kek_id, sv.kek_version, sv.sensitive, sv.plaintext IS NOT NULL AS v_plain, sv.plaintext AS v_plaintext,
|
|
382
|
+
ids.e1 AS e1_id, e1.project_id AS e1_project, e1.slug AS e1_slug, e1.owner AS e1_owner, e1.parent_id AS e1_parent, e1.protected AS e1_protected, e1.archived_at IS NOT NULL AS e1_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e1.project_id) AS e1_project_archived, e1.seal AS e1_seal,
|
|
383
|
+
s.environment_id AS e2_id, e2.project_id AS e2_project, e2.slug AS e2_slug, e2.owner AS e2_owner, e2.parent_id AS e2_parent, e2.protected AS e2_protected, e2.archived_at IS NOT NULL AS e2_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e2.project_id) AS e2_project_archived, e2.seal AS e2_seal,
|
|
384
|
+
ids.e3 AS e3_id, e3.project_id AS e3_project, e3.slug AS e3_slug, e3.owner AS e3_owner, e3.parent_id AS e3_parent, e3.protected AS e3_protected, e3.archived_at IS NOT NULL AS e3_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e3.project_id) AS e3_project_archived, e3.seal AS e3_seal,
|
|
385
|
+
CASE WHEN ve.owner IS NOT NULL THEN ve.parent END AS e4_id, e4.project_id AS e4_project, e4.slug AS e4_slug, e4.owner AS e4_owner, e4.parent_id AS e4_parent, e4.protected AS e4_protected, e4.archived_at IS NOT NULL AS e4_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e4.project_id) AS e4_project_archived, e4.seal AS e4_seal,
|
|
386
|
+
b.last AS bulk_last,
|
|
387
|
+
-- Each input's object key, computed once: a key in a probe's own condition is computed again for every row it reads (0022, decision 3).
|
|
388
|
+
public.log_object('credential', v_credential::text) AS k_credential,
|
|
389
|
+
public.log_object('principal', c.principal) AS k_member,
|
|
390
|
+
public.log_object('secret', v_secret::text) AS k_secret,
|
|
391
|
+
public.log_object('secret', ref.source_secret_id::text) AS k_source,
|
|
392
|
+
public.log_object('environment', ids.e1::text) AS k_e1,
|
|
393
|
+
public.log_object('environment', s.environment_id::text) AS k_e2,
|
|
394
|
+
public.log_object('environment', ids.e3::text) AS k_e3,
|
|
395
|
+
public.log_object('environment', (CASE WHEN ve.owner IS NOT NULL THEN ve.parent END)::text) AS k_e4
|
|
396
|
+
FROM (SELECT 1) one
|
|
397
|
+
LEFT JOIN LATERAL (SELECT x.number, x.mac FROM public.log_changes x ORDER BY x.number DESC LIMIT 1) n ON true
|
|
398
|
+
LEFT JOIN public.log_fence f ON f.only_row
|
|
399
|
+
-- The roles and the policy, both rows in one read of their one-page table.
|
|
400
|
+
LEFT JOIN LATERAL (
|
|
401
|
+
SELECT max(g.value) FILTER (WHERE g.name = 'roles') AS r_value, (array_agg(g.seal) FILTER (WHERE g.name = 'roles'))[1] AS r_seal,
|
|
402
|
+
max(g.value) FILTER (WHERE g.name = 'policy') AS p_value, (array_agg(g.seal) FILTER (WHERE g.name = 'policy'))[1] AS p_seal
|
|
403
|
+
FROM public.log_config g
|
|
404
|
+
) cfg ON true
|
|
405
|
+
-- The cap as the policy's newest change says it (CapSealed): every policy change logs the whole policy as its `value`.
|
|
406
|
+
LEFT JOIN LATERAL (
|
|
407
|
+
SELECT x.number, x.seal, x.payload FROM public.log_changes x WHERE x.object = public.log_object('policy', 'instance') ORDER BY x.number DESC LIMIT 1
|
|
408
|
+
) lpol ON true
|
|
409
|
+
LEFT JOIN public.credentials c ON c.id = v_credential
|
|
410
|
+
LEFT JOIN public.vault_members m ON m.principal = c.principal
|
|
411
|
+
-- An archived key is gone (`secret.delete`): no object.
|
|
412
|
+
LEFT JOIN public.secrets s ON s.id = v_secret AND s.archived_at IS NULL
|
|
413
|
+
-- A key with no version of its own is a reference while its newest reference row names a source and has not ended (`ended_at`): an ended one's holder reads as no value, no source (`ended`).
|
|
414
|
+
LEFT JOIN LATERAL (
|
|
415
|
+
SELECT CASE WHEN y.ended_at IS NULL THEN y.source_secret_id END AS source_secret_id, CASE WHEN y.ended_at IS NULL THEN y.id END AS reference_id FROM public.secret_references y WHERE y.secret_id = s.id ORDER BY y.created_seq DESC LIMIT 1
|
|
416
|
+
) ref ON s.current_version_id IS NULL
|
|
417
|
+
LEFT JOIN public.secrets src ON src.id = ref.source_secret_id AND src.archived_at IS NULL
|
|
418
|
+
LEFT JOIN LATERAL (
|
|
419
|
+
SELECT CASE WHEN y.ended_at IS NULL THEN y.source_secret_id END AS source_secret_id, CASE WHEN y.ended_at IS NULL THEN y.id END AS reference_id FROM public.secret_references y WHERE y.secret_id = src.id ORDER BY y.created_seq DESC LIMIT 1
|
|
420
|
+
) xref ON src.current_version_id IS NULL
|
|
421
|
+
-- The value's version: the key's own, or its source's.
|
|
422
|
+
LEFT JOIN public.secret_versions sv ON sv.id = CASE WHEN ref.source_secret_id IS NULL THEN s.current_version_id ELSE src.current_version_id END
|
|
423
|
+
-- The environments `useEnvironments` names, each by the id the rows name, whether or not its row is there: e1 the
|
|
424
|
+
-- place read through, e2 the key's own, e3 its value's (a reference's source's) where that is another, and e4
|
|
425
|
+
-- the parent of its value's when that is a personal config.
|
|
426
|
+
LEFT JOIN LATERAL (
|
|
427
|
+
SELECT CASE WHEN v_place IS DISTINCT FROM s.environment_id THEN v_place END AS e1,
|
|
428
|
+
CASE WHEN src.environment_id IS DISTINCT FROM s.environment_id THEN src.environment_id END AS e3
|
|
429
|
+
) ids ON true
|
|
430
|
+
LEFT JOIN public.environments e1 ON e1.id = ids.e1
|
|
431
|
+
LEFT JOIN public.environments e2 ON e2.id = s.environment_id
|
|
432
|
+
LEFT JOIN public.environments e3 ON e3.id = ids.e3
|
|
433
|
+
-- The value's environment's row: the key's own, unless it is a reference, whose source is elsewhere or gone.
|
|
434
|
+
LEFT JOIN LATERAL (
|
|
435
|
+
SELECT CASE WHEN ref.source_secret_id IS NULL OR (src.id IS NOT NULL AND ids.e3 IS NULL) THEN e2.owner ELSE e3.owner END AS owner,
|
|
436
|
+
CASE WHEN ref.source_secret_id IS NULL OR (src.id IS NOT NULL AND ids.e3 IS NULL) THEN e2.parent_id ELSE e3.parent_id END AS parent
|
|
437
|
+
) ve ON true
|
|
438
|
+
LEFT JOIN public.environments e4 ON e4.id = ve.parent AND ve.owner IS NOT NULL
|
|
439
|
+
LEFT JOIN public.log_bulk b ON b.principal = c.principal AND b.secret = v_secret
|
|
440
|
+
WHERE NOT dry
|
|
441
|
+
),
|
|
442
|
+
v AS MATERIALIZED (
|
|
443
|
+
SELECT i.*, cap.refresh, cap.period,
|
|
444
|
+
-- What no key decides comes first (T158; Probing.tla, AdmissionBeforeKey; review d3 of #416): the
|
|
445
|
+
-- floor, the deadline, the clock, the cap's config; then what the names decide, which the vault
|
|
446
|
+
-- answers on the caller's standing first.
|
|
447
|
+
CASE WHEN i.at_floor IS NULL OR p_at < i.at_floor OR (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN 'late'
|
|
448
|
+
WHEN p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN 'ahead'
|
|
449
|
+
WHEN i.cap_count IS NULL OR i.cap_window IS NULL THEN 'unconfigured'
|
|
450
|
+
-- What the names name, read by them: no credential of that token, no live place of those names.
|
|
451
|
+
WHEN v_named IS NOT NULL THEN v_named
|
|
452
|
+
-- The ids the vault passed are not the ones the names name now (D153): the names' rows are read,
|
|
453
|
+
-- and nothing is written or locked.
|
|
454
|
+
WHEN NOT v_right THEN 'moved'
|
|
455
|
+
WHEN p_token_hash IS NOT NULL AND (coalesce(i.e1_archived, false) OR coalesce(i.e2_archived, false) OR coalesce(i.e3_archived, false)
|
|
456
|
+
OR coalesce(i.e1_project_archived, false) OR coalesce(i.e2_project_archived, false) OR coalesce(i.e3_project_archived, false)) THEN 'archived'
|
|
457
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND NOT coalesce(i.bulk_last > p_at - i.cap_window - cap.refresh, false) THEN 'lock'
|
|
458
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT moving AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND i.bulk_last < cap.period THEN 'share' END AS state,
|
|
459
|
+
greatest(lc.number, lm.number, lr.number, i.lpol_number, ls.number, lx.number, l1.number, l2.number, l3.number, l4.number) AS inputs,
|
|
460
|
+
lc.seal AS c_last, lm.seal AS m_last, (lm.payload::jsonb ->> 'old')::bigint AS m_old, lr.seal AS r_last, ls.seal AS s_last, lx.seal AS x_last,
|
|
461
|
+
l1.seal AS e1_last, l2.seal AS e2_last, l3.seal AS e3_last, l4.seal AS e4_last,
|
|
462
|
+
coalesce(i.bulk_last > p_at - i.cap_window - cap.refresh, false) AS is_in,
|
|
463
|
+
-- The other keys in the set, up to the cap: read only under the principal's lock, for a key not in it.
|
|
464
|
+
CASE WHEN locked AND i.p IS NOT NULL AND NOT coalesce(i.bulk_last > p_at - i.cap_window - cap.refresh, false) THEN (
|
|
465
|
+
SELECT count(*)::integer FROM (
|
|
466
|
+
SELECT 1 FROM public.log_bulk o
|
|
467
|
+
WHERE o.principal = i.p AND o.secret <> v_secret AND o.last > p_at - i.cap_window - cap.refresh - public.deadline_max() - 60000
|
|
468
|
+
LIMIT i.cap_count
|
|
469
|
+
) z
|
|
470
|
+
) ELSE 0 END AS n_others,
|
|
471
|
+
-- A repeat rides its cover only while every part of this holds: never on a key whose bulk row is missing (CoverFresh).
|
|
472
|
+
coalesce(p_cover IS NOT NULL AND i.bulk_last >= cap.period AND p_cover > coalesce(greatest(lc.number, lm.number, lr.number, i.lpol_number, ls.number, lx.number, l1.number, l2.number, l3.number, l4.number), 0)
|
|
473
|
+
AND EXISTS (SELECT 1 FROM public.log_uses u WHERE u.at = p_cover_at AND u.request_id = p_cover_request AND u.number = p_cover
|
|
474
|
+
AND u.credential = p_credential AND u.secret = p_secret AND u.purpose = p_purpose AND u.place IS NOT DISTINCT FROM p_place), false) AS is_covered
|
|
475
|
+
FROM i
|
|
476
|
+
CROSS JOIN LATERAL (SELECT greatest(1, least(60000, i.cap_window / 10)) AS refresh) r0
|
|
477
|
+
CROSS JOIN LATERAL (SELECT r0.refresh, p_at - ((p_at % r0.refresh) + r0.refresh) % r0.refresh AS period) cap
|
|
478
|
+
-- Each input's newest change: one index-only probe each (log_changes_object_idx holds the seal), the principal's
|
|
479
|
+
-- with its payload, which names the 0.14 entry it accounts for.
|
|
480
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_credential ORDER BY x.number DESC LIMIT 1) lc ON true
|
|
481
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal, x.payload FROM public.log_changes x WHERE x.object = i.k_member ORDER BY x.number DESC LIMIT 1) lm ON i.k_member IS NOT NULL
|
|
482
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = public.log_object('roles', 'instance') ORDER BY x.number DESC LIMIT 1) lr ON true
|
|
483
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_secret ORDER BY x.number DESC LIMIT 1) ls ON true
|
|
484
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_source ORDER BY x.number DESC LIMIT 1) lx ON i.k_source IS NOT NULL
|
|
485
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_e1 ORDER BY x.number DESC LIMIT 1) l1 ON i.k_e1 IS NOT NULL
|
|
486
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_e2 ORDER BY x.number DESC LIMIT 1) l2 ON i.k_e2 IS NOT NULL
|
|
487
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_e3 ORDER BY x.number DESC LIMIT 1) l3 ON i.k_e3 IS NOT NULL
|
|
488
|
+
LEFT JOIN LATERAL (SELECT x.number, x.seal FROM public.log_changes x WHERE x.object = i.k_e4 ORDER BY x.number DESC LIMIT 1) l4 ON i.k_e4 IS NOT NULL
|
|
489
|
+
),
|
|
490
|
+
w AS (
|
|
491
|
+
INSERT INTO public.log_uses (number, at, request_id, credential, secret, purpose, mac, place)
|
|
492
|
+
SELECT nextval('public.log_number'), p_at, p_request, p_credential, p_secret, p_purpose, p_mac, p_place
|
|
493
|
+
FROM v WHERE v.state IS NULL AND NOT v.is_covered AND NOT (p_token_hash IS NOT NULL AND (v.s_project IS NULL OR coalesce(v.v_plain, false)))
|
|
494
|
+
RETURNING log_uses.number
|
|
495
|
+
)
|
|
496
|
+
SELECT v.state, (SELECT w.number FROM w), CASE WHEN v.state IS NULL AND v.is_covered THEN p_cover END, v.newest, v.newest_mac, v.inputs, true,
|
|
497
|
+
v.p, v.c_kind, v.c_scope, v.c_revoked, v.c_expires, v.c_seal, v.c_last,
|
|
498
|
+
v.m_status, v.m_owner, v.m_role, v.m_scope, v.m_seal, v.m_last, v.m_old,
|
|
499
|
+
(SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.subject_principal = v.p AND a.decision = 'allow'
|
|
500
|
+
AND a.action IN ('member.add', 'member.restore', 'member.owner', 'member.role', 'member.remove', 'access.grant', 'access.revoke') ORDER BY a.seq DESC LIMIT 1),
|
|
501
|
+
gr.g_project, gr.g_environment, gr.g_slug, gr.g_role, gr.g_expires,
|
|
502
|
+
v.r_value, v.r_seal, v.r_last,
|
|
503
|
+
v.p_value, v.p_seal, v.p_last, v.cap_count, v.cap_window,
|
|
504
|
+
v.s_project, v.s_environment, v.s_version, v.s_source, v.s_seal, v.s_last,
|
|
505
|
+
v.x_project, v.x_environment, v.x_version, v.x_source, v.x_seal, v.x_last,
|
|
506
|
+
v.envelope_version, v.ciphertext, v.iv, v.auth_tag, v.wrapped_dek, v.kek_provider, v.kek_id, v.kek_version, v.sensitive, v.v_plain,
|
|
507
|
+
v.e1_id, v.e1_project, v.e1_slug, v.e1_owner, v.e1_parent, v.e1_protected, v.e1_archived, v.e1_seal, v.e1_last,
|
|
508
|
+
v.e2_id, v.e2_project, v.e2_slug, v.e2_owner, v.e2_parent, v.e2_protected, v.e2_archived, v.e2_seal, v.e2_last,
|
|
509
|
+
v.e3_id, v.e3_project, v.e3_slug, v.e3_owner, v.e3_parent, v.e3_protected, v.e3_archived, v.e3_seal, v.e3_last,
|
|
510
|
+
v.e4_id, v.e4_project, v.e4_slug, v.e4_owner, v.e4_parent, v.e4_protected, v.e4_archived, v.e4_seal, v.e4_last,
|
|
511
|
+
v.is_in, v.n_others, v.v_plaintext, v.e1_project_archived, v.e2_project_archived, v.e3_project_archived, v.e4_project_archived, v.s_reference, v.x_reference, v.c_generation, v.m_generation, v.c_token_hash,
|
|
512
|
+
CASE WHEN p_token_hash IS NOT NULL THEN json_build_object(
|
|
513
|
+
'now', CURRENT_TIMESTAMP, 'credential', v.c_row, 'identity', (SELECT to_json(x) FROM public.identities x WHERE x.id = (v.c_row ->> 'identity_id')::uuid),
|
|
514
|
+
'policy', (SELECT a.seq::text 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),
|
|
515
|
+
'project', (SELECT to_json(q) FROM public.projects q WHERE q.id = coalesce(v.e1_project, v.e2_project)),
|
|
516
|
+
'environment', (SELECT to_json(e) FROM public.environments e WHERE e.id = coalesce(v.e1_id, v.e2_id)), 'parent', NULL,
|
|
517
|
+
'secrets', CASE WHEN v.s_row IS NULL THEN '[]'::json ELSE json_build_array(json_build_object('secret', v.s_row, 'version', v.sv_row)) END) END,
|
|
518
|
+
NULL::bytea
|
|
519
|
+
INTO r
|
|
520
|
+
FROM v
|
|
521
|
+
-- The principal's grants as five lists, one entry per grant, lapsed ones included; an environment's grant with its project, which its row finds through `environments`.
|
|
522
|
+
LEFT JOIN LATERAL (
|
|
523
|
+
SELECT array_agg(coalesce(g.project_id, ge.project_id)) AS g_project, array_agg(g.environment_id) AS g_environment,
|
|
524
|
+
array_agg(g.environment_slug) AS g_slug, array_agg(g.role) AS g_role, array_agg(g.expires_at) AS g_expires
|
|
525
|
+
FROM public.vault_grants g LEFT JOIN public.environments ge ON ge.id = g.environment_id
|
|
526
|
+
WHERE g.principal = v.p
|
|
527
|
+
) gr ON v.m_status IS NOT NULL;
|
|
528
|
+
ELSIF NOT whole THEN
|
|
529
|
+
-- The isolate's seals, and its cap, read at the change it names: no probe, only whether a change came since
|
|
530
|
+
-- (then nothing is written: 'reprobe').
|
|
531
|
+
WITH i AS MATERIALIZED (
|
|
532
|
+
SELECT n.number AS newest, n.mac AS newest_mac, coalesce(n.number = p_known AND n.mac = p_known_mac, false) AS cached,
|
|
533
|
+
f.at_floor,
|
|
534
|
+
c.principal AS p, c.kind AS c_kind, c.scope AS c_scope, c.revoked_at IS NOT NULL AS c_revoked,
|
|
535
|
+
floor(extract(epoch FROM least(c.expires_at, c.not_after)) * 1000)::bigint AS c_expires, c.seal AS c_seal,
|
|
536
|
+
m.status AS m_status, m.owner AS m_owner, m.role AS m_role, m.scope AS m_scope, m.seal AS m_seal, c.generation AS c_generation, m.generation AS m_generation,
|
|
537
|
+
-- What the request named, for the names it checks, and the rows the app checks, when it names them (0044).
|
|
538
|
+
c.token_hash AS c_token_hash,
|
|
539
|
+
CASE WHEN p_token_hash IS NOT NULL THEN to_json(c) END AS c_row, CASE WHEN p_token_hash IS NOT NULL THEN to_json(s) END AS s_row,
|
|
540
|
+
CASE WHEN p_token_hash IS NOT NULL AND s.current_version_id IS NOT NULL THEN to_json(sv) END AS sv_row,
|
|
541
|
+
cfg.r_value, cfg.r_seal, cfg.p_value, cfg.p_seal,
|
|
542
|
+
s.project_id AS s_project, s.environment_id AS s_environment, s.current_version_id AS s_version, NULL::uuid AS s_source, NULL::uuid AS s_reference, s.seal AS s_seal,
|
|
543
|
+
NULL::uuid AS x_project, NULL::uuid AS x_environment, NULL::uuid AS x_version, NULL::uuid AS x_source, NULL::uuid AS x_reference, NULL::bytea AS x_seal,
|
|
544
|
+
sv.envelope_version, sv.ciphertext, sv.iv, sv.auth_tag, sv.wrapped_dek, sv.kek_provider, sv.kek_id, sv.kek_version, sv.sensitive, sv.plaintext IS NOT NULL AS v_plain, sv.plaintext AS v_plaintext,
|
|
545
|
+
NULL::uuid AS e1_id, NULL::uuid AS e1_project, NULL::text AS e1_slug, NULL::text AS e1_owner, NULL::uuid AS e1_parent, NULL::boolean AS e1_protected, NULL::boolean AS e1_archived, NULL::boolean AS e1_project_archived, NULL::bytea AS e1_seal,
|
|
546
|
+
s.environment_id AS e2_id, e2.project_id AS e2_project, e2.slug AS e2_slug, e2.owner AS e2_owner, e2.parent_id AS e2_parent, e2.protected AS e2_protected, e2.archived_at IS NOT NULL AS e2_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e2.project_id) AS e2_project_archived, e2.seal AS e2_seal,
|
|
547
|
+
NULL::uuid AS e3_id, NULL::uuid AS e3_project, NULL::text AS e3_slug, NULL::text AS e3_owner, NULL::uuid AS e3_parent, NULL::boolean AS e3_protected, NULL::boolean AS e3_archived, NULL::boolean AS e3_project_archived, NULL::bytea AS e3_seal,
|
|
548
|
+
CASE WHEN e2.owner IS NOT NULL THEN e2.parent_id END AS e4_id, e4.project_id AS e4_project, e4.slug AS e4_slug, e4.owner AS e4_owner, e4.parent_id AS e4_parent, e4.protected AS e4_protected, e4.archived_at IS NOT NULL AS e4_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e4.project_id) AS e4_project_archived, e4.seal AS e4_seal,
|
|
549
|
+
b.last AS bulk_last
|
|
550
|
+
FROM (SELECT 1) one
|
|
551
|
+
LEFT JOIN LATERAL (SELECT x.number, x.mac FROM public.log_changes x ORDER BY x.number DESC LIMIT 1) n ON true
|
|
552
|
+
LEFT JOIN public.log_fence f ON f.only_row
|
|
553
|
+
-- The roles and the policy, both rows in one read of their one-page table.
|
|
554
|
+
LEFT JOIN LATERAL (
|
|
555
|
+
SELECT max(g.value) FILTER (WHERE g.name = 'roles') AS r_value, (array_agg(g.seal) FILTER (WHERE g.name = 'roles'))[1] AS r_seal,
|
|
556
|
+
max(g.value) FILTER (WHERE g.name = 'policy') AS p_value, (array_agg(g.seal) FILTER (WHERE g.name = 'policy'))[1] AS p_seal
|
|
557
|
+
FROM public.log_config g
|
|
558
|
+
) cfg ON true
|
|
559
|
+
LEFT JOIN public.credentials c ON c.id = v_credential
|
|
560
|
+
LEFT JOIN public.vault_members m ON m.principal = c.principal
|
|
561
|
+
-- An archived key is gone (`secret.delete`): no object.
|
|
562
|
+
LEFT JOIN public.secrets s ON s.id = v_secret AND s.archived_at IS NULL
|
|
563
|
+
-- The key's own version, its own environment, and that environment's parent when it is a personal config.
|
|
564
|
+
LEFT JOIN public.secret_versions sv ON sv.id = s.current_version_id
|
|
565
|
+
LEFT JOIN public.environments e2 ON e2.id = s.environment_id
|
|
566
|
+
LEFT JOIN public.environments e4 ON e4.id = e2.parent_id AND e2.owner IS NOT NULL
|
|
567
|
+
LEFT JOIN public.log_bulk b ON b.principal = c.principal AND b.secret = v_secret
|
|
568
|
+
WHERE NOT dry
|
|
569
|
+
),
|
|
570
|
+
v AS MATERIALIZED (
|
|
571
|
+
SELECT i.*, c_refresh AS refresh, c_period AS period,
|
|
572
|
+
CASE WHEN NOT i.cached THEN 'reprobe'
|
|
573
|
+
WHEN i.s_project IS NOT NULL AND i.s_version IS NULL THEN 'whole'
|
|
574
|
+
-- What no key decides comes first (T158; Probing.tla, AdmissionBeforeKey; review d3 of #416): the
|
|
575
|
+
-- floor, the deadline, the clock, the cap's config; then what the names decide, which the vault
|
|
576
|
+
-- answers on the caller's standing first.
|
|
577
|
+
WHEN i.at_floor IS NULL OR p_at < i.at_floor OR (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN 'late'
|
|
578
|
+
WHEN p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN 'ahead'
|
|
579
|
+
-- What the names name, read by them: no credential of that token, no live place of those names.
|
|
580
|
+
WHEN v_named IS NOT NULL THEN v_named
|
|
581
|
+
-- The ids the vault passed are not the ones the names name now (D153): the names' rows are read,
|
|
582
|
+
-- and nothing is written or locked.
|
|
583
|
+
WHEN NOT v_right THEN 'moved'
|
|
584
|
+
WHEN p_token_hash IS NOT NULL AND (coalesce(i.e1_archived, false) OR coalesce(i.e2_archived, false) OR coalesce(i.e3_archived, false)
|
|
585
|
+
OR coalesce(i.e1_project_archived, false) OR coalesce(i.e2_project_archived, false) OR coalesce(i.e3_project_archived, false)) THEN 'archived'
|
|
586
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND NOT coalesce(i.bulk_last > p_at - p_cap_window - c_refresh, false) THEN 'lock'
|
|
587
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT moving AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND i.bulk_last < c_period THEN 'share' END AS state,
|
|
588
|
+
coalesce(i.bulk_last > p_at - p_cap_window - c_refresh, false) AS is_in,
|
|
589
|
+
-- The other keys in the set, up to the cap: read only under the principal's lock, for a key not in it.
|
|
590
|
+
CASE WHEN locked AND i.p IS NOT NULL AND NOT coalesce(i.bulk_last > p_at - p_cap_window - c_refresh, false) THEN (
|
|
591
|
+
SELECT count(*)::integer FROM (
|
|
592
|
+
SELECT 1 FROM public.log_bulk o
|
|
593
|
+
WHERE o.principal = i.p AND o.secret <> v_secret AND o.last > p_at - p_cap_window - c_refresh - public.deadline_max() - 60000
|
|
594
|
+
LIMIT p_cap_count
|
|
595
|
+
) z
|
|
596
|
+
) ELSE 0 END AS n_others,
|
|
597
|
+
-- A repeat rides its cover only while every part of this holds: never on a key whose bulk row is missing (CoverFresh).
|
|
598
|
+
coalesce(p_cover IS NOT NULL AND i.bulk_last >= c_period
|
|
599
|
+
AND EXISTS (SELECT 1 FROM public.log_uses u WHERE u.at = p_cover_at AND u.request_id = p_cover_request AND u.number = p_cover
|
|
600
|
+
AND u.credential = p_credential AND u.secret = p_secret AND u.purpose = p_purpose AND u.place IS NOT DISTINCT FROM p_place), false) AS is_covered
|
|
601
|
+
FROM i
|
|
602
|
+
),
|
|
603
|
+
w AS (
|
|
604
|
+
INSERT INTO public.log_uses (number, at, request_id, credential, secret, purpose, mac, place)
|
|
605
|
+
SELECT nextval('public.log_number'), p_at, p_request, p_credential, p_secret, p_purpose, p_mac, p_place
|
|
606
|
+
-- The isolate held its cover to the cached seals' numbers when it read them.
|
|
607
|
+
FROM v WHERE v.state IS NULL AND NOT v.is_covered AND NOT (p_token_hash IS NOT NULL AND (v.s_project IS NULL OR coalesce(v.v_plain, false)))
|
|
608
|
+
RETURNING log_uses.number
|
|
609
|
+
)
|
|
610
|
+
SELECT v.state, (SELECT w.number FROM w), CASE WHEN v.state IS NULL AND v.is_covered THEN p_cover END, v.newest, v.newest_mac, NULL::bigint, false,
|
|
611
|
+
v.p, v.c_kind, v.c_scope, v.c_revoked, v.c_expires, v.c_seal, NULL::bytea,
|
|
612
|
+
v.m_status, v.m_owner, v.m_role, v.m_scope, v.m_seal, NULL::bytea, NULL::bigint,
|
|
613
|
+
(SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.subject_principal = v.p AND a.decision = 'allow'
|
|
614
|
+
AND a.action IN ('member.add', 'member.restore', 'member.owner', 'member.role', 'member.remove', 'access.grant', 'access.revoke') ORDER BY a.seq DESC LIMIT 1),
|
|
615
|
+
gr.g_project, gr.g_environment, gr.g_slug, gr.g_role, gr.g_expires,
|
|
616
|
+
v.r_value, v.r_seal, NULL::bytea,
|
|
617
|
+
v.p_value, v.p_seal, NULL::bytea, p_cap_count, p_cap_window,
|
|
618
|
+
v.s_project, v.s_environment, v.s_version, v.s_source, v.s_seal, NULL::bytea,
|
|
619
|
+
v.x_project, v.x_environment, v.x_version, v.x_source, v.x_seal, NULL::bytea,
|
|
620
|
+
v.envelope_version, v.ciphertext, v.iv, v.auth_tag, v.wrapped_dek, v.kek_provider, v.kek_id, v.kek_version, v.sensitive, v.v_plain,
|
|
621
|
+
v.e1_id, v.e1_project, v.e1_slug, v.e1_owner, v.e1_parent, v.e1_protected, v.e1_archived, v.e1_seal, NULL::bytea,
|
|
622
|
+
v.e2_id, v.e2_project, v.e2_slug, v.e2_owner, v.e2_parent, v.e2_protected, v.e2_archived, v.e2_seal, NULL::bytea,
|
|
623
|
+
v.e3_id, v.e3_project, v.e3_slug, v.e3_owner, v.e3_parent, v.e3_protected, v.e3_archived, v.e3_seal, NULL::bytea,
|
|
624
|
+
v.e4_id, v.e4_project, v.e4_slug, v.e4_owner, v.e4_parent, v.e4_protected, v.e4_archived, v.e4_seal, NULL::bytea,
|
|
625
|
+
v.is_in, v.n_others, v.v_plaintext, v.e1_project_archived, v.e2_project_archived, v.e3_project_archived, v.e4_project_archived, v.s_reference, v.x_reference, v.c_generation, v.m_generation, v.c_token_hash,
|
|
626
|
+
CASE WHEN p_token_hash IS NOT NULL THEN json_build_object(
|
|
627
|
+
'now', CURRENT_TIMESTAMP, 'credential', v.c_row, 'identity', (SELECT to_json(x) FROM public.identities x WHERE x.id = (v.c_row ->> 'identity_id')::uuid),
|
|
628
|
+
'policy', (SELECT a.seq::text 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),
|
|
629
|
+
'project', (SELECT to_json(q) FROM public.projects q WHERE q.id = coalesce(v.e1_project, v.e2_project)),
|
|
630
|
+
'environment', (SELECT to_json(e) FROM public.environments e WHERE e.id = coalesce(v.e1_id, v.e2_id)), 'parent', NULL,
|
|
631
|
+
'secrets', CASE WHEN v.s_row IS NULL THEN '[]'::json ELSE json_build_array(json_build_object('secret', v.s_row, 'version', v.sv_row)) END) END,
|
|
632
|
+
NULL::bytea
|
|
633
|
+
INTO r
|
|
634
|
+
FROM v
|
|
635
|
+
-- The principal's grants as five lists, one entry per grant, lapsed ones included; an environment's grant with its project, which its row finds through `environments`.
|
|
636
|
+
LEFT JOIN LATERAL (
|
|
637
|
+
SELECT array_agg(coalesce(g.project_id, ge.project_id)) AS g_project, array_agg(g.environment_id) AS g_environment,
|
|
638
|
+
array_agg(g.environment_slug) AS g_slug, array_agg(g.role) AS g_role, array_agg(g.expires_at) AS g_expires
|
|
639
|
+
FROM public.vault_grants g LEFT JOIN public.environments ge ON ge.id = g.environment_id
|
|
640
|
+
WHERE g.principal = v.p
|
|
641
|
+
) gr ON v.m_status IS NOT NULL;
|
|
642
|
+
ELSE
|
|
643
|
+
WITH i AS MATERIALIZED (
|
|
644
|
+
SELECT n.number AS newest, n.mac AS newest_mac, coalesce(n.number = p_known AND n.mac = p_known_mac, false) AS cached,
|
|
645
|
+
f.at_floor,
|
|
646
|
+
c.principal AS p, c.kind AS c_kind, c.scope AS c_scope, c.revoked_at IS NOT NULL AS c_revoked,
|
|
647
|
+
floor(extract(epoch FROM least(c.expires_at, c.not_after)) * 1000)::bigint AS c_expires, c.seal AS c_seal,
|
|
648
|
+
m.status AS m_status, m.owner AS m_owner, m.role AS m_role, m.scope AS m_scope, m.seal AS m_seal, c.generation AS c_generation, m.generation AS m_generation,
|
|
649
|
+
-- What the request named, for the names it checks, and the rows the app checks, when it names them (0044).
|
|
650
|
+
c.token_hash AS c_token_hash,
|
|
651
|
+
CASE WHEN p_token_hash IS NOT NULL THEN to_json(c) END AS c_row, CASE WHEN p_token_hash IS NOT NULL THEN to_json(s) END AS s_row,
|
|
652
|
+
CASE WHEN p_token_hash IS NOT NULL AND s.current_version_id IS NOT NULL THEN to_json(sv) END AS sv_row,
|
|
653
|
+
cfg.r_value, cfg.r_seal, cfg.p_value, cfg.p_seal,
|
|
654
|
+
s.project_id AS s_project, s.environment_id AS s_environment, s.current_version_id AS s_version, ref.source_secret_id AS s_source, ref.reference_id AS s_reference, s.seal AS s_seal,
|
|
655
|
+
src.project_id AS x_project, src.environment_id AS x_environment, src.current_version_id AS x_version, xref.source_secret_id AS x_source, xref.reference_id AS x_reference, src.seal AS x_seal,
|
|
656
|
+
sv.envelope_version, sv.ciphertext, sv.iv, sv.auth_tag, sv.wrapped_dek, sv.kek_provider, sv.kek_id, sv.kek_version, sv.sensitive, sv.plaintext IS NOT NULL AS v_plain, sv.plaintext AS v_plaintext,
|
|
657
|
+
ids.e1 AS e1_id, e1.project_id AS e1_project, e1.slug AS e1_slug, e1.owner AS e1_owner, e1.parent_id AS e1_parent, e1.protected AS e1_protected, e1.archived_at IS NOT NULL AS e1_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e1.project_id) AS e1_project_archived, e1.seal AS e1_seal,
|
|
658
|
+
s.environment_id AS e2_id, e2.project_id AS e2_project, e2.slug AS e2_slug, e2.owner AS e2_owner, e2.parent_id AS e2_parent, e2.protected AS e2_protected, e2.archived_at IS NOT NULL AS e2_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e2.project_id) AS e2_project_archived, e2.seal AS e2_seal,
|
|
659
|
+
ids.e3 AS e3_id, e3.project_id AS e3_project, e3.slug AS e3_slug, e3.owner AS e3_owner, e3.parent_id AS e3_parent, e3.protected AS e3_protected, e3.archived_at IS NOT NULL AS e3_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e3.project_id) AS e3_project_archived, e3.seal AS e3_seal,
|
|
660
|
+
CASE WHEN ve.owner IS NOT NULL THEN ve.parent END AS e4_id, e4.project_id AS e4_project, e4.slug AS e4_slug, e4.owner AS e4_owner, e4.parent_id AS e4_parent, e4.protected AS e4_protected, e4.archived_at IS NOT NULL AS e4_archived, (SELECT q.archived_at IS NOT NULL FROM public.projects q WHERE q.id = e4.project_id) AS e4_project_archived, e4.seal AS e4_seal,
|
|
661
|
+
b.last AS bulk_last
|
|
662
|
+
FROM (SELECT 1) one
|
|
663
|
+
LEFT JOIN LATERAL (SELECT x.number, x.mac FROM public.log_changes x ORDER BY x.number DESC LIMIT 1) n ON true
|
|
664
|
+
LEFT JOIN public.log_fence f ON f.only_row
|
|
665
|
+
-- The roles and the policy, both rows in one read of their one-page table.
|
|
666
|
+
LEFT JOIN LATERAL (
|
|
667
|
+
SELECT max(g.value) FILTER (WHERE g.name = 'roles') AS r_value, (array_agg(g.seal) FILTER (WHERE g.name = 'roles'))[1] AS r_seal,
|
|
668
|
+
max(g.value) FILTER (WHERE g.name = 'policy') AS p_value, (array_agg(g.seal) FILTER (WHERE g.name = 'policy'))[1] AS p_seal
|
|
669
|
+
FROM public.log_config g
|
|
670
|
+
) cfg ON true
|
|
671
|
+
LEFT JOIN public.credentials c ON c.id = v_credential
|
|
672
|
+
LEFT JOIN public.vault_members m ON m.principal = c.principal
|
|
673
|
+
-- An archived key is gone (`secret.delete`): no object.
|
|
674
|
+
LEFT JOIN public.secrets s ON s.id = v_secret AND s.archived_at IS NULL
|
|
675
|
+
-- A key with no version of its own is a reference while its newest reference row names a source and has not ended (`ended_at`): an ended one's holder reads as no value, no source (`ended`).
|
|
676
|
+
LEFT JOIN LATERAL (
|
|
677
|
+
SELECT CASE WHEN y.ended_at IS NULL THEN y.source_secret_id END AS source_secret_id, CASE WHEN y.ended_at IS NULL THEN y.id END AS reference_id FROM public.secret_references y WHERE y.secret_id = s.id ORDER BY y.created_seq DESC LIMIT 1
|
|
678
|
+
) ref ON s.current_version_id IS NULL
|
|
679
|
+
LEFT JOIN public.secrets src ON src.id = ref.source_secret_id AND src.archived_at IS NULL
|
|
680
|
+
LEFT JOIN LATERAL (
|
|
681
|
+
SELECT CASE WHEN y.ended_at IS NULL THEN y.source_secret_id END AS source_secret_id, CASE WHEN y.ended_at IS NULL THEN y.id END AS reference_id FROM public.secret_references y WHERE y.secret_id = src.id ORDER BY y.created_seq DESC LIMIT 1
|
|
682
|
+
) xref ON src.current_version_id IS NULL
|
|
683
|
+
-- The value's version: the key's own, or its source's.
|
|
684
|
+
LEFT JOIN public.secret_versions sv ON sv.id = CASE WHEN ref.source_secret_id IS NULL THEN s.current_version_id ELSE src.current_version_id END
|
|
685
|
+
-- The environments `useEnvironments` names, each by the id the rows name, whether or not its row is there: e1 the
|
|
686
|
+
-- place read through, e2 the key's own, e3 its value's (a reference's source's) where that is another, and e4
|
|
687
|
+
-- the parent of its value's when that is a personal config.
|
|
688
|
+
LEFT JOIN LATERAL (
|
|
689
|
+
SELECT CASE WHEN v_place IS DISTINCT FROM s.environment_id THEN v_place END AS e1,
|
|
690
|
+
CASE WHEN src.environment_id IS DISTINCT FROM s.environment_id THEN src.environment_id END AS e3
|
|
691
|
+
) ids ON true
|
|
692
|
+
LEFT JOIN public.environments e1 ON e1.id = ids.e1
|
|
693
|
+
LEFT JOIN public.environments e2 ON e2.id = s.environment_id
|
|
694
|
+
LEFT JOIN public.environments e3 ON e3.id = ids.e3
|
|
695
|
+
-- The value's environment's row: the key's own, unless it is a reference, whose source is elsewhere or gone.
|
|
696
|
+
LEFT JOIN LATERAL (
|
|
697
|
+
SELECT CASE WHEN ref.source_secret_id IS NULL OR (src.id IS NOT NULL AND ids.e3 IS NULL) THEN e2.owner ELSE e3.owner END AS owner,
|
|
698
|
+
CASE WHEN ref.source_secret_id IS NULL OR (src.id IS NOT NULL AND ids.e3 IS NULL) THEN e2.parent_id ELSE e3.parent_id END AS parent
|
|
699
|
+
) ve ON true
|
|
700
|
+
LEFT JOIN public.environments e4 ON e4.id = ve.parent AND ve.owner IS NOT NULL
|
|
701
|
+
LEFT JOIN public.log_bulk b ON b.principal = c.principal AND b.secret = v_secret
|
|
702
|
+
WHERE NOT dry
|
|
703
|
+
),
|
|
704
|
+
v AS MATERIALIZED (
|
|
705
|
+
SELECT i.*, c_refresh AS refresh, c_period AS period,
|
|
706
|
+
CASE WHEN NOT i.cached THEN 'reprobe'
|
|
707
|
+
-- What no key decides comes first (T158; Probing.tla, AdmissionBeforeKey; review d3 of #416): the
|
|
708
|
+
-- floor, the deadline, the clock, the cap's config; then what the names decide, which the vault
|
|
709
|
+
-- answers on the caller's standing first.
|
|
710
|
+
WHEN i.at_floor IS NULL OR p_at < i.at_floor OR (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN 'late'
|
|
711
|
+
WHEN p_at > (extract(epoch FROM clock_timestamp()) * 1000)::bigint + 60000 THEN 'ahead'
|
|
712
|
+
-- What the names name, read by them: no credential of that token, no live place of those names.
|
|
713
|
+
WHEN v_named IS NOT NULL THEN v_named
|
|
714
|
+
-- The ids the vault passed are not the ones the names name now (D153): the names' rows are read,
|
|
715
|
+
-- and nothing is written or locked.
|
|
716
|
+
WHEN NOT v_right THEN 'moved'
|
|
717
|
+
WHEN p_token_hash IS NOT NULL AND (coalesce(i.e1_archived, false) OR coalesce(i.e2_archived, false) OR coalesce(i.e3_archived, false)
|
|
718
|
+
OR coalesce(i.e1_project_archived, false) OR coalesce(i.e2_project_archived, false) OR coalesce(i.e3_project_archived, false)) THEN 'archived'
|
|
719
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND NOT coalesce(i.bulk_last > p_at - p_cap_window - c_refresh, false) THEN 'lock'
|
|
720
|
+
WHEN i.p IS NOT NULL AND NOT locked AND NOT moving AND NOT (p_token_hash IS NOT NULL AND (i.s_project IS NULL OR coalesce(i.v_plain, false))) AND i.bulk_last < c_period THEN 'share' END AS state,
|
|
721
|
+
coalesce(i.bulk_last > p_at - p_cap_window - c_refresh, false) AS is_in,
|
|
722
|
+
-- The other keys in the set, up to the cap: read only under the principal's lock, for a key not in it.
|
|
723
|
+
CASE WHEN locked AND i.p IS NOT NULL AND NOT coalesce(i.bulk_last > p_at - p_cap_window - c_refresh, false) THEN (
|
|
724
|
+
SELECT count(*)::integer FROM (
|
|
725
|
+
SELECT 1 FROM public.log_bulk o
|
|
726
|
+
WHERE o.principal = i.p AND o.secret <> v_secret AND o.last > p_at - p_cap_window - c_refresh - public.deadline_max() - 60000
|
|
727
|
+
LIMIT p_cap_count
|
|
728
|
+
) z
|
|
729
|
+
) ELSE 0 END AS n_others,
|
|
730
|
+
-- A repeat rides its cover only while every part of this holds: never on a key whose bulk row is missing (CoverFresh).
|
|
731
|
+
coalesce(p_cover IS NOT NULL AND i.bulk_last >= c_period
|
|
732
|
+
AND EXISTS (SELECT 1 FROM public.log_uses u WHERE u.at = p_cover_at AND u.request_id = p_cover_request AND u.number = p_cover
|
|
733
|
+
AND u.credential = p_credential AND u.secret = p_secret AND u.purpose = p_purpose AND u.place IS NOT DISTINCT FROM p_place), false) AS is_covered
|
|
734
|
+
FROM i
|
|
735
|
+
),
|
|
736
|
+
w AS (
|
|
737
|
+
INSERT INTO public.log_uses (number, at, request_id, credential, secret, purpose, mac, place)
|
|
738
|
+
SELECT nextval('public.log_number'), p_at, p_request, p_credential, p_secret, p_purpose, p_mac, p_place
|
|
739
|
+
-- The isolate held its cover to the cached seals' numbers when it read them.
|
|
740
|
+
FROM v WHERE v.state IS NULL AND NOT v.is_covered AND NOT (p_token_hash IS NOT NULL AND (v.s_project IS NULL OR coalesce(v.v_plain, false)))
|
|
741
|
+
RETURNING log_uses.number
|
|
742
|
+
)
|
|
743
|
+
SELECT v.state, (SELECT w.number FROM w), CASE WHEN v.state IS NULL AND v.is_covered THEN p_cover END, v.newest, v.newest_mac, NULL::bigint, false,
|
|
744
|
+
v.p, v.c_kind, v.c_scope, v.c_revoked, v.c_expires, v.c_seal, NULL::bytea,
|
|
745
|
+
v.m_status, v.m_owner, v.m_role, v.m_scope, v.m_seal, NULL::bytea, NULL::bigint,
|
|
746
|
+
(SELECT a.seq FROM public.audit_log a WHERE a.author = 'vault' AND a.subject_principal = v.p AND a.decision = 'allow'
|
|
747
|
+
AND a.action IN ('member.add', 'member.restore', 'member.owner', 'member.role', 'member.remove', 'access.grant', 'access.revoke') ORDER BY a.seq DESC LIMIT 1),
|
|
748
|
+
gr.g_project, gr.g_environment, gr.g_slug, gr.g_role, gr.g_expires,
|
|
749
|
+
v.r_value, v.r_seal, NULL::bytea,
|
|
750
|
+
v.p_value, v.p_seal, NULL::bytea, p_cap_count, p_cap_window,
|
|
751
|
+
v.s_project, v.s_environment, v.s_version, v.s_source, v.s_seal, NULL::bytea,
|
|
752
|
+
v.x_project, v.x_environment, v.x_version, v.x_source, v.x_seal, NULL::bytea,
|
|
753
|
+
v.envelope_version, v.ciphertext, v.iv, v.auth_tag, v.wrapped_dek, v.kek_provider, v.kek_id, v.kek_version, v.sensitive, v.v_plain,
|
|
754
|
+
v.e1_id, v.e1_project, v.e1_slug, v.e1_owner, v.e1_parent, v.e1_protected, v.e1_archived, v.e1_seal, NULL::bytea,
|
|
755
|
+
v.e2_id, v.e2_project, v.e2_slug, v.e2_owner, v.e2_parent, v.e2_protected, v.e2_archived, v.e2_seal, NULL::bytea,
|
|
756
|
+
v.e3_id, v.e3_project, v.e3_slug, v.e3_owner, v.e3_parent, v.e3_protected, v.e3_archived, v.e3_seal, NULL::bytea,
|
|
757
|
+
v.e4_id, v.e4_project, v.e4_slug, v.e4_owner, v.e4_parent, v.e4_protected, v.e4_archived, v.e4_seal, NULL::bytea,
|
|
758
|
+
v.is_in, v.n_others, v.v_plaintext, v.e1_project_archived, v.e2_project_archived, v.e3_project_archived, v.e4_project_archived, v.s_reference, v.x_reference, v.c_generation, v.m_generation, v.c_token_hash,
|
|
759
|
+
CASE WHEN p_token_hash IS NOT NULL THEN json_build_object(
|
|
760
|
+
'now', CURRENT_TIMESTAMP, 'credential', v.c_row, 'identity', (SELECT to_json(x) FROM public.identities x WHERE x.id = (v.c_row ->> 'identity_id')::uuid),
|
|
761
|
+
'policy', (SELECT a.seq::text 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),
|
|
762
|
+
'project', (SELECT to_json(q) FROM public.projects q WHERE q.id = coalesce(v.e1_project, v.e2_project)),
|
|
763
|
+
'environment', (SELECT to_json(e) FROM public.environments e WHERE e.id = coalesce(v.e1_id, v.e2_id)), 'parent', NULL,
|
|
764
|
+
'secrets', CASE WHEN v.s_row IS NULL THEN '[]'::json ELSE json_build_array(json_build_object('secret', v.s_row, 'version', v.sv_row)) END) END,
|
|
765
|
+
NULL::bytea
|
|
766
|
+
INTO r
|
|
767
|
+
FROM v
|
|
768
|
+
-- The principal's grants as five lists, one entry per grant, lapsed ones included; an environment's grant with its project, which its row finds through `environments`.
|
|
769
|
+
LEFT JOIN LATERAL (
|
|
770
|
+
SELECT array_agg(coalesce(g.project_id, ge.project_id)) AS g_project, array_agg(g.environment_id) AS g_environment,
|
|
771
|
+
array_agg(g.environment_slug) AS g_slug, array_agg(g.role) AS g_role, array_agg(g.expires_at) AS g_expires
|
|
772
|
+
FROM public.vault_grants g LEFT JOIN public.environments ge ON ge.id = g.environment_id
|
|
773
|
+
WHERE g.principal = v.p
|
|
774
|
+
) gr ON v.m_status IS NOT NULL;
|
|
775
|
+
END IF;
|
|
776
|
+
IF dry THEN
|
|
777
|
+
-- The forms in turn: probing plain, probing whole, cached plain, cached whole; then the call itself, under the fence.
|
|
778
|
+
form := form + 1;
|
|
779
|
+
IF form < 4 THEN
|
|
780
|
+
probe := form < 2;
|
|
781
|
+
whole := form % 2 = 1;
|
|
782
|
+
CONTINUE;
|
|
783
|
+
END IF;
|
|
784
|
+
PERFORM set_config('coffre.use_planned', '0045', false);
|
|
785
|
+
dry := false;
|
|
786
|
+
probe := first_probe;
|
|
787
|
+
whole := first_whole;
|
|
788
|
+
PERFORM pg_advisory_xact_lock_shared(1668244070, 0);
|
|
789
|
+
CONTINUE;
|
|
790
|
+
END IF;
|
|
791
|
+
-- A named call answered `moved` or `archived` is decided by the vault before anything else is done (T158, D153):
|
|
792
|
+
-- it is read again probing, so that every row's newest seal is there to check, in this call.
|
|
793
|
+
IF r.state IN ('moved', 'archived') AND p_token_hash IS NOT NULL AND NOT probe THEN
|
|
794
|
+
probe := true;
|
|
795
|
+
CONTINUE;
|
|
796
|
+
END IF;
|
|
797
|
+
-- A use the statement took writes its entry or rides its cover, never neither: but a named one of a key the place
|
|
798
|
+
-- does not hold, or of a plain value, which writes none.
|
|
799
|
+
IF r.state IS NULL AND r.number IS NULL AND r.covered IS NULL AND NOT (p_token_hash IS NOT NULL AND (r.s_project IS NULL OR coalesce(r.v_plain, false))) THEN
|
|
800
|
+
RAISE EXCEPTION 'a use wrote no entry and rode no cover' USING ERRCODE = 'XX000';
|
|
801
|
+
END IF;
|
|
802
|
+
IF r.state = 'reprobe' THEN probe := true; CONTINUE; END IF;
|
|
803
|
+
IF r.state = 'whole' THEN whole := true; CONTINUE; END IF;
|
|
804
|
+
-- A use whose key would take its principal's lock (to enter the cap's set, or move on in it), named or by ids,
|
|
805
|
+
-- takes it only in the vault's call after it decided the use on the rows of this one (`p_decided`; T158, D153;
|
|
806
|
+
-- formal/Probing.tla, DecideBeforeLock, DecideEachLock; HotPathOne, NoLockWithoutStanding). Otherwise the call ends
|
|
807
|
+
-- here, nothing locked or written, and answers the rows it read (`decide`): the vault refuses a use it may not make
|
|
808
|
+
-- on them, the same as for a key never made, and makes one it may, in a second call.
|
|
809
|
+
IF r.state IN ('lock', 'share') AND NOT p_decided AND NOT locked AND NOT moving THEN
|
|
810
|
+
r.state := 'decide';
|
|
811
|
+
EXIT;
|
|
812
|
+
END IF;
|
|
813
|
+
EXIT WHEN r.state IS DISTINCT FROM 'lock' AND r.state IS DISTINCT FROM 'share';
|
|
814
|
+
-- Its key's bulk row is missing or out of the set: this use may enter it, so it takes its principal's lock exclusive
|
|
815
|
+
-- and reads again. Or the row is in the set, from an earlier period: this use may move it on, so it takes the lock
|
|
816
|
+
-- shared, beside the principal's other uses that move a key on, and reads again.
|
|
817
|
+
-- Holding it shared, a key never leaves the set (SharedStaysInSet) unless a row was put back under it: then the
|
|
818
|
+
-- call ends, refused as late, rather than ask for the lock exclusive while it holds it shared (two such would
|
|
819
|
+
-- deadlock).
|
|
820
|
+
IF r.state = 'lock' AND moving THEN
|
|
821
|
+
RAISE EXCEPTION 'its key left the cap''s set under the principal''s lock: a row put back' USING ERRCODE = '57014';
|
|
822
|
+
END IF;
|
|
823
|
+
IF r.state = 'lock' THEN
|
|
824
|
+
PERFORM pg_advisory_xact_lock(1668244066, hashtext(r.c_principal));
|
|
825
|
+
locked := true;
|
|
826
|
+
ELSE
|
|
827
|
+
PERFORM pg_advisory_xact_lock_shared(1668244066, hashtext(r.c_principal));
|
|
828
|
+
moving := true;
|
|
829
|
+
END IF;
|
|
830
|
+
END LOOP;
|
|
831
|
+
-- The cap's row, written only by a call that took the principal's lock, in a statement of its own after the one that
|
|
832
|
+
-- read, in the same transaction, under the same locks: the uses that read their key fresh, nearly all, run no write.
|
|
833
|
+
-- A key in the set moves on to this use's period, only ever up. A key entering the set, under the lock exclusive, is
|
|
834
|
+
-- made or moved on from a window past. Every request counts, whatever the Worker then decides but the cap (HotPathOneStaleBulk).
|
|
835
|
+
IF r.state IS NULL AND (locked OR moving) THEN
|
|
836
|
+
b_refresh := greatest(1, least(60000, r.cap_window / 10));
|
|
837
|
+
b_period := p_at - ((p_at % b_refresh) + b_refresh) % b_refresh;
|
|
838
|
+
IF r.in_set THEN
|
|
839
|
+
-- One use moves the key to this period: the one that takes a lock of the key and period's own without waiting.
|
|
840
|
+
-- The others take it shared, which waits only for that one's end, never for each other, and read the row again,
|
|
841
|
+
-- locking none: moved, they leave it. An UPDATE that waits on a row another is moving, then finds it moved,
|
|
842
|
+
-- keeps the row locked to its own commit, and queues the next. If the one moving it rolled back, the others
|
|
843
|
+
-- elect another the same way, on the lock's next round.
|
|
844
|
+
LOOP
|
|
845
|
+
b_lock := hashtextextended(r.c_principal || '/' || p_secret::text || '/' || b_period::text || '/' || b_round::text, 1668244066);
|
|
846
|
+
IF pg_try_advisory_xact_lock(b_lock) THEN
|
|
847
|
+
UPDATE public.log_bulk SET last = b_period WHERE principal = r.c_principal AND secret = p_secret AND last < b_period;
|
|
848
|
+
EXIT;
|
|
849
|
+
END IF;
|
|
850
|
+
PERFORM pg_advisory_xact_lock_shared(b_lock);
|
|
851
|
+
EXIT WHEN NOT EXISTS (SELECT 1 FROM public.log_bulk WHERE principal = r.c_principal AND secret = p_secret AND last < b_period);
|
|
852
|
+
b_round := b_round + 1;
|
|
853
|
+
END LOOP;
|
|
854
|
+
ELSIF r.others < r.cap_count THEN
|
|
855
|
+
INSERT INTO public.log_bulk (principal, secret, last) VALUES (r.c_principal, p_secret, b_period)
|
|
856
|
+
ON CONFLICT ON CONSTRAINT log_bulk_pkey DO UPDATE SET last = greatest(log_bulk.last, EXCLUDED.last);
|
|
857
|
+
END IF;
|
|
858
|
+
END IF;
|
|
859
|
+
-- The deadline holds in Postgres to the end: a call that ran past it rolls back and writes nothing. The hot login's lock_timeout and statement_timeout bound the waits.
|
|
860
|
+
IF r.state IS NULL AND (extract(epoch FROM clock_timestamp()) * 1000)::bigint > p_deadline THEN
|
|
861
|
+
RAISE EXCEPTION 'past its deadline' USING ERRCODE = '57014';
|
|
862
|
+
END IF;
|
|
863
|
+
-- What a named call's names name: the ids the vault makes the use by once it has decided, and the place's slugs.
|
|
864
|
+
IF p_token_hash IS NOT NULL THEN
|
|
865
|
+
r.n_credential := v_credential;
|
|
866
|
+
r.n_secret := v_secret;
|
|
867
|
+
r.n_place := v_np;
|
|
868
|
+
r.n_project := v_project;
|
|
869
|
+
r.n_environment := v_environment;
|
|
870
|
+
r.n_standing := v_stands;
|
|
871
|
+
END IF;
|
|
872
|
+
RETURN to_json(r);
|
|
873
|
+
END
|
|
874
|
+
$$;
|
|
875
|
+
--> statement-breakpoint
|
|
876
|
+
REVOKE ALL ON FUNCTION use_credential(uuid, uuid, bigint, bigint, smallint, uuid, bytea, bigint, bigint, bigint, bigint, bigint, bytea, integer, bigint, bytea, text, text, text, boolean, boolean, text[], boolean, text[], jsonb) FROM PUBLIC;
|
|
877
|
+
--> statement-breakpoint
|
|
878
|
+
GRANT EXECUTE ON FUNCTION use_credential(uuid, uuid, bigint, bigint, smallint, uuid, bytea, bigint, bigint, bigint, bigint, bigint, bytea, integer, bigint, bytea, text, text, text, boolean, boolean, text[], boolean, text[], jsonb) TO coffre_vault;
|