@reventlessdev/reventless-aws 3.0.0-alpha.295 → 3.0.0-alpha.296
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 +7 -0
- package/package.json +4 -4
- package/src/components/Api/AppSync_Adapter.res +24 -2
- package/src/components/Api/AppSync_Adapter.res.mjs +13 -4
- package/src/components/Api/AppSync_SdlDecorate.res +45 -1
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +19 -1
- package/tests/AppSync_AdapterTest.res +68 -5
- package/tests/AppSync_AdapterTest.res.mjs +55 -5
- package/tests/AppSync_SdlDecorateTest.res +36 -0
- package/tests/AppSync_SdlDecorateTest.res.mjs +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.296 (2026-08-14)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **aws:** give every field and type an explicit Cognito directive ([37c1063](https://github.com/ReventlessDev/reventless-core/commit/37c1063560aaa6ee200e74d58b9de4d6881441fd))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.295 (2026-08-14)
|
|
7
14
|
|
|
8
15
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.296",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
15
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.10",
|
|
16
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
16
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
17
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
18
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.6",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.75",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
21
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.140",
|
|
22
23
|
"@reventlessdev/reventless-core": "3.0.0-alpha.233",
|
|
23
24
|
"@reventlessdev/reventless-postgres": "3.0.0-alpha.97",
|
|
24
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
25
25
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.113",
|
|
26
|
-
"@reventlessdev/reventless-
|
|
26
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.31"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
|
@@ -355,6 +355,12 @@ let injectAwsAuth = (
|
|
|
355
355
|
}
|
|
356
356
|
})
|
|
357
357
|
|
|
358
|
+
// A field with no group restriction gets the group-less Cognito directive
|
|
359
|
+
// rather than no directive at all. Same reachability under `defaultAction:
|
|
360
|
+
// ALLOW`; the difference only shows once the default is DENY, where an
|
|
361
|
+
// unstamped field is refused. See AppSync_SdlDecorate.cognitoOpenDirective.
|
|
362
|
+
let openDirective = AppSync_SdlDecorate.cognitoOpenDirective
|
|
363
|
+
|
|
358
364
|
let augmentedMutations = parts.mutations->Array.map(field => {
|
|
359
365
|
let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
|
|
360
366
|
let groups = mutationAuthMap->Dict.get(fieldName)
|
|
@@ -363,7 +369,7 @@ let injectAwsAuth = (
|
|
|
363
369
|
} else {
|
|
364
370
|
switch groups {
|
|
365
371
|
| Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
|
|
366
|
-
| None => field
|
|
372
|
+
| None => `${field}\n ${openDirective}`
|
|
367
373
|
}
|
|
368
374
|
}
|
|
369
375
|
})
|
|
@@ -379,11 +385,23 @@ let injectAwsAuth = (
|
|
|
379
385
|
} else {
|
|
380
386
|
switch groups {
|
|
381
387
|
| Some(groups) => `${field} ${_formatGroupsDirective(groups)}`
|
|
382
|
-
| None => field
|
|
388
|
+
| None => `${field} ${openDirective}`
|
|
383
389
|
}
|
|
384
390
|
}
|
|
385
391
|
})
|
|
386
392
|
|
|
393
|
+
// Subscriptions carried no directive at all on this path — they are reads and
|
|
394
|
+
// need one for the same reason queries do. A subscription is never IAM-marked
|
|
395
|
+
// (the deploy caller does not subscribe), so it takes the Cognito arm only:
|
|
396
|
+
// the mutation's groups where the subscribed field has them, otherwise open.
|
|
397
|
+
let augmentedSubscriptions = parts.subscriptions->Array.map(field => {
|
|
398
|
+
let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
|
|
399
|
+
switch mutationAuthMap->Dict.get(fieldName) {
|
|
400
|
+
| Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
|
|
401
|
+
| None => `${field}\n ${openDirective}`
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
|
|
387
405
|
// Type-level dual-auth for every type the callable entries' responses reach —
|
|
388
406
|
// the node type, its Connection/Edge wrappers, and nested state types all
|
|
389
407
|
// share the entry's returnTypeName prefix.
|
|
@@ -400,6 +418,7 @@ let injectAwsAuth = (
|
|
|
400
418
|
types: augmentedTypes,
|
|
401
419
|
mutations: augmentedMutations,
|
|
402
420
|
queries: augmentedQueries,
|
|
421
|
+
subscriptions: augmentedSubscriptions,
|
|
403
422
|
})
|
|
404
423
|
}
|
|
405
424
|
|
|
@@ -444,6 +463,9 @@ let stitchStandaloneWithAwsDirectives = (
|
|
|
444
463
|
ReventlessCore.GraphQL_Stitcher.stitchStandalone(~fragment)
|
|
445
464
|
->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
|
|
446
465
|
->stampSharedIamTypes
|
|
466
|
+
// Last: every remaining undirectived type takes the group-less Cognito arm.
|
|
467
|
+
// After stampSharedIamTypes so the shared traversal types keep `@aws_iam`.
|
|
468
|
+
->AppSync_SdlDecorate.stampAllTypesCognito
|
|
447
469
|
}
|
|
448
470
|
|
|
449
471
|
// ── Provider implementation ────────────────────────────────────────────────
|
|
@@ -216,7 +216,7 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
216
216
|
} else if (groups !== undefined) {
|
|
217
217
|
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
|
|
218
218
|
} else {
|
|
219
|
-
return field;
|
|
219
|
+
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
|
|
220
220
|
}
|
|
221
221
|
});
|
|
222
222
|
let augmentedQueries = parts.queries.map(field => {
|
|
@@ -228,7 +228,16 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
228
228
|
} else if (groups !== undefined) {
|
|
229
229
|
return field + ` ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
|
|
230
230
|
} else {
|
|
231
|
-
return field;
|
|
231
|
+
return field + ` ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
let augmentedSubscriptions = parts.subscriptions.map(field => {
|
|
235
|
+
let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
|
|
236
|
+
let groups = mutationAuthMap[fieldName];
|
|
237
|
+
if (groups !== undefined) {
|
|
238
|
+
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
|
|
239
|
+
} else {
|
|
240
|
+
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
|
|
232
241
|
}
|
|
233
242
|
});
|
|
234
243
|
let augmentedTypes = parts.types.map(decl => {
|
|
@@ -243,7 +252,7 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
243
252
|
types: augmentedTypes,
|
|
244
253
|
mutations: augmentedMutations,
|
|
245
254
|
queries: augmentedQueries,
|
|
246
|
-
subscriptions:
|
|
255
|
+
subscriptions: augmentedSubscriptions,
|
|
247
256
|
subscriptionSources: parts.subscriptionSources
|
|
248
257
|
});
|
|
249
258
|
}
|
|
@@ -255,7 +264,7 @@ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
|
|
|
255
264
|
|
|
256
265
|
function stitchStandaloneWithAwsDirectives(fragment) {
|
|
257
266
|
let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
|
|
258
|
-
return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment), sources));
|
|
267
|
+
return AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment), sources)));
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
|
|
@@ -87,6 +87,18 @@ let formatCognitoGroupsDirective = (groups: array<string>): string => {
|
|
|
87
87
|
`@aws_cognito_user_pools(cognito_groups: [${quoted}])`
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// The group-less Cognito directive: reachable by any authenticated Cognito
|
|
91
|
+
// caller. Emitted where a field or type is deliberately open — `AllowAuthenticated`,
|
|
92
|
+
// `AllowAnonymous`, a field carrying no permission at all, and every object type.
|
|
93
|
+
//
|
|
94
|
+
// Under `userPoolConfig.defaultAction: ALLOW` this is a no-op: an undirectived
|
|
95
|
+
// field is already reachable by any authenticated caller, so stamping it changes
|
|
96
|
+
// nothing. It exists so the default can be flipped to DENY, where "no directive"
|
|
97
|
+
// stops meaning "open" and starts meaning "refused" — at which point anything
|
|
98
|
+
// left unstamped breaks. Stamping first and flipping second is what keeps that
|
|
99
|
+
// flip from being a guess.
|
|
100
|
+
let cognitoOpenDirective = "@aws_cognito_user_pools"
|
|
101
|
+
|
|
90
102
|
// Multi-auth directive for a field/type that must accept BOTH Cognito and IAM.
|
|
91
103
|
// `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps it
|
|
92
104
|
// open to any authenticated Cognito user. `@aws_iam` admits the deploy-time
|
|
@@ -94,7 +106,7 @@ let formatCognitoGroupsDirective = (groups: array<string>): string => {
|
|
|
94
106
|
let formatDualAuthDirective = (groups: option<array<string>>): string => {
|
|
95
107
|
let cognito = switch groups {
|
|
96
108
|
| Some(g) => formatCognitoGroupsDirective(g)
|
|
97
|
-
| None =>
|
|
109
|
+
| None => cognitoOpenDirective
|
|
98
110
|
}
|
|
99
111
|
`${cognito} @aws_iam`
|
|
100
112
|
}
|
|
@@ -150,6 +162,38 @@ let stampSharedIamTypes = (sdl: string): string =>
|
|
|
150
162
|
acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
|
|
151
163
|
)
|
|
152
164
|
|
|
165
|
+
// Stamp every object `type` declaration that does not already carry an auth
|
|
166
|
+
// directive with the group-less Cognito arm. Run on the ASSEMBLED SDL, AFTER
|
|
167
|
+
// stampSharedIamTypes so the shared traversal types keep their `@aws_iam` arm
|
|
168
|
+
// (this pass skips anything already carrying an `@aws_` directive).
|
|
169
|
+
//
|
|
170
|
+
// Types, not just fields: on a multi-auth API with `defaultAction: DENY`,
|
|
171
|
+
// AppSync refuses an undirectived TYPE even when the caller passed the field's
|
|
172
|
+
// own gate — response shaping walks `…Connection` → `…Edge` → node → nested
|
|
173
|
+
// state types and dies one level in. Entry gating stays on the fields; a type
|
|
174
|
+
// stamp only restores the accessibility an undirectived type has today.
|
|
175
|
+
//
|
|
176
|
+
// Only `type` declarations take auth directives — `input`, `enum`, `union` and
|
|
177
|
+
// `interface` do not, and stamping them is a schema error.
|
|
178
|
+
let stampAllTypesCognito = (sdl: string): string =>
|
|
179
|
+
sdl
|
|
180
|
+
->String.split("\n")
|
|
181
|
+
->Array.map(line =>
|
|
182
|
+
if line->String.startsWith("type ") && !(line->String.includes("@aws_")) {
|
|
183
|
+
switch line->String.indexOfOpt("{") {
|
|
184
|
+
| Some(i) =>
|
|
185
|
+
line->String.slice(~start=0, ~end=i) ++
|
|
186
|
+
cognitoOpenDirective ++
|
|
187
|
+
" " ++
|
|
188
|
+
line->String.slice(~start=i, ~end=line->String.length)
|
|
189
|
+
| None => line
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
line
|
|
193
|
+
}
|
|
194
|
+
)
|
|
195
|
+
->Array.join("\n")
|
|
196
|
+
|
|
153
197
|
// ── Merged-API canonical stamping ─────────────────────────────────────────────
|
|
154
198
|
// Under AppSync Merged APIs the admin source API owns the shared traversal
|
|
155
199
|
// types; `@canonical` makes its definition win over every plugin source's copy
|
|
@@ -42,8 +42,10 @@ function formatCognitoGroupsDirective(groups) {
|
|
|
42
42
|
return `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
let cognitoOpenDirective = "@aws_cognito_user_pools";
|
|
46
|
+
|
|
45
47
|
function formatDualAuthDirective(groups) {
|
|
46
|
-
let cognito = groups !== undefined ? formatCognitoGroupsDirective(groups) :
|
|
48
|
+
let cognito = groups !== undefined ? formatCognitoGroupsDirective(groups) : cognitoOpenDirective;
|
|
47
49
|
return cognito + ` @aws_iam`;
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -86,6 +88,20 @@ function stampSharedIamTypes(sdl) {
|
|
|
86
88
|
return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
function stampAllTypesCognito(sdl) {
|
|
92
|
+
return sdl.split("\n").map(line => {
|
|
93
|
+
if (!(line.startsWith("type ") && !line.includes("@aws_"))) {
|
|
94
|
+
return line;
|
|
95
|
+
}
|
|
96
|
+
let i = Stdlib_String.indexOfOpt(line, "{");
|
|
97
|
+
if (i !== undefined) {
|
|
98
|
+
return line.slice(0, i) + cognitoOpenDirective + " " + line.slice(i, line.length);
|
|
99
|
+
} else {
|
|
100
|
+
return line;
|
|
101
|
+
}
|
|
102
|
+
}).join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
let canonicalTypeNames = [
|
|
90
106
|
"PageInfo",
|
|
91
107
|
"CommandAccepted",
|
|
@@ -126,10 +142,12 @@ function stampCanonicalTypes(sdl) {
|
|
|
126
142
|
export {
|
|
127
143
|
injectAwsSubscribe,
|
|
128
144
|
formatCognitoGroupsDirective,
|
|
145
|
+
cognitoOpenDirective,
|
|
129
146
|
formatDualAuthDirective,
|
|
130
147
|
injectAwsAuthAll,
|
|
131
148
|
sharedIamTypeNames,
|
|
132
149
|
stampSharedIamTypes,
|
|
150
|
+
stampAllTypesCognito,
|
|
133
151
|
canonicalTypeNames,
|
|
134
152
|
stampCanonicalTypes,
|
|
135
153
|
}
|
|
@@ -218,7 +218,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
218
218
|
)
|
|
219
219
|
})
|
|
220
220
|
|
|
221
|
-
testSync("AllowAuthenticated emits
|
|
221
|
+
testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
|
|
222
222
|
let fp = Dict.fromArray([(
|
|
223
223
|
"p_Add",
|
|
224
224
|
Reventless.Authorization.AllowAuthenticated,
|
|
@@ -231,7 +231,11 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
231
231
|
~queryEntries=[],
|
|
232
232
|
)
|
|
233
233
|
let parts = decodeFragment(aug)
|
|
234
|
-
|
|
234
|
+
// Open, not ungated: the field carries the group-less directive so it stays
|
|
235
|
+
// reachable once defaultAction flips to DENY, but names no group.
|
|
236
|
+
let m = parts.mutations->Array.getUnsafe(0)
|
|
237
|
+
expect(m)->toContain("@aws_cognito_user_pools")
|
|
238
|
+
expect(m)->not_->toContain("cognito_groups")
|
|
235
239
|
})
|
|
236
240
|
|
|
237
241
|
testSync("DenyAll emits sentinel __deny_all__ group", () => {
|
|
@@ -273,7 +277,9 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
273
277
|
| None => JsError.throwWithMessage("missing archive field")
|
|
274
278
|
}
|
|
275
279
|
switch add {
|
|
276
|
-
| Some(f) =>
|
|
280
|
+
| Some(f) =>
|
|
281
|
+
expect(f)->toContain("@aws_cognito_user_pools")
|
|
282
|
+
expect(f)->not_->toContain("cognito_groups")
|
|
277
283
|
| None => JsError.throwWithMessage("missing add field")
|
|
278
284
|
}
|
|
279
285
|
})
|
|
@@ -338,7 +344,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
338
344
|
expect(m)->not_->toContain(`"Admin"`)
|
|
339
345
|
})
|
|
340
346
|
|
|
341
|
-
testSync("AllowAuthenticated on a field overrides the legacy authorization (
|
|
347
|
+
testSync("AllowAuthenticated on a field overrides the legacy authorization (drops the group)", () => {
|
|
342
348
|
let fp = Dict.fromArray([(
|
|
343
349
|
"p_X",
|
|
344
350
|
Reventless.Authorization.AllowAuthenticated,
|
|
@@ -359,7 +365,11 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
359
365
|
~queryEntries=[],
|
|
360
366
|
)
|
|
361
367
|
let parts = decodeFragment(aug)
|
|
362
|
-
|
|
368
|
+
// The spec-level AllowAuthenticated wins over the legacy `authorization.group`:
|
|
369
|
+
// the field keeps a directive, but no longer names a group.
|
|
370
|
+
let m = parts.mutations->Array.getUnsafe(0)
|
|
371
|
+
expect(m)->toContain("@aws_cognito_user_pools")
|
|
372
|
+
expect(m)->not_->toContain("cognito_groups")
|
|
363
373
|
})
|
|
364
374
|
})
|
|
365
375
|
|
|
@@ -851,3 +861,56 @@ describe("AppSync_Adapter.waitForMergeSuccess", () => {
|
|
|
851
861
|
expect(message.contents)->toContain("MERGE_IN_PROGRESS")
|
|
852
862
|
})
|
|
853
863
|
})
|
|
864
|
+
|
|
865
|
+
// ── Subscription stamping ───────────────────────────────────────────────────
|
|
866
|
+
//
|
|
867
|
+
// `injectAwsAuth` re-encoded `subscriptions` untouched, so every plugin
|
|
868
|
+
// subscription reached the deployed SDL with no auth directive at all — 22 of
|
|
869
|
+
// them on the hybrid domain API. Harmless under `defaultAction: ALLOW`, refused
|
|
870
|
+
// under DENY. A subscription is a read and takes the Cognito arm; it is never
|
|
871
|
+
// IAM-marked, because the deploy-time caller does not subscribe.
|
|
872
|
+
|
|
873
|
+
describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
|
|
874
|
+
let makeFragment = subscriptionFields =>
|
|
875
|
+
ReventlessCore.GraphQL_Stitcher.encode({
|
|
876
|
+
types: [],
|
|
877
|
+
mutations: ["p_Ship(id: ID!): CommandResult!"],
|
|
878
|
+
queries: [],
|
|
879
|
+
subscriptions: subscriptionFields,
|
|
880
|
+
subscriptionSources: [],
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
let entry = (~fieldNames, ~fieldPermissions): ReventlessInfra.Api.mutationSchemaEntry => {
|
|
884
|
+
fieldNames,
|
|
885
|
+
commandSchema: S.unknown,
|
|
886
|
+
fieldPermissions,
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
testSync("an unannotated subscription gets the group-less Cognito directive", () => {
|
|
890
|
+
let frag = makeFragment(["onSomething: CommandResult"])
|
|
891
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[])
|
|
892
|
+
let s = decodeFragment(aug).subscriptions->Array.getUnsafe(0)
|
|
893
|
+
expect(s)->toContain("@aws_cognito_user_pools")
|
|
894
|
+
expect(s)->not_->toContain("cognito_groups")
|
|
895
|
+
expect(s)->not_->toContain("@aws_iam")
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
testSync("a subscription named like a gated mutation inherits its groups", () => {
|
|
899
|
+
let fp = Dict.fromArray([("p_Ship", Reventless.Authorization.AllowGroups(["Fulfilment"]))])
|
|
900
|
+
let frag = makeFragment(["p_Ship: CommandResult"])
|
|
901
|
+
let aug = AppSync_Adapter.injectAwsAuth(
|
|
902
|
+
frag,
|
|
903
|
+
~mutationEntries=[entry(~fieldNames=["p_Ship"], ~fieldPermissions=fp)],
|
|
904
|
+
~queryEntries=[],
|
|
905
|
+
)
|
|
906
|
+
let s = decodeFragment(aug).subscriptions->Array.getUnsafe(0)
|
|
907
|
+
expect(s)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Fulfilment"])`)
|
|
908
|
+
expect(s)->not_->toContain("@aws_iam")
|
|
909
|
+
})
|
|
910
|
+
|
|
911
|
+
testSync("subscription fields survive the round-trip", () => {
|
|
912
|
+
let frag = makeFragment(["onA: CommandResult", "onB: CommandResult"])
|
|
913
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[])
|
|
914
|
+
expect(decodeFragment(aug).subscriptions->Array.length)->toBe(2)
|
|
915
|
+
})
|
|
916
|
+
})
|
|
@@ -161,7 +161,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
161
161
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
162
162
|
globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
|
|
163
163
|
});
|
|
164
|
-
globalThis.test("AllowAuthenticated emits
|
|
164
|
+
globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
|
|
165
165
|
let fp = Object.fromEntries([[
|
|
166
166
|
"p_Add",
|
|
167
167
|
"AllowAuthenticated"
|
|
@@ -170,7 +170,9 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
170
170
|
let frag = makeFragment(["p_Add(id: ID!): String"], []);
|
|
171
171
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
172
172
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
173
|
-
|
|
173
|
+
let m = parts.mutations[0];
|
|
174
|
+
globalThis.expect(m).toContain("@aws_cognito_user_pools");
|
|
175
|
+
globalThis.expect(m).not.toContain("cognito_groups");
|
|
174
176
|
});
|
|
175
177
|
globalThis.test("DenyAll emits sentinel __deny_all__ group", () => {
|
|
176
178
|
let fp = Object.fromEntries([[
|
|
@@ -215,7 +217,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
215
217
|
Stdlib_JsError.throwWithMessage("missing archive field");
|
|
216
218
|
}
|
|
217
219
|
if (add !== undefined) {
|
|
218
|
-
globalThis.expect(add).
|
|
220
|
+
globalThis.expect(add).toContain("@aws_cognito_user_pools");
|
|
221
|
+
globalThis.expect(add).not.toContain("cognito_groups");
|
|
219
222
|
return;
|
|
220
223
|
} else {
|
|
221
224
|
return Stdlib_JsError.throwWithMessage("missing add field");
|
|
@@ -269,7 +272,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
269
272
|
globalThis.expect(m).toContain(`cognito_groups: ["Manager"]`);
|
|
270
273
|
globalThis.expect(m).not.toContain(`"Admin"`);
|
|
271
274
|
});
|
|
272
|
-
globalThis.test("AllowAuthenticated on a field overrides the legacy authorization (
|
|
275
|
+
globalThis.test("AllowAuthenticated on a field overrides the legacy authorization (drops the group)", () => {
|
|
273
276
|
let fp = Object.fromEntries([[
|
|
274
277
|
"p_X",
|
|
275
278
|
"AllowAuthenticated"
|
|
@@ -289,7 +292,9 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
289
292
|
let frag = makeFragment(["p_X(id: ID!): String"], []);
|
|
290
293
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
291
294
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
292
|
-
|
|
295
|
+
let m = parts.mutations[0];
|
|
296
|
+
globalThis.expect(m).toContain("@aws_cognito_user_pools");
|
|
297
|
+
globalThis.expect(m).not.toContain("cognito_groups");
|
|
293
298
|
});
|
|
294
299
|
});
|
|
295
300
|
|
|
@@ -746,6 +751,51 @@ globalThis.describe("AppSync_Adapter.waitForMergeSuccess", () => {
|
|
|
746
751
|
});
|
|
747
752
|
});
|
|
748
753
|
|
|
754
|
+
globalThis.describe("AppSync_Adapter.injectAwsAuth — subscriptions", () => {
|
|
755
|
+
let makeFragment = subscriptionFields => GraphQL_Stitcher$ReventlessCore.encode({
|
|
756
|
+
types: [],
|
|
757
|
+
mutations: ["p_Ship(id: ID!): CommandResult!"],
|
|
758
|
+
queries: [],
|
|
759
|
+
subscriptions: subscriptionFields,
|
|
760
|
+
subscriptionSources: []
|
|
761
|
+
});
|
|
762
|
+
let entry = (fieldNames, fieldPermissions) => ({
|
|
763
|
+
fieldNames: fieldNames,
|
|
764
|
+
commandSchema: S.unknown,
|
|
765
|
+
fieldPermissions: fieldPermissions
|
|
766
|
+
});
|
|
767
|
+
globalThis.test("an unannotated subscription gets the group-less Cognito directive", () => {
|
|
768
|
+
let frag = makeFragment(["onSomething: CommandResult"]);
|
|
769
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], []);
|
|
770
|
+
let s = GraphQL_Stitcher$ReventlessCore.decode(aug).subscriptions[0];
|
|
771
|
+
globalThis.expect(s).toContain("@aws_cognito_user_pools");
|
|
772
|
+
globalThis.expect(s).not.toContain("cognito_groups");
|
|
773
|
+
globalThis.expect(s).not.toContain("@aws_iam");
|
|
774
|
+
});
|
|
775
|
+
globalThis.test("a subscription named like a gated mutation inherits its groups", () => {
|
|
776
|
+
let fp = Object.fromEntries([[
|
|
777
|
+
"p_Ship",
|
|
778
|
+
{
|
|
779
|
+
TAG: "AllowGroups",
|
|
780
|
+
_0: ["Fulfilment"]
|
|
781
|
+
}
|
|
782
|
+
]]);
|
|
783
|
+
let frag = makeFragment(["p_Ship: CommandResult"]);
|
|
784
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry(["p_Ship"], fp)], []);
|
|
785
|
+
let s = GraphQL_Stitcher$ReventlessCore.decode(aug).subscriptions[0];
|
|
786
|
+
globalThis.expect(s).toContain(`@aws_cognito_user_pools(cognito_groups: ["Fulfilment"])`);
|
|
787
|
+
globalThis.expect(s).not.toContain("@aws_iam");
|
|
788
|
+
});
|
|
789
|
+
globalThis.test("subscription fields survive the round-trip", () => {
|
|
790
|
+
let frag = makeFragment([
|
|
791
|
+
"onA: CommandResult",
|
|
792
|
+
"onB: CommandResult"
|
|
793
|
+
]);
|
|
794
|
+
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [], []);
|
|
795
|
+
globalThis.expect(GraphQL_Stitcher$ReventlessCore.decode(aug).subscriptions.length).toBe(2);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
749
799
|
export {
|
|
750
800
|
decodeFragment,
|
|
751
801
|
}
|
|
@@ -152,3 +152,39 @@ describe("AppSync_SdlDecorate.stampCanonicalTypes", () => {
|
|
|
152
152
|
expect(AppSync_SdlDecorate.stampCanonicalTypes(once))->toBe(once)
|
|
153
153
|
})
|
|
154
154
|
})
|
|
155
|
+
|
|
156
|
+
// ── Type-level Cognito stamping (defaultAction: DENY groundwork) ─────────────
|
|
157
|
+
//
|
|
158
|
+
// With `defaultAction: ALLOW` an undirectived type is reachable by any
|
|
159
|
+
// authenticated Cognito caller, so this pass changes nothing observable. It
|
|
160
|
+
// exists so the default can be flipped to DENY, where an undirectived type is
|
|
161
|
+
// refused mid-traversal even for a caller who passed the field's own gate.
|
|
162
|
+
|
|
163
|
+
describe("AppSync_SdlDecorate.stampAllTypesCognito", () => {
|
|
164
|
+
testSync("stamps an undirectived object type", () => {
|
|
165
|
+
let sdl = AppSync_SdlDecorate.stampAllTypesCognito("type Product {\n id: ID!\n}")
|
|
166
|
+
expect(sdl)->toContain("type Product @aws_cognito_user_pools {")
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
testSync("leaves an already-directived type alone, keeping its @aws_iam arm", () => {
|
|
170
|
+
let sdl =
|
|
171
|
+
AppSync_SdlDecorate.stampSharedIamTypes(
|
|
172
|
+
"type PageInfo {\n hasNextPage: Boolean!\n}",
|
|
173
|
+
)->AppSync_SdlDecorate.stampAllTypesCognito
|
|
174
|
+
expect(sdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam {")
|
|
175
|
+
// Not double-stamped.
|
|
176
|
+
expect(sdl->String.includes("@aws_cognito_user_pools @aws_cognito_user_pools"))->toBe(false)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
testSync("does not stamp input, enum, union or interface declarations", () => {
|
|
180
|
+
let sdl = AppSync_SdlDecorate.stampAllTypesCognito(
|
|
181
|
+
"input ProductFilter {\n id: ID\n}\n\nenum Status {\n Active\n}\n\nunion CommandResult = CommandAccepted | CommandRejected\n\ninterface Node {\n id: ID!\n}",
|
|
182
|
+
)
|
|
183
|
+
expect(sdl->String.includes("@aws_cognito_user_pools"))->toBe(false)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
testSync("is idempotent", () => {
|
|
187
|
+
let once = AppSync_SdlDecorate.stampAllTypesCognito("type Product {\n id: ID!\n}")
|
|
188
|
+
expect(AppSync_SdlDecorate.stampAllTypesCognito(once))->toBe(once)
|
|
189
|
+
})
|
|
190
|
+
})
|
|
@@ -132,6 +132,26 @@ globalThis.describe("AppSync_SdlDecorate.stampCanonicalTypes", () => {
|
|
|
132
132
|
});
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
+
globalThis.describe("AppSync_SdlDecorate.stampAllTypesCognito", () => {
|
|
136
|
+
globalThis.test("stamps an undirectived object type", () => {
|
|
137
|
+
let sdl = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito("type Product {\n id: ID!\n}");
|
|
138
|
+
globalThis.expect(sdl).toContain("type Product @aws_cognito_user_pools {");
|
|
139
|
+
});
|
|
140
|
+
globalThis.test("leaves an already-directived type alone, keeping its @aws_iam arm", () => {
|
|
141
|
+
let sdl = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes("type PageInfo {\n hasNextPage: Boolean!\n}"));
|
|
142
|
+
globalThis.expect(sdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam {");
|
|
143
|
+
globalThis.expect(sdl.includes("@aws_cognito_user_pools @aws_cognito_user_pools")).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
globalThis.test("does not stamp input, enum, union or interface declarations", () => {
|
|
146
|
+
let sdl = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito("input ProductFilter {\n id: ID\n}\n\nenum Status {\n Active\n}\n\nunion CommandResult = CommandAccepted | CommandRejected\n\ninterface Node {\n id: ID!\n}");
|
|
147
|
+
globalThis.expect(sdl.includes("@aws_cognito_user_pools")).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
globalThis.test("is idempotent", () => {
|
|
150
|
+
let once = AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito("type Product {\n id: ID!\n}");
|
|
151
|
+
globalThis.expect(AppSync_SdlDecorate$ReventlessAws.stampAllTypesCognito(once)).toBe(once);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
135
155
|
export {
|
|
136
156
|
sources,
|
|
137
157
|
stitchedSdl,
|