@stigmer/server 3.27.0 → 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 +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -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 +22 -0
|
@@ -1,21 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
* measurement, not an assumption, decides whether a driver needs
|
|
2
|
+
* The built-in evaluator's cost, measured — the plan's storage-impact line
|
|
3
|
+
* ("a measurement, not an assumption, decides whether a driver needs
|
|
4
4
|
* anything"). Gated on `AUTHORIZATION_MEASURE=1` and SKIPPED otherwise
|
|
5
5
|
* (the `TEST_DATABASE_URL` idiom: an environment variable read once,
|
|
6
6
|
* `describe.skipIf`), because ten thousand rows on Postgres take a while
|
|
7
|
-
* and the
|
|
8
|
-
*
|
|
9
|
-
* different engine's
|
|
7
|
+
* and the numbers are recorded in a project log, not asserted: timing on
|
|
8
|
+
* a shared machine is noise in a unit suite. No budget is ruled for open
|
|
9
|
+
* source, and the cloud's 300 ms is a different engine's; an edition that
|
|
10
|
+
* serves teams over this evaluator holds a page of a paged list to 300 ms
|
|
11
|
+
* and a point check to 50 ms, read from these lines, and records the
|
|
12
|
+
* single 10,000-candidate call (a request that asks for every row, the
|
|
13
|
+
* enumeration lanes) as its worst case.
|
|
10
14
|
*
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Two axes. The CANDIDATE count (1k and 10k rows of the listed kind) and
|
|
16
|
+
* the POLICY TABLE every check and every list batch reads a person's rows
|
|
17
|
+
* from through the IamPolicy port (indexed by principal on the OSS
|
|
18
|
+
* adapter; resource-store.ts).
|
|
19
|
+
* Every shape runs at an Enterprise-sized population: one organization of
|
|
20
|
+
* 1,000 members in 50 teams of 20, teams granted `viewer` on half the
|
|
21
|
+
* candidate agents, one person granted directly on 2% of them, and other
|
|
22
|
+
* members' direct grants filling the table to the candidate count (10,000
|
|
23
|
+
* rows at 10k; the base population alone is past 1k).
|
|
24
|
+
*
|
|
25
|
+
* The shapes, each timed once after the seed, both drivers:
|
|
26
|
+
* - the five open-source shapes: the restrict verb over a pure
|
|
27
|
+
* owner-only kind (session), over a kind with one userset hop
|
|
28
|
+
* (org-visible agents for a member), over the parent hop at two
|
|
29
|
+
* densities (executions in ten sessions, and in a thousand), over the
|
|
30
|
+
* kind whose `derived` rule reads the row (instances of one agent);
|
|
31
|
+
* the enumeration verb over the blueprint kind (the console library's
|
|
32
|
+
* path);
|
|
33
|
+
* - five Enterprise shapes over private agents: a team member's grants
|
|
34
|
+
* (the team hop through the model's organization bound); the same for
|
|
35
|
+
* a person in five teams (one more policy read per team); a person's
|
|
36
|
+
* direct grants; a former member who keeps the team's row and is
|
|
37
|
+
* admitted by none of it (the bound's correctness at volume); and the
|
|
38
|
+
* enumeration verb for the team member;
|
|
39
|
+
* - pages, as the paged lanes run them (pipeline/steps/list-page.ts): a
|
|
40
|
+
* page that fills in one batch (one scope call of 100 candidates) and
|
|
41
|
+
* a caller's worst page (five calls of 100, the examine budget of 500,
|
|
42
|
+
* each call its own source as `admit` makes it), for the team member,
|
|
43
|
+
* the direct grantee and an organization member, at the population of
|
|
44
|
+
* 10,000 policy rows;
|
|
45
|
+
* - point checks: one `can_view` through a team grant and one through a
|
|
46
|
+
* direct grant, each over 100 checks with a fresh source per check
|
|
47
|
+
* (the Authorizer's own shape), the mean reported.
|
|
48
|
+
*
|
|
49
|
+
* Every shape states what its caller keeps, so a measurement over an
|
|
50
|
+
* evaluator that answered wrongly measures nothing. The model is the
|
|
51
|
+
* built-in one with the Enterprise team type beside it
|
|
52
|
+
* (enterprise-model.ts); the open-source shapes answer the same under it.
|
|
19
53
|
*/
|
|
20
54
|
import { create } from "@bufbuild/protobuf";
|
|
21
55
|
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -23,8 +57,11 @@ import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
23
57
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
24
58
|
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
25
59
|
import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
60
|
+
import type { IamPolicySpec } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
|
|
26
61
|
import { IdentityAccountSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
27
62
|
import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
|
|
63
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
64
|
+
import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
28
65
|
|
|
29
66
|
import {
|
|
30
67
|
IAM_POLICY_API_VERSION,
|
|
@@ -33,7 +70,7 @@ import {
|
|
|
33
70
|
} from "../../domain/iampolicy/constants.js";
|
|
34
71
|
import { newResourceIamPolicyStore } from "../../domain/iampolicy/resource-store.js";
|
|
35
72
|
import type { IamPolicyStore } from "../../domain/iampolicy/store.js";
|
|
36
|
-
import { orgRole } from "../../domain/iampolicy/__tests__/support.js";
|
|
73
|
+
import { orgRole, triple } from "../../domain/iampolicy/__tests__/support.js";
|
|
37
74
|
import { accountIdFor } from "../../domain/identityaccount/constants.js";
|
|
38
75
|
import { newResourceIdentityAccountStore } from "../../domain/identityaccount/resource-store.js";
|
|
39
76
|
import type { IdentityAccountStore } from "../../domain/identityaccount/store.js";
|
|
@@ -42,11 +79,12 @@ import type { CallerIdentity } from "../../extensions/identity.js";
|
|
|
42
79
|
import type { ListEntryMeta } from "../../extensions/list-read-scope.js";
|
|
43
80
|
import { kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
44
81
|
import { rowAuthorizationFactsOf } from "../../pipeline/steps/authorization-facts.js";
|
|
82
|
+
import { newBuiltInAuthorizer } from "../authorizer.js";
|
|
45
83
|
import { newBuiltInListReadScope } from "../list-read-scope.js";
|
|
46
|
-
import { builtInModel } from "../model/index.js";
|
|
47
84
|
import type { KindDeclaration } from "../model/rewrite.js";
|
|
48
85
|
import { driverFixtures, dropPostgresFixture } from "./drivers.js";
|
|
49
86
|
import type { OpenedStore } from "./drivers.js";
|
|
87
|
+
import { enterpriseModel, teamDeclaration } from "./enterprise-model.js";
|
|
50
88
|
import { fixtureRow } from "./support.js";
|
|
51
89
|
import type { FixtureRowFacts } from "./support.js";
|
|
52
90
|
|
|
@@ -56,10 +94,57 @@ const SIZES = [1_000, 10_000];
|
|
|
56
94
|
const FOUNDER = accountIdFor("auth0|founder");
|
|
57
95
|
const MEMBER = accountIdFor("auth0|member");
|
|
58
96
|
const VIEWER = accountIdFor("auth0|viewer");
|
|
97
|
+
/** In team 0. */
|
|
98
|
+
const TEAM_MEMBER = accountIdFor("auth0|team-member");
|
|
99
|
+
/** In teams 0 to 4. */
|
|
100
|
+
const MULTI_TEAM = accountIdFor("auth0|multi-team");
|
|
101
|
+
/** Granted `viewer` directly on 2% of the candidate agents. */
|
|
102
|
+
const DIRECT = accountIdFor("auth0|direct");
|
|
103
|
+
/** Still holds team 0's `member` row; holds no organization role. */
|
|
104
|
+
const FORMER = accountIdFor("auth0|former");
|
|
59
105
|
const ORG = "acme";
|
|
60
106
|
|
|
107
|
+
const TEAMS = 50;
|
|
108
|
+
const TEAM_SIZE = 20;
|
|
109
|
+
const MEMBERS = TEAMS * TEAM_SIZE;
|
|
110
|
+
const MULTI_TEAM_COUNT = 5;
|
|
111
|
+
const POINT_CHECKS = 100;
|
|
112
|
+
/** The page loop's largest page and its examine budget (pipeline/steps/list-page.ts). */
|
|
113
|
+
const PAGE_SIZE = 100;
|
|
114
|
+
const PAGE_EXAMINE_BUDGET = 500;
|
|
115
|
+
/** The population the pages run at: a 10,000-row policy table. */
|
|
116
|
+
const PAGE_POPULATION = 10_000;
|
|
117
|
+
/**
|
|
118
|
+
* A case seeds up to twenty thousand rows one write at a time before its
|
|
119
|
+
* timer starts; the suite's 15 s budget is for tests, not for this seed.
|
|
120
|
+
*/
|
|
121
|
+
const MEASURE_TIMEOUT_MS = 600_000;
|
|
122
|
+
|
|
123
|
+
function teamId(i: number): string {
|
|
124
|
+
return `tm_${i}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function populationMemberId(i: number): string {
|
|
128
|
+
return `ida_pop_${i}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** The candidate agent of index `i`: private, the founder's. */
|
|
132
|
+
function enterpriseAgentId(i: number): string {
|
|
133
|
+
return `agt_e_${i}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The team granted `viewer` on agent `i`, or undefined: every even agent, round-robin over the teams. */
|
|
137
|
+
function teamGrantedOn(i: number): number | undefined {
|
|
138
|
+
return i % 2 === 0 ? i % TEAMS : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Whether `DIRECT` is granted `viewer` on agent `i`: 2% of the agents. */
|
|
142
|
+
function directGrantOn(i: number): boolean {
|
|
143
|
+
return i % 50 === 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
61
146
|
function declared(type: string): KindDeclaration {
|
|
62
|
-
const declaration =
|
|
147
|
+
const declaration = enterpriseModel.byType(type);
|
|
63
148
|
if (declaration === undefined) {
|
|
64
149
|
throw new Error(`${type} is declared`);
|
|
65
150
|
}
|
|
@@ -97,6 +182,37 @@ function sessionRow(i: number): FixtureRowFacts {
|
|
|
97
182
|
};
|
|
98
183
|
}
|
|
99
184
|
|
|
185
|
+
function enterpriseAgentRow(i: number): FixtureRowFacts {
|
|
186
|
+
return {
|
|
187
|
+
id: enterpriseAgentId(i),
|
|
188
|
+
org: ORG,
|
|
189
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
190
|
+
createdBy: FOUNDER,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** How many of `n` agents a person in teams `0..teams-1` views through team grants. */
|
|
195
|
+
function keptThroughTeams(n: number, teams: number): number {
|
|
196
|
+
let kept = 0;
|
|
197
|
+
for (let i = 0; i < n; i += 1) {
|
|
198
|
+
const team = teamGrantedOn(i);
|
|
199
|
+
if (team !== undefined && team < teams) {
|
|
200
|
+
kept += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return kept;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function keptDirectly(n: number): number {
|
|
207
|
+
let kept = 0;
|
|
208
|
+
for (let i = 0; i < n; i += 1) {
|
|
209
|
+
if (directGrantOn(i)) {
|
|
210
|
+
kept += 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return kept;
|
|
214
|
+
}
|
|
215
|
+
|
|
100
216
|
const SHAPES: ReadonlyArray<Shape> = [
|
|
101
217
|
{
|
|
102
218
|
name: "restrict: sessions, owner-only, the founder's own",
|
|
@@ -191,8 +307,138 @@ const SHAPES: ReadonlyArray<Shape> = [
|
|
|
191
307
|
// other; the cost measured is the rule's read of every candidate row.
|
|
192
308
|
kept: () => 1,
|
|
193
309
|
},
|
|
310
|
+
{
|
|
311
|
+
name: "restrict: private agents shared with the caller's team (the team hop through the organization bound)",
|
|
312
|
+
kind: "agent",
|
|
313
|
+
caller: TEAM_MEMBER,
|
|
314
|
+
verb: "restrict",
|
|
315
|
+
row: enterpriseAgentRow,
|
|
316
|
+
kept: (n) => keptThroughTeams(n, 1),
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "restrict: private agents shared with any of the caller's five teams (one policy read more per team)",
|
|
320
|
+
kind: "agent",
|
|
321
|
+
caller: MULTI_TEAM,
|
|
322
|
+
verb: "restrict",
|
|
323
|
+
row: enterpriseAgentRow,
|
|
324
|
+
kept: (n) => keptThroughTeams(n, MULTI_TEAM_COUNT),
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "restrict: private agents granted to the caller directly (a per-resource grant)",
|
|
328
|
+
kind: "agent",
|
|
329
|
+
caller: DIRECT,
|
|
330
|
+
verb: "restrict",
|
|
331
|
+
row: enterpriseAgentRow,
|
|
332
|
+
kept: keptDirectly,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "restrict: a former member keeps the team's row and is admitted by none of it",
|
|
336
|
+
kind: "agent",
|
|
337
|
+
caller: FORMER,
|
|
338
|
+
verb: "restrict",
|
|
339
|
+
row: enterpriseAgentRow,
|
|
340
|
+
kept: () => 0,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "enumerate: private agents for the team member (the library's path)",
|
|
344
|
+
kind: "agent",
|
|
345
|
+
caller: TEAM_MEMBER,
|
|
346
|
+
verb: "enumerate",
|
|
347
|
+
row: enterpriseAgentRow,
|
|
348
|
+
kept: (n) => keptThroughTeams(n, 1),
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
interface PageShape {
|
|
353
|
+
readonly name: string;
|
|
354
|
+
readonly caller: string;
|
|
355
|
+
/** The candidate of index `i`, an agent. */
|
|
356
|
+
row(i: number): FixtureRowFacts;
|
|
357
|
+
/** How many of the first `n` candidates the caller keeps. */
|
|
358
|
+
kept(n: number): number;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const PAGE_SHAPES: ReadonlyArray<PageShape> = [
|
|
362
|
+
{
|
|
363
|
+
name: "the team member's private agents shared with their team",
|
|
364
|
+
caller: TEAM_MEMBER,
|
|
365
|
+
row: enterpriseAgentRow,
|
|
366
|
+
kept: (n) => keptThroughTeams(n, 1),
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "the direct grantee's private agents granted to them",
|
|
370
|
+
caller: DIRECT,
|
|
371
|
+
row: enterpriseAgentRow,
|
|
372
|
+
kept: keptDirectly,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "an organization member's org-visible agents",
|
|
376
|
+
caller: MEMBER,
|
|
377
|
+
row: (i) => ({
|
|
378
|
+
id: `agt_${i}`,
|
|
379
|
+
org: ORG,
|
|
380
|
+
visibility: ApiResourceVisibility.visibility_org,
|
|
381
|
+
createdBy: FOUNDER,
|
|
382
|
+
}),
|
|
383
|
+
kept: (n) => n,
|
|
384
|
+
},
|
|
194
385
|
];
|
|
195
386
|
|
|
387
|
+
/** The Enterprise population's IamPolicy rows at candidate size `n`. */
|
|
388
|
+
function populationPolicies(n: number): IamPolicySpec[] {
|
|
389
|
+
const specs: IamPolicySpec[] = [
|
|
390
|
+
orgRole(FOUNDER, "owner", ORG),
|
|
391
|
+
orgRole(MEMBER, "member", ORG),
|
|
392
|
+
orgRole(VIEWER, "viewer", ORG),
|
|
393
|
+
orgRole(TEAM_MEMBER, "member", ORG),
|
|
394
|
+
orgRole(MULTI_TEAM, "member", ORG),
|
|
395
|
+
orgRole(DIRECT, "member", ORG),
|
|
396
|
+
];
|
|
397
|
+
const membership = (accountId: string, team: number): IamPolicySpec =>
|
|
398
|
+
triple({ kind: "identity_account", id: accountId }, "member", {
|
|
399
|
+
kind: "team",
|
|
400
|
+
id: teamId(team),
|
|
401
|
+
});
|
|
402
|
+
specs.push(membership(TEAM_MEMBER, 0), membership(FORMER, 0));
|
|
403
|
+
for (let team = 0; team < MULTI_TEAM_COUNT; team += 1) {
|
|
404
|
+
specs.push(membership(MULTI_TEAM, team));
|
|
405
|
+
}
|
|
406
|
+
for (let i = 0; i < MEMBERS; i += 1) {
|
|
407
|
+
specs.push(orgRole(populationMemberId(i), "member", ORG));
|
|
408
|
+
specs.push(membership(populationMemberId(i), i % TEAMS));
|
|
409
|
+
}
|
|
410
|
+
const viewerOn = (
|
|
411
|
+
principal: { kind: string; id: string; relation?: string },
|
|
412
|
+
i: number,
|
|
413
|
+
): IamPolicySpec =>
|
|
414
|
+
triple(principal, "viewer", { kind: "agent", id: enterpriseAgentId(i) });
|
|
415
|
+
const unclaimed: number[] = [];
|
|
416
|
+
for (let i = 0; i < n; i += 1) {
|
|
417
|
+
const team = teamGrantedOn(i);
|
|
418
|
+
if (team !== undefined) {
|
|
419
|
+
specs.push(
|
|
420
|
+
viewerOn({ kind: "team", id: teamId(team), relation: "member" }, i),
|
|
421
|
+
);
|
|
422
|
+
} else if (directGrantOn(i)) {
|
|
423
|
+
specs.push(viewerOn({ kind: "identity_account", id: DIRECT }, i));
|
|
424
|
+
} else {
|
|
425
|
+
unclaimed.push(i);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Other members' direct grants fill the table to the candidate count.
|
|
429
|
+
const filler = Math.min(unclaimed.length, Math.max(0, n - specs.length));
|
|
430
|
+
for (let k = 0; k < filler; k += 1) {
|
|
431
|
+
const i = unclaimed[k] ?? 0;
|
|
432
|
+
specs.push(
|
|
433
|
+
viewerOn(
|
|
434
|
+
{ kind: "identity_account", id: populationMemberId(i % MEMBERS) },
|
|
435
|
+
i,
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return specs;
|
|
440
|
+
}
|
|
441
|
+
|
|
196
442
|
afterAll(dropPostgresFixture);
|
|
197
443
|
|
|
198
444
|
describe
|
|
@@ -202,12 +448,13 @@ describe
|
|
|
202
448
|
ApiResourceKind.iam_policy,
|
|
203
449
|
ApiResourceKind.identity_account,
|
|
204
450
|
ApiResourceKind.organization,
|
|
451
|
+
ApiResourceKind.team,
|
|
205
452
|
ApiResourceKind.agent,
|
|
206
453
|
ApiResourceKind.session,
|
|
207
454
|
ApiResourceKind.agent_execution,
|
|
208
455
|
ApiResourceKind.agent_instance,
|
|
209
456
|
]),
|
|
210
|
-
)("the built-in
|
|
457
|
+
)("the built-in evaluator's cost on $name", (fixture) => {
|
|
211
458
|
describe.skipIf(fixture.skip)("measured", () => {
|
|
212
459
|
let opened: OpenedStore;
|
|
213
460
|
let policies: IamPolicyStore;
|
|
@@ -225,6 +472,31 @@ describe
|
|
|
225
472
|
return row;
|
|
226
473
|
}
|
|
227
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Writes the population's rows through the generic Store, where the
|
|
477
|
+
* OSS adapter reads every policy from: the port's `save` would read
|
|
478
|
+
* each id before writing it, doubling a seed of ten thousand rows.
|
|
479
|
+
* Returns the policy table's size.
|
|
480
|
+
*/
|
|
481
|
+
async function seedPopulation(n: number): Promise<number> {
|
|
482
|
+
const specs = populationPolicies(n);
|
|
483
|
+
for (const spec of specs) {
|
|
484
|
+
const id = policyIdFor(spec);
|
|
485
|
+
await opened.store.saveResource(
|
|
486
|
+
ApiResourceKind.iam_policy,
|
|
487
|
+
id,
|
|
488
|
+
IamPolicySchema,
|
|
489
|
+
create(IamPolicySchema, {
|
|
490
|
+
apiVersion: IAM_POLICY_API_VERSION,
|
|
491
|
+
kind: IAM_POLICY_KIND,
|
|
492
|
+
metadata: { id },
|
|
493
|
+
spec,
|
|
494
|
+
}),
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return specs.length;
|
|
498
|
+
}
|
|
499
|
+
|
|
228
500
|
beforeEach(async () => {
|
|
229
501
|
opened = await fixture.open();
|
|
230
502
|
policies = newResourceIamPolicyStore(opened.store);
|
|
@@ -233,6 +505,10 @@ describe
|
|
|
233
505
|
[FOUNDER, "auth0|founder"],
|
|
234
506
|
[MEMBER, "auth0|member"],
|
|
235
507
|
[VIEWER, "auth0|viewer"],
|
|
508
|
+
[TEAM_MEMBER, "auth0|team-member"],
|
|
509
|
+
[MULTI_TEAM, "auth0|multi-team"],
|
|
510
|
+
[DIRECT, "auth0|direct"],
|
|
511
|
+
[FORMER, "auth0|former"],
|
|
236
512
|
] as const) {
|
|
237
513
|
await accounts.save(
|
|
238
514
|
create(IdentityAccountSchema, {
|
|
@@ -250,17 +526,16 @@ describe
|
|
|
250
526
|
visibility: ApiResourceVisibility.visibility_private,
|
|
251
527
|
createdBy: FOUNDER,
|
|
252
528
|
});
|
|
253
|
-
for (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
spec,
|
|
529
|
+
for (let team = 0; team < TEAMS; team += 1) {
|
|
530
|
+
await opened.store.saveResource(
|
|
531
|
+
ApiResourceKind.team,
|
|
532
|
+
teamId(team),
|
|
533
|
+
teamDeclaration.schema,
|
|
534
|
+
fixtureRow(teamDeclaration, {
|
|
535
|
+
id: teamId(team),
|
|
536
|
+
org: ORG,
|
|
537
|
+
visibility: ApiResourceVisibility.visibility_private,
|
|
538
|
+
createdBy: FOUNDER,
|
|
264
539
|
}),
|
|
265
540
|
);
|
|
266
541
|
}
|
|
@@ -272,14 +547,63 @@ describe
|
|
|
272
547
|
|
|
273
548
|
for (const shape of SHAPES) {
|
|
274
549
|
for (const n of SIZES) {
|
|
275
|
-
it(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
550
|
+
it(
|
|
551
|
+
`${shape.name} at ${n} rows`,
|
|
552
|
+
async () => {
|
|
553
|
+
const declaration = declared(shape.kind);
|
|
554
|
+
const policyRows = await seedPopulation(n);
|
|
555
|
+
for (const parent of shape.parents ?? []) {
|
|
556
|
+
await save(parent.type, parent.facts);
|
|
557
|
+
}
|
|
558
|
+
const entries: ListEntryMeta[] = [];
|
|
559
|
+
for (let i = 0; i < n; i += 1) {
|
|
560
|
+
const row = await save(shape.kind, shape.row(i, n));
|
|
561
|
+
entries.push({
|
|
562
|
+
...rowAuthorizationFactsOf(declaration.kind, row),
|
|
563
|
+
labels: {},
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const scope = newBuiltInListReadScope({
|
|
567
|
+
store: opened.store,
|
|
568
|
+
policies,
|
|
569
|
+
accounts,
|
|
570
|
+
logger: silentLogger,
|
|
571
|
+
model: enterpriseModel,
|
|
572
|
+
});
|
|
573
|
+
const started = performance.now();
|
|
574
|
+
const kept =
|
|
575
|
+
shape.verb === "restrict"
|
|
576
|
+
? await scope.restrictListEntries(
|
|
577
|
+
resolved(shape.caller),
|
|
578
|
+
declaration.kind,
|
|
579
|
+
entries,
|
|
580
|
+
)
|
|
581
|
+
: await scope.authorizedResourceIds(
|
|
582
|
+
resolved(shape.caller),
|
|
583
|
+
declaration.kind,
|
|
584
|
+
);
|
|
585
|
+
const elapsedMs = Math.round(performance.now() - started);
|
|
586
|
+
// Every shape states what its caller keeps, so a measurement
|
|
587
|
+
// over a scope that answered wrongly measures nothing.
|
|
588
|
+
expect(kept.size).toBe(shape.kept?.(n) ?? n);
|
|
589
|
+
console.log(
|
|
590
|
+
`[measure] ${fixture.name} ${kindEnumName(declaration.kind)} ${shape.verb} n=${n} policies=${policyRows} elapsed=${elapsedMs}ms — ${shape.name}`,
|
|
591
|
+
);
|
|
592
|
+
},
|
|
593
|
+
MEASURE_TIMEOUT_MS,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (const shape of PAGE_SHAPES) {
|
|
599
|
+
it(
|
|
600
|
+
`pages: ${shape.name}, at a ${PAGE_POPULATION}-row population`,
|
|
601
|
+
async () => {
|
|
602
|
+
const declaration = declared("agent");
|
|
603
|
+
const policyRows = await seedPopulation(PAGE_POPULATION);
|
|
280
604
|
const entries: ListEntryMeta[] = [];
|
|
281
|
-
for (let i = 0; i <
|
|
282
|
-
const row = await save(
|
|
605
|
+
for (let i = 0; i < PAGE_EXAMINE_BUDGET; i += 1) {
|
|
606
|
+
const row = await save("agent", shape.row(i));
|
|
283
607
|
entries.push({
|
|
284
608
|
...rowAuthorizationFactsOf(declaration.kind, row),
|
|
285
609
|
labels: {},
|
|
@@ -290,27 +614,73 @@ describe
|
|
|
290
614
|
policies,
|
|
291
615
|
accounts,
|
|
292
616
|
logger: silentLogger,
|
|
617
|
+
model: enterpriseModel,
|
|
293
618
|
});
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
619
|
+
const caller = resolved(shape.caller);
|
|
620
|
+
// A page's batches run one after another, each through the scope
|
|
621
|
+
// with its own source, exactly as the page loop calls `admit`.
|
|
622
|
+
async function page(batches: number): Promise<number> {
|
|
623
|
+
let kept = 0;
|
|
624
|
+
for (let b = 0; b < batches; b += 1) {
|
|
625
|
+
const batch = entries.slice(b * PAGE_SIZE, (b + 1) * PAGE_SIZE);
|
|
626
|
+
kept += (
|
|
627
|
+
await scope.restrictListEntries(caller, declaration.kind, batch)
|
|
628
|
+
).size;
|
|
629
|
+
}
|
|
630
|
+
return kept;
|
|
631
|
+
}
|
|
632
|
+
for (const batches of [1, PAGE_EXAMINE_BUDGET / PAGE_SIZE]) {
|
|
633
|
+
const started = performance.now();
|
|
634
|
+
const kept = await page(batches);
|
|
635
|
+
const elapsedMs = Math.round(performance.now() - started);
|
|
636
|
+
expect(kept).toBe(shape.kept(batches * PAGE_SIZE));
|
|
637
|
+
console.log(
|
|
638
|
+
`[measure] ${fixture.name} agent page calls=${batches} candidates=${batches * PAGE_SIZE} policies=${policyRows} elapsed=${elapsedMs}ms — ${batches === 1 ? "a page that fills in one batch" : "the worst page"}: ${shape.name}`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
MEASURE_TIMEOUT_MS,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
for (const [caller, agent, grant] of [
|
|
647
|
+
[TEAM_MEMBER, 0, "a team grant"],
|
|
648
|
+
[DIRECT, 1, "a direct grant"],
|
|
649
|
+
] as const) {
|
|
650
|
+
for (const n of SIZES) {
|
|
651
|
+
it(
|
|
652
|
+
`point check: can_view through ${grant} at the ${n}-candidate population`,
|
|
653
|
+
async () => {
|
|
654
|
+
const policyRows = await seedPopulation(n);
|
|
655
|
+
await save("agent", enterpriseAgentRow(agent));
|
|
656
|
+
const authorizer = newBuiltInAuthorizer({
|
|
657
|
+
store: opened.store,
|
|
658
|
+
policies,
|
|
659
|
+
accounts,
|
|
660
|
+
edition: ServerEdition.oss,
|
|
661
|
+
logger: silentLogger,
|
|
662
|
+
model: enterpriseModel,
|
|
663
|
+
});
|
|
664
|
+
const check = {
|
|
665
|
+
permission: IamPermission.can_view,
|
|
666
|
+
resourceKind: ApiResourceKind.agent,
|
|
667
|
+
resourceId: enterpriseAgentId(agent),
|
|
668
|
+
};
|
|
669
|
+
const started = performance.now();
|
|
670
|
+
for (let k = 0; k < POINT_CHECKS; k += 1) {
|
|
671
|
+
// Each call builds its own source, the Authorizer's shape: no
|
|
672
|
+
// memo carries from one check to the next.
|
|
673
|
+
expect(
|
|
674
|
+
await authorizer.authorize(resolved(caller), check),
|
|
675
|
+
).toEqual({ kind: "allow" });
|
|
676
|
+
}
|
|
677
|
+
const meanMs = (performance.now() - started) / POINT_CHECKS;
|
|
678
|
+
console.log(
|
|
679
|
+
`[measure] ${fixture.name} agent point-check policies=${policyRows} mean=${meanMs.toFixed(2)}ms over ${POINT_CHECKS} — can_view through ${grant}`,
|
|
680
|
+
);
|
|
681
|
+
},
|
|
682
|
+
MEASURE_TIMEOUT_MS,
|
|
683
|
+
);
|
|
314
684
|
}
|
|
315
685
|
}
|
|
316
686
|
});
|
|
@@ -47,8 +47,10 @@
|
|
|
47
47
|
*
|
|
48
48
|
* Cost, per check (measured at slice 4): the target row (1), the caller's
|
|
49
49
|
* account (1 primary-key read; 2 for a caller a composition verifier left
|
|
50
|
-
* idp-shaped), the person's IamPolicy rows (1
|
|
51
|
-
* source
|
|
50
|
+
* idp-shaped), the person's IamPolicy rows (1 read of the port, once per
|
|
51
|
+
* source, indexed by principal on the OSS adapter; one more per team the
|
|
52
|
+
* person holds, which open source never has — derived-tuples.ts), and one
|
|
53
|
+
* row per parent hop the model walks.
|
|
52
54
|
*/
|
|
53
55
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
54
56
|
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|