@stigmer/server 3.27.1 → 3.28.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.
Files changed (245) hide show
  1. package/README.md +2 -0
  2. package/dist/authorization/authorizer.d.ts +7 -5
  3. package/dist/authorization/authorizer.d.ts.map +1 -1
  4. package/dist/authorization/authorizer.js +7 -5
  5. package/dist/authorization/authorizer.js.map +1 -1
  6. package/dist/authorization/derived-tuples.d.ts +3 -1
  7. package/dist/authorization/derived-tuples.d.ts.map +1 -1
  8. package/dist/authorization/derived-tuples.js +52 -2
  9. package/dist/authorization/derived-tuples.js.map +1 -1
  10. package/dist/authorization/evaluator.d.ts +9 -10
  11. package/dist/authorization/evaluator.d.ts.map +1 -1
  12. package/dist/authorization/evaluator.js.map +1 -1
  13. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  14. package/dist/authorization/list-read-scope.js +29 -18
  15. package/dist/authorization/list-read-scope.js.map +1 -1
  16. package/dist/authorization/model/bindings.d.ts +41 -0
  17. package/dist/authorization/model/bindings.d.ts.map +1 -0
  18. package/dist/authorization/model/bindings.js +89 -0
  19. package/dist/authorization/model/bindings.js.map +1 -0
  20. package/dist/authorization/model/data/authorization-model.json +3114 -0
  21. package/dist/authorization/model/default-of.d.ts +2 -2
  22. package/dist/authorization/model/execution-viewer.d.ts +3 -0
  23. package/dist/authorization/model/execution-viewer.d.ts.map +1 -0
  24. package/dist/authorization/model/execution-viewer.js +50 -0
  25. package/dist/authorization/model/execution-viewer.js.map +1 -0
  26. package/dist/authorization/model/index.d.ts +33 -21
  27. package/dist/authorization/model/index.d.ts.map +1 -1
  28. package/dist/authorization/model/index.js +74 -52
  29. package/dist/authorization/model/index.js.map +1 -1
  30. package/dist/authorization/model/openfga-json.d.ts +18 -0
  31. package/dist/authorization/model/openfga-json.d.ts.map +1 -0
  32. package/dist/authorization/model/openfga-json.js +266 -0
  33. package/dist/authorization/model/openfga-json.js.map +1 -0
  34. package/dist/authorization/model/rewrite.d.ts +39 -67
  35. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  36. package/dist/authorization/model/rewrite.js +1 -117
  37. package/dist/authorization/model/rewrite.js.map +1 -1
  38. package/dist/authorization/strict-json.d.ts +20 -0
  39. package/dist/authorization/strict-json.d.ts.map +1 -0
  40. package/dist/authorization/strict-json.js +45 -0
  41. package/dist/authorization/strict-json.js.map +1 -0
  42. package/dist/boot/compose.d.ts.map +1 -1
  43. package/dist/boot/compose.js +9 -7
  44. package/dist/boot/compose.js.map +1 -1
  45. package/dist/boot/list-indexes.d.ts.map +1 -1
  46. package/dist/boot/list-indexes.js +6 -0
  47. package/dist/boot/list-indexes.js.map +1 -1
  48. package/dist/domain/iampolicy/list-index.d.ts +2 -0
  49. package/dist/domain/iampolicy/list-index.d.ts.map +1 -0
  50. package/dist/domain/iampolicy/list-index.js +32 -0
  51. package/dist/domain/iampolicy/list-index.js.map +1 -0
  52. package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
  53. package/dist/domain/iampolicy/resource-store.js +24 -11
  54. package/dist/domain/iampolicy/resource-store.js.map +1 -1
  55. package/dist/domain/workflowexecution/list-index.d.ts +1 -1
  56. package/dist/extensions/list-read-scope.d.ts +20 -3
  57. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  58. package/dist/extensions/list-read-scope.js +22 -2
  59. package/dist/extensions/list-read-scope.js.map +1 -1
  60. package/dist/index.d.ts +8 -8
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +20 -5
  63. package/dist/index.js.map +1 -1
  64. package/package.json +7 -6
  65. package/src/authorization/README.md +48 -39
  66. package/src/authorization/__tests__/authorizer.test.ts +5 -14
  67. package/src/authorization/__tests__/derived-tuples.test.ts +192 -17
  68. package/src/authorization/__tests__/evaluator.test.ts +53 -160
  69. package/src/authorization/__tests__/facts.test.ts +5 -11
  70. package/src/authorization/__tests__/list-read-scope.measure.test.ts +424 -62
  71. package/src/authorization/__tests__/list-read-scope.test.ts +15 -14
  72. package/src/authorization/__tests__/organization-directory.test.ts +2 -2
  73. package/src/authorization/__tests__/store-test-kit.ts +246 -0
  74. package/src/authorization/__tests__/store-tests.test.ts +91 -223
  75. package/src/authorization/__tests__/support.ts +30 -2
  76. package/src/authorization/__tests__/throwaway-model.ts +75 -0
  77. package/src/authorization/__tests__/wire-permissions.test.ts +141 -107
  78. package/src/authorization/authorizer.ts +7 -5
  79. package/src/authorization/derived-tuples.ts +105 -16
  80. package/src/authorization/evaluator.ts +8 -10
  81. package/src/authorization/list-read-scope.ts +37 -22
  82. package/src/authorization/model/__tests__/openfga-json.test.ts +353 -0
  83. package/src/authorization/model/__tests__/registry.test.ts +102 -276
  84. package/src/authorization/model/bindings.ts +131 -0
  85. package/src/authorization/model/data/authorization-model.json +3114 -0
  86. package/src/authorization/model/default-of.ts +2 -2
  87. package/src/authorization/model/execution-viewer.ts +56 -0
  88. package/src/authorization/model/index.ts +81 -72
  89. package/src/authorization/model/openfga-json.ts +310 -0
  90. package/src/authorization/model/rewrite.ts +39 -188
  91. package/src/authorization/strict-json.ts +52 -0
  92. package/src/boot/__tests__/list-indexes.test.ts +4 -0
  93. package/src/boot/compose.ts +9 -7
  94. package/src/boot/list-indexes.ts +6 -0
  95. package/src/domain/iampolicy/__tests__/resource-store.test.ts +65 -5
  96. package/src/domain/iampolicy/list-index.ts +33 -0
  97. package/src/domain/iampolicy/resource-store.ts +24 -11
  98. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +3 -6
  99. package/src/extensions/list-read-scope.ts +32 -4
  100. package/src/index.ts +20 -44
  101. package/dist/authorization/model/agent.d.ts +0 -2
  102. package/dist/authorization/model/agent.d.ts.map +0 -1
  103. package/dist/authorization/model/agent.js +0 -43
  104. package/dist/authorization/model/agent.js.map +0 -1
  105. package/dist/authorization/model/agent_channel.d.ts +0 -2
  106. package/dist/authorization/model/agent_channel.d.ts.map +0 -1
  107. package/dist/authorization/model/agent_channel.js +0 -39
  108. package/dist/authorization/model/agent_channel.js.map +0 -1
  109. package/dist/authorization/model/agent_execution.d.ts +0 -2
  110. package/dist/authorization/model/agent_execution.d.ts.map +0 -1
  111. package/dist/authorization/model/agent_execution.js +0 -25
  112. package/dist/authorization/model/agent_execution.js.map +0 -1
  113. package/dist/authorization/model/agent_instance.d.ts +0 -2
  114. package/dist/authorization/model/agent_instance.d.ts.map +0 -1
  115. package/dist/authorization/model/agent_instance.js +0 -48
  116. package/dist/authorization/model/agent_instance.js.map +0 -1
  117. package/dist/authorization/model/agent_share.d.ts +0 -2
  118. package/dist/authorization/model/agent_share.d.ts.map +0 -1
  119. package/dist/authorization/model/agent_share.js +0 -29
  120. package/dist/authorization/model/agent_share.js.map +0 -1
  121. package/dist/authorization/model/api_key.d.ts +0 -2
  122. package/dist/authorization/model/api_key.d.ts.map +0 -1
  123. package/dist/authorization/model/api_key.js +0 -27
  124. package/dist/authorization/model/api_key.js.map +0 -1
  125. package/dist/authorization/model/artifact.d.ts +0 -2
  126. package/dist/authorization/model/artifact.d.ts.map +0 -1
  127. package/dist/authorization/model/artifact.js +0 -27
  128. package/dist/authorization/model/artifact.js.map +0 -1
  129. package/dist/authorization/model/channel_app.d.ts +0 -2
  130. package/dist/authorization/model/channel_app.d.ts.map +0 -1
  131. package/dist/authorization/model/channel_app.js +0 -28
  132. package/dist/authorization/model/channel_app.js.map +0 -1
  133. package/dist/authorization/model/environment.d.ts +0 -2
  134. package/dist/authorization/model/environment.d.ts.map +0 -1
  135. package/dist/authorization/model/environment.js +0 -33
  136. package/dist/authorization/model/environment.js.map +0 -1
  137. package/dist/authorization/model/execution_context.d.ts +0 -2
  138. package/dist/authorization/model/execution_context.d.ts.map +0 -1
  139. package/dist/authorization/model/execution_context.js +0 -19
  140. package/dist/authorization/model/execution_context.js.map +0 -1
  141. package/dist/authorization/model/iam_policy.d.ts +0 -2
  142. package/dist/authorization/model/iam_policy.d.ts.map +0 -1
  143. package/dist/authorization/model/iam_policy.js +0 -31
  144. package/dist/authorization/model/iam_policy.js.map +0 -1
  145. package/dist/authorization/model/identity_account.d.ts +0 -2
  146. package/dist/authorization/model/identity_account.d.ts.map +0 -1
  147. package/dist/authorization/model/identity_account.js +0 -25
  148. package/dist/authorization/model/identity_account.js.map +0 -1
  149. package/dist/authorization/model/mcp_server.d.ts +0 -2
  150. package/dist/authorization/model/mcp_server.d.ts.map +0 -1
  151. package/dist/authorization/model/mcp_server.js +0 -38
  152. package/dist/authorization/model/mcp_server.js.map +0 -1
  153. package/dist/authorization/model/memory.d.ts +0 -2
  154. package/dist/authorization/model/memory.d.ts.map +0 -1
  155. package/dist/authorization/model/memory.js +0 -26
  156. package/dist/authorization/model/memory.js.map +0 -1
  157. package/dist/authorization/model/oauth_app.d.ts +0 -2
  158. package/dist/authorization/model/oauth_app.d.ts.map +0 -1
  159. package/dist/authorization/model/oauth_app.js +0 -31
  160. package/dist/authorization/model/oauth_app.js.map +0 -1
  161. package/dist/authorization/model/organization.d.ts +0 -2
  162. package/dist/authorization/model/organization.d.ts.map +0 -1
  163. package/dist/authorization/model/organization.js +0 -58
  164. package/dist/authorization/model/organization.js.map +0 -1
  165. package/dist/authorization/model/platform_client.d.ts +0 -2
  166. package/dist/authorization/model/platform_client.d.ts.map +0 -1
  167. package/dist/authorization/model/platform_client.js +0 -31
  168. package/dist/authorization/model/platform_client.js.map +0 -1
  169. package/dist/authorization/model/plugin.d.ts +0 -2
  170. package/dist/authorization/model/plugin.d.ts.map +0 -1
  171. package/dist/authorization/model/plugin.js +0 -45
  172. package/dist/authorization/model/plugin.js.map +0 -1
  173. package/dist/authorization/model/schedule.d.ts +0 -2
  174. package/dist/authorization/model/schedule.d.ts.map +0 -1
  175. package/dist/authorization/model/schedule.js +0 -34
  176. package/dist/authorization/model/schedule.js.map +0 -1
  177. package/dist/authorization/model/session.d.ts +0 -2
  178. package/dist/authorization/model/session.d.ts.map +0 -1
  179. package/dist/authorization/model/session.js +0 -38
  180. package/dist/authorization/model/session.js.map +0 -1
  181. package/dist/authorization/model/skill.d.ts +0 -2
  182. package/dist/authorization/model/skill.d.ts.map +0 -1
  183. package/dist/authorization/model/skill.js +0 -39
  184. package/dist/authorization/model/skill.js.map +0 -1
  185. package/dist/authorization/model/workflow.d.ts +0 -2
  186. package/dist/authorization/model/workflow.d.ts.map +0 -1
  187. package/dist/authorization/model/workflow.js +0 -39
  188. package/dist/authorization/model/workflow.js.map +0 -1
  189. package/dist/authorization/model/workflow_execution.d.ts +0 -2
  190. package/dist/authorization/model/workflow_execution.d.ts.map +0 -1
  191. package/dist/authorization/model/workflow_execution.js +0 -33
  192. package/dist/authorization/model/workflow_execution.js.map +0 -1
  193. package/dist/authorization/model/workflow_instance.d.ts +0 -2
  194. package/dist/authorization/model/workflow_instance.d.ts.map +0 -1
  195. package/dist/authorization/model/workflow_instance.js +0 -96
  196. package/dist/authorization/model/workflow_instance.js.map +0 -1
  197. package/dist/authorization/store-test-kit.d.ts +0 -62
  198. package/dist/authorization/store-test-kit.d.ts.map +0 -1
  199. package/dist/authorization/store-test-kit.js +0 -265
  200. package/dist/authorization/store-test-kit.js.map +0 -1
  201. package/src/authorization/__tests__/fixtures/fga/agent-channel-owner.fga.yaml +0 -112
  202. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +0 -177
  203. package/src/authorization/__tests__/fixtures/fga/agent-share-owner.fga.yaml +0 -109
  204. package/src/authorization/__tests__/fixtures/fga/artifact-org-and-owner.fga.yaml +0 -128
  205. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +0 -318
  206. package/src/authorization/__tests__/fixtures/fga/channel-app-owner.fga.yaml +0 -125
  207. package/src/authorization/__tests__/fixtures/fga/channel-participation.fga.yaml +0 -201
  208. package/src/authorization/__tests__/fixtures/fga/channel-session-visibility.fga.yaml +0 -187
  209. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +0 -214
  210. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +0 -129
  211. package/src/authorization/__tests__/fixtures/fga/memory-subject-only.fga.yaml +0 -72
  212. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +0 -308
  213. package/src/authorization/__tests__/fixtures/fga/org-shared-environment.fga.yaml +0 -144
  214. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +0 -199
  215. package/src/authorization/__tests__/fixtures/fga/provider-standing-viewer.fga.yaml +0 -40
  216. package/src/authorization/__tests__/fixtures/fga/reserved-label-writer.fga.yaml +0 -41
  217. package/src/authorization/__tests__/fixtures/fga/schedule-owner.fga.yaml +0 -113
  218. package/src/authorization/__tests__/fixtures/fga/schedule-session-visibility.fga.yaml +0 -214
  219. package/src/authorization/__tests__/fixtures/fga/session-personal-resource.fga.yaml +0 -117
  220. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +0 -190
  221. package/src/authorization/model/agent.ts +0 -64
  222. package/src/authorization/model/agent_channel.ts +0 -57
  223. package/src/authorization/model/agent_execution.ts +0 -33
  224. package/src/authorization/model/agent_instance.ts +0 -66
  225. package/src/authorization/model/agent_share.ts +0 -40
  226. package/src/authorization/model/api_key.ts +0 -28
  227. package/src/authorization/model/artifact.ts +0 -38
  228. package/src/authorization/model/channel_app.ts +0 -39
  229. package/src/authorization/model/environment.ts +0 -48
  230. package/src/authorization/model/execution_context.ts +0 -20
  231. package/src/authorization/model/iam_policy.ts +0 -42
  232. package/src/authorization/model/identity_account.ts +0 -26
  233. package/src/authorization/model/mcp_server.ts +0 -59
  234. package/src/authorization/model/memory.ts +0 -27
  235. package/src/authorization/model/oauth_app.ts +0 -43
  236. package/src/authorization/model/organization.ts +0 -59
  237. package/src/authorization/model/platform_client.ts +0 -43
  238. package/src/authorization/model/plugin.ts +0 -66
  239. package/src/authorization/model/schedule.ts +0 -49
  240. package/src/authorization/model/session.ts +0 -50
  241. package/src/authorization/model/skill.ts +0 -60
  242. package/src/authorization/model/workflow.ts +0 -60
  243. package/src/authorization/model/workflow_execution.ts +0 -46
  244. package/src/authorization/model/workflow_instance.ts +0 -125
  245. package/src/authorization/store-test-kit.ts +0 -395
