@reventlessdev/reventless-aws 3.0.0-alpha.369 → 3.0.0-alpha.371

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 (38) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +11 -11
  3. package/scripts/ProvisionAdmin.res +2 -2
  4. package/scripts/ProvisionAdmin.res.mjs +7 -6
  5. package/src/Platform.res +56 -7
  6. package/src/Platform.res.mjs +46 -7
  7. package/src/Platform_ProvidedGroups.res +97 -0
  8. package/src/Platform_ProvidedGroups.res.mjs +103 -0
  9. package/src/Platform_Stack.res +6 -3
  10. package/src/Platform_Stack.res.mjs +3 -3
  11. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -1
  12. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  13. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +1 -1
  14. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +1 -1
  15. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
  16. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -0
  17. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -0
  18. package/src/components/Api/AppSync_Adapter.res +4 -3
  19. package/src/components/Api/AppSync_SdlDecorate.res +7 -3
  20. package/src/components/Api/AppSync_SdlDecorate.res.mjs +4 -3
  21. package/src/util/Util_OwnerScopeEnv.res +9 -0
  22. package/src/util/Util_OwnerScopeEnv.res.mjs +14 -1
  23. package/src/util/Util_ShellConfig.res +4 -0
  24. package/src/util/Util_ShellConfig.res.mjs +5 -1
  25. package/tests/AppSync_AdapterTest.res +46 -23
  26. package/tests/AppSync_AdapterTest.res.mjs +35 -27
  27. package/tests/PgPipeline_IntegrationTest.res +2 -1
  28. package/tests/PgQueryDbUnion_IntegrationTest.res +2 -1
  29. package/tests/Platform_ProvidedGroupsTest.res +52 -0
  30. package/tests/Platform_ProvidedGroupsTest.res.mjs +98 -0
  31. package/tests/Util_OwnerScopeEnvTest.res +48 -2
  32. package/tests/Util_OwnerScopeEnvTest.res.mjs +49 -3
  33. package/tests/Util_ShellConfigTest.res +17 -0
  34. package/tests/Util_ShellConfigTest.res.mjs +18 -0
  35. package/tests/integration/AggTestAggregate.res +2 -1
  36. package/tests/integration/EpCompositeSlice.res +2 -1
  37. package/tests/integration/EpTestSlice.res +2 -1
  38. package/tests/integration/QueryDbUnion_DynamoDb_IntegrationTest.res +2 -1
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
7
7
  import * as RowImage$Reventless from "@reventlessdev/reventless-spec/src/semantic/RowImage.res.mjs";
8
8
  import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
9
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
9
10
  import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
10
11
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
11
12
  import * as PgQueryResolver_Lambda$ReventlessAws from "../QueryDb/PgQueryResolver_Lambda.res.mjs";
@@ -75,7 +76,7 @@ function registerBinding(pushdowns, pgConnection, entry, spec) {
75
76
  capability: GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(entry.readModelName, spec.stateSchema),
76
77
  labelField: entry.labelField,
77
78
  includeIdParam: entry.includeIdParam,
78
- authorization: spec.authorization,
79
+ authorization: Authorization$Reventless.named(spec.authorization),
79
80
  ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0],
80
81
  retiredField: Stdlib_Option.map(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.field),
81
82
  retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values),
@@ -246,6 +246,7 @@ let makeFromCodeAsset: (
246
246
  // concludes nobody is elevated and stamps an operator's on-behalf write with
247
247
  // the operator's own id.
248
248
  Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables)
249
+ Util_OwnerScopeEnv.applyRoleGroupsDefault(variables)
249
250
 
250
251
  // ESM self-containment (Option C): every code archive built by
251
252
  // Util_Bundle.buildCodeArchive ships register-hook.mjs + layer-resolver.mjs at
@@ -104,6 +104,7 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
104
104
  });
105
105
  Util_LambdaLogging$ReventlessAws.applyLogLevelDefault(variables);
