@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.
- package/CHANGELOG.md +22 -0
- package/package.json +11 -11
- package/scripts/ProvisionAdmin.res +2 -2
- package/scripts/ProvisionAdmin.res.mjs +7 -6
- package/src/Platform.res +56 -7
- package/src/Platform.res.mjs +46 -7
- package/src/Platform_ProvidedGroups.res +97 -0
- package/src/Platform_ProvidedGroups.res.mjs +103 -0
- package/src/Platform_Stack.res +6 -3
- package/src/Platform_Stack.res.mjs +3 -3
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +1 -1
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -0
- package/src/components/Api/AppSync_Adapter.res +4 -3
- package/src/components/Api/AppSync_SdlDecorate.res +7 -3
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +4 -3
- package/src/util/Util_OwnerScopeEnv.res +9 -0
- package/src/util/Util_OwnerScopeEnv.res.mjs +14 -1
- package/src/util/Util_ShellConfig.res +4 -0
- package/src/util/Util_ShellConfig.res.mjs +5 -1
- package/tests/AppSync_AdapterTest.res +46 -23
- package/tests/AppSync_AdapterTest.res.mjs +35 -27
- package/tests/PgPipeline_IntegrationTest.res +2 -1
- package/tests/PgQueryDbUnion_IntegrationTest.res +2 -1
- package/tests/Platform_ProvidedGroupsTest.res +52 -0
- package/tests/Platform_ProvidedGroupsTest.res.mjs +98 -0
- package/tests/Util_OwnerScopeEnvTest.res +48 -2
- package/tests/Util_OwnerScopeEnvTest.res.mjs +49 -3
- package/tests/Util_ShellConfigTest.res +17 -0
- package/tests/Util_ShellConfigTest.res.mjs +18 -0
- package/tests/integration/AggTestAggregate.res +2 -1
- package/tests/integration/EpCompositeSlice.res +2 -1
- package/tests/integration/EpTestSlice.res +2 -1
- 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). `
|
|
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". `
|
|
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
|
-
|
|
140
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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("
|
|
186
|
+
testSync("AllowRoles with several roles emits comma-separated groups", () => {
|
|
187
187
|
let fp = Dict.fromArray([
|
|
188
|
-
(
|
|
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.
|
|
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.
|
|
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([
|
|
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.
|
|
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.
|
|
468
|
-
("p_Other", Reventless.Authorization.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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([
|
|
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("
|
|
133
|
+
globalThis.test("AllowRoles with several roles emits comma-separated groups", () => {
|
|
133
134
|
let fp = Object.fromEntries([[
|
|
134
|
-
"
|
|
135
|
+
"p_X",
|
|
135
136
|
{
|
|
136
|
-
TAG: "
|
|
137
|
-
_0: [
|
|
137
|
+
TAG: "AllowRoles",
|
|
138
|
+
_0: [
|
|
139
|
+
"Admin",
|
|
140
|
+
"Editor"
|
|
141
|
+
]
|
|
138
142
|
}
|
|
139
143
|
]]);
|
|
140
|
-
let entry = mutationEntry(["
|
|
141
|
-
let frag = makeFragment(["
|
|
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
|
-
|
|
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("
|
|
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
|
-
"
|
|
157
|
+
"p_Rename",
|
|
150
158
|
{
|
|
151
|
-
TAG: "
|
|
159
|
+
TAG: "AllowRoles",
|
|
152
160
|
_0: [
|
|
153
161
|
"Admin",
|
|
154
|
-
"
|
|
162
|
+
"Merchandiser"
|
|
155
163
|
]
|
|
156
164
|
}
|
|
157
165
|
]]);
|
|
158
|
-
let entry = mutationEntry(["
|
|
159
|
-
let frag = makeFragment(["
|
|
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
|
-
|
|
162
|
-
globalThis.expect(
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
438
|
+
TAG: "AllowRoles",
|
|
431
439
|
_0: ["Admin"]
|
|
432
440
|
}
|
|
433
441
|
],
|
|
434
442
|
[
|
|
435
443
|
"p_Other",
|
|
436
444
|
{
|
|
437
|
-
TAG: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
})
|