@@ -1,21 +1,55 @@
1
1
  /**
2
- * The list scope's cost, measured — the plan's storage-impact line ("a
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 number is recorded in the project's execution log, not asserted:
8
- * no budget is ruled for open source, and the cloud's 300 ms is a
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
- * Five shapes at 1k and 10k rows, on both drivers, each timed once after
12
- * the seed: the restrict verb over a pure owner-only kind (session), over
13
- * a kind with one userset hop (org-visible agents for a member), over the
14
- * parent hop at two densities (executions in ten sessions, and in a
15
- * thousand), and over the kind whose `derived` rule reads the row
16
- * (instances of one agent); the enumeration verb over the blueprint kind,
17
- * which is the console library's path. The seed reuses the module's
18
- * fixtures so the rows are the rows the other tests reason about.
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, which declares the Enterprise team type beside the
52
+ * open-source ones.
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,12 +79,13 @@ 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";
50
- import { fixtureRow } from "./support.js";
87
+ import { builtInModel } from "../model/index.js";
88
+ import { fixtureRow, storedDeclaration } from "./support.js";
51
89
  import type { FixtureRowFacts } from "./support.js";
52
90
 
53
91
  const MEASURE = process.env["AUTHORIZATION_MEASURE"] === "1";
@@ -56,14 +94,53 @@ 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
 
61
- function declared(type: string): KindDeclaration {
62
- const declaration = builtInModel.byType(type);
63
- if (declaration === undefined) {
64
- throw new Error(`${type} is declared`);
65
- }
66
- return declaration;
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;
67
144
  }
68
145
 
69
146
  function resolved(accountId: string): CallerIdentity {
@@ -97,6 +174,37 @@ function sessionRow(i: number): FixtureRowFacts {
97
174
  };
98
175
  }
99
176
 
177
+ function enterpriseAgentRow(i: number): FixtureRowFacts {
178
+ return {
179
+ id: enterpriseAgentId(i),
180
+ org: ORG,
181
+ visibility: ApiResourceVisibility.visibility_private,
182
+ createdBy: FOUNDER,
183
+ };
184
+ }
185
+
186
+ /** How many of `n` agents a person in teams `0..teams-1` views through team grants. */
187
+ function keptThroughTeams(n: number, teams: number): number {
188
+ let kept = 0;
189
+ for (let i = 0; i < n; i += 1) {
190
+ const team = teamGrantedOn(i);
191
+ if (team !== undefined && team < teams) {
192
+ kept += 1;
193
+ }
194
+ }
195
+ return kept;
196
+ }
197
+
198
+ function keptDirectly(n: number): number {
199
+ let kept = 0;
200
+ for (let i = 0; i < n; i += 1) {
201
+ if (directGrantOn(i)) {
202
+ kept += 1;
203
+ }
204
+ }
205
+ return kept;
206
+ }
207
+
100
208
  const SHAPES: ReadonlyArray<Shape> = [
101
209
  {
102
210
  name: "restrict: sessions, owner-only, the founder's own",
@@ -191,8 +299,138 @@ const SHAPES: ReadonlyArray<Shape> = [
191
299
  // other; the cost measured is the rule's read of every candidate row.
192
300
  kept: () => 1,
193
301
  },
302
+ {
303
+ name: "restrict: private agents shared with the caller's team (the team hop through the organization bound)",
304
+ kind: "agent",
305
+ caller: TEAM_MEMBER,
306
+ verb: "restrict",
307
+ row: enterpriseAgentRow,
308
+ kept: (n) => keptThroughTeams(n, 1),
309
+ },
310
+ {
311
+ name: "restrict: private agents shared with any of the caller's five teams (one policy read more per team)",
312
+ kind: "agent",
313
+ caller: MULTI_TEAM,
314
+ verb: "restrict",
315
+ row: enterpriseAgentRow,
316
+ kept: (n) => keptThroughTeams(n, MULTI_TEAM_COUNT),
317
+ },
318
+ {
319
+ name: "restrict: private agents granted to the caller directly (a per-resource grant)",
320
+ kind: "agent",
321
+ caller: DIRECT,
322
+ verb: "restrict",
323
+ row: enterpriseAgentRow,
324
+ kept: keptDirectly,
325
+ },
326
+ {
327
+ name: "restrict: a former member keeps the team's row and is admitted by none of it",
328
+ kind: "agent",
329
+ caller: FORMER,
330
+ verb: "restrict",
331
+ row: enterpriseAgentRow,
332
+ kept: () => 0,
333
+ },
334
+ {
335
+ name: "enumerate: private agents for the team member (the library's path)",
336
+ kind: "agent",
337
+ caller: TEAM_MEMBER,
338
+ verb: "enumerate",
339
+ row: enterpriseAgentRow,
340
+ kept: (n) => keptThroughTeams(n, 1),
341
+ },
194
342
  ];
195
343
 
344
+ interface PageShape {
345
+ readonly name: string;
346
+ readonly caller: string;
347
+ /** The candidate of index `i`, an agent. */
348
+ row(i: number): FixtureRowFacts;
349
+ /** How many of the first `n` candidates the caller keeps. */
350
+ kept(n: number): number;
351
+ }
352
+
353
+ const PAGE_SHAPES: ReadonlyArray<PageShape> = [
354
+ {
355
+ name: "the team member's private agents shared with their team",
356
+ caller: TEAM_MEMBER,
357
+ row: enterpriseAgentRow,
358
+ kept: (n) => keptThroughTeams(n, 1),
359
+ },
360
+ {
361
+ name: "the direct grantee's private agents granted to them",
362
+ caller: DIRECT,
363
+ row: enterpriseAgentRow,
364
+ kept: keptDirectly,
365
+ },
366
+ {
367
+ name: "an organization member's org-visible agents",
368
+ caller: MEMBER,
369
+ row: (i) => ({
370
+ id: `agt_${i}`,
371
+ org: ORG,
372
+ visibility: ApiResourceVisibility.visibility_org,
373
+ createdBy: FOUNDER,
374
+ }),
375
+ kept: (n) => n,
376
+ },
377
+ ];
378
+
379
+ /** The Enterprise population's IamPolicy rows at candidate size `n`. */
380
+ function populationPolicies(n: number): IamPolicySpec[] {
381
+ const specs: IamPolicySpec[] = [
382
+ orgRole(FOUNDER, "owner", ORG),
383
+ orgRole(MEMBER, "member", ORG),
384
+ orgRole(VIEWER, "viewer", ORG),
385
+ orgRole(TEAM_MEMBER, "member", ORG),
386
+ orgRole(MULTI_TEAM, "member", ORG),
387
+ orgRole(DIRECT, "member", ORG),
388
+ ];
389
+ const membership = (accountId: string, team: number): IamPolicySpec =>
390
+ triple({ kind: "identity_account", id: accountId }, "member", {
391
+ kind: "team",
392
+ id: teamId(team),
393
+ });
394
+ specs.push(membership(TEAM_MEMBER, 0), membership(FORMER, 0));
395
+ for (let team = 0; team < MULTI_TEAM_COUNT; team += 1) {
396
+ specs.push(membership(MULTI_TEAM, team));
397
+ }
398
+ for (let i = 0; i < MEMBERS; i += 1) {
399
+ specs.push(orgRole(populationMemberId(i), "member", ORG));
400
+ specs.push(membership(populationMemberId(i), i % TEAMS));
401
+ }
402
+ const viewerOn = (
403
+ principal: { kind: string; id: string; relation?: string },
404
+ i: number,
405
+ ): IamPolicySpec =>
406
+ triple(principal, "viewer", { kind: "agent", id: enterpriseAgentId(i) });
407
+ const unclaimed: number[] = [];
408
+ for (let i = 0; i < n; i += 1) {
409
+ const team = teamGrantedOn(i);
410
+ if (team !== undefined) {
411
+ specs.push(
412
+ viewerOn({ kind: "team", id: teamId(team), relation: "member" }, i),
413
+ );
414
+ } else if (directGrantOn(i)) {
415
+ specs.push(viewerOn({ kind: "identity_account", id: DIRECT }, i));
416
+ } else {
417
+ unclaimed.push(i);
418
+ }
419
+ }
420
+ // Other members' direct grants fill the table to the candidate count.
421
+ const filler = Math.min(unclaimed.length, Math.max(0, n - specs.length));
422
+ for (let k = 0; k < filler; k += 1) {
423
+ const i = unclaimed[k] ?? 0;
424
+ specs.push(
425
+ viewerOn(
426
+ { kind: "identity_account", id: populationMemberId(i % MEMBERS) },
427
+ i,
428
+ ),
429
+ );
430
+ }
431
+ return specs;
432
+ }
433
+
196
434
  afterAll(dropPostgresFixture);
197
435
 
198
436
  describe
@@ -202,19 +440,20 @@ describe
202
440
  ApiResourceKind.iam_policy,
203
441
  ApiResourceKind.identity_account,
204
442
  ApiResourceKind.organization,
443
+ ApiResourceKind.team,
205
444
  ApiResourceKind.agent,
206
445
  ApiResourceKind.session,
207
446
  ApiResourceKind.agent_execution,
208
447
  ApiResourceKind.agent_instance,
209
448
  ]),
