@stigmer/server 3.27.1 → 3.27.2
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/authorization/authorizer.d.ts +4 -2
- package/dist/authorization/authorizer.d.ts.map +1 -1
- package/dist/authorization/authorizer.js +4 -2
- package/dist/authorization/authorizer.js.map +1 -1
- package/dist/authorization/derived-tuples.d.ts +3 -1
- package/dist/authorization/derived-tuples.d.ts.map +1 -1
- package/dist/authorization/derived-tuples.js +46 -1
- package/dist/authorization/derived-tuples.js.map +1 -1
- package/dist/boot/list-indexes.d.ts.map +1 -1
- package/dist/boot/list-indexes.js +6 -0
- package/dist/boot/list-indexes.js.map +1 -1
- package/dist/domain/iampolicy/list-index.d.ts +2 -0
- package/dist/domain/iampolicy/list-index.d.ts.map +1 -0
- package/dist/domain/iampolicy/list-index.js +32 -0
- package/dist/domain/iampolicy/list-index.js.map +1 -0
- package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
- package/dist/domain/iampolicy/resource-store.js +24 -11
- package/dist/domain/iampolicy/resource-store.js.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +2 -1
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js +2 -1
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/README.md +7 -5
- package/src/authorization/__tests__/derived-tuples.test.ts +173 -0
- package/src/authorization/__tests__/enterprise-model.ts +59 -0
- package/src/authorization/__tests__/list-read-scope.measure.test.ts +425 -55
- package/src/authorization/authorizer.ts +4 -2
- package/src/authorization/derived-tuples.ts +98 -14
- package/src/boot/__tests__/list-indexes.test.ts +4 -0
- package/src/boot/list-indexes.ts +6 -0
- package/src/domain/iampolicy/__tests__/resource-store.test.ts +65 -5
- package/src/domain/iampolicy/list-index.ts +33 -0
- package/src/domain/iampolicy/resource-store.ts +24 -11
- package/src/extensions/list-read-scope.ts +2 -1
- package/src/index.ts +5 -0
|
@@ -26,18 +26,39 @@
|
|
|
26
26
|
* rules' one predicate). The person comparison itself is the
|
|
27
27
|
* evaluator's, over the aliases `Person` carries.
|
|
28
28
|
*
|
|
29
|
-
* The SOURCE joins two records per (object, relation): the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* read
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
29
|
+
* The SOURCE joins two records per (object, relation): the IamPolicy rows
|
|
30
|
+
* a check can meet on the object, and the tuples derived from the
|
|
31
|
+
* object's row (loaded once per object through the loader, which a
|
|
32
|
+
* declaration's `derived` rule also reads related rows through). The rows
|
|
33
|
+
* are read ONCE per source through `findByPrincipal`, keyed by the
|
|
34
|
+
* principals a check can reach the person through:
|
|
35
|
+
*
|
|
36
|
+
* - the person's account — every role and every grant made to them;
|
|
37
|
+
* - every team the person holds `member` on — a grant made to
|
|
38
|
+
* `team:<id>#member` names the team, never the person, so it is read
|
|
39
|
+
* by the team's id. The set is complete without recursion: a team's
|
|
40
|
+
* membership is a direct grant only and a team never contains a team,
|
|
41
|
+
* so every team that can admit the person appears among the person's
|
|
42
|
+
* own rows. The rows grant nothing by being read: the evaluator still
|
|
43
|
+
* asks whether the person is the team's member, which the model bounds
|
|
44
|
+
* by the organization (`member: [identity_account] and viewer from
|
|
45
|
+
* organization`), so a person who left the organization reads the
|
|
46
|
+
* team's rows and is admitted by none of them.
|
|
47
|
+
*
|
|
48
|
+
* Open source serves no team (the kind's tier is enterprise), so the
|
|
49
|
+
* person's rows name none and the second read never happens: one read per
|
|
50
|
+
* source, an indexed read on the OSS adapter (its list index by
|
|
51
|
+
* principal) whatever the number of organizations walked or rows held.
|
|
52
|
+
* An edition that composes teams over the built-in
|
|
53
|
+
* evaluator pays one read more per team the person holds. The rows are
|
|
54
|
+
* indexed by (object, relation) once, so a list's thousands of candidates
|
|
55
|
+
* each find theirs without walking the rest.
|
|
56
|
+
*
|
|
57
|
+
* An absent row is no tuples — the target's own not-found is the driver's
|
|
58
|
+
* arm, and a parent that no longer exists simply grants nothing, as in the
|
|
59
|
+
* cloud; any other store fault propagates so the driver folds it to
|
|
60
|
+
* `unavailable`. One source per check (or per list call for the list
|
|
61
|
+
* scope): its memo is the check's.
|
|
41
62
|
*
|
|
42
63
|
* A source may be SEEDED with the facts of objects the caller already
|
|
43
64
|
* holds (the list scope's candidates, decoded by the lane and carried on
|
|
@@ -102,6 +123,10 @@ export const ROW_RECORDED_OWNER_KINDS: ReadonlySet<ApiResourceKind> = new Set([
|
|
|
102
123
|
ApiResourceKind.organization,
|
|
103
124
|
]);
|
|
104
125
|
|
|
126
|
+
/** The FGA type a team grant names, and the relation that makes a person one of its members (the model's `team#member`). */
|
|
127
|
+
const TEAM_TYPE = kindEnumName(ApiResourceKind.team);
|
|
128
|
+
const TEAM_MEMBER_RELATION = "member";
|
|
129
|
+
|
|
105
130
|
function account(id: string): Subject {
|
|
106
131
|
return { form: "object", object: { type: ACCOUNT_TYPE, id } };
|
|
107
132
|
}
|
|
@@ -209,7 +234,9 @@ export interface DerivedTupleSource extends TupleSource {
|
|
|
209
234
|
* The person's own IamPolicy rows as tuples — the ONE read of them this
|
|
210
235
|
* source makes, shared with `tuplesOf`. A list-shaped consumer (the
|
|
211
236
|
* organization directory; the list scope) reads its candidate objects
|
|
212
|
-
* from here instead of scanning the port a second time.
|
|
237
|
+
* from here instead of scanning the port a second time. The rows granted
|
|
238
|
+
* to the person's teams are not among them: they name the team, and
|
|
239
|
+
* `tuplesOf` serves them where a check meets them.
|
|
213
240
|
*/
|
|
214
241
|
personTuples(): Promise<ReadonlyArray<Tuple>>;
|
|
215
242
|
}
|
|
@@ -235,6 +262,9 @@ export function newDerivedTupleSource(
|
|
|
235
262
|
);
|
|
236
263
|
}
|
|
237
264
|
let personRows: Promise<ReadonlyArray<Tuple>> | undefined;
|
|
265
|
+
let reachableRows:
|
|
266
|
+
| Promise<ReadonlyMap<string, ReadonlyArray<Tuple>>>
|
|
267
|
+
| undefined;
|
|
238
268
|
|
|
239
269
|
const loader: RowLoader = {
|
|
240
270
|
load(object) {
|
|
@@ -284,6 +314,38 @@ export function newDerivedTupleSource(
|
|
|
284
314
|
return personRows;
|
|
285
315
|
}
|
|
286
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Every row a check can meet, keyed by (object, relation): the person's
|
|
319
|
+
* own, then the rows granted to each team the person holds `member` on
|
|
320
|
+
* (the module header says why that set is complete). A person in no
|
|
321
|
+
* team costs nothing beyond their own rows.
|
|
322
|
+
*/
|
|
323
|
+
function reachableTuples(): Promise<
|
|
324
|
+
ReadonlyMap<string, ReadonlyArray<Tuple>>
|
|
325
|
+
> {
|
|
326
|
+
if (reachableRows === undefined) {
|
|
327
|
+
reachableRows = rowTuples().then(async (own) => {
|
|
328
|
+
const teams = new Set<string>();
|
|
329
|
+
for (const tuple of own) {
|
|
330
|
+
if (
|
|
331
|
+
tuple.object.type === TEAM_TYPE &&
|
|
332
|
+
tuple.relation === TEAM_MEMBER_RELATION &&
|
|
333
|
+
tuple.object.id !== ""
|
|
334
|
+
) {
|
|
335
|
+
teams.add(tuple.object.id);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const granted = await Promise.all(
|
|
339
|
+
[...teams].map((team) =>
|
|
340
|
+
deps.policies.findByPrincipal(TEAM_TYPE, team),
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
return indexByPair([...own, ...granted.flat().map(tupleOfRow)]);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return reachableRows;
|
|
347
|
+
}
|
|
348
|
+
|
|
287
349
|
return {
|
|
288
350
|
loader,
|
|
289
351
|
personTuples: rowTuples,
|
|
@@ -292,7 +354,8 @@ export function newDerivedTupleSource(
|
|
|
292
354
|
tuple.relation === relation &&
|
|
293
355
|
tuple.object.type === object.type &&
|
|
294
356
|
tuple.object.id === object.id;
|
|
295
|
-
const fromRows =
|
|
357
|
+
const fromRows =
|
|
358
|
+
(await reachableTuples()).get(pairKey(object, relation)) ?? [];
|
|
296
359
|
const declaration = model.byType(object.type);
|
|
297
360
|
if (declaration === undefined) {
|
|
298
361
|
return fromRows;
|
|
@@ -337,6 +400,27 @@ async function loadRow(
|
|
|
337
400
|
}
|
|
338
401
|
}
|
|
339
402
|
|
|
403
|
+
/** The key a tuple is served under: its object and relation. */
|
|
404
|
+
function pairKey(object: ObjectRef, relation: string): string {
|
|
405
|
+
return `${formatObjectRef(object)}#${relation}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function indexByPair(
|
|
409
|
+
tuples: ReadonlyArray<Tuple>,
|
|
410
|
+
): ReadonlyMap<string, ReadonlyArray<Tuple>> {
|
|
411
|
+
const index = new Map<string, Tuple[]>();
|
|
412
|
+
for (const tuple of tuples) {
|
|
413
|
+
const key = pairKey(tuple.object, tuple.relation);
|
|
414
|
+
const held = index.get(key);
|
|
415
|
+
if (held === undefined) {
|
|
416
|
+
index.set(key, [tuple]);
|
|
417
|
+
} else {
|
|
418
|
+
held.push(tuple);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return index;
|
|
422
|
+
}
|
|
423
|
+
|
|
340
424
|
/** An IamPolicy row as the tuple it records: `resource#relation@principal`. */
|
|
341
425
|
function tupleOfRow(row: IamPolicy): Tuple {
|
|
342
426
|
const spec = row.spec;
|
|
@@ -27,6 +27,10 @@ const PINNED: Readonly<
|
|
|
27
27
|
fingerprint:
|
|
28
28
|
"artifact{agent_execution=field:spec.source.agent_execution_id,workflow_execution=field:spec.source.workflow_execution_id}",
|
|
29
29
|
},
|
|
30
|
+
iam_policy: {
|
|
31
|
+
revision: 1,
|
|
32
|
+
fingerprint: "iam_policy{principal=field:spec.principal.id}",
|
|
33
|
+
},
|
|
30
34
|
session: {
|
|
31
35
|
revision: 1,
|
|
32
36
|
fingerprint:
|
package/src/boot/list-indexes.ts
CHANGED
|
@@ -10,9 +10,14 @@
|
|
|
10
10
|
* parent's rows without decoding the whole kind; the kinds here were
|
|
11
11
|
* chosen on the hosted edition's row counts and sizes (2026-09-23: every
|
|
12
12
|
* other org-scoped kind held under a hundred rows and a hundred kilobytes).
|
|
13
|
+
* A kind also joins when a port read on every authorization check would
|
|
14
|
+
* otherwise decode the whole kind: `iam_policy`, whose adapter reads a
|
|
15
|
+
* person's rows by principal (measured 2026-09-24: about 4.6 microseconds
|
|
16
|
+
* per row per read, 47 ms a check at ten thousand rows on sqlite).
|
|
13
17
|
*/
|
|
14
18
|
import { agentExecutionListIndex } from "../domain/agentexecution/list-index.js";
|
|
15
19
|
import { artifactListIndex } from "../domain/artifact/list-index.js";
|
|
20
|
+
import { iamPolicyListIndex } from "../domain/iampolicy/list-index.js";
|
|
16
21
|
import { sessionListIndex } from "../domain/session/list-index.js";
|
|
17
22
|
import { workflowExecutionListIndex } from "../domain/workflowexecution/list-index.js";
|
|
18
23
|
import type { ListIndexDeclaration } from "../store/list-index.js";
|
|
@@ -20,6 +25,7 @@ import type { ListIndexDeclaration } from "../store/list-index.js";
|
|
|
20
25
|
export const LIST_INDEXES: ReadonlyArray<ListIndexDeclaration> = [
|
|
21
26
|
agentExecutionListIndex,
|
|
22
27
|
artifactListIndex,
|
|
28
|
+
iamPolicyListIndex,
|
|
23
29
|
sessionListIndex,
|
|
24
30
|
workflowExecutionListIndex,
|
|
25
31
|
];
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Runs the IamPolicyStore port-contract kit (../store-contract.ts) over the
|
|
3
3
|
* OSS adapter (../resource-store.ts) on both drivers through the drivers'
|
|
4
4
|
* own fixtures (sqlite always; Postgres under TEST_DATABASE_URL), and pins
|
|
5
|
-
* the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* the two behaviours that are the OSS adapter's rather than the port's:
|
|
6
|
+
* save refuses a policy whose id is not the triple's derived id, the
|
|
7
|
+
* invariant that makes "one row per triple" the primary key's job in open
|
|
8
|
+
* source; and a principal's rows are read through the kind's list index,
|
|
9
|
+
* never by decoding the whole kind, because the built-in authorizer reads
|
|
10
|
+
* them on every check.
|
|
9
11
|
*
|
|
10
12
|
* The kit's case list is pinned by name so a case cannot drop out unnoticed:
|
|
11
13
|
* the cloud driver's test iterates the same export over `cloud.iam_policy`
|
|
@@ -21,6 +23,7 @@ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/
|
|
|
21
23
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
22
24
|
import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
23
25
|
|
|
26
|
+
import { LIST_INDEXES } from "../../../boot/list-indexes.js";
|
|
24
27
|
import type { Store } from "../../../store/interface.js";
|
|
25
28
|
import { PostgresStore } from "../../../store/postgres/store.js";
|
|
26
29
|
import {
|
|
@@ -82,7 +85,11 @@ const postgresFixture: DriverFixture = {
|
|
|
82
85
|
if (postgresDatabase === undefined) {
|
|
83
86
|
postgresDatabase = await createTestDatabase();
|
|
84
87
|
}
|
|
85
|
-
const store = await PostgresStore.open(
|
|
88
|
+
const store = await PostgresStore.open(
|
|
89
|
+
postgresDatabase.databaseUrl,
|
|
90
|
+
undefined,
|
|
91
|
+
{ listIndexes: LIST_INDEXES },
|
|
92
|
+
);
|
|
86
93
|
await store.deleteResourcesByKind(ApiResourceKind.iam_policy);
|
|
87
94
|
return { store, close: () => store.close() };
|
|
88
95
|
},
|
|
@@ -160,6 +167,59 @@ describe.each([sqliteFixture, postgresFixture])(
|
|
|
160
167
|
spec,
|
|
161
168
|
);
|
|
162
169
|
});
|
|
170
|
+
|
|
171
|
+
it("reads a principal's rows through the list index, never by decoding the whole kind", async () => {
|
|
172
|
+
let scans = 0;
|
|
173
|
+
const target = opened.store;
|
|
174
|
+
// Every method runs on the real store; only the scan is counted.
|
|
175
|
+
const counted = new Proxy(target, {
|
|
176
|
+
get(store, property) {
|
|
177
|
+
if (property === "listResources") {
|
|
178
|
+
return (kind: ApiResourceKind) => {
|
|
179
|
+
if (kind === ApiResourceKind.iam_policy) {
|
|
180
|
+
scans += 1;
|
|
181
|
+
}
|
|
182
|
+
return store.listResources(kind);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const value: unknown = Reflect.get(store, property, store);
|
|
186
|
+
return typeof value === "function" ? value.bind(store) : value;
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const policies = newResourceIamPolicyStore(counted);
|
|
190
|
+
for (const [principal, relation] of [
|
|
191
|
+
["ida_wtr3jcf281yfk9xx61kj59fsme", "admin"],
|
|
192
|
+
["ida_wtr3jcf281yfk9xx61kj59fsme", "member"],
|
|
193
|
+
["ida_0hlf2yb5mhkgb3bdzkkrptqf4d", "member"],
|
|
194
|
+
] as const) {
|
|
195
|
+
const spec = orgRole(principal, relation, "acme");
|
|
196
|
+
await policies.save(
|
|
197
|
+
create(IamPolicySchema, {
|
|
198
|
+
apiVersion: IAM_POLICY_API_VERSION,
|
|
199
|
+
kind: IAM_POLICY_KIND,
|
|
200
|
+
metadata: create(ApiResourceMetadataSchema, {
|
|
201
|
+
id: policyIdFor(spec),
|
|
202
|
+
}),
|
|
203
|
+
spec,
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const found = await policies.findByPrincipal(
|
|
208
|
+
"identity_account",
|
|
209
|
+
"ida_wtr3jcf281yfk9xx61kj59fsme",
|
|
210
|
+
);
|
|
211
|
+
expect(found.map((policy) => policy.spec?.relation).sort()).toEqual([
|
|
212
|
+
"admin",
|
|
213
|
+
"member",
|
|
214
|
+
]);
|
|
215
|
+
expect(
|
|
216
|
+
await policies.findByPrincipal(
|
|
217
|
+
"team",
|
|
218
|
+
"ida_wtr3jcf281yfk9xx61kj59fsme",
|
|
219
|
+
),
|
|
220
|
+
).toEqual([]);
|
|
221
|
+
expect(scans).toBe(0);
|
|
222
|
+
});
|
|
163
223
|
});
|
|
164
224
|
},
|
|
165
225
|
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IamPolicy list index (store/list-index.ts): `principal` is the key
|
|
3
|
+
* the open-source adapter's `findByPrincipal` reads (resource-store.ts).
|
|
4
|
+
* It is not a lane's index: the built-in authorizer reads a person's rows,
|
|
5
|
+
* and the rows granted to each team they belong to, on every check and
|
|
6
|
+
* every list batch (authorization/derived-tuples.ts), and without the key
|
|
7
|
+
* each of those reads decodes the whole kind — a table that grows with
|
|
8
|
+
* every member, organization and grant.
|
|
9
|
+
*
|
|
10
|
+
* The key is the principal's id alone: a key reads one string field, and
|
|
11
|
+
* an id carries its kind's prefix (`ida_` for an account, `tm_` for a
|
|
12
|
+
* team), so the index narrows to one principal and the adapter's
|
|
13
|
+
* predicate on (kind, id) keeps the answer exact whatever the index
|
|
14
|
+
* returns. A policy is not organization-scoped the way a lane's rows are,
|
|
15
|
+
* so the read names no organization; the key table's lookup index leads
|
|
16
|
+
* with the key, not the organization (the v6 migration of each driver).
|
|
17
|
+
*
|
|
18
|
+
* A change to `keys` bumps `revision` (boot/__tests__/list-indexes.test.ts
|
|
19
|
+
* pins the pair).
|
|
20
|
+
*/
|
|
21
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
22
|
+
import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
23
|
+
|
|
24
|
+
import { declareListIndex, field } from "../../store/list-index.js";
|
|
25
|
+
|
|
26
|
+
export const iamPolicyListIndex = declareListIndex({
|
|
27
|
+
kind: ApiResourceKind.iam_policy,
|
|
28
|
+
schema: IamPolicySchema,
|
|
29
|
+
revision: 1,
|
|
30
|
+
keys: {
|
|
31
|
+
principal: field("spec.principal.id"),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
@@ -4,14 +4,21 @@
|
|
|
4
4
|
* §3a, §4). The composition root installs it when no extension registers
|
|
5
5
|
* `drivers.iamPolicyStore`.
|
|
6
6
|
*
|
|
7
|
-
* Reads. `findById` is a PRIMARY-KEY read.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Reads. `findById` is a PRIMARY-KEY read. `findByPrincipal` is an
|
|
8
|
+
* INDEXED read through the kind's list index (list-index.ts), because the
|
|
9
|
+
* built-in authorizer calls it on every check and every list batch — for
|
|
10
|
+
* the person, and once more per team they belong to — and a scan there
|
|
11
|
+
* costs a decode of every row the server holds, per call (measured
|
|
12
|
+
* 2026-09-24: about 4.6 microseconds per row, 47 ms a check at ten
|
|
13
|
+
* thousand rows on sqlite). The index narrows by the principal's id; the
|
|
14
|
+
* (kind, id) predicate decides, so the answer is exact whatever the index
|
|
15
|
+
* returns, and the store keeps it exact while an older binary still
|
|
16
|
+
* writes (store/interface.ts, `queryResources`). Every other find decodes
|
|
17
|
+
* `listResources(iam_policy)` and filters in memory: they serve the grant
|
|
18
|
+
* path and the access lists, not the check, and the row count behind them
|
|
19
|
+
* is members × organizations on a self-host. The filters are the cloud
|
|
20
|
+
* store's WHERE clauses restated over the proto (store.ts names each), so
|
|
21
|
+
* a driver's test over either edition reads the same contract.
|
|
15
22
|
*
|
|
16
23
|
* Writes. `save` refuses a policy whose id is not its triple's derived id
|
|
17
24
|
* (constants.ts policyIdFor): open source has no legacy random ids, and a
|
|
@@ -43,6 +50,7 @@ import { kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
|
43
50
|
import { ResourceNotFoundError } from "../../store/interface.js";
|
|
44
51
|
import type { Store } from "../../store/interface.js";
|
|
45
52
|
import { USER_GRANT_PRINCIPAL_KINDS, policyIdFor } from "./constants.js";
|
|
53
|
+
import { iamPolicyListIndex } from "./list-index.js";
|
|
46
54
|
import { DuplicatePolicyError } from "./store.js";
|
|
47
55
|
import type { IamPolicyStore } from "./store.js";
|
|
48
56
|
|
|
@@ -72,7 +80,7 @@ export function newResourceIamPolicyStore(store: Store): IamPolicyStore {
|
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
/** Every row of the kind, decoded — the scan behind every
|
|
83
|
+
/** Every row of the kind, decoded — the scan behind every read but by id and by principal. */
|
|
76
84
|
async function all(): Promise<ReadonlyArray<IamPolicy>> {
|
|
77
85
|
const rows = await store.listResources(KIND);
|
|
78
86
|
return rows.map((bytes) => fromBinary(IamPolicySchema, bytes));
|
|
@@ -102,8 +110,13 @@ export function newResourceIamPolicyStore(store: Store): IamPolicyStore {
|
|
|
102
110
|
|
|
103
111
|
findById: readById,
|
|
104
112
|
|
|
105
|
-
findByPrincipal(principalKind, principalId) {
|
|
106
|
-
|
|
113
|
+
async findByPrincipal(principalKind, principalId) {
|
|
114
|
+
const rows = await store.queryResources(iamPolicyListIndex, {
|
|
115
|
+
anyKey: [{ name: "principal", value: principalId }],
|
|
116
|
+
});
|
|
117
|
+
return rows
|
|
118
|
+
.map((row) => fromBinary(IamPolicySchema, row.data))
|
|
119
|
+
.filter((policy) => onPrincipal(policy, principalKind, principalId));
|
|
107
120
|
},
|
|
108
121
|
|
|
109
122
|
findByResource(resourceKind, resourceId) {
|
|
@@ -224,7 +224,8 @@ export interface ScopedListResource {
|
|
|
224
224
|
/**
|
|
225
225
|
* The ONE consumption idiom for post-scan lanes (the shared-step
|
|
226
226
|
* discipline of pipeline/steps/authorization-tuples.ts, rendered as a
|
|
227
|
-
* helper because half the lanes are direct handlers
|
|
227
|
+
* helper because half the lanes are direct handlers; on the barrel, so a
|
|
228
|
+
* composition's lane over its own table narrows the same way):
|
|
228
229
|
*
|
|
229
230
|
* - no scope composed → the input array unchanged (byte-identity;
|
|
230
231
|
* `requestOrg` deliberately not consulted — the OSS single-tenant
|
package/src/index.ts
CHANGED
|
@@ -274,6 +274,11 @@ export type {
|
|
|
274
274
|
ListReadScope,
|
|
275
275
|
ListEntryMeta,
|
|
276
276
|
} from "./extensions/list-read-scope.js";
|
|
277
|
+
// The one consumption idiom of the list read scope for post-scan list lanes:
|
|
278
|
+
// exported so a list lane a composition serves from its own table narrows
|
|
279
|
+
// its rows exactly as the library's lanes do, instead of building each
|
|
280
|
+
// candidate's authorization facts by hand.
|
|
281
|
+
export { restrictListByReadScope } from "./extensions/list-read-scope.js";
|
|
277
282
|
// The stigmer-cloud#572 seam: the identity a schedule fire acts as
|
|
278
283
|
// (drivers.scheduleFireCaller) — the composition mints it per fire; the
|
|
279
284
|
// RunStarter propagates it through the R5 in-process header. A mint that
|