@coffre/db 0.14.2 → 0.14.4
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/dist/connect.d.ts +1 -1
- package/dist/{database-Ds1Y79h6.d.ts → database-CFhTnnzM.d.ts} +1 -1
- package/dist/dialect.d.ts +4 -3
- package/dist/dialect.js +1 -0
- package/dist/grants.d.ts +1 -1
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/log.d.ts +1 -1
- package/dist/migrate.d.ts +21 -5
- package/dist/migrate.js +49 -9
- package/dist/migrations/postgres/0019_backup_role.sql +63 -0
- package/dist/migrations/postgres/0020_vault_releases.sql +36 -0
- package/dist/migrations/postgres/meta/0019_snapshot.json +3295 -0
- package/dist/migrations/postgres/meta/0020_snapshot.json +3334 -0
- package/dist/migrations/postgres/meta/_journal.json +14 -0
- package/dist/migrations/sqlite/0017_vault_releases.sql +26 -0
- package/dist/migrations/sqlite/meta/0017_snapshot.json +3037 -0
- package/dist/migrations/sqlite/meta/_journal.json +7 -0
- package/dist/portable.d.ts +2 -2
- package/dist/schema-sqlite.d.ts +75 -1
- package/dist/schema-sqlite.js +21 -1
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +21 -0
- package/dist/{schema-DcVkJhCf.d.ts → schema-z8GcN_Xu.d.ts} +72 -2
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +23 -2
- package/package.json +2 -2
- package/src/dialect.ts +3 -2
- package/src/migrate-main.ts +6 -3
- package/src/migrate.ts +49 -10
- package/src/migrations/postgres/0019_backup_role.sql +63 -0
- package/src/migrations/postgres/0020_vault_releases.sql +36 -0
- package/src/migrations/postgres/meta/0019_snapshot.json +3295 -0
- package/src/migrations/postgres/meta/0020_snapshot.json +3334 -0
- package/src/migrations/postgres/meta/_journal.json +14 -0
- package/src/migrations/sqlite/0017_vault_releases.sql +26 -0
- package/src/migrations/sqlite/meta/0017_snapshot.json +3037 -0
- package/src/migrations/sqlite/meta/_journal.json +7 -0
- package/src/schema.sqlite.ts +22 -0
- package/src/schema.ts +24 -0
package/dist/connect.d.ts
CHANGED
package/dist/dialect.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as Queryable } from "./database-
|
|
1
|
+
import { i as Queryable } from "./database-CFhTnnzM.js";
|
|
2
2
|
import { SQL, SQLWrapper } from "drizzle-orm";
|
|
3
3
|
import { PgTable } from "drizzle-orm/pg-core";
|
|
4
4
|
//#region src/dialect.d.ts
|
|
@@ -28,10 +28,11 @@ export declare function engineOf(db: Queryable): Engine;
|
|
|
28
28
|
*
|
|
29
29
|
* `no key update` is for rows others reference by foreign key: it keeps out
|
|
30
30
|
* every other writer of the row, but not an insert that points at it.
|
|
31
|
+
* `share` keeps out every writer, but not another `share`.
|
|
31
32
|
*/
|
|
32
33
|
export declare function forUpdate<Query extends {
|
|
33
|
-
for: (strength: 'update' | 'no key update') => unknown;
|
|
34
|
-
}>(db: Queryable, query: Query, strength?: 'update' | 'no key update'): ReturnType<Query['for']>;
|
|
34
|
+
for: (strength: 'update' | 'no key update' | 'share') => unknown;
|
|
35
|
+
}>(db: Queryable, query: Query, strength?: 'update' | 'no key update' | 'share'): ReturnType<Query['for']>;
|
|
35
36
|
/**
|
|
36
37
|
* The rows a hand-written query answers, as the driver gives them: what
|
|
37
38
|
* Postgres's `execute` and SQLite's `all` each call a result. Column names
|
package/dist/dialect.js
CHANGED
|
@@ -15,6 +15,7 @@ function engineOf(db) {
|
|
|
15
15
|
*
|
|
16
16
|
* `no key update` is for rows others reference by foreign key: it keeps out
|
|
17
17
|
* every other writer of the row, but not an insert that points at it.
|
|
18
|
+
* `share` keeps out every writer, but not another `share`.
|
|
18
19
|
*/
|
|
19
20
|
function forUpdate(db, query, strength = "update") {
|
|
20
21
|
if (engineOf(db) === "sqlite") return query;
|
package/dist/grants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as Queryable } from "./database-
|
|
1
|
+
import { i as Queryable } from "./database-CFhTnnzM.js";
|
|
2
2
|
import { Authenticity, Newest } from "./log.js";
|
|
3
3
|
import { StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
import { Conversion, ConversionInput, InstanceRole, Scope } from "@coffre/core/access";
|
package/dist/hyperdrive.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { g as schema_d_exports } from "./schema-
|
|
2
|
-
import { a as Transaction, c as tablesOf, i as Queryable, l as prepared, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-
|
|
1
|
+
import { g as schema_d_exports } from "./schema-z8GcN_Xu.js";
|
|
2
|
+
import { a as Transaction, c as tablesOf, i as Queryable, l as prepared, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-CFhTnnzM.js";
|
|
3
3
|
export { Database, PoolLike, QueryOutsideTransaction, Queryable, Transaction, createDatabase, own, prepared, schema_d_exports as schema, tablesOf };
|
package/dist/log.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as Transaction, i as Queryable } from "./database-
|
|
1
|
+
import { a as Transaction, i as Queryable } from "./database-CFhTnnzM.js";
|
|
2
2
|
import { Placeholder, SQL } from "drizzle-orm";
|
|
3
3
|
import { LogKey, StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
//#region src/log.d.ts
|
package/dist/migrate.d.ts
CHANGED
|
@@ -3,6 +3,16 @@ import { PoolClient } from "pg";
|
|
|
3
3
|
//#region src/migrate.d.ts
|
|
4
4
|
/** Reassert database privileges that pg_dump and CREATE DATABASE TEMPLATE do not carry. */
|
|
5
5
|
export declare function restrictDatabase(client: Pick<PoolClient, 'query'>): Promise<void>;
|
|
6
|
+
/** Why `login` cannot be the backup login, before anything runs, or null when it may. */
|
|
7
|
+
export declare function backupLoginProblem(login: string): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* The migrations are applied, by this run or an earlier one, and their
|
|
10
|
+
* ledger checked, but reasserting the privileges after them failed: unlike
|
|
11
|
+
* a migration's failure, this one may have changed the database.
|
|
12
|
+
*/
|
|
13
|
+
export declare class AfterMigrations extends Error {
|
|
14
|
+
constructor(error: unknown);
|
|
15
|
+
}
|
|
6
16
|
/** An owner of coffre's objects whose privileges the login lacks: not a member of it, or one that does not inherit them. */
|
|
7
17
|
export type ForeignOwner = {
|
|
8
18
|
name: string;
|
|
@@ -80,11 +90,17 @@ export declare function migrationStatus(url: string): Promise<{
|
|
|
80
90
|
notOwner: NotOwner | null;
|
|
81
91
|
}>;
|
|
82
92
|
/**
|
|
83
|
-
* Apply missing migrations, check their history and reassert database
|
|
84
|
-
* privileges, as a login that can
|
|
85
|
-
*
|
|
93
|
+
* Apply missing migrations, check their history, and reassert database
|
|
94
|
+
* privileges, on every run, even with nothing to apply, as a login that can
|
|
95
|
+
* change coffre's objects: another is refused, NotOwner, before anything
|
|
96
|
+
* changes. `onPlan` hears what there is to apply before it is, under
|
|
86
97
|
* the migration lock, and may stop the run by throwing: all of it then goes
|
|
87
|
-
* in one call, with no word of each.
|
|
98
|
+
* in one call, with no word of each. `backupLogin` names an existing role to
|
|
99
|
+
* grant the backup group, `coffre_backup`, on Postgres; it is never made, and
|
|
100
|
+
* never revoked when a later run names none.
|
|
88
101
|
*/
|
|
89
|
-
export declare function migrateDatabase(url: string, onPlan
|
|
102
|
+
export declare function migrateDatabase(url: string, { onPlan, backupLogin }?: {
|
|
103
|
+
onPlan?: (plan: MigrationPlan) => void | Promise<void>;
|
|
104
|
+
backupLogin?: string;
|
|
105
|
+
}): Promise<void>;
|
|
90
106
|
//#endregion
|
package/dist/migrate.js
CHANGED
|
@@ -19,11 +19,34 @@ const MIGRATION_LOCK_KEY = "7165058122361679213";
|
|
|
19
19
|
async function restrictDatabase(client) {
|
|
20
20
|
await client.query(`DO $$ BEGIN
|
|
21
21
|
EXECUTE format(
|
|
22
|
-
'REVOKE CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC, coffre_app, coffre_runtime, coffre_vault, coffre_vault_runtime',
|
|
22
|
+
'REVOKE CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC, coffre_app, coffre_runtime, coffre_vault, coffre_vault_runtime, coffre_backup',
|
|
23
23
|
current_database()
|
|
24
24
|
);
|
|
25
25
|
END $$`);
|
|
26
26
|
}
|
|
27
|
+
/** Why `login` cannot be the backup login, before anything runs, or null when it may. */
|
|
28
|
+
function backupLoginProblem(login) {
|
|
29
|
+
if (login === "") return "the backup login is empty: name the role to grant coffre_backup";
|
|
30
|
+
if ([
|
|
31
|
+
"coffre_backup",
|
|
32
|
+
"coffre_app",
|
|
33
|
+
"coffre_vault",
|
|
34
|
+
"coffre_runtime",
|
|
35
|
+
"coffre_vault_runtime"
|
|
36
|
+
].includes(login)) return `the backup login is ${login}, one of coffre's own roles: name the login that backs up`;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The migrations are applied, by this run or an earlier one, and their
|
|
41
|
+
* ledger checked, but reasserting the privileges after them failed: unlike
|
|
42
|
+
* a migration's failure, this one may have changed the database.
|
|
43
|
+
*/
|
|
44
|
+
var AfterMigrations = class extends Error {
|
|
45
|
+
constructor(error) {
|
|
46
|
+
super(`the database is at the migrated schema, but reasserting its privileges failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
47
|
+
this.name = "AfterMigrations";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
27
50
|
/**
|
|
28
51
|
* The login cannot change coffre's objects, the ledger's schema, the tables
|
|
29
52
|
* and the functions: their owners are roles whose privileges it does not
|
|
@@ -171,7 +194,14 @@ async function postgresMigrator(url) {
|
|
|
171
194
|
await client.query("SELECT pg_advisory_unlock($1::bigint)", [MIGRATION_LOCK_KEY]);
|
|
172
195
|
},
|
|
173
196
|
migrate: (migrationsFolder) => migrate(drizzle(client), { migrationsFolder }),
|
|
174
|
-
restrict
|
|
197
|
+
async restrict(backupLogin) {
|
|
198
|
+
await restrictDatabase(client);
|
|
199
|
+
if (backupLogin !== void 0) {
|
|
200
|
+
const { rowCount } = await client.query("SELECT 1 FROM pg_roles WHERE rolname::text = $1", [backupLogin]);
|
|
201
|
+
if (rowCount === 0) throw new Error(`role ${client.escapeIdentifier(backupLogin)} does not exist: make it, coffre never does`);
|
|
202
|
+
await client.query(`GRANT coffre_backup TO ${client.escapeIdentifier(backupLogin)} WITH ADMIN FALSE, INHERIT TRUE, SET FALSE`);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
175
205
|
async notOwner({ relations, functions }) {
|
|
176
206
|
const { rows } = await client.query(FOREIGN_OWNERS, [relations, functions]);
|
|
177
207
|
if (rows.length === 0) return null;
|
|
@@ -238,14 +268,20 @@ async function migrationStatus(url) {
|
|
|
238
268
|
}
|
|
239
269
|
}
|
|
240
270
|
/**
|
|
241
|
-
* Apply missing migrations, check their history and reassert database
|
|
242
|
-
* privileges, as a login that can
|
|
243
|
-
*
|
|
271
|
+
* Apply missing migrations, check their history, and reassert database
|
|
272
|
+
* privileges, on every run, even with nothing to apply, as a login that can
|
|
273
|
+
* change coffre's objects: another is refused, NotOwner, before anything
|
|
274
|
+
* changes. `onPlan` hears what there is to apply before it is, under
|
|
244
275
|
* the migration lock, and may stop the run by throwing: all of it then goes
|
|
245
|
-
* in one call, with no word of each.
|
|
276
|
+
* in one call, with no word of each. `backupLogin` names an existing role to
|
|
277
|
+
* grant the backup group, `coffre_backup`, on Postgres; it is never made, and
|
|
278
|
+
* never revoked when a later run names none.
|
|
246
279
|
*/
|
|
247
|
-
async function migrateDatabase(url, onPlan) {
|
|
280
|
+
async function migrateDatabase(url, { onPlan, backupLogin } = {}) {
|
|
248
281
|
const engine = engineOfUrl(url);
|
|
282
|
+
const problem = backupLogin === void 0 ? null : backupLoginProblem(backupLogin);
|
|
283
|
+
if (problem !== null) throw new Error(problem);
|
|
284
|
+
if (engine === "sqlite" && backupLogin !== void 0) throw new Error("SQLite has no roles, so no backup login");
|
|
249
285
|
const expected = await expectedMigrations(engine);
|
|
250
286
|
const migrator = await {
|
|
251
287
|
postgres: postgresMigrator,
|
|
@@ -267,7 +303,11 @@ async function migrateDatabase(url, onPlan) {
|
|
|
267
303
|
});
|
|
268
304
|
await migrator.migrate(migrationsFolder(engine));
|
|
269
305
|
verifyHistory(expected, await migrator.applied(), true);
|
|
270
|
-
|
|
306
|
+
try {
|
|
307
|
+
await migrator.restrict(backupLogin);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw new AfterMigrations(error);
|
|
310
|
+
}
|
|
271
311
|
} finally {
|
|
272
312
|
try {
|
|
273
313
|
if (locked) await migrator.unlock();
|
|
@@ -277,4 +317,4 @@ async function migrateDatabase(url, onPlan) {
|
|
|
277
317
|
}
|
|
278
318
|
}
|
|
279
319
|
//#endregion
|
|
280
|
-
export { DatabaseAhead, NotOwner, managedObjects, migrateDatabase, migrationStatus, migrationsFolder, restrictDatabase };
|
|
320
|
+
export { AfterMigrations, DatabaseAhead, NotOwner, backupLoginProblem, managedObjects, migrateDatabase, migrationStatus, migrationsFolder, restrictDatabase };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
-- Backups (W2356): the group coffre_backup reads every row and writes none, so a nightly pg_dump need not hold the owner's password. No login is made or granted here: `coffre migrate --backup-login <role>` grants the group to the role it names. Reading every table is not enough: the audit log's row-level security shows its rows only to the roles a policy names, so pg_dump as a role that reads every table (pg_read_all_data) is refused the log, or with --enable-row-security dumps no entry. It only adds a role, read grants and a read policy: the release before never meets them.
|
|
2
|
+
DO $$
|
|
3
|
+
BEGIN
|
|
4
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'coffre_backup') THEN
|
|
5
|
+
-- No migration since the baseline made a role: a login that migrates as a member of the owner inherits its privileges, never its CREATEROLE.
|
|
6
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = current_user AND (rolsuper OR rolcreaterole)) THEN
|
|
7
|
+
RAISE EXCEPTION '% may not create roles, and this migration makes coffre_backup: migrate as a login with CREATEROLE, or have one run CREATE ROLE coffre_backup NOLOGIN; GRANT coffre_backup TO % WITH ADMIN TRUE, INHERIT FALSE, SET FALSE first', current_user, current_user;
|
|
8
|
+
END IF;
|
|
9
|
+
-- Roles are the server's, and the migration lock each database's: another database's 0019 may make it meanwhile.
|
|
10
|
+
BEGIN
|
|
11
|
+
CREATE ROLE coffre_backup NOLOGIN INHERIT NOCREATEDB NOCREATEROLE;
|
|
12
|
+
EXCEPTION WHEN duplicate_object OR unique_violation THEN
|
|
13
|
+
NULL;
|
|
14
|
+
END;
|
|
15
|
+
END IF;
|
|
16
|
+
-- Managed owners cannot ALTER the superuser-only attributes, even to turn them off.
|
|
17
|
+
IF EXISTS (
|
|
18
|
+
SELECT 1 FROM pg_roles WHERE rolname = 'coffre_backup'
|
|
19
|
+
AND (rolsuper OR rolreplication OR rolbypassrls)
|
|
20
|
+
) THEN
|
|
21
|
+
RAISE EXCEPTION 'coffre_backup has unsafe role attributes: SUPERUSER, REPLICATION and BYPASSRLS must be off';
|
|
22
|
+
END IF;
|
|
23
|
+
-- It would pass on what it is a member of to its members.
|
|
24
|
+
IF EXISTS (SELECT 1 FROM pg_auth_members WHERE member = 'coffre_backup'::regrole) THEN
|
|
25
|
+
RAISE EXCEPTION 'coffre_backup is a member of another role, whose privileges its members would inherit: revoke its memberships';
|
|
26
|
+
END IF;
|
|
27
|
+
-- Revoking cannot take an owner's rights away: its members would write what it owns.
|
|
28
|
+
IF EXISTS (
|
|
29
|
+
SELECT 1 FROM pg_shdepend
|
|
30
|
+
WHERE refclassid = 'pg_authid'::regclass AND refobjid = 'coffre_backup'::regrole AND deptype = 'o'
|
|
31
|
+
) THEN
|
|
32
|
+
RAISE EXCEPTION 'coffre_backup owns database objects, which its members could change: reassign them';
|
|
33
|
+
END IF;
|
|
34
|
+
-- Only when it must: altering a role takes CREATEROLE too.
|
|
35
|
+
IF EXISTS (
|
|
36
|
+
SELECT 1 FROM pg_roles WHERE rolname = 'coffre_backup'
|
|
37
|
+
AND (rolcanlogin OR NOT rolinherit OR rolcreatedb OR rolcreaterole)
|
|
38
|
+
) THEN
|
|
39
|
+
ALTER ROLE coffre_backup NOLOGIN INHERIT NOCREATEDB NOCREATEROLE;
|
|
40
|
+
END IF;
|
|
41
|
+
END
|
|
42
|
+
$$;
|
|
43
|
+
--> statement-breakpoint
|
|
44
|
+
-- coffre's own, by name, as the baseline grants: public may hold a database's other tables (FOREIGN_OWNERS in migrate.ts), which are not coffre's to grant, whichever login migrates, a superuser included. A group made before this keeps no other privilege on them. A later migration that makes a table grants it SELECT too, as it grants coffre_app, rather than default privileges, which would give it every table the login makes, coffre's or not, and none another login makes.
|
|
45
|
+
REVOKE ALL ON SCHEMA public, drizzle FROM coffre_backup;
|
|
46
|
+
--> statement-breakpoint
|
|
47
|
+
GRANT USAGE ON SCHEMA public, drizzle TO coffre_backup;
|
|
48
|
+
--> statement-breakpoint
|
|
49
|
+
REVOKE ALL ON audit_chain_head, audit_log, capped_alerts, consumed_tokens, credentials, device_authorizations, dismissed_keys, enrollments, environments, identities, mcp_approvals, mcp_connections, oauth_clients, projects, secret_references, secret_versions, secrets, service_bindings, vault_grants, vault_members FROM coffre_backup;
|
|
50
|
+
--> statement-breakpoint
|
|
51
|
+
REVOKE ALL ON ALL TABLES IN SCHEMA drizzle FROM coffre_backup;
|
|
52
|
+
--> statement-breakpoint
|
|
53
|
+
REVOKE ALL ON ALL SEQUENCES IN SCHEMA drizzle FROM coffre_backup;
|
|
54
|
+
--> statement-breakpoint
|
|
55
|
+
REVOKE ALL ON FUNCTION audit_log_append_only(), secret_versions_erase_only(), prune_consumed_tokens(integer) FROM coffre_backup;
|
|
56
|
+
--> statement-breakpoint
|
|
57
|
+
GRANT SELECT ON audit_chain_head, audit_log, capped_alerts, consumed_tokens, credentials, device_authorizations, dismissed_keys, enrollments, environments, identities, mcp_approvals, mcp_connections, oauth_clients, projects, secret_references, secret_versions, secrets, service_bindings, vault_grants, vault_members TO coffre_backup;
|
|
58
|
+
--> statement-breakpoint
|
|
59
|
+
GRANT SELECT ON ALL TABLES IN SCHEMA drizzle TO coffre_backup;
|
|
60
|
+
--> statement-breakpoint
|
|
61
|
+
GRANT SELECT ON ALL SEQUENCES IN SCHEMA drizzle TO coffre_backup;
|
|
62
|
+
--> statement-breakpoint
|
|
63
|
+
CREATE POLICY audit_log_backup_read ON audit_log FOR SELECT TO coffre_backup USING (true);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
-- Releases (W199): each key the vault released to a principal, and when last, so that its bulk limit counts the keys released within a window from a row per key, not from every read the principal ever made. The database keeps it, from each vault `secret.read` allowed, in the append's own transaction; the release before neither reads it nor knows it is kept, and its appends keep it too.
|
|
2
|
+
CREATE TABLE "vault_releases" (
|
|
3
|
+
"principal" text NOT NULL,
|
|
4
|
+
"key" text NOT NULL,
|
|
5
|
+
"released_at" bigint NOT NULL,
|
|
6
|
+
CONSTRAINT "vault_releases_pkey" PRIMARY KEY("principal","key")
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
-- Room in each page for the next version of its rows: a read moves its keys' times on in place (a HOT update), so the
|
|
10
|
+
-- table and its key stay a row a key, however often each is read: the page prunes the versions before as it is read.
|
|
11
|
+
ALTER TABLE vault_releases SET (fillfactor = 50);--> statement-breakpoint
|
|
12
|
+
-- Each key the entry names, as the count read them before: the secret, or each item of a read's list at one place, or, for an entry that names none, which only a forged one is, the entry itself. As the owner, so that no login may write the table: the vault's appends reach it only through the log.
|
|
13
|
+
CREATE FUNCTION vault_releases_keep() RETURNS trigger
|
|
14
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
|
|
15
|
+
BEGIN
|
|
16
|
+
INSERT INTO public.vault_releases AS kept (principal, key, released_at)
|
|
17
|
+
SELECT DISTINCT NEW.actor, coalesce(NEW.secret_id::text, CASE jsonb_typeof(one -> 'secretId') WHEN 'string' THEN one ->> 'secretId' END, 'entry:' || NEW.seq), NEW.occurred_at
|
|
18
|
+
FROM (SELECT) AS entry
|
|
19
|
+
LEFT JOIN LATERAL jsonb_path_query(CASE WHEN NEW.secret_id IS NULL THEN NEW.metadata::jsonb END, 'strict $.secrets ? (@.type() == "array")[*]', '{}', true) AS one ON true
|
|
20
|
+
ON CONFLICT (principal, key) DO UPDATE SET released_at = greatest(kept.released_at, excluded.released_at);
|
|
21
|
+
RETURN NULL;
|
|
22
|
+
END
|
|
23
|
+
$$;--> statement-breakpoint
|
|
24
|
+
-- A trigger runs its function whoever may execute it: no login may call it otherwise.
|
|
25
|
+
REVOKE ALL ON FUNCTION vault_releases_keep() FROM PUBLIC;--> statement-breakpoint
|
|
26
|
+
CREATE TRIGGER audit_log_releases AFTER INSERT ON audit_log FOR EACH ROW
|
|
27
|
+
WHEN (NEW.author = 'vault' AND NEW.action = 'secret.read' AND NEW.decision = 'allow')
|
|
28
|
+
EXECUTE FUNCTION vault_releases_keep();--> statement-breakpoint
|
|
29
|
+
-- Then the releases already in the log, in the same transaction: an append waits from the trigger to the commit, and none is missed.
|
|
30
|
+
INSERT INTO vault_releases (principal, key, released_at)
|
|
31
|
+
SELECT e.actor, coalesce(e.secret_id::text, CASE jsonb_typeof(one -> 'secretId') WHEN 'string' THEN one ->> 'secretId' END, 'entry:' || e.seq), max(e.occurred_at)
|
|
32
|
+
FROM audit_log e
|
|
33
|
+
LEFT JOIN LATERAL jsonb_path_query(CASE WHEN e.secret_id IS NULL THEN e.metadata::jsonb END, 'strict $.secrets ? (@.type() == "array")[*]', '{}', true) AS one ON true
|
|
34
|
+
WHERE e.author = 'vault' AND e.action = 'secret.read' AND e.decision = 'allow'
|
|
35
|
+
GROUP BY 1, 2;--> statement-breakpoint
|
|
36
|
+
GRANT SELECT ON vault_releases TO coffre_vault, coffre_backup;
|