210
- )("the built-in list read scope's cost on $name", (fixture) => {
449
+ )("the built-in evaluator's cost on $name", (fixture) => {
211
450
  describe.skipIf(fixture.skip)("measured", () => {
212
451
  let opened: OpenedStore;
213
452
  let policies: IamPolicyStore;
214
453
  let accounts: IdentityAccountStore;
215
454
 
216
455
  async function save(type: string, facts: FixtureRowFacts): Promise<object> {
217
- const declaration = declared(type);
456
+ const declaration = storedDeclaration(type);
218
457
  const row = fixtureRow(declaration, facts);
219
458
  await opened.store.saveResource(
220
459
  declaration.kind,
@@ -225,6 +464,31 @@ describe
225
464
  return row;
226
465
  }
227
466
 
467
+ /**
468
+ * Writes the population's rows through the generic Store, where the
469
+ * OSS adapter reads every policy from: the port's `save` would read
470
+ * each id before writing it, doubling a seed of ten thousand rows.
471
+ * Returns the policy table's size.
472
+ */
473
+ async function seedPopulation(n: number): Promise<number> {
474
+ const specs = populationPolicies(n);
475
+ for (const spec of specs) {
476
+ const id = policyIdFor(spec);
477
+ await opened.store.saveResource(
478
+ ApiResourceKind.iam_policy,
479
+ id,
480
+ IamPolicySchema,
481
+ create(IamPolicySchema, {
482
+ apiVersion: IAM_POLICY_API_VERSION,
483
+ kind: IAM_POLICY_KIND,
484
+ metadata: { id },
485
+ spec,
486
+ }),
487
+ );
488
+ }
489
+ return specs.length;
490
+ }
491
+
228
492
  beforeEach(async () => {
229
493
  opened = await fixture.open();
230
494
  policies = newResourceIamPolicyStore(opened.store);
@@ -233,6 +497,10 @@ describe
233
497
  [FOUNDER, "auth0|founder"],
234
498
  [MEMBER, "auth0|member"],
235
499
  [VIEWER, "auth0|viewer"],
500
+ [TEAM_MEMBER, "auth0|team-member"],
501
+ [MULTI_TEAM, "auth0|multi-team"],
502
+ [DIRECT, "auth0|direct"],
503
+ [FORMER, "auth0|former"],
236
504
  ] as const) {
237
505
  await accounts.save(
238
506
  create(IdentityAccountSchema, {
@@ -250,17 +518,16 @@ describe
250
518
  visibility: ApiResourceVisibility.visibility_private,
251
519
  createdBy: FOUNDER,
252
520
  });
253
- for (const spec of [
254
- orgRole(FOUNDER, "owner", ORG),
255
- orgRole(MEMBER, "member", ORG),
256
- orgRole(VIEWER, "viewer", ORG),
257
- ]) {
258
- await policies.save(
259
- create(IamPolicySchema, {
260
- apiVersion: IAM_POLICY_API_VERSION,
261
- kind: IAM_POLICY_KIND,
262
- metadata: { id: policyIdFor(spec) },
263
- spec,
521
+ for (let team = 0; team < TEAMS; team += 1) {
522
+ await opened.store.saveResource(
523
+ ApiResourceKind.team,
524
+ teamId(team),
525
+ storedDeclaration("team").schema,
526
+ fixtureRow(storedDeclaration("team"), {
527
+ id: teamId(team),
528
+ org: ORG,
529
+ visibility: ApiResourceVisibility.visibility_private,
530
+ createdBy: FOUNDER,
264
531
  }),
265
532
  );
266
533
  }
@@ -272,14 +539,63 @@ describe
272
539
 
273
540
  for (const shape of SHAPES) {
274
541
  for (const n of SIZES) {
275
- it(`${shape.name} at ${n} rows`, async () => {
276
- const declaration = declared(shape.kind);
277
- for (const parent of shape.parents ?? []) {
278
- await save(parent.type, parent.facts);
279
- }
542
+ it(
543
+ `${shape.name} at ${n} rows`,
544
+ async () => {
545
+ const declaration = storedDeclaration(shape.kind);
546
+ const policyRows = await seedPopulation(n);
547
+ for (const parent of shape.parents ?? []) {
548
+ await save(parent.type, parent.facts);
549
+ }
550
+ const entries: ListEntryMeta[] = [];
551
+ for (let i = 0; i < n; i += 1) {
552
+ const row = await save(shape.kind, shape.row(i, n));
553
+ entries.push({
554
+ ...rowAuthorizationFactsOf(declaration.kind, row),
555
+ labels: {},
556
+ });
557
+ }
558
+ const scope = newBuiltInListReadScope({
559
+ store: opened.store,
560
+ policies,
561
+ accounts,
562
+ logger: silentLogger,
563
+ model: builtInModel,
564
+ });
565
+ const started = performance.now();
566
+ const kept =
567
+ shape.verb === "restrict"
568
+ ? await scope.restrictListEntries(
569
+ resolved(shape.caller),
570
+ declaration.kind,
571
+ entries,
572
+ )
573
+ : await scope.authorizedResourceIds(
574
+ resolved(shape.caller),
575
+ declaration.kind,
576
+ );
577
+ const elapsedMs = Math.round(performance.now() - started);
578
+ // Every shape states what its caller keeps, so a measurement
579
+ // over a scope that answered wrongly measures nothing.
580
+ expect(kept.size).toBe(shape.kept?.(n) ?? n);
581
+ console.log(
582
+ `[measure] ${fixture.name} ${kindEnumName(declaration.kind)} ${shape.verb} n=${n} policies=${policyRows} elapsed=${elapsedMs}ms — ${shape.name}`,
583
+ );
584
+ },
585
+ MEASURE_TIMEOUT_MS,
586
+ );
587
+ }
588
+ }
589
+
590
+ for (const shape of PAGE_SHAPES) {
591
+ it(
592
+ `pages: ${shape.name}, at a ${PAGE_POPULATION}-row population`,
593
+ async () => {
594
+ const declaration = storedDeclaration("agent");
595
+ const policyRows = await seedPopulation(PAGE_POPULATION);
280
596
  const entries: ListEntryMeta[] = [];
281
- for (let i = 0; i < n; i += 1) {
282
- const row = await save(shape.kind, shape.row(i, n));
597
+ for (let i = 0; i < PAGE_EXAMINE_BUDGET; i += 1) {
598
+ const row = await save("agent", shape.row(i));
283
599
  entries.push({
284
600
  ...rowAuthorizationFactsOf(declaration.kind, row),
285
601
  labels: {},
@@ -290,27 +606,73 @@ describe
290
606
  policies,
291
607
  accounts,
292
608
  logger: silentLogger,
609
+ model: builtInModel,
293
610
  });
294
- const started = performance.now();
295
- const kept =
296
- shape.verb === "restrict"
297
- ? await scope.restrictListEntries(
298
- resolved(shape.caller),
299
- declaration.kind,
300
- entries,
301
- )
302
- : await scope.authorizedResourceIds(
303
- resolved(shape.caller),
304
- declaration.kind,
305
- );
306
- const elapsedMs = Math.round(performance.now() - started);
307
- // Every shape states what its caller keeps, so a measurement
308
- // over a scope that answered wrongly measures nothing.
309
- expect(kept.size).toBe(shape.kept?.(n) ?? n);
310
- console.log(
311
- `[measure] ${fixture.name} ${kindEnumName(declaration.kind)} ${shape.verb} n=${n} elapsed=${elapsedMs}ms — ${shape.name}`,
312
- );
313
- });
611
+ const caller = resolved(shape.caller);
612
+ // A page's batches run one after another, each through the scope
613
+ // with its own source, exactly as the page loop calls `admit`.
614
+ async function page(batches: number): Promise<number> {
615
+ let kept = 0;
616
+ for (let b = 0; b < batches; b += 1) {
617
+ const batch = entries.slice(b * PAGE_SIZE, (b + 1) * PAGE_SIZE);
618
+ kept += (
619
+ await scope.restrictListEntries(caller, declaration.kind, batch)
620
+ ).size;
621
+ }
622
+ return kept;
623
+ }
624
+ for (const batches of [1, PAGE_EXAMINE_BUDGET / PAGE_SIZE]) {
625
+ const started = performance.now();
626
+ const kept = await page(batches);
627
+ const elapsedMs = Math.round(performance.now() - started);
628
+ expect(kept).toBe(shape.kept(batches * PAGE_SIZE));
629
+ console.log(
630
+ `[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}`,
631
+ );
632
+ }
633
+ },
634
+ MEASURE_TIMEOUT_MS,
635
+ );
636
+ }
637
+
638
+ for (const [caller, agent, grant] of [
639
+ [TEAM_MEMBER, 0, "a team grant"],
640
+ [DIRECT, 1, "a direct grant"],
641
+ ] as const) {
642
+ for (const n of SIZES) {
643
+ it(
644
+ `point check: can_view through ${grant} at the ${n}-candidate population`,
645
+ async () => {
646
+ const policyRows = await seedPopulation(n);
647
+ await save("agent", enterpriseAgentRow(agent));
648
+ const authorizer = newBuiltInAuthorizer({
649
+ store: opened.store,
650
+ policies,
651
+ accounts,
652
+ edition: ServerEdition.oss,
653
+ logger: silentLogger,
654
+ model: builtInModel,
655
+ });
656
+ const check = {
657
+ permission: IamPermission.can_view,
658
+ resourceKind: ApiResourceKind.agent,
659
+ resourceId: enterpriseAgentId(agent),
660
+ };
661
+ const started = performance.now();
662
+ for (let k = 0; k < POINT_CHECKS; k += 1) {
663
+ // Each call builds its own source, the Authorizer's shape: no
664
+ // memo carries from one check to the next.
665
+ expect(
666
+ await authorizer.authorize(resolved(caller), check),
667
+ ).toEqual({ kind: "allow" });
668
+ }
669
+ const meanMs = (performance.now() - started) / POINT_CHECKS;
670
+ console.log(
671
+ `[measure] ${fixture.name} agent point-check policies=${policyRows} mean=${meanMs.toFixed(2)}ms over ${POINT_CHECKS} — can_view through ${grant}`,
672
+ );
673
+ },
674
+ MEASURE_TIMEOUT_MS,
675
+ );
314
676
  }
315
677
  }
316
678
  });
@@ -53,7 +53,10 @@ import type {
53
53
  ListEntryMeta,
54
54
  ListReadScope,
55
55
  } from "../../extensions/list-read-scope.js";
56
- import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
56
+ import {
57
+ InternalCallerOfferedError,
58
+ restrictListByReadScope,
59
+ } from "../../extensions/list-read-scope.js";
57
60
  import { kindEnumName } from "../../pipeline/apiresource-meta.js";
58
61
  import { rowAuthorizationFactsOf } from "../../pipeline/steps/authorization-facts.js";
59
62
  import type { Store } from "../../store/interface.js";
@@ -63,7 +66,7 @@ import { builtInModel } from "../model/index.js";
63
66
  import type { KindDeclaration } from "../model/rewrite.js";
64
67
  import { driverFixtures, dropPostgresFixture } from "./drivers.js";
65
68
  import type { OpenedStore } from "./drivers.js";
66
- import { fixtureRow } from "./support.js";
69
+ import { fixtureRow, storedDeclaration } from "./support.js";
67
70
  import type { FixtureRowFacts } from "./support.js";
68
71
 
69
72
  const FOUNDER = accountIdFor("auth0|founder");
@@ -94,14 +97,6 @@ const SEEDED_KINDS = [
94
97
  ApiResourceKind.agent_instance,
95
98
  ];
96
99
 
97
- function declared(type: string): KindDeclaration {
98
- const declaration = builtInModel.byType(type);
99
- if (declaration === undefined) {
100
- throw new Error(`${type} is declared`);
101
- }
102
- return declaration;
103
- }
104
-
105
100
  function resolved(accountId: string): CallerIdentity {
106
101
  return {
107
102
  identityId: accountId,
@@ -161,7 +156,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
161
156
  type: string,
162
157
  facts: FixtureRowFacts,
163
158
  ): Promise<void> {
164
- const declaration = declared(type);
159
+ const declaration = storedDeclaration(type);
165
160
  const row = fixtureRow(declaration, facts);
166
161
  await opened.store.saveResource(
167
162
  declaration.kind,
@@ -608,9 +603,15 @@ describe.each(driverFixtures(SEEDED_KINDS))(
608
603
  () => undefined,
609
604
  (error: unknown) => error,
610
605
  );
611
- expect(refusal).toBeInstanceOf(AuthorizationEvaluationError);
612
- expect((refusal as AuthorizationEvaluationError).reason).toBe(
613
- "internal-caller-offered",
606
+ // The seam's own refusal, the one a composition's driver throws
607
+ // too; never the evaluator's fault, since nothing was evaluated.
608
+ expect(refusal).toBeInstanceOf(InternalCallerOfferedError);
609
+ expect(refusal).not.toBeInstanceOf(AuthorizationEvaluationError);
610
+ expect((refusal as InternalCallerOfferedError).kind).toBe(
611
+ ApiResourceKind.agent,
612
+ );
613
+ expect((refusal as InternalCallerOfferedError).message).toContain(
614
+ "kind 'agent'",
614
615
  );
615
616
  // Refused before any read: the server was never evaluated as a person.
616
617
  expect(accountReads).toBe(0);
@@ -38,7 +38,7 @@ import { builtInModel } from "../model/index.js";
38
38
  import { newBuiltInOrganizationDirectory } from "../organization-directory.js";
39
39
  import { driverFixtures, dropPostgresFixture } from "./drivers.js";
40
40
  import type { OpenedStore } from "./drivers.js";
41
- import { fixtureRow } from "./support.js";
41
+ import { fixtureRow, storedDeclaration } from "./support.js";
42
42
 
43
43
  const FOUNDER = accountIdFor("auth0|founder");
44
44
  const MEMBER = accountIdFor("auth0|member");
@@ -81,7 +81,7 @@ describe.each(
81
81
  let principalReads: number;
82
82
 
83
83
  async function organization(id: string) {
84
- const declaration = builtInModel.byType("organization")!;
84
+ const declaration = storedDeclaration("organization");
85
85
  await opened.store.saveResource(
86
86
  ApiResourceKind.organization,
87
87
  id,