106
106
  Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables);
107
+ Util_OwnerScopeEnv$ReventlessAws.applyRoleGroupsDefault(variables);
107
108
  variables["NODE_OPTIONS"] = Util_Bundle$ReventlessAws.esmLoaderNodeOptions;
108
109
  variables["ESM_FALLBACK_DIRS"] = Util_Bundle$ReventlessAws.esmFallbackDirs;
109
110
  if (!RuntimeExtension$ReventlessCore.isEmpty()) {
@@ -181,12 +181,13 @@ let rec waitForMergeSuccess = async (
181
181
  // from `@@reventless.authorize` / `@authorize` PPX annotations.
182
182
  //
183
183
  // When both are present on the same field, the spec-level permission wins
184
- // (it is more specific). `AllowGroups([g1, g2, ...])` emits
185
- // `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`.
184
+ // (it is more specific). `AllowRoles([r1, r2, ...])` emits
185
+ // `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`, each `g` the group
186
+ // the deployment maps that role to.
186
187
  // `AllowAuthenticated` emits the group-less `@aws_cognito_user_pools` — the same
187
188
  // reachability an undirectived field already has, but stated rather than left
188
189
  // implicit, so `assertGateable` can tell "deliberately open" from "nobody
189
- // stamped this". `AllowGroups([])` and `DenyAll` emit a sentinel `__deny_all__`
190
+ // stamped this". `AllowRoles([])` and `DenyAll` emit a sentinel `__deny_all__`
190
191
  // group that no Cognito user can belong to — effectively blocking the field at
191
192
  // the API layer.
192
193
  //
@@ -136,8 +136,12 @@ type fieldGate =
136
136
 
137
137
  let permissionToGate = (permission: Reventless.Authorization.permission): fieldGate =>
138
138
  switch permission {
139
- | AllowGroups([]) => Groups(["__deny_all__"])
140
- | AllowGroups(groups) => Groups(groups)
139
+ // The groups the roles map to: the deploy program is where the mapping is known.
140
+ | AllowRoles(_) =>
141
+ switch Reventless.Authorization.groupsOf(permission) {
142
+ | [] => Groups(["__deny_all__"])
143
+ | groups => Groups(groups)
144
+ }
141
145
  | DenyAll => Groups(["__deny_all__"])
142
146
  | AllowAuthenticated => AnyAuthenticated
143
147
  | AllowAnonymous => Anonymous
@@ -165,7 +169,7 @@ let refuseAnonymousFields = (fieldNames: array<string>): 'a =>
165
169
  `Anonymous fields on AWS are deferred. When they land it will be as a separate\n` ++
166
170
  `API carrying only the unauthenticated fields. Do not add API_KEY to this one:\n` ++
167
171
  `every field here would then have to say whether the key reaches it, and one\n` ++
168
- `that forgot would be open. Until then, declare the field AllowAuthenticated\n` ++ `(or AllowGroups) and mean it.`,
172
+ `that forgot would be open. Until then, declare the field AllowAuthenticated\n` ++ `(or AllowRoles) and mean it.`,
169
173
  )
170
174
 
171
175
  // Injects the Cognito group gate on ALL mutation, query, and subscription fields
@@ -4,6 +4,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
6
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
7
8
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
8
9
 
9
10
  function injectAwsSubscribe(sdl, sources) {
@@ -65,7 +66,7 @@ function permissionToGate(permission) {
65
66
  };
66
67
  }
67
68
  } else {
68
- let groups = permission._0;
69
+ let groups = Authorization$Reventless.groupsOf(permission);
69
70
  if (groups.length !== 0) {
70
71
  return {
71
72
  TAG: "Groups",
@@ -81,7 +82,7 @@ function permissionToGate(permission) {
81
82
  }
82
83
 
83
84
  function refuseAnonymousFields(fieldNames) {
84
- return Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` + (` ` + fieldNames.length.toString() + ` field(s) declare AllowAnonymous:\n`) + (` ` + fieldNames.join(", ") + `\n\n`) + `AppSync has no anonymous authorization mode. This API is provisioned with\nAMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\ndirective available is @aws_cognito_user_pools — which means ANY\nAUTHENTICATED caller, the opposite of what the spec declares. Mutations have\nno runtime authorization check to correct it: on this platform the directive\nis the whole enforcement.\n\nThe local platform DOES honour AllowAnonymous (its resolvers call\nAuthorization.isAllowed), so a spec that passes locally can still not be\ndeployable here. That divergence is the reason this refuses instead of\nemitting a directive that reads as gated and is not.\n\nAnonymous fields on AWS are deferred. When they land it will be as a separate\nAPI carrying only the unauthenticated fields. Do not add API_KEY to this one:\nevery field here would then have to say whether the key reaches it, and one\nthat forgot would be open. Until then, declare the field AllowAuthenticated\n(or AllowGroups) and mean it.`);
85
+ return Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` + (` ` + fieldNames.length.toString() + ` field(s) declare AllowAnonymous:\n`) + (` ` + fieldNames.join(", ") + `\n\n`) + `AppSync has no anonymous authorization mode. This API is provisioned with\nAMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\ndirective available is @aws_cognito_user_pools — which means ANY\nAUTHENTICATED caller, the opposite of what the spec declares. Mutations have\nno runtime authorization check to correct it: on this platform the directive\nis the whole enforcement.\n\nThe local platform DOES honour AllowAnonymous (its resolvers call\nAuthorization.isAllowed), so a spec that passes locally can still not be\ndeployable here. That divergence is the reason this refuses instead of\nemitting a directive that reads as gated and is not.\n\nAnonymous fields on AWS are deferred. When they land it will be as a separate\nAPI carrying only the unauthenticated fields. Do not add API_KEY to this one:\nevery field here would then have to say whether the key reaches it, and one\nthat forgot would be open. Until then, declare the field AllowAuthenticated\n(or AllowRoles) and mean it.`);
85
86
  }
86
87
 
87
88
  function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
@@ -388,4 +389,4 @@ export {
388
389
  canonicalTypeNames,
389
390
  stampCanonicalTypes,
390
391
  }
391
- /* GraphQL_Stitcher-ReventlessCore Not a pure module */
392
+ /* Authorization-Reventless Not a pure module */
@@ -46,3 +46,12 @@ let applyElevatedGroupsDefault = (variables: dict<Pulumi.Input.t<string>>) =>
46
46
  if variables->Dict.get(key)->Option.isNone {
47
47
  entry()->Option.forEach(((k, v)) => variables->Dict.set(k, v))
48
48
  }
49
+
50
+ /** The renamed roles, for the same two-process reason: a runtime that evaluates
51
+ a rule (the SQL-backed view reads) never runs the root that stated the
52
+ mapping. Only the renamed ones travel; every other role is its own group. */
53
+ let applyRoleGroupsDefault = (variables: dict<Pulumi.Input.t<string>>) =>
54
+ switch (variables->Dict.get(Reventless.Role.envKey), Reventless.Role.envValue()) {
55
+ | (None, Some(value)) => variables->Dict.set(Reventless.Role.envKey, value->Pulumi.Input.make)
56
+ | _ => ()
57
+ }
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
4
5
  import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
5
6
 
6
7
  let key = "REVENTLESS_ELEVATED_GROUPS";
@@ -23,9 +24,21 @@ function applyElevatedGroupsDefault(variables) {
23
24
  }
24
25
  }
25
26
 
27
+ function applyRoleGroupsDefault(variables) {
28
+ let match = variables[Role$Reventless.envKey];
29
+ let match$1 = Role$Reventless.envValue();
30
+ if (match !== undefined || match$1 === undefined) {
31
+ return;
32
+ } else {
33
+ variables[Role$Reventless.envKey] = match$1;
34
+ return;
35
+ }
36
+ }
37
+
26
38
  export {
27
39
  key,
28
40
  entry,
29
41
  applyElevatedGroupsDefault,
42
+ applyRoleGroupsDefault,
30
43
  }
31
- /* OwnerScope-Reventless Not a pure module */
44
+ /* Role-Reventless Not a pure module */
@@ -98,6 +98,10 @@ let fields = (
98
98
  ): dict<JSON.t> => {
99
99
  let out = computed->Dict.fromArray
100
100
 
101
+ // Computed, like the endpoints: the group the admin API is gated on, from the
102
+ // deploy's own role mapping.
103
+ ReventlessCore.Platform_AdminGroup.configEntry()->Option.forEach(((k, v)) => out->Dict.set(k, v))
104
+
101
105
  // The shell imports the module this key names, and imports nothing when the
102
106
  // key is absent — so writing the object without naming it here would ship a
103
107
  // file nothing fetches. Computed rather than left to `shellConfig` for the
@@ -5,6 +5,7 @@ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Platform$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/Platform.res.mjs";
7
7
  import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
8
+ import * as Platform_AdminGroup$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminGroup.res.mjs";
8
9
  import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
9
10
 
10
11
  let journeyManifestsKey = "journeyManifestUrls";
@@ -59,6 +60,9 @@ function subscriptionEndpoint(httpsEndpoint) {
59
60
 
60
61
  function fields(computed, viewModes, bakedManifest, uiSlotsFile, shellConfig) {
61
62
  let out = Object.fromEntries(computed);
63
+ Stdlib_Option.forEach(Platform_AdminGroup$ReventlessCore.configEntry(), param => {
64
+ out[param[0]] = param[1];
65
+ });
62
66
  Stdlib_Option.forEach(uiSlotsFile, param => {
63
67
  out[Platform_UiSlots$ReventlessCore.configKey] = Platform_UiSlots$ReventlessCore.url;
64
68
  });
@@ -103,4 +107,4 @@ export {
103
107
  subscriptionEndpoint,
104
108
  fields,
105
109
  }
106
- /* No side effect */
110
+ /* Platform_AdminGroup-ReventlessCore Not a pure module */
@@ -183,20 +183,16 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
183
183
  ?permission,
184
184
  }
185
185
 
186
- testSync("AllowGroups([\"Admin\"]) emits cognito_groups: [\"Admin\"] on mutation", () => {
186
+ testSync("AllowRoles with several roles emits comma-separated groups", () => {
187
187
  let fp = Dict.fromArray([
188
- ("catalog_Category_Archive", Reventless.Authorization.AllowGroups(["Admin"])),
188
+ (
189
+ "p_X",
190
+ Reventless.Authorization.AllowRoles([
191
+ Reventless.Role.make("Admin"),
192
+ Reventless.Role.make("Editor"),
193
+ ]),
194
+ ),
189
195
  ])
190
- let entry = mutationEntry(~fieldNames=["catalog_Category_Archive"], ~fieldPermissions=fp)
191
- let frag = makeFragment(["catalog_Category_Archive(id: ID!): String"], [])
192
- let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
193
- let parts = decodeFragment(aug)
194
- let m = parts.mutations->Array.getUnsafe(0)
195
- expect(m)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
196
- })
197
-
198
- testSync("AllowGroups multi-group emits comma-separated groups", () => {
199
- let fp = Dict.fromArray([("p_X", Reventless.Authorization.AllowGroups(["Admin", "Editor"]))])
200
196
  let entry = mutationEntry(~fieldNames=["p_X"], ~fieldPermissions=fp)
201
197
  let frag = makeFragment(["p_X(id: ID!): String"], [])
202
198
  let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
@@ -206,6 +202,29 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
206
202
  )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`)
207
203
  })
208
204
 
205
+ // The deploy program knows the mapping, so the directive carries the group a
206
+ // role maps to — never the role, which no token carries.
207
+ testSync("AllowRoles emits the groups its roles map to", () => {
208
+ Reventless.Role.clearGroups()
209
+ Reventless.Role.setGroups([(Reventless.Role.make("Merchandiser"), "shop-merch-team")])
210
+ let fp = Dict.fromArray([
211
+ (
212
+ "p_Rename",
213
+ Reventless.Authorization.AllowRoles([
214
+ Reventless.Role.make("Admin"),
215
+ Reventless.Role.make("Merchandiser"),
216
+ ]),
217
+ ),
218
+ ])
219
+ let entry = mutationEntry(~fieldNames=["p_Rename"], ~fieldPermissions=fp)
220
+ let frag = makeFragment(["p_Rename(id: ID!): String"], [])
221
+ let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
222
+ Reventless.Role.clearGroups()
223
+ expect(
224
+ decodeFragment(aug).mutations->Array.getUnsafe(0),
225
+ )->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`)
226
+ })
227
+
209
228
  testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
210
229
  let fp = Dict.fromArray([("p_Add", Reventless.Authorization.AllowAuthenticated)])
211
230
  let entry = mutationEntry(~fieldNames=["p_Add"], ~fieldPermissions=fp)
@@ -230,7 +249,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
230
249
 
231
250
  testSync("Per-field permissions: only annotated fields get directives", () => {
232
251
  let fp = Dict.fromArray([
233
- ("p_Archive", Reventless.Authorization.AllowGroups(["Admin"])),
252
+ ("p_Archive", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
234
253
  ("p_Add", Reventless.Authorization.AllowAuthenticated),
235
254
  ])
236
255
  let entry = mutationEntry(~fieldNames=["p_Archive", "p_Add"], ~fieldPermissions=fp)
@@ -255,7 +274,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
255
274
  let entry = queryEntry(
256
275
  ~single="p_Item",
257
276
  ~list="p_Items",
258
- ~permission=Some(Reventless.Authorization.AllowGroups(["Manager"])),
277
+ ~permission=Some(Reventless.Authorization.AllowRoles([Reventless.Role.make("Manager")])),
259
278
  )
260
279
  // Production query SDL emitted by GraphQL_FragmentGenerator always has an
261
280
  // arg list (Relay pagination on the list field), so extractLeadingName's
@@ -362,7 +381,9 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
362
381
  testSync("Spec-level permission wins over legacy authorization field", () => {
363
382
  // Legacy {tableName, group} says "Admin"; spec-level says "Manager".
364
383
  // Spec-level must win.
365
- let fp = Dict.fromArray([("p_X", Reventless.Authorization.AllowGroups(["Manager"]))])
384
+ let fp = Dict.fromArray([
385
+ ("p_X", Reventless.Authorization.AllowRoles([Reventless.Role.make("Manager")])),
386
+ ])
366
387
  let entry: ReventlessInfra.Api.mutationSchemaEntry = {
367
388
  fieldNames: ["p_X"],
368
389
  commandSchema: S.unknown,
@@ -427,7 +448,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
427
448
  fieldNames: ["p_Sync"],
428
449
  commandSchema: S.unknown,
429
450
  fieldPermissions: Dict.fromArray([
430
- ("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
451
+ ("p_Sync", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
431
452
  ]),
432
453
  systemCallable: true,
433
454
  }
@@ -464,8 +485,8 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
464
485
  fieldNames: ["p_Sync", "p_Other"],
465
486
  commandSchema: S.unknown,
466
487
  fieldPermissions: Dict.fromArray([
467
- ("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
468
- ("p_Other", Reventless.Authorization.AllowGroups(["Admin"])),
488
+ ("p_Sync", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
489
+ ("p_Other", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
469
490
  ]),
470
491
  // systemCallable applies to every field in the entry; split the fields so only
471
492
  // p_Sync opts in.
@@ -475,7 +496,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
475
496
  fieldNames: ["p_Sync"],
476
497
  commandSchema: S.unknown,
477
498
  fieldPermissions: Dict.fromArray([
478
- ("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
499
+ ("p_Sync", Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")])),
479
500
  ]),
480
501
  systemCallable: true,
481
502
  }
@@ -501,7 +522,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
501
522
  returnTypeName: "p_Item",
502
523
  stateSchema: S.unknown,
503
524
  authorization: None,
504
- permission: Reventless.Authorization.AllowGroups(["Admin"]),
525
+ permission: Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")]),
505
526
  systemCallable: true,
506
527
  }
507
528
  let frag = makeFragment(
@@ -532,7 +553,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
532
553
  returnTypeName: "p_Item",
533
554
  stateSchema: S.unknown,
534
555
  authorization: None,
535
- permission: Reventless.Authorization.AllowGroups(["Admin"]),
556
+ permission: Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")]),
536
557
  systemCallable: true,
537
558
  }
538
559
  let frag = makeFragment(
@@ -583,7 +604,7 @@ describe("AppSync_Adapter — type-level dual-auth", () => {
583
604
  returnTypeName: "p_Item",
584
605
  stateSchema: S.unknown,
585
606
  authorization: None,
586
- permission: Reventless.Authorization.AllowGroups(["Admin"]),
607
+ permission: Reventless.Authorization.AllowRoles([Reventless.Role.make("Admin")]),
587
608
  systemCallable: true,
588
609
  }
589
610
 
@@ -958,7 +979,9 @@ describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
958
979
  })
959
980
 
960
981
  testSync("a subscription named like a gated mutation inherits its groups", () => {
961
- let fp = Dict.fromArray([("p_Ship", Reventless.Authorization.AllowGroups(["Fulfilment"]))])
982
+ let fp = Dict.fromArray([
983
+ ("p_Ship", Reventless.Authorization.AllowRoles([Reventless.Role.make("Fulfilment")])),
984
+ ])
962
985
  let frag = makeFragment(["p_Ship: CommandResult"])
963
986
  let aug = AppSync_Adapter.injectAwsAuth(
964
987
  frag,
@@ -4,6 +4,7 @@ import * as Sury from "sury";
4
4
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
7
8
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
8
9
  import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
9
10
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
@@ -129,37 +130,44 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
129
130
  authorization: undefined,
130
131
  permission: permission
131
132
  });
132
- globalThis.test("AllowGroups([\"Admin\"]) emits cognito_groups: [\"Admin\"] on mutation", () => {
133
+ globalThis.test("AllowRoles with several roles emits comma-separated groups", () => {
133
134
  let fp = Object.fromEntries([[
134
- "catalog_Category_Archive",
135
+ "p_X",
135
136
  {
136
- TAG: "AllowGroups",
137
- _0: ["Admin"]
137
+ TAG: "AllowRoles",
138
+ _0: [
139
+ "Admin",
140
+ "Editor"
141
+ ]
138
142
  }
139
143
  ]]);
140
- let entry = mutationEntry(["catalog_Category_Archive"], fp);
141
- let frag = makeFragment(["catalog_Category_Archive(id: ID!): String"], []);
144
+ let entry = mutationEntry(["p_X"], fp);
145
+ let frag = makeFragment(["p_X(id: ID!): String"], []);
142
146
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
143
147
  let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
144
- let m = parts.mutations[0];
145
- globalThis.expect(m).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
148
+ globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
146
149
  });
147
- globalThis.test("AllowGroups multi-group emits comma-separated groups", () => {
150
+ globalThis.test("AllowRoles emits the groups its roles map to", () => {
151
+ Role$Reventless.clearGroups();
152
+ Role$Reventless.setGroups([[
153
+ "Merchandiser",
154
+ "shop-merch-team"
155
+ ]]);
148
156
  let fp = Object.fromEntries([[
149
- "p_X",
157
+ "p_Rename",
150
158
  {
151
- TAG: "AllowGroups",
159
+ TAG: "AllowRoles",
152
160
  _0: [
153
161
  "Admin",
154
- "Editor"
162
+ "Merchandiser"
155
163
  ]
156
164
  }
157
165
  ]]);
158
- let entry = mutationEntry(["p_X"], fp);
159
- let frag = makeFragment(["p_X(id: ID!): String"], []);
166
+ let entry = mutationEntry(["p_Rename"], fp);
167
+ let frag = makeFragment(["p_Rename(id: ID!): String"], []);
160
168
  let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
161
- let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
162
- globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
169
+ Role$Reventless.clearGroups();
170
+ globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "shop-merch-team"])`);
163
171
  });
164
172
  globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
165
173
  let fp = Object.fromEntries([[
@@ -190,7 +198,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
190
198
  [
191
199
  "p_Archive",
192
200
  {
193
- TAG: "AllowGroups",
201
+ TAG: "AllowRoles",
194
202
  _0: ["Admin"]
195
203
  }
196
204
  ],
@@ -226,7 +234,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
226
234
  });
227
235
  globalThis.test("Query permission applies to BOTH single and list field names", () => {
228
236
  let entry = queryEntry("p_Item", "p_Items", {
229
- TAG: "AllowGroups",
237
+ TAG: "AllowRoles",
230
238
  _0: ["Manager"]
231
239
  });
232
240
  let frag = makeFragment([], [
@@ -318,7 +326,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
318
326
  let fp = Object.fromEntries([[
319
327
  "p_X",
320
328
  {
321
- TAG: "AllowGroups",
329
+ TAG: "AllowRoles",
322
330
  _0: ["Manager"]
323
331
  }
324
332
  ]]);
@@ -380,7 +388,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
380
388
  let entry_fieldPermissions = Object.fromEntries([[
381
389
  "p_Sync",
382
390
  {
383
- TAG: "AllowGroups",
391
+ TAG: "AllowRoles",
384
392
  _0: ["Admin"]
385
393
  }
386
394
  ]]);
@@ -427,14 +435,14 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
427
435
  [
428
436
  "p_Sync",
429
437
  {
430
- TAG: "AllowGroups",
438
+ TAG: "AllowRoles",
431
439
  _0: ["Admin"]
432
440
  }
433
441
  ],
434
442
  [
435
443
  "p_Other",
436
444
  {
437
- TAG: "AllowGroups",
445
+ TAG: "AllowRoles",
438
446
  _0: ["Admin"]
439
447
  }
440
448
  ]
@@ -450,7 +458,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
450
458
  let iamEntry_fieldPermissions = Object.fromEntries([[
451
459
  "p_Sync",
452
460
  {
453
- TAG: "AllowGroups",
461
+ TAG: "AllowRoles",
454
462
  _0: ["Admin"]
455
463
  }
456
464
  ]]);
@@ -481,7 +489,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
481
489
  });
482
490
  globalThis.test("systemCallable query applies dual-auth to BOTH single and list fields", () => {
483
491
  let entry_permission = {
484
- TAG: "AllowGroups",
492
+ TAG: "AllowRoles",
485
493
  _0: ["Admin"]
486
494
  };
487
495
  let entry_systemCallable = true;
@@ -514,7 +522,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
514
522
  });
515
523
  globalThis.test("systemCallable dual-auth extends to derived query fields (Items/ByIds/By<Index>)", () => {
516
524
  let entry_permission = {
517
- TAG: "AllowGroups",
525
+ TAG: "AllowRoles",
518
526
  _0: ["Admin"]
519
527
  };
520
528
  let entry_systemCallable = true;
@@ -554,7 +562,7 @@ globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
554
562
  subscriptionSources: []
555
563
  });
556
564
  let callableEntry_permission = {
557
- TAG: "AllowGroups",
565
+ TAG: "AllowRoles",
558
566
  _0: ["Admin"]
559
567
  };
560
568
  let callableEntry_systemCallable = true;
@@ -845,7 +853,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
845
853
  let fp = Object.fromEntries([[
846
854
  "p_Ship",
847
855
  {
848
- TAG: "AllowGroups",
856
+ TAG: "AllowRoles",
849
857
  _0: ["Fulfilment"]
850
858
  }
851
859
  ]]);
@@ -35,7 +35,8 @@ module OrdersSpec = {
35
35
 
36
36
  let config = Reventless.ReadModel.config()
37
37
  let subIdConfig = None
38
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
38
+ type role = Reventless.Role.name
39
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
39
40
  let visibility: Reventless.Visibility.t = Public
40
41
  }
41
42
 
@@ -30,7 +30,8 @@ module CustomersSpec = {
30
30
 
31
31
  let config = Reventless.ReadModel.config()
32
32
  let subIdConfig = None
33
- let authorization: Reventless.Authorization.permission = AllowAuthenticated
33
+ type role = Reventless.Role.name
34
+ let authorization: Reventless.Authorization.rule<role> = AllowAuthenticated
34
35
  let visibility: Reventless.Visibility.t = Public
35
36
  }
36
37
 
@@ -0,0 +1,52 @@
1
+ open JestGlobals
2
+
3
+ // What the role check compares a plugin's roles with on AWS: the groups the user
4
+ // pool reports, found through the platform stack's exports.
5
+
6
+ module Cognito = AwsSdk.CognitoIdentityServiceProvider
7
+
8
+ let group = (name): Cognito.ListGroupsCommand.group => {groupName: name}
9
+
10
+ describe("Platform_ProvidedGroups.listAll", () => {
11
+ test("follows NextToken until the last page", async () => {
12
+ let seen = []
13
+ let send = async (input: Cognito.ListGroupsCommand.input): Cognito.ListGroupsCommand.output => {
14
+ seen->Array.push(input.nextToken)
15
+ switch input.nextToken {
16
+ | None => {groups: [group("Admin"), group("Shopper")], nextToken: "p2"}
17
+ | Some(_) => {groups: [group("Merchandiser")]}
18
+ }
19
+ }
20
+ let groups = await Platform_ProvidedGroups.listAll(~send, ~userPoolId="p1")
21
+ expect((groups, seen))->toEqual((["Admin", "Shopper", "Merchandiser"], [None, Some("p2")]))
22
+ })
23
+ })
24
+
25
+ describe("Platform_ProvidedGroups.poolFromExports", () => {
26
+ let from = (pairs: array<(string, JSON.t)>) => {
27
+ let exports = Dict.fromArray(pairs)
28
+ Platform_ProvidedGroups.poolFromExports(~get=key => exports->Dict.get(key))
29
+ }
30
+
31
+ testSync("reads the pool id and region", () =>
32
+ expect(
33
+ from([
34
+ ("identityProviderId", JSON.Encode.string("p1")),
35
+ ("identityProviderRegion", JSON.Encode.string("eu-west-1")),
36
+ ]),
37
+ )->toEqual(Some({Platform_ProvidedGroups.id: "p1", region: Some("eu-west-1")}))
38
+ )
39
+
40
+ testSync("reads them nested under default, as an ESM platform exports them", () =>
41
+ expect(
42
+ from([
43
+ (
44
+ "default",
45
+ JSON.Encode.object(Dict.fromArray([("cognitoUserPoolId", JSON.Encode.string("p-old"))])),
46
+ ),
47
+ ]),
48
+ )->toEqual(Some({Platform_ProvidedGroups.id: "p-old", region: None}))
49
+ )
50
+
51
+ testSync("no pool id is no pool to list", () => expect(from([]))->toEqual(None))
52
+ })