@coffre/vault 0.10.1 → 0.11.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/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js +2 -2
- package/dist/{index-CByXzyi-.d.ts → index-zkW6DEm9.d.ts} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +2 -2
- package/dist/{src-D82jyprb.js → src-B0d-W-SI.js} +4 -3
- package/dist/{vault-B0YiPBgi.js → vault-DEjPmLNt.js} +590 -192
- package/package.json +3 -3
- package/src/config.ts +6 -5
- package/src/decide.ts +198 -0
- package/src/snapshot.postgres.ts +97 -0
- package/src/snapshot.sqlite.ts +103 -0
- package/src/snapshot.ts +191 -0
- package/src/store.ts +28 -37
- package/src/vault.ts +189 -183
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { ACCESS_ACTIONS, POLICY_REVISION, checkpointMessage, checkpointVerifier, describeAccessFault, handsBack, policyRevision } from "@coffre/core/vault";
|
|
1
|
+
import { ACCESS_ACTIONS, ACCESS_ACTIONS as ACCESS_ACTIONS$1, POLICY_REVISION, checkpointMessage, checkpointVerifier, describeAccessFault, handsBack, policyRevision } from "@coffre/core/vault";
|
|
2
2
|
import { DEK_BYTES, KEY_CHECK, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE, KekBadClaimError, KekCancelledError, KekUnavailableError, LocalKekProvider } from "@coffre/core/kek";
|
|
3
3
|
import { createHash, createHmac, hkdfSync, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { prepared, tablesOf } from "@coffre/db";
|
|
5
|
-
import { DEFAULT_POLICY, EVERYWHERE, EVERY_ENVIRONMENT, FIXED_ROLES, INSTANCE_ROLES, NOTHING, PROJECT_ONLY_PERMISSIONS, ROLES, ROLE_ID, UNWRITTEN_FORK_MS, allows, assignableToEnvironment, definitionProblem, givesService, grantKind, isBuiltinRole, isCredentialScope, isInstanceRole, isRole, isScope, managesService, mayManageAccess, normalDefinition, normalScope, ownsInstance, runsInstance, setsUpServices, unscoped, withinCredential } from "@coffre/core/access";
|
|
5
|
+
import { DEFAULT_POLICY, EVERYWHERE, EVERY_ENVIRONMENT, FIXED_ROLES, INSTANCE_ROLES, NOTHING, PROJECT_ONLY_PERMISSIONS, ROLES, ROLE_ID, UNWRITTEN_FORK_MS, allows, assignableToEnvironment, definitionProblem, givesService, grantKind, isBuiltinRole, isCredentialScope, isInstanceRole, isRole, isScope, managesService, mayManageAccess, normalDefinition, normalScope, ownsInstance, ownsProject, readsSensitive, runsInstance, setsUpServices, unscoped, useOnly, withinCredential } from "@coffre/core/access";
|
|
6
6
|
import { GENESIS_HASH, deriveLogKey, verifyEntries } from "@coffre/core/audit";
|
|
7
7
|
import { checkContext, ownerKey } from "@coffre/core/envelope";
|
|
8
|
-
import { shownMember } from "@coffre/core/schemas";
|
|
9
|
-
import { SNAPSHOT, clockMillis, engineOf, forUpdate, isUniqueViolation, isUnreachable, tombstone, truth } from "@coffre/db/dialect";
|
|
8
|
+
import { TOMBSTONE, shownMember } from "@coffre/core/schemas";
|
|
9
|
+
import { SNAPSHOT, clockMillis, engineOf, forUpdate, isUniqueViolation, isUnreachable, rowsOf, tombstone, truth } from "@coffre/db/dialect";
|
|
10
10
|
import { accessEntriesAbout, everyProjectConversion, insertGrant, liveProjects, newestAccess, readGrants, reportStands, roleColumns, storedRole } from "@coffre/db/grants";
|
|
11
|
-
import { appendEntries, entryColumns, lockLogHead, missingEntry, newestAuthentic, newestVaultEntries,
|
|
11
|
+
import { LogHeadMismatch, appendEntries, entryColumns, lockLogHead, missingEntry, newestAuthentic, newestVaultEntries, withinBytes } from "@coffre/db/log";
|
|
12
12
|
import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
13
13
|
//#region src/checkpoint.ts
|
|
14
14
|
const PKCS8_ED25519 = [
|
|
@@ -43,6 +43,138 @@ async function signer(seed) {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
//#endregion
|
|
46
|
+
//#region src/decide.ts
|
|
47
|
+
/**
|
|
48
|
+
* Where a decision checks a permission: an environment, with its slug as
|
|
49
|
+
* the store has it when it was read and is in the project claimed;
|
|
50
|
+
* otherwise none, which no grant on a slug matches.
|
|
51
|
+
*/
|
|
52
|
+
function placeIn(place, environments) {
|
|
53
|
+
const environment = environments.get(place.environmentId);
|
|
54
|
+
const slug = environment?.projectId === place.projectId ? environment.slug : null;
|
|
55
|
+
return {
|
|
56
|
+
projectId: place.projectId,
|
|
57
|
+
environmentId: place.environmentId,
|
|
58
|
+
environmentSlug: slug
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A key of a personal config is its owner's alone (docs/design/forking.md,
|
|
63
|
+
* section 4): no grant or role opens or wraps it, an Owner's included,
|
|
64
|
+
* only its owner, while they read the environment it is a config of,
|
|
65
|
+
* whatever the call is. Nor does any reference reach into or out of one.
|
|
66
|
+
* Undefined for a key of any other environment, decided as ever.
|
|
67
|
+
*/
|
|
68
|
+
function personalRefusal(reader, principal, permission, secret, at, environments) {
|
|
69
|
+
const own = environments.get(secret.environmentId);
|
|
70
|
+
const elsewhere = at.environmentId !== secret.environmentId;
|
|
71
|
+
const held = elsewhere ? environments.get(at.environmentId) : own;
|
|
72
|
+
if (own?.owner == null && held?.owner == null) return void 0;
|
|
73
|
+
if (elsewhere || own === void 0 || principal !== own.owner || own.parentId === null) return "personal";
|
|
74
|
+
const parent = placeIn({
|
|
75
|
+
projectId: secret.projectId,
|
|
76
|
+
environmentId: own.parentId
|
|
77
|
+
}, environments);
|
|
78
|
+
const gone = own.deleted || (environments.get(own.parentId)?.deleted ?? true);
|
|
79
|
+
const refused = refuses(reader, "secret.use", parent, gone) === null ? null : refuses(reader, "secret.read", parent, gone);
|
|
80
|
+
if (refused !== null) return refused;
|
|
81
|
+
return withinCredential(reader.live, permission === "secret.read" ? "secret.use" : permission, parent) ? null : "no_grant";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Why `reader` may not do `permission` at `where`, or null if they may. A
|
|
85
|
+
* deleted place is refused before any grant is asked, so no kind of grant,
|
|
86
|
+
* on the project, the environment or every project, can reach into one.
|
|
87
|
+
*/
|
|
88
|
+
function refuses(reader, permission, where, deleted) {
|
|
89
|
+
if (reader.status === "tampered") return "tampered";
|
|
90
|
+
if (reader.status === "removed") return "removed";
|
|
91
|
+
if (reader.status === "unknown") return "not_a_member";
|
|
92
|
+
if (deleted) return "deleted";
|
|
93
|
+
if (allows(reader.live, permission, where)) return null;
|
|
94
|
+
return allows(reader.all, permission, where) ? "expired" : "no_grant";
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What refuses each key of a call by where it is, before any `vet`: its
|
|
98
|
+
* personal config's owner, any one of the permissions it `needs` besides,
|
|
99
|
+
* `permission` itself, at `at`, its reference's holder or where it is; and
|
|
100
|
+
* a key it `owns` that its reader does not own the project of.
|
|
101
|
+
*/
|
|
102
|
+
function keyRefusals(reader, call) {
|
|
103
|
+
const { principal, permission, secrets, at, environments, needs } = call;
|
|
104
|
+
const gone = (place) => environments.get(place.environmentId)?.deleted ?? false;
|
|
105
|
+
const refusalOf = (secret, i) => {
|
|
106
|
+
const personal = personalRefusal(reader, principal, permission, secret, at[i], environments);
|
|
107
|
+
if (personal !== void 0) return personal;
|
|
108
|
+
const also = needs[i];
|
|
109
|
+
if (also != null) {
|
|
110
|
+
const place = placeIn(at[i], environments);
|
|
111
|
+
const refused = also.map((need) => refuses(reader, need, place, gone(secret) || gone(at[i])));
|
|
112
|
+
if (refused.every((code) => code !== null)) return refused[0] ?? "no_grant";
|
|
113
|
+
}
|
|
114
|
+
return refuses(reader, permission, placeIn(at[i], environments), gone(secret) || gone(at[i]));
|
|
115
|
+
};
|
|
116
|
+
return secrets.map((secret, i) => {
|
|
117
|
+
const code = refusalOf(secret, i);
|
|
118
|
+
return code === null && call.owns?.[i] === true && !ownsProject(reader.live, secret.projectId) ? "not_allowed" : code;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Each key's refusal once its sensitivity is weighed too, after `vet`. A
|
|
123
|
+
* sensitive value is only ever used for a person, never handed back, by
|
|
124
|
+
* whatever credential of theirs asks; and handed back to a service account
|
|
125
|
+
* only on a credential an Owner of its own project issued, a reference's
|
|
126
|
+
* source's too, whose value it is (`readsSensitive`, R32). And a fork
|
|
127
|
+
* copies one, for anyone, a service account too, only where its app says
|
|
128
|
+
* it keeps it sensitive (an app of 0.9, mid-deploy, would write the copy as
|
|
129
|
+
* a secret its people read), and never one read through a reference.
|
|
130
|
+
*/
|
|
131
|
+
function sensitiveRefusals(codes, reader, call) {
|
|
132
|
+
const copying = call.action === "secret.read" && call.purpose === "copy";
|
|
133
|
+
const handedBack = call.action === "secret.read" && handsBack({
|
|
134
|
+
purpose: call.purpose ?? "",
|
|
135
|
+
keepsSensitive: call.keepsSensitive === true
|
|
136
|
+
});
|
|
137
|
+
const person = call.principal.startsWith("user:");
|
|
138
|
+
return call.secrets.map((secret, i) => {
|
|
139
|
+
const code = codes[i] ?? null;
|
|
140
|
+
if (code !== null || secret.sensitive !== true) return code;
|
|
141
|
+
if (copying && call.keepsSensitive !== true || copying && call.viaReference?.[i] === true || handedBack && person) return "sensitive";
|
|
142
|
+
if (handedBack && !readsSensitive(reader.live, secret.projectId)) return "not_owner_issued";
|
|
143
|
+
return null;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Each key's refusal once a workload's listing is weighed too, after `vet`
|
|
148
|
+
* and before its sensitivity (docs/design/workload-access.md, section 6.1).
|
|
149
|
+
* A workload is handed back, for any purpose but the proxy's use, only a
|
|
150
|
+
* value its credential lists where the value is held (`useOnly`): each
|
|
151
|
+
* secret is that place, a reference's source, a fork's parent, whatever
|
|
152
|
+
* place the call checked its permission at. A copy too, kept sensitive or
|
|
153
|
+
* not: it would land where the workload may read it. The vault opens no
|
|
154
|
+
* plain value, so every key here is a secret or a sensitive one.
|
|
155
|
+
*/
|
|
156
|
+
function useOnlyRefusals(codes, reader, call) {
|
|
157
|
+
const handedBack = call.action === "secret.read" && call.purpose !== "use";
|
|
158
|
+
return call.secrets.map((secret, i) => codes[i] ?? (handedBack && useOnly(reader.live, secret) ? "use_only" : null));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Why `reader` may not make a reference at `holder` to `source`, or null, past
|
|
162
|
+
* what only the database settles (a source of another claim, a reference to
|
|
163
|
+
* itself). A personal config's values are bound to its owner: no reference
|
|
164
|
+
* holds one, nor reads one. Making one hands its source's value to whoever
|
|
165
|
+
* reads the holder, whatever its nature becomes: its maker writes the holder,
|
|
166
|
+
* reads the source there, as any direct access, and, for a sensitive one,
|
|
167
|
+
* which nobody reads, uses it there too (R24). A workload refers only to a
|
|
168
|
+
* value it is handed back, a plain one or one it lists (`useOnly`): it
|
|
169
|
+
* would otherwise move a value it uses to a place whose keys it reads.
|
|
170
|
+
*/
|
|
171
|
+
function referenceRefusal(reader, holder, source, environments, nature) {
|
|
172
|
+
if (environments.get(holder.environmentId)?.owner != null || environments.get(source.environmentId)?.owner != null) return "personal";
|
|
173
|
+
const gone = (place) => environments.get(place.environmentId)?.deleted ?? false;
|
|
174
|
+
const at = placeIn(source, environments);
|
|
175
|
+
return refuses(reader, "secret.write", placeIn(holder, environments), gone(holder)) ?? refuses(reader, "secret.read", at, gone(source)) ?? (nature === "sensitive" ? refuses(reader, "secret.use", at, gone(source)) : null) ?? (nature !== "plain" && useOnly(reader.live, source) ? "use_only" : null);
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
46
178
|
//#region src/store.ts
|
|
47
179
|
/**
|
|
48
180
|
* On Postgres, fail a lock wait in this transaction after `ms`, rather than
|
|
@@ -75,7 +207,7 @@ function memberColumns(db) {
|
|
|
75
207
|
};
|
|
76
208
|
}
|
|
77
209
|
/** A member as their row says (`storedRole`). */
|
|
78
|
-
function memberOf({ owner, role, scope, ...row }) {
|
|
210
|
+
function memberOf$1({ owner, role, scope, ...row }) {
|
|
79
211
|
return {
|
|
80
212
|
...row,
|
|
81
213
|
...storedRole({
|
|
@@ -95,7 +227,7 @@ function storedChange(change) {
|
|
|
95
227
|
}
|
|
96
228
|
async function member(db, principal) {
|
|
97
229
|
const [row] = await memberRow(db, { principal });
|
|
98
|
-
return row === void 0 ? void 0 : memberOf(row);
|
|
230
|
+
return row === void 0 ? void 0 : memberOf$1(row);
|
|
99
231
|
}
|
|
100
232
|
const memberRow = prepared((db) => {
|
|
101
233
|
const { vaultMembers } = tablesOf(db);
|
|
@@ -103,7 +235,7 @@ const memberRow = prepared((db) => {
|
|
|
103
235
|
});
|
|
104
236
|
async function allMembers(db) {
|
|
105
237
|
const { vaultMembers } = tablesOf(db);
|
|
106
|
-
return (await db.select(memberColumns(db)).from(vaultMembers).orderBy(asc(vaultMembers.principal))).map(memberOf);
|
|
238
|
+
return (await db.select(memberColumns(db)).from(vaultMembers).orderBy(asc(vaultMembers.principal))).map(memberOf$1);
|
|
107
239
|
}
|
|
108
240
|
/**
|
|
109
241
|
* The rows that say their member is an active Owner of the whole instance
|
|
@@ -113,7 +245,7 @@ async function allMembers(db) {
|
|
|
113
245
|
*/
|
|
114
246
|
async function instanceOwners(db) {
|
|
115
247
|
const { vaultMembers } = tablesOf(db);
|
|
116
|
-
return (await db.select(memberColumns(db)).from(vaultMembers).where(and(eq(vaultMembers.status, "active"), eq(vaultMembers.role, "owner"))).orderBy(asc(vaultMembers.principal))).map(memberOf).filter(ownsInstance);
|
|
248
|
+
return (await db.select(memberColumns(db)).from(vaultMembers).where(and(eq(vaultMembers.status, "active"), eq(vaultMembers.role, "owner"))).orderBy(asc(vaultMembers.principal))).map(memberOf$1).filter(ownsInstance);
|
|
117
249
|
}
|
|
118
250
|
/**
|
|
119
251
|
* Lock these members' rows for the rest of the transaction, in one order
|
|
@@ -124,7 +256,7 @@ async function instanceOwners(db) {
|
|
|
124
256
|
async function lockMembers(tx, principals) {
|
|
125
257
|
const { vaultMembers } = tablesOf(tx);
|
|
126
258
|
const rows = await forUpdate(tx, tx.select(memberColumns(tx)).from(vaultMembers).where(inArray(vaultMembers.principal, [...new Set(principals)])).orderBy(asc(vaultMembers.principal)), "no key update");
|
|
127
|
-
return new Map(rows.map((row) => [row.principal, memberOf(row)]));
|
|
259
|
+
return new Map(rows.map((row) => [row.principal, memberOf$1(row)]));
|
|
128
260
|
}
|
|
129
261
|
/**
|
|
130
262
|
* Add `row`. A principal has one row: when two decisions admit the same
|
|
@@ -221,25 +353,6 @@ async function everyProjectHolders(db) {
|
|
|
221
353
|
const { vaultGrants } = tablesOf(db);
|
|
222
354
|
return (await db.selectDistinct({ principal: vaultGrants.principal }).from(vaultGrants).where(and(isNull(vaultGrants.projectId), isNull(vaultGrants.environmentId))).orderBy(asc(vaultGrants.principal))).map((row) => row.principal);
|
|
223
355
|
}
|
|
224
|
-
/** The action of an entry that releases a key, which the bulk limit counts. */
|
|
225
|
-
const RELEASE = "secret.read";
|
|
226
|
-
/**
|
|
227
|
-
* How many keys the vault has released to `principal` since `after`: one
|
|
228
|
-
* for an entry about one secret, and as many as it lists for one about
|
|
229
|
-
* several (`secrets`), a read logged once per place.
|
|
230
|
-
*/
|
|
231
|
-
async function releasesSince(db, principal, after) {
|
|
232
|
-
const [{ n }] = await released(db, {
|
|
233
|
-
principal,
|
|
234
|
-
after
|
|
235
|
-
});
|
|
236
|
-
return Number(n ?? 0);
|
|
237
|
-
}
|
|
238
|
-
const released = prepared((db) => {
|
|
239
|
-
const { auditLog } = tablesOf(db);
|
|
240
|
-
const listed = engineOf(db) === "postgres" ? sql`jsonb_array_length((${auditLog.metadata})::jsonb -> 'secrets')` : sql`json_array_length(${auditLog.metadata}, '$.secrets')`;
|
|
241
|
-
return db.select({ n: sql`SUM(COALESCE(${listed}, 1))` }).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.actor, sql.placeholder("principal")), eq(auditLog.action, RELEASE), eq(auditLog.decision, "allow"), gt(auditLog.occurredAt, sql.placeholder("after"))));
|
|
242
|
-
});
|
|
243
356
|
/** Rows of the log as the chain's codec reads them: `author` and `decision` are checked by the table. */
|
|
244
357
|
function stored(rows) {
|
|
245
358
|
return rows;
|
|
@@ -364,8 +477,10 @@ async function natures(db, secretIds) {
|
|
|
364
477
|
}
|
|
365
478
|
/**
|
|
366
479
|
* For each version a rewrap would bring back into its secret, its nature
|
|
367
|
-
*
|
|
368
|
-
*
|
|
480
|
+
* when it is an old value, no looser than it was stored nor than its value
|
|
481
|
+
* was made since (the newest version of its very ciphertext), not the
|
|
482
|
+
* secret's current one; null for the current one, which a mark wraps again
|
|
483
|
+
* with no value changing.
|
|
369
484
|
*/
|
|
370
485
|
async function restoredNatures(db, items) {
|
|
371
486
|
if (items.length === 0) return [];
|
|
@@ -375,8 +490,8 @@ async function restoredNatures(db, items) {
|
|
|
375
490
|
version: secrets.currentVersionId
|
|
376
491
|
}).from(secrets).where(inArray(secrets.id, [...new Set(items.map((item) => item.secretId))])), db.select({
|
|
377
492
|
id: secretVersions.id,
|
|
378
|
-
sensitive: secretVersions.sensitive,
|
|
379
|
-
plain:
|
|
493
|
+
sensitive: sql`(${secretVersions.sensitive} OR (${heldAs(secretVersions, sql`same.sensitive`)}))`,
|
|
494
|
+
plain: sql`(${secretVersions.plaintext} IS NOT NULL AND (${heldAs(secretVersions, sql`same.plaintext IS NOT NULL`)}))`
|
|
380
495
|
}).from(secretVersions).where(inArray(secretVersions.id, [...new Set(items.map((item) => item.from))]))]);
|
|
381
496
|
const currentOf = new Map(current.map((row) => [row.id, row.version]));
|
|
382
497
|
const natureOf = new Map(versions.map((row) => [row.id, Boolean(row.sensitive) ? "sensitive" : Boolean(row.plain) ? "plain" : "secret"]));
|
|
@@ -436,7 +551,7 @@ async function talliedAfter(db, after, limit) {
|
|
|
436
551
|
/** The vault's newest allowed access entry about each member who has one. */
|
|
437
552
|
async function newestAccessEntries(db) {
|
|
438
553
|
const { auditLog } = tablesOf(db);
|
|
439
|
-
const newest = db.select({ seq: sql`max(${auditLog.seq})` }).from(auditLog).where(and(eq(auditLog.author, "vault"), inArray(auditLog.action, [...ACCESS_ACTIONS]), eq(auditLog.decision, "allow"))).groupBy(auditLog.subjectPrincipal);
|
|
554
|
+
const newest = db.select({ seq: sql`max(${auditLog.seq})` }).from(auditLog).where(and(eq(auditLog.author, "vault"), inArray(auditLog.action, [...ACCESS_ACTIONS$1]), eq(auditLog.decision, "allow"))).groupBy(auditLog.subjectPrincipal);
|
|
440
555
|
const rows = stored(await db.select(entryColumns(db)).from(auditLog).where(inArray(auditLog.seq, newest)));
|
|
441
556
|
return new Map(rows.flatMap((row) => row.subjectPrincipal === null ? [] : [[row.subjectPrincipal, row]]));
|
|
442
557
|
}
|
|
@@ -486,7 +601,8 @@ async function versions(db, ids) {
|
|
|
486
601
|
currentVersionId: secrets.currentVersionId,
|
|
487
602
|
sensitive: secretVersions.sensitive,
|
|
488
603
|
newest: sql`(SELECT max(newest.version) FROM secret_versions newest WHERE newest.secret_id = ${secretVersions.secretId})`,
|
|
489
|
-
newestSensitive: sql`(SELECT newest.sensitive FROM secret_versions newest WHERE newest.secret_id = ${secretVersions.secretId} ORDER BY newest.version DESC LIMIT 1)
|
|
604
|
+
newestSensitive: sql`(SELECT newest.sensitive FROM secret_versions newest WHERE newest.secret_id = ${secretVersions.secretId} ORDER BY newest.version DESC LIMIT 1)`,
|
|
605
|
+
heldSensitive: sql`(${heldAs(secretVersions, sql`same.sensitive`)})`
|
|
490
606
|
}).from(secretVersions).innerJoin(secrets, eq(secrets.id, secretVersions.secretId)).innerJoin(environments, eq(environments.id, secrets.environmentId)).innerJoin(projects, eq(projects.id, secrets.projectId)).where(and(inArray(secretVersions.id, [...new Set(ids)]), or(sealed$1(secretVersions.wrappedDek), tombstone(environments.slug), tombstone(projects.slug))))).map((row) => ({
|
|
491
607
|
id: row.id,
|
|
492
608
|
deleted: row.deleted,
|
|
@@ -505,9 +621,18 @@ async function versions(db, ids) {
|
|
|
505
621
|
bytes: row.wrappedDek.toString("base64")
|
|
506
622
|
},
|
|
507
623
|
current: row.currentVersionId === row.id && Number(row.newest) === row.version,
|
|
508
|
-
newestSensitive: Boolean(row.newestSensitive)
|
|
624
|
+
newestSensitive: Boolean(row.newestSensitive),
|
|
625
|
+
heldSensitive: Boolean(row.heldSensitive)
|
|
509
626
|
}));
|
|
510
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* `column` of the newest version of a version's secret with its very ciphertext: its value, as it is held now. Known
|
|
630
|
+
* by its IV and tag, which under one data key name one ciphertext, and which, unlike the ciphertext, no erasure empties.
|
|
631
|
+
*/
|
|
632
|
+
function heldAs(secretVersions, column) {
|
|
633
|
+
return sql`SELECT ${column} FROM secret_versions same WHERE same.secret_id = ${secretVersions.secretId}
|
|
634
|
+
AND same.iv = ${secretVersions.iv} AND same.auth_tag = ${secretVersions.authTag} ORDER BY same.version DESC LIMIT 1`;
|
|
635
|
+
}
|
|
511
636
|
//#endregion
|
|
512
637
|
//#region src/log.ts
|
|
513
638
|
/**
|
|
@@ -994,6 +1119,255 @@ function sealed(key, member, grants) {
|
|
|
994
1119
|
return member.mac.length === expected.length && timingSafeEqual(member.mac, expected);
|
|
995
1120
|
}
|
|
996
1121
|
//#endregion
|
|
1122
|
+
//#region src/snapshot.postgres.ts
|
|
1123
|
+
/** An entry of `audit_log` aliased `e`, as JSON (`RawEntry`): whole numbers past 2^53 as text, bytes as hex. */
|
|
1124
|
+
const ENTRY$1 = sql.raw(`json_build_object(
|
|
1125
|
+
'seq', e.seq::text, 'author', e.author, 'keyId', e.key_id, 'occurredAt', e.occurred_at, 'actor', e.actor, 'action', e.action,
|
|
1126
|
+
'decision', e.decision, 'code', e.code, 'subjectPrincipal', e.subject_principal, 'projectId', e.project_id,
|
|
1127
|
+
'environmentId', e.environment_id, 'secretId', e.secret_id, 'secretVersionId', e.secret_version_id,
|
|
1128
|
+
'operationId', e.operation_id, 'requestId', e.request_id, 'sourceIp', e.source_ip, 'relatedSeq', e.related_seq::text,
|
|
1129
|
+
'metadata', e.metadata, 'prevHash', encode(e.prev_hash, 'hex'), 'mac', encode(e.mac, 'hex'), 'hash', encode(e.hash, 'hex'))`);
|
|
1130
|
+
const NOW$1 = sql.raw("(extract(epoch from clock_timestamp()) * 1000)::bigint");
|
|
1131
|
+
/** `snapshot`'s statement on Postgres: one row, one JSON column. Each part reads one index range. */
|
|
1132
|
+
function snapshotQuery$1(asked) {
|
|
1133
|
+
const principals = sql`${sql.param([...asked.principals])}::text[]`;
|
|
1134
|
+
const environments = sql`${sql.param([...asked.environments ?? []])}::uuid[]`;
|
|
1135
|
+
const references = sql`${sql.param((asked.references ?? []).map(String))}::bigint[]`;
|
|
1136
|
+
const actions = sql`${sql.param([...ACCESS_ACTIONS])}::text[]`;
|
|
1137
|
+
const tomb = `%${TOMBSTONE}%`;
|
|
1138
|
+
const released = asked.released;
|
|
1139
|
+
const head = asked.head === true ? sql`, head AS (SELECT next_seq, head_hash FROM audit_chain_head WHERE only_row FOR UPDATE SKIP LOCKED)` : sql``;
|
|
1140
|
+
return sql`
|
|
1141
|
+
WITH policy AS (
|
|
1142
|
+
SELECT * FROM audit_log WHERE author = 'vault' AND action = 'policy.change' AND decision = 'allow' ORDER BY seq DESC LIMIT 1
|
|
1143
|
+
)${head}
|
|
1144
|
+
SELECT json_build_object(
|
|
1145
|
+
'at', ${NOW$1},
|
|
1146
|
+
'head', ${asked.head === true ? sql`(SELECT json_build_object('nextSeq', next_seq::text, 'hash', encode(head_hash, 'hex')) FROM head)` : sql`NULL`},
|
|
1147
|
+
'last', ${asked.head === true ? sql`(SELECT json_build_object('seq', seq::text, 'hash', encode(hash, 'hex')) FROM audit_log ORDER BY seq DESC LIMIT 1)` : sql`NULL`},
|
|
1148
|
+
'rotation', ${asked.head === true ? sql`(SELECT ${ENTRY$1} FROM audit_log e WHERE author = 'vault' AND action = 'key.rotate' AND decision = 'allow' ORDER BY seq DESC LIMIT 1)` : sql`NULL`},
|
|
1149
|
+
'members', ${asked.members === true ? sql`(SELECT coalesce(json_agg(json_build_object(
|
|
1150
|
+
'principal', principal, 'status', status, 'owner', owner, 'role', role, 'scope', scope, 'generation', generation,
|
|
1151
|
+
'createdAt', created_at, 'createdBy', created_by, 'statusChangedAt', status_changed_at, 'statusChangedBy', status_changed_by,
|
|
1152
|
+
'accessSeq', access_seq::text, 'mac', encode(mac, 'hex'))), '[]') FROM vault_members WHERE principal = ANY(${principals}))` : sql`'[]'::json`},
|
|
1153
|
+
'grants', (
|
|
1154
|
+
SELECT coalesce(json_agg(json_build_object(
|
|
1155
|
+
'principal', g.principal, 'projectId', coalesce(g.project_id, env.project_id), 'environmentId', g.environment_id,
|
|
1156
|
+
'environmentSlug', g.environment_slug, 'role', g.role, 'expiresAt', g.expires_at, 'grantedAt', g.granted_at, 'grantedBy', g.granted_by)), '[]')
|
|
1157
|
+
FROM vault_grants g LEFT JOIN environments env ON env.id = g.environment_id
|
|
1158
|
+
WHERE g.principal = ANY(${principals})
|
|
1159
|
+
),
|
|
1160
|
+
'access', (
|
|
1161
|
+
SELECT coalesce(json_agg(${ENTRY$1}), '[]')
|
|
1162
|
+
FROM unnest(${principals}) AS asked (principal)
|
|
1163
|
+
CROSS JOIN LATERAL (
|
|
1164
|
+
SELECT * FROM audit_log
|
|
1165
|
+
WHERE author = 'vault' AND subject_principal = asked.principal AND action = ANY(${actions}) AND decision = 'allow'
|
|
1166
|
+
ORDER BY seq DESC LIMIT 1
|
|
1167
|
+
) e
|
|
1168
|
+
),
|
|
1169
|
+
'policy', (SELECT ${ENTRY$1} FROM policy e),
|
|
1170
|
+
'environments', (
|
|
1171
|
+
SELECT coalesce(json_agg(json_build_object(
|
|
1172
|
+
'id', env.id, 'projectId', env.project_id, 'slug', env.slug, 'owner', env.owner, 'parentId', env.parent_id,
|
|
1173
|
+
'deleted', env.slug LIKE ${tomb} OR project.slug LIKE ${tomb},
|
|
1174
|
+
'archived', env.archived_at IS NOT NULL OR project.archived_at IS NOT NULL)), '[]')
|
|
1175
|
+
FROM environments env JOIN projects project ON project.id = env.project_id
|
|
1176
|
+
-- One list of ids, each found by the primary key.
|
|
1177
|
+
WHERE env.id = ANY(ARRAY(
|
|
1178
|
+
SELECT unnest(${environments})
|
|
1179
|
+
UNION SELECT parent_id FROM environments WHERE id = ANY(${environments}) AND owner IS NOT NULL
|
|
1180
|
+
-- What the newest policy protects, by the ids its JSON names; one that is no UUID names nothing.
|
|
1181
|
+
UNION SELECT (place #>> '{}')::uuid FROM policy, jsonb_path_query(policy.metadata::jsonb, '$.policy.protected[*].environmentId') AS place
|
|
1182
|
+
WHERE (place #>> '{}') ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
1183
|
+
))
|
|
1184
|
+
),
|
|
1185
|
+
'ended', (
|
|
1186
|
+
SELECT coalesce(json_agg(related_seq::text), '[]') FROM audit_log
|
|
1187
|
+
WHERE author = 'vault' AND action = 'reference.end' AND decision = 'allow' AND related_seq = ANY(${references})
|
|
1188
|
+
),
|
|
1189
|
+
'released', ${released === void 0 ? sql`0` : sql`(
|
|
1190
|
+
SELECT count(DISTINCT key) FROM (
|
|
1191
|
+
-- A read of one key names it; of several at one place, lists them (readsByPlace, vault.ts). An entry, or an
|
|
1192
|
+
-- item of its list, that names no key, which only a forged entry is, counts as one key of its own.
|
|
1193
|
+
SELECT coalesce(e.secret_id::text, CASE jsonb_typeof(one -> 'secretId') WHEN 'string' THEN one ->> 'secretId' END, 'entry:' || e.seq) FROM audit_log e
|
|
1194
|
+
-- Each item of a list, never what lax paths would make of an object; silent, as strict paths fail on what is missing.
|
|
1195
|
+
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
|
|
1196
|
+
WHERE e.author = 'vault' AND e.actor = ${released.principal} AND e.action = 'secret.read' AND e.decision = 'allow'
|
|
1197
|
+
AND e.occurred_at > ${NOW$1} + ${released.clockOffsetMs} - ${released.windowMs}
|
|
1198
|
+
UNION ALL SELECT unnest(${sql.param([...released.keys])}::text[])
|
|
1199
|
+
) AS released (key)
|
|
1200
|
+
)`}
|
|
1201
|
+
) AS snapshot`;
|
|
1202
|
+
}
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/snapshot.sqlite.ts
|
|
1205
|
+
/**
|
|
1206
|
+
* An entry of `audit_log` aliased `e`, as JSON (`RawEntry`), bytes as hex.
|
|
1207
|
+
* A JSON value loses its type leaving a subquery, so whoever takes one
|
|
1208
|
+
* from a subquery wraps it in `json(…)`.
|
|
1209
|
+
*/
|
|
1210
|
+
const ENTRY = sql.raw(`json_object(
|
|
1211
|
+
'seq', e.seq, 'author', e.author, 'keyId', e.key_id, 'occurredAt', e.occurred_at, 'actor', e.actor, 'action', e.action,
|
|
1212
|
+
'decision', e.decision, 'code', e.code, 'subjectPrincipal', e.subject_principal, 'projectId', e.project_id,
|
|
1213
|
+
'environmentId', e.environment_id, 'secretId', e.secret_id, 'secretVersionId', e.secret_version_id,
|
|
1214
|
+
'operationId', e.operation_id, 'requestId', e.request_id, 'sourceIp', e.source_ip, 'relatedSeq', e.related_seq,
|
|
1215
|
+
'metadata', e.metadata, 'prevHash', hex(e.prev_hash), 'mac', hex(e.mac), 'hash', hex(e.hash))`);
|
|
1216
|
+
const NOW = sql.raw(`CAST(unixepoch('subsec') * 1000 AS INTEGER)`);
|
|
1217
|
+
/**
|
|
1218
|
+
* `snapshot`'s statement on SQLite, the same as on Postgres. Nothing is
|
|
1219
|
+
* locked: one transaction runs at a time (connect.ts), so the head is the
|
|
1220
|
+
* last entry's.
|
|
1221
|
+
*/
|
|
1222
|
+
function snapshotQuery(asked) {
|
|
1223
|
+
const principals = JSON.stringify(asked.principals);
|
|
1224
|
+
const environments = JSON.stringify(asked.environments ?? []);
|
|
1225
|
+
const references = JSON.stringify((asked.references ?? []).map(Number));
|
|
1226
|
+
const actions = JSON.stringify(ACCESS_ACTIONS);
|
|
1227
|
+
const tomb = `%${TOMBSTONE}%`;
|
|
1228
|
+
const released = asked.released;
|
|
1229
|
+
const head = asked.head === true;
|
|
1230
|
+
return sql`
|
|
1231
|
+
WITH policy AS (
|
|
1232
|
+
SELECT * FROM audit_log WHERE author = 'vault' AND action = 'policy.change' AND decision = 'allow' ORDER BY seq DESC LIMIT 1
|
|
1233
|
+
)
|
|
1234
|
+
SELECT json_object(
|
|
1235
|
+
'at', ${NOW},
|
|
1236
|
+
'head', ${head ? sql`json((SELECT json_object('nextSeq', next_seq, 'hash', hex(head_hash)) FROM audit_chain_head WHERE only_row))` : sql`NULL`},
|
|
1237
|
+
'last', ${head ? sql`json((SELECT json_object('seq', seq, 'hash', hex(hash)) FROM audit_log ORDER BY seq DESC LIMIT 1))` : sql`NULL`},
|
|
1238
|
+
'rotation', ${head ? sql`json((SELECT ${ENTRY} FROM audit_log e WHERE author = 'vault' AND action = 'key.rotate' AND decision = 'allow' ORDER BY seq DESC LIMIT 1))` : sql`NULL`},
|
|
1239
|
+
'members', json(${asked.members === true ? sql`(SELECT json_group_array(json_object(
|
|
1240
|
+
'principal', principal, 'status', status, 'owner', owner, 'role', role, 'scope', scope, 'generation', generation,
|
|
1241
|
+
'createdAt', created_at, 'createdBy', created_by, 'statusChangedAt', status_changed_at, 'statusChangedBy', status_changed_by,
|
|
1242
|
+
'accessSeq', access_seq, 'mac', hex(mac))) FROM vault_members WHERE principal IN (SELECT value FROM json_each(${principals})))` : sql`'[]'`}),
|
|
1243
|
+
'grants', json((
|
|
1244
|
+
SELECT json_group_array(json_object(
|
|
1245
|
+
'principal', g.principal, 'projectId', coalesce(g.project_id, env.project_id), 'environmentId', g.environment_id,
|
|
1246
|
+
'environmentSlug', g.environment_slug, 'role', g.role, 'expiresAt', g.expires_at, 'grantedAt', g.granted_at, 'grantedBy', g.granted_by))
|
|
1247
|
+
FROM vault_grants g LEFT JOIN environments env ON env.id = g.environment_id
|
|
1248
|
+
WHERE g.principal IN (SELECT value FROM json_each(${principals}))
|
|
1249
|
+
)),
|
|
1250
|
+
'access', json((
|
|
1251
|
+
SELECT json_group_array(json(newest)) FROM (
|
|
1252
|
+
SELECT (
|
|
1253
|
+
SELECT ${ENTRY} FROM audit_log e
|
|
1254
|
+
WHERE author = 'vault' AND subject_principal = asked.value AND action IN (SELECT value FROM json_each(${actions})) AND decision = 'allow'
|
|
1255
|
+
ORDER BY seq DESC LIMIT 1
|
|
1256
|
+
) AS newest
|
|
1257
|
+
FROM json_each(${principals}) AS asked
|
|
1258
|
+
) WHERE newest IS NOT NULL
|
|
1259
|
+
)),
|
|
1260
|
+
'policy', json((SELECT ${ENTRY} FROM policy e)),
|
|
1261
|
+
'environments', json((
|
|
1262
|
+
SELECT json_group_array(json_object(
|
|
1263
|
+
'id', env.id, 'projectId', env.project_id, 'slug', env.slug, 'owner', env.owner, 'parentId', env.parent_id,
|
|
1264
|
+
'deleted', env.slug LIKE ${tomb} OR project.slug LIKE ${tomb},
|
|
1265
|
+
'archived', env.archived_at IS NOT NULL OR project.archived_at IS NOT NULL))
|
|
1266
|
+
FROM environments env JOIN projects project ON project.id = env.project_id
|
|
1267
|
+
WHERE env.id IN (SELECT value FROM json_each(${environments}))
|
|
1268
|
+
OR env.id IN (SELECT parent_id FROM environments WHERE id IN (SELECT value FROM json_each(${environments})) AND owner IS NOT NULL)
|
|
1269
|
+
OR env.id IN (
|
|
1270
|
+
SELECT json_extract(place.value, '$.environmentId')
|
|
1271
|
+
FROM policy, json_each(policy.metadata, '$.policy.protected') AS place
|
|
1272
|
+
-- Of a forged entry's, an element that is no object names nothing: json_extract would fail on a string.
|
|
1273
|
+
WHERE json_valid(policy.metadata) AND json_type(policy.metadata, '$.policy.protected') = 'array' AND place.type = 'object'
|
|
1274
|
+
)
|
|
1275
|
+
)),
|
|
1276
|
+
'ended', json((
|
|
1277
|
+
SELECT json_group_array(related_seq) FROM audit_log
|
|
1278
|
+
WHERE author = 'vault' AND action = 'reference.end' AND decision = 'allow' AND related_seq IN (SELECT value FROM json_each(${references}))
|
|
1279
|
+
)),
|
|
1280
|
+
'released', ${released === void 0 ? sql`0` : sql`(
|
|
1281
|
+
SELECT count(DISTINCT key) FROM (
|
|
1282
|
+
-- A read of one key names it; of several at one place, lists them (readsByPlace, vault.ts). An entry, or an
|
|
1283
|
+
-- item of its list, that names no key, which only a forged entry is, counts as one key of its own.
|
|
1284
|
+
SELECT coalesce(e.secret_id, CASE WHEN one.type = 'object' AND json_type(one.value, '$.secretId') = 'text' THEN json_extract(one.value, '$.secretId') END, 'entry:' || e.seq) AS key
|
|
1285
|
+
FROM audit_log e
|
|
1286
|
+
LEFT JOIN json_each(CASE WHEN e.secret_id IS NULL AND json_type(e.metadata, '$.secrets') = 'array' THEN e.metadata END, '$.secrets') AS one
|
|
1287
|
+
WHERE e.author = 'vault' AND e.actor = ${released.principal} AND e.action = 'secret.read' AND e.decision = 'allow'
|
|
1288
|
+
AND e.occurred_at > ${NOW} + ${released.clockOffsetMs} - ${released.windowMs}
|
|
1289
|
+
UNION ALL SELECT value FROM json_each(${JSON.stringify(released.keys)})
|
|
1290
|
+
)
|
|
1291
|
+
)`}
|
|
1292
|
+
) AS snapshot`;
|
|
1293
|
+
}
|
|
1294
|
+
//#endregion
|
|
1295
|
+
//#region src/snapshot.ts
|
|
1296
|
+
async function snapshot(db, asked) {
|
|
1297
|
+
const read = () => readOnce(db, asked);
|
|
1298
|
+
const first = await read();
|
|
1299
|
+
if (asked.head !== true || first.head !== null && current(first.head)) return first;
|
|
1300
|
+
if (first.head === null) await lockLogHead(db);
|
|
1301
|
+
const again = await read();
|
|
1302
|
+
if (again.head === null) throw new Error("the log's head, held by this transaction, was read as held by another");
|
|
1303
|
+
if (!current(again.head)) throw new LogHeadMismatch(`the log's head says entry ${again.head.nextSeq} comes next, but the log ends ${again.head.last === null ? "empty" : `at entry ${again.head.last.seq}`}, or with another hash`);
|
|
1304
|
+
return again;
|
|
1305
|
+
}
|
|
1306
|
+
/** Whether the head names the last entry the same statement saw: nothing was appended between its snapshot and its lock. */
|
|
1307
|
+
function current(head) {
|
|
1308
|
+
if (head.last === null) return head.nextSeq === 0n;
|
|
1309
|
+
return head.nextSeq === head.last.seq + 1n && head.headHash.equals(head.last.hash);
|
|
1310
|
+
}
|
|
1311
|
+
async function readOnce(db, asked) {
|
|
1312
|
+
const query = (engineOf(db) === "postgres" ? snapshotQuery$1 : snapshotQuery)(asked);
|
|
1313
|
+
const [row] = await rowsOf(db, query);
|
|
1314
|
+
const raw = typeof row.snapshot === "string" ? JSON.parse(row.snapshot) : row.snapshot;
|
|
1315
|
+
return {
|
|
1316
|
+
at: Number(raw.at),
|
|
1317
|
+
members: new Map(raw.members.map((member) => [member.principal, memberOf(member)])),
|
|
1318
|
+
grants: new Map(asked.principals.map((principal) => [principal, raw.grants.filter((grant) => grant.principal === principal).map(grantOf)])),
|
|
1319
|
+
newestAccess: new Map(raw.access.map((entry) => [entry.subjectPrincipal, entryOf(entry)])),
|
|
1320
|
+
newestPolicy: raw.policy === null ? null : entryOf(raw.policy),
|
|
1321
|
+
environments: new Map(raw.environments.map(({ id, ...environment }) => [id, {
|
|
1322
|
+
...environment,
|
|
1323
|
+
deleted: Boolean(environment.deleted),
|
|
1324
|
+
archived: Boolean(environment.archived)
|
|
1325
|
+
}])),
|
|
1326
|
+
ended: new Set(raw.ended.map((seq) => BigInt(seq))),
|
|
1327
|
+
released: Number(raw.released ?? 0),
|
|
1328
|
+
head: raw.head === null ? null : {
|
|
1329
|
+
nextSeq: BigInt(raw.head.nextSeq),
|
|
1330
|
+
headHash: Buffer.from(raw.head.hash, "hex"),
|
|
1331
|
+
last: raw.last === null ? null : {
|
|
1332
|
+
seq: BigInt(raw.last.seq),
|
|
1333
|
+
hash: Buffer.from(raw.last.hash, "hex")
|
|
1334
|
+
},
|
|
1335
|
+
rotation: raw.rotation === null ? null : entryOf(raw.rotation)
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
/** An entry as the chain's codec reads it, from what `entryJson` wrote. */
|
|
1340
|
+
function entryOf(raw) {
|
|
1341
|
+
return {
|
|
1342
|
+
...raw,
|
|
1343
|
+
seq: BigInt(raw.seq),
|
|
1344
|
+
author: raw.author,
|
|
1345
|
+
occurredAt: Number(raw.occurredAt),
|
|
1346
|
+
relatedSeq: raw.relatedSeq === null ? null : BigInt(raw.relatedSeq),
|
|
1347
|
+
prevHash: Buffer.from(raw.prevHash, "hex"),
|
|
1348
|
+
mac: Buffer.from(raw.mac, "hex"),
|
|
1349
|
+
hash: Buffer.from(raw.hash, "hex")
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
function grantOf(raw) {
|
|
1353
|
+
return {
|
|
1354
|
+
...raw,
|
|
1355
|
+
expiresAt: raw.expiresAt === null ? null : Number(raw.expiresAt),
|
|
1356
|
+
grantedAt: Number(raw.grantedAt)
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function memberOf({ owner, ...raw }) {
|
|
1360
|
+
return memberOf$1({
|
|
1361
|
+
...raw,
|
|
1362
|
+
owner: owner === true || owner === 1,
|
|
1363
|
+
generation: Number(raw.generation),
|
|
1364
|
+
createdAt: Number(raw.createdAt),
|
|
1365
|
+
statusChangedAt: Number(raw.statusChangedAt),
|
|
1366
|
+
accessSeq: BigInt(raw.accessSeq),
|
|
1367
|
+
mac: Buffer.from(raw.mac, "hex")
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
//#endregion
|
|
997
1371
|
//#region src/accounting.ts
|
|
998
1372
|
const KEY_ACTIONS = /* @__PURE__ */ new Set([
|
|
999
1373
|
"secret.read",
|
|
@@ -1358,7 +1732,7 @@ var Walk = class {
|
|
|
1358
1732
|
};
|
|
1359
1733
|
/** The action of the vault's entry that signs a prefix of the log. */
|
|
1360
1734
|
const CHECKPOINT = "audit.checkpoint";
|
|
1361
|
-
const ACCESS = new Set(ACCESS_ACTIONS);
|
|
1735
|
+
const ACCESS = new Set(ACCESS_ACTIONS$1);
|
|
1362
1736
|
/** JSON with every object's keys sorted: one text for one value, however it was built. */
|
|
1363
1737
|
function canonical(value) {
|
|
1364
1738
|
return JSON.stringify(value, (_, inner) => inner !== null && typeof inner === "object" && !Array.isArray(inner) ? Object.fromEntries(Object.entries(inner).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) : inner);
|
|
@@ -1493,7 +1867,7 @@ var Outage = class {
|
|
|
1493
1867
|
}
|
|
1494
1868
|
};
|
|
1495
1869
|
/** A rewrap that would make a value less sensitive than it is. */
|
|
1496
|
-
const LOOSER = "a value is made less sensitive only by writing it again: an old version comes back, and a key is made stricter, as sensitive as its newest version or more";
|
|
1870
|
+
const LOOSER = "a value is made less sensitive only by writing it again: an old version comes back, and a key is made stricter, as sensitive as its newest version or more; only its current sensitive value is made a secret in place, by an Owner of its project";
|
|
1497
1871
|
function refusal(code, message) {
|
|
1498
1872
|
return {
|
|
1499
1873
|
code,
|
|
@@ -1511,6 +1885,8 @@ const MESSAGES = {
|
|
|
1511
1885
|
ended: "the reference read through was ended",
|
|
1512
1886
|
personal: "a personal config's values are its owner's alone",
|
|
1513
1887
|
sensitive: "a sensitive value is never shown to anyone, Owners included: `coffre run` gives your app a placeholder for it through the credential proxy, and a service account reads it",
|
|
1888
|
+
not_owner_issued: "a service account reads a sensitive value only on a token or trust binding an Owner of its project issued",
|
|
1889
|
+
use_only: "a workload uses this value through coffre and never holds it: list it in its binding's reads, where the value is held, if it must",
|
|
1514
1890
|
not_allowed: "not allowed to change this",
|
|
1515
1891
|
last_owner: "the last Owner of the whole instance stays one: make someone else an Owner first",
|
|
1516
1892
|
invalid: "not something the rules allow",
|
|
@@ -1521,21 +1897,30 @@ const MESSAGES = {
|
|
|
1521
1897
|
};
|
|
1522
1898
|
/**
|
|
1523
1899
|
* What the one asking holds through the credential their request came in
|
|
1524
|
-
* on: no more than its scope (`allows`)
|
|
1525
|
-
*
|
|
1900
|
+
* on: no more than its scope (`allows`), and, a workload's, handed back
|
|
1901
|
+
* only what the scope lists (`useOnly`). A scope that is not one, a kind of
|
|
1902
|
+
* caller its principal cannot be, or a workload with no scope to list what
|
|
1903
|
+
* it reads, is a bug of the app's, which fails the call. None said is an
|
|
1904
|
+
* app older than 0.11, which has no workloads (`Through`).
|
|
1526
1905
|
*/
|
|
1527
|
-
function through(standing, { credentialScope }) {
|
|
1528
|
-
|
|
1529
|
-
if (!
|
|
1906
|
+
function through(standing, { credentialScope, caller }) {
|
|
1907
|
+
const { principal } = standing;
|
|
1908
|
+
if (caller !== void 0 && !(caller === "person" ? principal.startsWith("user:") : (caller === "bearer" || caller === "workload") && principal.startsWith("token:"))) throw new Error(`caller must be person for a user: principal, bearer or workload for a token: one, got: ${String(caller)}`);
|
|
1909
|
+
if (caller === "workload" && credentialScope == null) throw new Error("a workload caller comes with its credential scope, which lists what it reads");
|
|
1910
|
+
if (credentialScope != null && !isCredentialScope(credentialScope)) throw new Error("credentialScope must be a list of grants, one a place");
|
|
1911
|
+
const held = {
|
|
1912
|
+
...credentialScope == null ? {} : { credentialScope },
|
|
1913
|
+
...caller === void 0 ? {} : { kind: caller }
|
|
1914
|
+
};
|
|
1530
1915
|
return {
|
|
1531
1916
|
...standing,
|
|
1532
1917
|
live: {
|
|
1533
1918
|
...standing.live,
|
|
1534
|
-
|
|
1919
|
+
...held
|
|
1535
1920
|
},
|
|
1536
1921
|
all: {
|
|
1537
1922
|
...standing.all,
|
|
1538
|
-
|
|
1923
|
+
...held
|
|
1539
1924
|
}
|
|
1540
1925
|
};
|
|
1541
1926
|
}
|
|
@@ -1571,6 +1956,24 @@ var VaultService = class {
|
|
|
1571
1956
|
async #now(db) {
|
|
1572
1957
|
return await now(db) + this.#prepared.options.clockOffset();
|
|
1573
1958
|
}
|
|
1959
|
+
/** A snapshot's clock, as the vault keeps time (`#now`). */
|
|
1960
|
+
#at(reads) {
|
|
1961
|
+
return reads.at + this.#prepared.options.clockOffset();
|
|
1962
|
+
}
|
|
1963
|
+
/** What a decision about `principals` reads, and `asked` besides, in one statement: under the head when `head` is set. */
|
|
1964
|
+
#snapshot(tx, principals, asked, head) {
|
|
1965
|
+
const { released, ...rest } = asked;
|
|
1966
|
+
return snapshot(tx, {
|
|
1967
|
+
...rest,
|
|
1968
|
+
principals,
|
|
1969
|
+
head,
|
|
1970
|
+
...released === void 0 ? {} : { released: {
|
|
1971
|
+
...released,
|
|
1972
|
+
windowMs: this.#config.bulkLimit.windowMs,
|
|
1973
|
+
clockOffsetMs: this.#prepared.options.clockOffset()
|
|
1974
|
+
} }
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1574
1977
|
/**
|
|
1575
1978
|
* Decide in one transaction, with `principals`' rows locked first, then
|
|
1576
1979
|
* the log's head. A `Refused` or an `Outage` rolls back all but its own
|
|
@@ -1583,34 +1986,35 @@ var VaultService = class {
|
|
|
1583
1986
|
* alone takes the head later (`late`), once its key service has answered,
|
|
1584
1987
|
* which it must not wait on holding it, and decides again then (`#keys`).
|
|
1585
1988
|
*/
|
|
1586
|
-
async #decide(principals, decide, late = false) {
|
|
1989
|
+
async #decide(principals, decide, late = false, asked = {}) {
|
|
1587
1990
|
const superseded = await this.#settled();
|
|
1588
1991
|
if (superseded !== null) return {
|
|
1589
1992
|
ok: false,
|
|
1590
1993
|
refusal: refusal("wrong_kek", superseded)
|
|
1591
1994
|
};
|
|
1592
|
-
return this.#decideNow(principals, decide, late);
|
|
1995
|
+
return this.#decideNow(principals, decide, late, asked);
|
|
1593
1996
|
}
|
|
1594
1997
|
/** `#decide`, once the vault is settled. */
|
|
1595
|
-
async #decideNow(principals, decide, late = false) {
|
|
1998
|
+
async #decideNow(principals, decide, late = false, asked = {}) {
|
|
1596
1999
|
const reports = [];
|
|
1597
2000
|
for (let attempt = 1;; attempt += 1) try {
|
|
1598
|
-
return await this.#decideOnce(principals, decide, reports, late);
|
|
2001
|
+
return await this.#decideOnce(principals, decide, reports, late, asked);
|
|
1599
2002
|
} catch (error) {
|
|
1600
2003
|
if (attempt < 3 && (error instanceof Retry || isUniqueViolation(error))) continue;
|
|
1601
2004
|
throw error;
|
|
1602
2005
|
}
|
|
1603
2006
|
}
|
|
1604
|
-
async #decideOnce(principals, decide, reports, late) {
|
|
2007
|
+
async #decideOnce(principals, decide, reports, late, asked) {
|
|
1605
2008
|
try {
|
|
1606
2009
|
const result = await this.#db.transaction(async (tx) => {
|
|
1607
2010
|
await boundLockWaits(tx, LOCK_TIMEOUT_MS);
|
|
1608
2011
|
const members = principals.length === 0 ? /* @__PURE__ */ new Map() : await lockMembers(tx, principals);
|
|
1609
|
-
|
|
2012
|
+
const reads = await this.#snapshot(tx, principals, asked, !late);
|
|
1610
2013
|
const d = {
|
|
1611
2014
|
tx,
|
|
1612
2015
|
members,
|
|
1613
|
-
at:
|
|
2016
|
+
at: this.#at(reads),
|
|
2017
|
+
reads,
|
|
1614
2018
|
log: [],
|
|
1615
2019
|
writes: [],
|
|
1616
2020
|
touched: /* @__PURE__ */ new Set(),
|
|
@@ -1624,7 +2028,8 @@ var VaultService = class {
|
|
|
1624
2028
|
let at = d.at;
|
|
1625
2029
|
const accessSeq = /* @__PURE__ */ new Map();
|
|
1626
2030
|
if (entries.length > 0) {
|
|
1627
|
-
|
|
2031
|
+
if (d.reads.head === null) throw new Error("a decision appends only under the head it read with");
|
|
2032
|
+
const appended = await this.#append(tx, entries, d.underLock, d.reads);
|
|
1628
2033
|
at = appended.occurredAt;
|
|
1629
2034
|
entries.forEach((entry, i) => {
|
|
1630
2035
|
if (isAccessEntry(entry)) accessSeq.set(entry.subjectPrincipal, appended.seqStart + BigInt(i));
|
|
@@ -1689,12 +2094,12 @@ var VaultService = class {
|
|
|
1689
2094
|
* MAC are passed over, and reported: otherwise whoever can insert a row
|
|
1690
2095
|
* could lock any member out.
|
|
1691
2096
|
*/
|
|
1692
|
-
async #integrity(db, principal, row, grants, reports) {
|
|
2097
|
+
async #integrity(db, principal, row, grants, reports, reads) {
|
|
1693
2098
|
if (row !== void 0 && !this.#sealed(row, grants)) {
|
|
1694
2099
|
this.#report(reports, principal, "mac", row.mac.toString("hex"));
|
|
1695
2100
|
return "mac";
|
|
1696
2101
|
}
|
|
1697
|
-
const newest = await this.#newestAccess(db, principal, reports);
|
|
2102
|
+
const newest = await this.#newestAccess(db, principal, reports, reads);
|
|
1698
2103
|
const seq = typeof newest !== "string" ? newest.seq : newest === "unreached" ? void 0 : null;
|
|
1699
2104
|
if (row === void 0 ? seq === null : seq === row.accessSeq) return null;
|
|
1700
2105
|
if (row === void 0 && await member(db, principal) !== void 0) throw new Retry();
|
|
@@ -1702,8 +2107,10 @@ var VaultService = class {
|
|
|
1702
2107
|
return "stale";
|
|
1703
2108
|
}
|
|
1704
2109
|
/** The newest access entry about `principal` that carries the vault's MAC (`newestAccess` in @coffre/db/grants). */
|
|
1705
|
-
#newestAccess(db, principal, reports) {
|
|
1706
|
-
return newestAccess(db, principal, this.#check(reports, principal));
|
|
2110
|
+
#newestAccess(db, principal, reports, reads) {
|
|
2111
|
+
if (reads === void 0 || !reads.grants.has(principal)) return newestAccess(db, principal, this.#check(reports, principal));
|
|
2112
|
+
const newest = reads.newestAccess.get(principal);
|
|
2113
|
+
return newestAuthentic((limit, before) => before === void 0 ? Promise.resolve(newest === void 0 ? [] : [newest]) : accessEntriesAbout(db, principal, limit, before), this.#check(reports, principal));
|
|
1707
2114
|
}
|
|
1708
2115
|
/** Entries in the vault's name told apart by their MACs, each it did not write reported, about `principal`. */
|
|
1709
2116
|
#check(reports, principal) {
|
|
@@ -1801,7 +2208,7 @@ var VaultService = class {
|
|
|
1801
2208
|
}
|
|
1802
2209
|
async #replaceEveryProject(d, principal) {
|
|
1803
2210
|
const row = d.members.get(principal);
|
|
1804
|
-
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
2211
|
+
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports, d.reads);
|
|
1805
2212
|
if (row === void 0 || subject.status !== "active") return;
|
|
1806
2213
|
const everyProject = subject.stored.filter((grant) => grant.projectId === null && grant.environmentId === null);
|
|
1807
2214
|
if (everyProject.length === 0) return;
|
|
@@ -1953,16 +2360,31 @@ var VaultService = class {
|
|
|
1953
2360
|
* that an instance still running with the replaced KEK, as during a
|
|
1954
2361
|
* deploy, cannot write after the rotation.
|
|
1955
2362
|
*/
|
|
1956
|
-
#append(tx, entries, underLock = []) {
|
|
2363
|
+
async #append(tx, entries, underLock = [], held) {
|
|
1957
2364
|
const prepared = this.#prepared;
|
|
1958
|
-
|
|
1959
|
-
|
|
2365
|
+
const head = held?.head ?? void 0;
|
|
2366
|
+
const appended = await appendEntries(tx, prepared.logKey, entries, async (locked) => {
|
|
2367
|
+
const rotation = head === void 0 ? await latestVaultEntry(locked, [KEY_ROTATE]) : head.rotation ?? void 0;
|
|
1960
2368
|
if (rotation !== void 0 && !prepared.logKeys.some((key) => key.keyId === rotation.keyId)) {
|
|
1961
2369
|
prepared.superseded = `the log moved on to ${rotation.keyId}, a key this vault does not hold: a vault given a newer vault key replaced it, and a replaced key writes nothing more`;
|
|
1962
2370
|
throw new Error(prepared.superseded);
|
|
1963
2371
|
}
|
|
1964
2372
|
for (const check of underLock) await check(locked);
|
|
2373
|
+
}, head === void 0 ? void 0 : {
|
|
2374
|
+
head,
|
|
2375
|
+
last: head.last,
|
|
2376
|
+
at: held.at
|
|
1965
2377
|
});
|
|
2378
|
+
if (head !== void 0) held.head = {
|
|
2379
|
+
...head,
|
|
2380
|
+
nextSeq: appended.nextSeq,
|
|
2381
|
+
headHash: appended.headHash,
|
|
2382
|
+
last: {
|
|
2383
|
+
seq: appended.nextSeq - 1n,
|
|
2384
|
+
hash: appended.headHash
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
return appended;
|
|
1966
2388
|
}
|
|
1967
2389
|
async unwrap(input) {
|
|
1968
2390
|
const { principal } = input;
|
|
@@ -2019,10 +2441,9 @@ var VaultService = class {
|
|
|
2019
2441
|
permission: input.purpose === "use" ? "secret.use" : "secret.read",
|
|
2020
2442
|
secrets: items.map((item) => item.secret),
|
|
2021
2443
|
places: sealed.map((seal, i) => seal?.holder ?? items[i].secret),
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
},
|
|
2444
|
+
viaReference: sealed.map((seal) => seal !== null),
|
|
2445
|
+
references: ends,
|
|
2446
|
+
vet: ends.length === 0 ? void 0 : async (_, reads) => sealed.map((seal) => seal !== null && reads.ended.has(seal.seq) ? "ended" : null),
|
|
2026
2447
|
remote,
|
|
2027
2448
|
entry,
|
|
2028
2449
|
input
|
|
@@ -2084,13 +2505,19 @@ var VaultService = class {
|
|
|
2084
2505
|
...keyEntry("key.rewrap", principal, source.secret, "deny", "bad_claim", input),
|
|
2085
2506
|
secretVersionId: source.id
|
|
2086
2507
|
})));
|
|
2087
|
-
|
|
2508
|
+
const loosens = items.map((item, i) => item.secret.sensitive !== true && loaded.versions[i].secret.sensitive === true && loaded.versions[i].current);
|
|
2509
|
+
const strict = (i) => loaded.versions[i].secret.sensitive || loaded.versions[i].heldSensitive || loaded.versions[i].newestSensitive;
|
|
2510
|
+
if (items.some((item, i) => !loosens[i] && item.secret.sensitive !== true && strict(i))) return this.#refuseVersions(principal, "invalid", loaded.versions.map((source) => ({
|
|
2088
2511
|
...keyEntry("key.rewrap", principal, source.secret, "deny", "invalid", input),
|
|
2089
2512
|
secretVersionId: source.id
|
|
2090
2513
|
})), LOOSER);
|
|
2091
2514
|
const sources = new Map(items.map((item, i) => [item.secret, loaded.versions[i]]));
|
|
2515
|
+
const loosened = new Set(items.flatMap((item, i) => loosens[i] ? [item.secret] : []));
|
|
2092
2516
|
const entry = (secret, decision, code) => ({
|
|
2093
|
-
...keyEntry("key.rewrap", principal, secret, decision, code, input, {
|
|
2517
|
+
...keyEntry("key.rewrap", principal, secret, decision, code, input, {
|
|
2518
|
+
from: sources.get(secret).secret.version,
|
|
2519
|
+
...loosened.has(secret) ? { was: "sensitive" } : {}
|
|
2520
|
+
}),
|
|
2094
2521
|
secretVersionId: sources.get(secret).id
|
|
2095
2522
|
});
|
|
2096
2523
|
const remote = this.#remote(this.#config.keks.primary) || items.some(({ wrapped }) => this.#remote(this.#config.keks.providerOf(wrapped)));
|
|
@@ -2104,8 +2531,14 @@ var VaultService = class {
|
|
|
2104
2531
|
input,
|
|
2105
2532
|
vet: async (db) => {
|
|
2106
2533
|
const newest = await newestSensitive(db, items.map(({ secret }) => secret.secretId));
|
|
2107
|
-
|
|
2534
|
+
const again = new Map((await versions(db, items.map(({ secret }) => sources.get(secret).id))).map((version) => [version.id, version]));
|
|
2535
|
+
return items.map(({ secret }, i) => {
|
|
2536
|
+
const now = again.get(sources.get(secret).id);
|
|
2537
|
+
if (loosens[i]) return now?.current === true ? null : "invalid";
|
|
2538
|
+
return secret.sensitive !== true && (newest.has(secret.secretId) || now?.heldSensitive === true) ? "invalid" : null;
|
|
2539
|
+
});
|
|
2108
2540
|
},
|
|
2541
|
+
owns: loosens,
|
|
2109
2542
|
needs: async (db) => {
|
|
2110
2543
|
return (await restoredNatures(db, items.map(({ secret }) => ({
|
|
2111
2544
|
secretId: secret.secretId,
|
|
@@ -2203,38 +2636,47 @@ var VaultService = class {
|
|
|
2203
2636
|
});
|
|
2204
2637
|
const owners = /* @__PURE__ */ new Map();
|
|
2205
2638
|
const check = async (d) => {
|
|
2206
|
-
const reader = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports), call.input);
|
|
2639
|
+
const reader = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports, d.reads), call.input);
|
|
2207
2640
|
const at = secrets.map((secret, i) => call.places?.[i] ?? secret);
|
|
2208
|
-
const environments =
|
|
2209
|
-
const parents = [...environments.values()].flatMap((environment) => environment.owner === null || environment.parentId === null ? [] : [environment.parentId]);
|
|
2210
|
-
for (const [id, parent] of await environmentsById(d.tx, parents)) environments.set(id, parent);
|
|
2211
|
-
const gone = (place) => environments.get(place.environmentId)?.deleted ?? false;
|
|
2641
|
+
const environments = d.reads.environments;
|
|
2212
2642
|
owners.clear();
|
|
2213
2643
|
for (const [id, environment] of environments) if (environment.owner !== null) owners.set(id, ownerKey(environment.owner));
|
|
2214
2644
|
const needs = call.needs === void 0 ? [] : await call.needs(d.tx);
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
}
|
|
2224
|
-
return refuses(reader, call.permission, placeIn(at[i], environments), gone(secret) || gone(at[i]));
|
|
2645
|
+
let codes = keyRefusals(reader, {
|
|
2646
|
+
principal,
|
|
2647
|
+
permission: call.permission,
|
|
2648
|
+
secrets,
|
|
2649
|
+
at,
|
|
2650
|
+
environments,
|
|
2651
|
+
needs,
|
|
2652
|
+
owns: call.owns
|
|
2225
2653
|
});
|
|
2226
|
-
if (call.vet !== void 0 && !codes.some((code) => code !== null))
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
purpose: call.input.purpose
|
|
2230
|
-
|
|
2231
|
-
})
|
|
2232
|
-
|
|
2654
|
+
if (call.vet !== void 0 && !codes.some((code) => code !== null)) codes = await call.vet(d.tx, d.reads);
|
|
2655
|
+
codes = useOnlyRefusals(codes, reader, {
|
|
2656
|
+
action,
|
|
2657
|
+
purpose: call.input.purpose,
|
|
2658
|
+
secrets
|
|
2659
|
+
});
|
|
2660
|
+
codes = sensitiveRefusals(codes, reader, {
|
|
2661
|
+
action,
|
|
2662
|
+
principal,
|
|
2663
|
+
purpose: call.input.purpose,
|
|
2664
|
+
keepsSensitive: call.input.keepsSensitive,
|
|
2665
|
+
secrets,
|
|
2666
|
+
viaReference: call.viaReference
|
|
2233
2667
|
});
|
|
2234
2668
|
let first = codes.find((code) => code !== null) ?? null;
|
|
2235
|
-
if (first === null && action === "secret.read" &&
|
|
2669
|
+
if (first === null && action === "secret.read" && this.#overBulkLimit(d)) first = "bulk_limit";
|
|
2236
2670
|
if (first !== null) throw new Refused(refusal(first, MESSAGES[first]), secrets.map((secret, i) => entry(secret, "deny", codes[i] ?? first)));
|
|
2237
2671
|
};
|
|
2672
|
+
const reads = {
|
|
2673
|
+
environments: [...secrets, ...secrets.map((secret, i) => call.places?.[i] ?? secret)].map((place) => place.environmentId),
|
|
2674
|
+
references: call.references ?? [],
|
|
2675
|
+
...action === "secret.read" ? { released: {
|
|
2676
|
+
principal,
|
|
2677
|
+
keys: secrets.map((secret) => secret.secretId)
|
|
2678
|
+
} } : {}
|
|
2679
|
+
};
|
|
2238
2680
|
const intentId = call.remote ? randomUUID() : null;
|
|
2239
2681
|
let intentSeq = null;
|
|
2240
2682
|
const outcomeEntry = (secret, item, decision, code) => {
|
|
@@ -2253,7 +2695,7 @@ var VaultService = class {
|
|
|
2253
2695
|
if (call.remote) {
|
|
2254
2696
|
const intent = await this.#decide([principal], async (d) => {
|
|
2255
2697
|
await check(d);
|
|
2256
|
-
const at =
|
|
2698
|
+
const at = d.at;
|
|
2257
2699
|
return { seq: (await this.#append(d.tx, [{
|
|
2258
2700
|
actor: principal,
|
|
2259
2701
|
action: "key.intent",
|
|
@@ -2273,8 +2715,8 @@ var VaultService = class {
|
|
|
2273
2715
|
version: secret.version
|
|
2274
2716
|
}))
|
|
2275
2717
|
})
|
|
2276
|
-
}])).seqStart };
|
|
2277
|
-
});
|
|
2718
|
+
}], [], d.reads)).seqStart };
|
|
2719
|
+
}, false, reads);
|
|
2278
2720
|
if (!intent.ok) return intent;
|
|
2279
2721
|
intentSeq = intent.seq;
|
|
2280
2722
|
}
|
|
@@ -2291,7 +2733,7 @@ var VaultService = class {
|
|
|
2291
2733
|
await decided(d);
|
|
2292
2734
|
const { vet } = call;
|
|
2293
2735
|
if (late && vet !== void 0) d.underLock.push(async (locked) => {
|
|
2294
|
-
const codes = await vet(locked);
|
|
2736
|
+
const codes = await vet(locked, d.reads);
|
|
2295
2737
|
const first = codes.find((code) => code !== null);
|
|
2296
2738
|
if (first === void 0 || first === null) return;
|
|
2297
2739
|
throw new Refused(refusal(first, MESSAGES[first]), secrets.map((secret, item) => outcomeEntry(secret, item, "deny", codes[item] ?? first)));
|
|
@@ -2307,11 +2749,10 @@ var VaultService = class {
|
|
|
2307
2749
|
if (expired) throw new Outage(new KekUnavailableError("key operation exceeded its deadline"), entries());
|
|
2308
2750
|
if (done.length < outcomes.length) throw new Refused(refusal("bad_claim", MESSAGES.bad_claim), entries());
|
|
2309
2751
|
if (late) {
|
|
2310
|
-
await
|
|
2752
|
+
d.reads = await this.#snapshot(d.tx, [principal], reads, true);
|
|
2753
|
+
d.at = this.#at(d.reads);
|
|
2311
2754
|
await decided(d);
|
|
2312
|
-
|
|
2313
|
-
const now = await environmentsById(d.tx, places.map((place) => place.environmentId));
|
|
2314
|
-
if (places.some((place) => now.get(place.environmentId)?.deleted ?? false)) throw new Refused(refusal("deleted", MESSAGES.deleted), secrets.map((secret, i) => outcomeEntry(secret, i, "deny", "deleted")));
|
|
2755
|
+
if (secrets.flatMap((secret, i) => [secret, call.places?.[i] ?? secret]).some((place) => d.reads.environments.get(place.environmentId)?.deleted ?? false)) throw new Refused(refusal("deleted", MESSAGES.deleted), secrets.map((secret, i) => outcomeEntry(secret, i, "deny", "deleted")));
|
|
2315
2756
|
}
|
|
2316
2757
|
const released = secrets.map((secret, i) => outcomeEntry(secret, i, "allow", null));
|
|
2317
2758
|
d.log.push(...released);
|
|
@@ -2321,12 +2762,16 @@ var VaultService = class {
|
|
|
2321
2762
|
} finally {
|
|
2322
2763
|
for (const value of done) value.wipe();
|
|
2323
2764
|
}
|
|
2324
|
-
}, late);
|
|
2765
|
+
}, late, reads);
|
|
2325
2766
|
}
|
|
2326
|
-
/**
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2767
|
+
/**
|
|
2768
|
+
* Whether this read's keys would take the reader past the bulk limit: the
|
|
2769
|
+
* distinct keys released to them in the window, these among them. A key
|
|
2770
|
+
* read again exposes nothing new, so it counts once, whatever its version.
|
|
2771
|
+
* Counted under their row's lock, so exactly (`Reads`).
|
|
2772
|
+
*/
|
|
2773
|
+
#overBulkLimit(d) {
|
|
2774
|
+
return d.reads.released > this.#config.bulkLimit.count;
|
|
2330
2775
|
}
|
|
2331
2776
|
/**
|
|
2332
2777
|
* Make each holder a reference to its source: the actor writes the holder
|
|
@@ -2376,20 +2821,17 @@ var VaultService = class {
|
|
|
2376
2821
|
};
|
|
2377
2822
|
};
|
|
2378
2823
|
return this.#decide([principal], async (d) => {
|
|
2379
|
-
const reader = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports), input);
|
|
2824
|
+
const reader = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports, d.reads), input);
|
|
2380
2825
|
const natures$1 = await natures(d.tx, [...sources.values()].map((source) => source.secretId));
|
|
2381
2826
|
const environments = await environmentsById(d.tx, items.flatMap((item) => {
|
|
2382
2827
|
const source = sources.get(item.source.secretId);
|
|
2383
2828
|
return source === void 0 ? [item.holder.environmentId] : [item.holder.environmentId, source.environmentId];
|
|
2384
2829
|
}));
|
|
2385
|
-
const gone = (place) => environments.get(place.environmentId)?.deleted ?? false;
|
|
2386
2830
|
const codes = items.map((item) => {
|
|
2387
2831
|
const source = sources.get(item.source.secretId);
|
|
2388
2832
|
if (source === void 0 || projectOf.get(item.holder.environmentId) !== item.holder.projectId) return "bad_claim";
|
|
2389
2833
|
if (source.secretId === item.holder.secretId) return "invalid";
|
|
2390
|
-
|
|
2391
|
-
const at = placeIn(source, environments);
|
|
2392
|
-
return refuses(reader, "secret.write", placeIn(item.holder, environments), gone(item.holder)) ?? refuses(reader, "secret.read", at, gone(source)) ?? (natures$1.get(source.secretId) === "sensitive" ? refuses(reader, "secret.use", at, gone(source)) : null);
|
|
2834
|
+
return referenceRefusal(reader, item.holder, source, environments, natures$1.get(source.secretId) ?? "secret");
|
|
2393
2835
|
});
|
|
2394
2836
|
const first = codes.find((code) => code !== null) ?? null;
|
|
2395
2837
|
if (first !== null) throw new Refused(refusal(first, MESSAGES[first]), items.map((item, i) => entry(item, "deny", codes[i] ?? first)));
|
|
@@ -2455,7 +2897,7 @@ var VaultService = class {
|
|
|
2455
2897
|
return seals.map((seal) => done.has(seal.seq) ? "ended" : null);
|
|
2456
2898
|
};
|
|
2457
2899
|
return this.#decide([principal], async (d) => {
|
|
2458
|
-
const actor = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports), input);
|
|
2900
|
+
const actor = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports, d.reads), input);
|
|
2459
2901
|
const environments = await environmentsById(d.tx, seals.map((seal) => seal.holder.environmentId));
|
|
2460
2902
|
const codes = seals.map((seal) => {
|
|
2461
2903
|
const holder = refuses(actor, "secret.write", placeIn(seal.holder, environments), false);
|
|
@@ -2556,7 +2998,7 @@ var VaultService = class {
|
|
|
2556
2998
|
*/
|
|
2557
2999
|
async #makeOwner(d, principal, reason) {
|
|
2558
3000
|
const row = d.members.get(principal);
|
|
2559
|
-
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
3001
|
+
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports, d.reads);
|
|
2560
3002
|
if (subject.status === "active" && ownsInstance(subject.live)) return false;
|
|
2561
3003
|
const tampered = subject.status === "tampered";
|
|
2562
3004
|
if (tampered && await this.#logged(d.tx, principal) === void 0) return false;
|
|
@@ -2610,10 +3052,10 @@ var VaultService = class {
|
|
|
2610
3052
|
* What `principal` holds, their row checked first (`#integrity`): a row
|
|
2611
3053
|
* that fails holds nothing, and is `tampered`.
|
|
2612
3054
|
*/
|
|
2613
|
-
async #standing(db, principal, row, at, reports) {
|
|
3055
|
+
async #standing(db, principal, row, at, reports, reads) {
|
|
2614
3056
|
const none = NOTHING;
|
|
2615
|
-
const grants$1 = row === void 0 ? [] : await grants(db, principal);
|
|
2616
|
-
const fault = await this.#integrity(db, principal, row, grants$1, reports);
|
|
3057
|
+
const grants$1 = row === void 0 ? [] : reads?.grants.get(principal) ?? await grants(db, principal);
|
|
3058
|
+
const fault = await this.#integrity(db, principal, row, grants$1, reports, reads);
|
|
2617
3059
|
if (fault !== null) return {
|
|
2618
3060
|
principal,
|
|
2619
3061
|
status: "tampered",
|
|
@@ -2643,7 +3085,7 @@ var VaultService = class {
|
|
|
2643
3085
|
role: grant.role
|
|
2644
3086
|
}));
|
|
2645
3087
|
const { role, scope } = roleOf(principal, row);
|
|
2646
|
-
const { policy } = await this.#policyAt(db, reports, at);
|
|
3088
|
+
const { policy } = await this.#policyAt(db, reports, at, reads);
|
|
2647
3089
|
const service = !principal.startsWith("user:");
|
|
2648
3090
|
return {
|
|
2649
3091
|
principal,
|
|
@@ -2703,13 +3145,17 @@ var VaultService = class {
|
|
|
2703
3145
|
/** `access`, as the rows are now. */
|
|
2704
3146
|
async #read(principal, superseded) {
|
|
2705
3147
|
const read = async (db, reports) => {
|
|
2706
|
-
const
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
]
|
|
2711
|
-
|
|
2712
|
-
|
|
3148
|
+
const reads = await snapshot(db, {
|
|
3149
|
+
principals: [principal],
|
|
3150
|
+
members: true
|
|
3151
|
+
});
|
|
3152
|
+
const [row, held, at] = [
|
|
3153
|
+
reads.members.get(principal),
|
|
3154
|
+
reads.grants.get(principal),
|
|
3155
|
+
this.#at(reads)
|
|
3156
|
+
];
|
|
3157
|
+
const fault = await this.#integrity(db, principal, row, held, reports, reads);
|
|
3158
|
+
const { policy } = await this.#policyAt(db, reports, at, reads);
|
|
2713
3159
|
return this.#access(principal, row, held, at, fault !== null, policy);
|
|
2714
3160
|
};
|
|
2715
3161
|
const found = [];
|
|
@@ -2784,7 +3230,7 @@ var VaultService = class {
|
|
|
2784
3230
|
const expiresAt = change.expiresAt === null ? null : Date.parse(change.expiresAt);
|
|
2785
3231
|
if (Number.isNaN(expiresAt) || expiresAt !== null && expiresAt <= d.at) throw refused("invalid", "an end date must be in the future");
|
|
2786
3232
|
}
|
|
2787
|
-
const { policy } = await this.#policyAt(d.tx, d.reports, d.at);
|
|
3233
|
+
const { policy } = await this.#policyAt(d.tx, d.reports, d.at, d.reads);
|
|
2788
3234
|
for (const change of changes) {
|
|
2789
3235
|
if (change.role !== null && !isRole(change.role, policy)) throw refused("invalid", `no such role: ${change.role}`);
|
|
2790
3236
|
if (change.role !== null && change.environmentId !== null && !assignableToEnvironment(change.role, policy)) throw refused("invalid", `${change.role} can only be granted on a project`);
|
|
@@ -2800,13 +3246,13 @@ var VaultService = class {
|
|
|
2800
3246
|
environmentSlug: environment.slug
|
|
2801
3247
|
};
|
|
2802
3248
|
});
|
|
2803
|
-
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports), input);
|
|
3249
|
+
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports, d.reads), input);
|
|
2804
3250
|
if (acting.status === "tampered") throw refused("tampered");
|
|
2805
3251
|
const manages = where.every((place) => mayManageAccess(acting.live, place));
|
|
2806
3252
|
if (!manages && !(SERVICE.test(principal) && PERSON.test(actor))) throw refused("not_allowed");
|
|
2807
3253
|
if (actor === principal && changes.some((change) => change.role !== null)) throw refused("not_allowed", "nobody grants themselves a role: ask another admin");
|
|
2808
3254
|
const row = d.members.get(principal);
|
|
2809
|
-
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
3255
|
+
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports, d.reads);
|
|
2810
3256
|
if (subject.status === "tampered") throw refused("tampered", TAMPERED_SUBJECT);
|
|
2811
3257
|
const admitted = input.admit === true && row?.status !== "active" ? await this.#admitWith(d, acting, principal, row, input) : void 0;
|
|
2812
3258
|
if (admitted === void 0 && row?.status === "removed") throw refused("removed");
|
|
@@ -2906,7 +3352,7 @@ var VaultService = class {
|
|
|
2906
3352
|
const refused = (code, message = MESSAGES[code]) => new Refused(refusal(code, message), [accessEntry(actor, "member.add", principal, "deny", input, asked, code)]);
|
|
2907
3353
|
return this.#decide([actor, principal], async (d) => {
|
|
2908
3354
|
validateCorrelation(input);
|
|
2909
|
-
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports), input);
|
|
3355
|
+
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports, d.reads), input);
|
|
2910
3356
|
if (acting.status === "tampered") throw refused("tampered");
|
|
2911
3357
|
const service = SERVICE.test(principal) && (input.role ?? "member") === "member" && input.scope === void 0;
|
|
2912
3358
|
if (!runsInstance(acting.live)) await this.#setsUp(d, acting, service, refused);
|
|
@@ -2919,7 +3365,7 @@ var VaultService = class {
|
|
|
2919
3365
|
};
|
|
2920
3366
|
if (wanted !== void 0 && wanted.role !== "member" && !principal.startsWith("user:")) throw refused("invalid", "service accounts hold project grants only, never an instance role");
|
|
2921
3367
|
const row = d.members.get(principal);
|
|
2922
|
-
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
3368
|
+
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports, d.reads);
|
|
2923
3369
|
if (subject.status === "tampered") throw refused("tampered", TAMPERED_SUBJECT);
|
|
2924
3370
|
d.touched.add(principal);
|
|
2925
3371
|
d.grants.set(principal, [...subject.stored]);
|
|
@@ -3020,10 +3466,10 @@ var VaultService = class {
|
|
|
3020
3466
|
validateCorrelation(input);
|
|
3021
3467
|
if (!PRINCIPAL.test(principal)) throw refused("invalid", `not a member: ${shownMember(principal)}`);
|
|
3022
3468
|
const row = d.members.get(principal);
|
|
3023
|
-
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports), input);
|
|
3469
|
+
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports, d.reads), input);
|
|
3024
3470
|
if (acting.status === "tampered") throw refused("tampered");
|
|
3025
3471
|
const holder = acting.live;
|
|
3026
|
-
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
3472
|
+
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports, d.reads);
|
|
3027
3473
|
const held = subject.stored;
|
|
3028
3474
|
if (this.#listed(principal)) throw refused("not_allowed", declaredOwner(principal));
|
|
3029
3475
|
if (subject.status === "tampered") {
|
|
@@ -3127,20 +3573,21 @@ var VaultService = class {
|
|
|
3127
3573
|
* no role holds anything but Owner, otherwise whoever can insert rows
|
|
3128
3574
|
* could unprotect them. Its edits are then unknown, not none.
|
|
3129
3575
|
*/
|
|
3130
|
-
async #policyAt(db, reports, at) {
|
|
3131
|
-
const entry = await newestAuthentic((limit, before) => newestVaultEntries(db, POLICY, limit, before), this.#check(reports, null));
|
|
3576
|
+
async #policyAt(db, reports, at, reads) {
|
|
3577
|
+
const entry = await newestAuthentic((limit, before) => reads !== void 0 && before === void 0 ? Promise.resolve(reads.newestPolicy === null ? [] : [reads.newestPolicy]) : newestVaultEntries(db, POLICY, limit, before), this.#check(reports, null));
|
|
3132
3578
|
if (entry === "none") return {
|
|
3133
3579
|
policy: DEFAULT_POLICY,
|
|
3134
3580
|
edits: NO_EDITS,
|
|
3135
3581
|
readable: true
|
|
3136
3582
|
};
|
|
3137
3583
|
const edits = typeof entry === "string" ? null : readEdits(JSON.parse(entry.metadata));
|
|
3138
|
-
|
|
3584
|
+
if (edits === null) return {
|
|
3139
3585
|
policy: CLOSED_POLICY,
|
|
3140
3586
|
edits: NO_EDITS,
|
|
3141
3587
|
readable: false
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3588
|
+
};
|
|
3589
|
+
return {
|
|
3590
|
+
policy: livePolicy(entry === reads?.newestPolicy ? reads.environments : await environmentsById(db, edits.protected.map((place) => place.environmentId)), edits, at),
|
|
3144
3591
|
edits,
|
|
3145
3592
|
readable: true
|
|
3146
3593
|
};
|
|
@@ -3165,11 +3612,11 @@ var VaultService = class {
|
|
|
3165
3612
|
const refused = (code, message = MESSAGES[code]) => new Refused(refusal(code, message), [entry("deny", { change: describeChange(change) }, code)]);
|
|
3166
3613
|
return this.#decide([actor], async (d) => {
|
|
3167
3614
|
validateCorrelation(input);
|
|
3168
|
-
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports), input);
|
|
3615
|
+
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports, d.reads), input);
|
|
3169
3616
|
if (acting.status === "tampered") throw refused("tampered");
|
|
3170
3617
|
if (acting.status !== "active") throw refused(acting.status === "removed" ? "removed" : "not_a_member");
|
|
3171
|
-
const newest =
|
|
3172
|
-
const { policy: before, edits, readable } = await this.#policyAt(d.tx, d.reports, d.at);
|
|
3618
|
+
const newest = d.reads.newestPolicy?.seq ?? null;
|
|
3619
|
+
const { policy: before, edits, readable } = await this.#policyAt(d.tx, d.reports, d.at, d.reads);
|
|
3173
3620
|
if (revision !== void 0 && policyRevision(newest, before) !== revision) throw refused("policy_changed");
|
|
3174
3621
|
const owner = acting.live.credentialScope == null && ownsInstance(acting.live);
|
|
3175
3622
|
if (!readable && !owner) throw refused("not_allowed", "the vault cannot read its own policy: only an Owner of the whole instance writes it again, from the roles as they ship");
|
|
@@ -3220,12 +3667,12 @@ var VaultService = class {
|
|
|
3220
3667
|
at: d.at
|
|
3221
3668
|
}))].sort((a, b) => a.environmentId < b.environmentId ? -1 : 1);
|
|
3222
3669
|
} else {
|
|
3223
|
-
if (!
|
|
3224
|
-
next.protected = next.protected.filter((place) => !change.environmentIds.includes(place.environmentId));
|
|
3670
|
+
if (!ownsProject(acting.live, change.projectId)) throw refused("not_allowed", "only an Owner of its project, or of the whole instance, unprotects an environment");
|
|
3671
|
+
next.protected = next.protected.filter((place) => place.projectId !== change.projectId || !change.environmentIds.includes(place.environmentId));
|
|
3225
3672
|
}
|
|
3226
3673
|
} else throw refused("invalid", "a change is to a role, an instance role, or protection");
|
|
3227
3674
|
const answer = {
|
|
3228
|
-
policy: await
|
|
3675
|
+
policy: livePolicy(await environmentsById(d.tx, next.protected.map((place) => place.environmentId)), next, d.at),
|
|
3229
3676
|
seq: newest === null ? null : Number(newest)
|
|
3230
3677
|
};
|
|
3231
3678
|
if (!readable || JSON.stringify(next) !== JSON.stringify(edits)) {
|
|
@@ -3257,7 +3704,7 @@ var VaultService = class {
|
|
|
3257
3704
|
const refused = (code, message = MESSAGES[code]) => new Refused(refusal(code, message), [entry("deny", { serviceAccounts: input.settings?.serviceAccounts ?? null }, code)]);
|
|
3258
3705
|
return this.#decide([actor], async (d) => {
|
|
3259
3706
|
validateCorrelation(input);
|
|
3260
|
-
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports), input);
|
|
3707
|
+
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports, d.reads), input);
|
|
3261
3708
|
if (acting.status === "tampered") throw refused("tampered");
|
|
3262
3709
|
if (!runsInstance(acting.live)) throw refused("not_allowed", "only admins and owners of the whole instance change its settings");
|
|
3263
3710
|
if (!isScope(input.settings?.serviceAccounts)) throw refused("invalid", "who sets up service accounts is a scope: { projects, environments }, each all, only or except");
|
|
@@ -4226,54 +4673,6 @@ function viaDetail(via, seal) {
|
|
|
4226
4673
|
};
|
|
4227
4674
|
}
|
|
4228
4675
|
/**
|
|
4229
|
-
* Where a decision checks a permission: an environment, with its slug as
|
|
4230
|
-
* the store has it when it was read and is in the project claimed;
|
|
4231
|
-
* otherwise none, which no grant on a slug matches.
|
|
4232
|
-
*/
|
|
4233
|
-
function placeIn(place, environments) {
|
|
4234
|
-
const environment = environments.get(place.environmentId);
|
|
4235
|
-
const slug = environment?.projectId === place.projectId ? environment.slug : null;
|
|
4236
|
-
return {
|
|
4237
|
-
projectId: place.projectId,
|
|
4238
|
-
environmentId: place.environmentId,
|
|
4239
|
-
environmentSlug: slug
|
|
4240
|
-
};
|
|
4241
|
-
}
|
|
4242
|
-
/**
|
|
4243
|
-
* Why `reader` may not do `permission` at `where`, or null if they may. A
|
|
4244
|
-
* deleted place is refused before any grant is asked, so no kind of grant,
|
|
4245
|
-
* on the project, the environment or every project, can reach into one.
|
|
4246
|
-
*/
|
|
4247
|
-
/**
|
|
4248
|
-
* A key of a personal config is its owner's alone (docs/design/forking.md,
|
|
4249
|
-
* section 4): no grant or role opens or wraps it, an Owner's included,
|
|
4250
|
-
* only its owner, while they read the environment it is a config of,
|
|
4251
|
-
* whatever the call is. Nor does any reference reach into or out of one.
|
|
4252
|
-
* Undefined for a key of any other environment, decided as ever.
|
|
4253
|
-
*/
|
|
4254
|
-
function personalRefusal(reader, principal, permission, secret, at, environments) {
|
|
4255
|
-
const own = environments.get(secret.environmentId);
|
|
4256
|
-
const held = at.environmentId === secret.environmentId ? own : environments.get(at.environmentId);
|
|
4257
|
-
if (own?.owner == null && held?.owner == null) return void 0;
|
|
4258
|
-
if (own !== held || principal !== own?.owner || own.parentId === null) return "personal";
|
|
4259
|
-
const parent = placeIn({
|
|
4260
|
-
projectId: secret.projectId,
|
|
4261
|
-
environmentId: own.parentId
|
|
4262
|
-
}, environments);
|
|
4263
|
-
const gone = own.deleted || (environments.get(own.parentId)?.deleted ?? true);
|
|
4264
|
-
const refused = refuses(reader, "secret.use", parent, gone) === null ? null : refuses(reader, "secret.read", parent, gone);
|
|
4265
|
-
if (refused !== null) return refused;
|
|
4266
|
-
return withinCredential(reader.live, permission === "secret.read" ? "secret.use" : permission, parent) ? null : "no_grant";
|
|
4267
|
-
}
|
|
4268
|
-
function refuses(reader, permission, where, deleted) {
|
|
4269
|
-
if (reader.status === "tampered") return "tampered";
|
|
4270
|
-
if (reader.status === "removed") return "removed";
|
|
4271
|
-
if (reader.status === "unknown") return "not_a_member";
|
|
4272
|
-
if (deleted) return "deleted";
|
|
4273
|
-
if (allows(reader.live, permission, where)) return null;
|
|
4274
|
-
return allows(reader.all, permission, where) ? "expired" : "no_grant";
|
|
4275
|
-
}
|
|
4276
|
-
/**
|
|
4277
4676
|
* An entry about a key. A new secret's row is not committed when its key is
|
|
4278
4677
|
* wrapped, so a wrap names the secret in its payload; the others name it.
|
|
4279
4678
|
*/
|
|
@@ -4382,7 +4781,7 @@ function oneRead(group) {
|
|
|
4382
4781
|
}
|
|
4383
4782
|
/** Whether `entry` changes a member's access: what their row's `access_seq` names. */
|
|
4384
4783
|
function isAccessEntry(entry) {
|
|
4385
|
-
return entry.decision === "allow" && entry.subjectPrincipal !== void 0 && entry.subjectPrincipal !== null && ACCESS_ACTIONS.includes(entry.action);
|
|
4784
|
+
return entry.decision === "allow" && entry.subjectPrincipal !== void 0 && entry.subjectPrincipal !== null && ACCESS_ACTIONS$1.includes(entry.action);
|
|
4386
4785
|
}
|
|
4387
4786
|
/** An entry about a member's access. */
|
|
4388
4787
|
function accessEntry(actor, action, principal, decision, correlation, detail, code = null) {
|
|
@@ -4486,8 +4885,7 @@ function readEdits(stored) {
|
|
|
4486
4885
|
* keeps its slug, which a scoped instance role's filters read. The edits
|
|
4487
4886
|
* keep them all: no write is needed.
|
|
4488
4887
|
*/
|
|
4489
|
-
|
|
4490
|
-
const rows = await environmentsById(db, edits.protected.map((place) => place.environmentId));
|
|
4888
|
+
function livePolicy(rows, edits, at) {
|
|
4491
4889
|
const live = edits.protected.filter((place) => {
|
|
4492
4890
|
const row = rows.get(place.environmentId);
|
|
4493
4891
|
return row === void 0 ? at - place.at < UNWRITTEN_FORK_MS : !row.deleted;
|