@reventlessdev/reventless-aws 3.0.0-alpha.294 → 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 +14 -0
- package/package.json +9 -9
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +8 -1
- package/src/components/Api/AppSync_Adapter.res +43 -25
- package/src/components/Api/AppSync_Adapter.res.mjs +21 -24
- package/src/components/Api/AppSync_SdlDecorate.res +75 -15
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +29 -10
- package/tests/AppSync_AdapterTest.res +100 -22
- package/tests/AppSync_AdapterTest.res.mjs +75 -18
- package/tests/AppSync_SdlDecorateTest.res +40 -4
- package/tests/AppSync_SdlDecorateTest.res.mjs +24 -4
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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
|
+
|
|
13
|
+
# 3.0.0-alpha.295 (2026-08-14)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** enforce Cognito group gates that AppSync was silently ignoring ([1392638](https://github.com/ReventlessDev/reventless-core/commit/13926388c097500606f75c387630a19ee1a3aa71))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.294 (2026-08-14)
|
|
7
21
|
|
|
8
22
|
### 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": {
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.10",
|
|
16
16
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
17
17
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
18
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
19
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.
|
|
18
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.6",
|
|
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-
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-
|
|
25
|
-
"@reventlessdev/reventless-
|
|
26
|
-
"@reventlessdev/reventless-
|
|
22
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.140",
|
|
23
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.233",
|
|
24
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.97",
|
|
25
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.113",
|
|
26
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.31"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
|
@@ -6,13 +6,20 @@
|
|
|
6
6
|
// ([Auth_ActiveRoleTrigger.res] bundles it and attaches it to the user pool).
|
|
7
7
|
//
|
|
8
8
|
// The narrowing has to happen here rather than in our own enforcement code
|
|
9
|
-
// because `@authorize(AllowGroups([...]))` compiles to
|
|
9
|
+
// because `@authorize(AllowGroups([...]))` compiles to a field directive AppSync
|
|
10
10
|
// evaluates against `cognito:groups` **before any of our code executes**. A
|
|
11
11
|
// request header we invent could scope reads correctly and still leave every
|
|
12
12
|
// group-gated mutation callable — right about the data, wrong about the writes.
|
|
13
13
|
// Overriding the group claim is the one place every enforcement point already
|
|
14
14
|
// looks.
|
|
15
15
|
//
|
|
16
|
+
// That premise was false for as long as the directive was emitted as the
|
|
17
|
+
// single-mode `@aws_auth(...)`, which AppSync ignores on a multi-auth API — the
|
|
18
|
+
// gate was open and this trigger narrowed a claim nothing read for authorization.
|
|
19
|
+
// The directive is now `@aws_cognito_user_pools(...)`, which the service does
|
|
20
|
+
// enforce, so the reasoning above holds again. See
|
|
21
|
+
// `AppSync_SdlDecorate.formatCognitoGroupsDirective`.
|
|
22
|
+
//
|
|
16
23
|
// **Version 1 of the trigger, deliberately.** Group override and added ID-token
|
|
17
24
|
// claims are `V1_0` capabilities; `V2_0`/`V3_0` buy access-token customisation
|
|
18
25
|
// this does not need and are gated behind the Essentials and Plus feature plans.
|
|
@@ -171,9 +171,9 @@ let rec waitForMergeSuccess = async (
|
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// ──
|
|
175
|
-
// Injects @
|
|
176
|
-
// based on authorization metadata from schema entries.
|
|
174
|
+
// ── Cognito group directive injection ─────────────────────────────────────
|
|
175
|
+
// Injects @aws_cognito_user_pools(cognito_groups: [...]) directives into SDL
|
|
176
|
+
// field strings based on authorization metadata from schema entries.
|
|
177
177
|
//
|
|
178
178
|
// Two sources are merged:
|
|
179
179
|
// 1. The legacy `authorization?: {tableName, group}` field — single group
|
|
@@ -184,19 +184,22 @@ let rec waitForMergeSuccess = async (
|
|
|
184
184
|
//
|
|
185
185
|
// When both are present on the same field, the spec-level permission wins
|
|
186
186
|
// (it is more specific). `AllowGroups([g1, g2, ...])` emits
|
|
187
|
-
// `@
|
|
188
|
-
// `AllowAnonymous` emit no directive (with Cognito as
|
|
189
|
-
// reaching request is already authenticated). `AllowGroups([])`
|
|
190
|
-
// `DenyAll` emit a sentinel `__deny_all__` group that no Cognito user can
|
|
187
|
+
// `@aws_cognito_user_pools(cognito_groups: ["g1", "g2", ...])`.
|
|
188
|
+
// `AllowAuthenticated` / `AllowAnonymous` emit no directive (with Cognito as
|
|
189
|
+
// primary auth, any reaching request is already authenticated). `AllowGroups([])`
|
|
190
|
+
// and `DenyAll` emit a sentinel `__deny_all__` group that no Cognito user can
|
|
191
191
|
// belong to — effectively blocking the field at the API layer.
|
|
192
192
|
//
|
|
193
|
+
// NOT `@aws_auth(...)`: that is the single-mode form, which AppSync ignores on a
|
|
194
|
+
// multi-auth API — which every API this adapter provisions is. See
|
|
195
|
+
// `AppSync_SdlDecorate.formatCognitoGroupsDirective` for the full rationale and
|
|
196
|
+
// the observation that established it.
|
|
197
|
+
//
|
|
193
198
|
// ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
|
|
194
199
|
// A field flagged `systemCallable` (via `mutationSchemaEntry`/`querySchemaEntry`)
|
|
195
200
|
// must be reachable by BOTH the console UI (Cognito) and a deploy-time system
|
|
196
201
|
// caller signing SigV4 with ambient AWS credentials (`Util_AppSync_Caller`,
|
|
197
|
-
// the AWS_IAM additional auth provider).
|
|
198
|
-
// directive form and does not admit IAM on a multi-auth API. For these fields
|
|
199
|
-
// we emit the multi-auth form instead:
|
|
202
|
+
// the AWS_IAM additional auth provider). For these fields we append the IAM arm:
|
|
200
203
|
// @aws_cognito_user_pools(cognito_groups: [...]) @aws_iam
|
|
201
204
|
// The Cognito arm preserves the field's existing group gating (or, when the
|
|
202
205
|
// field carries no Cognito group restriction, a bare `@aws_cognito_user_pools`
|
|
@@ -215,24 +218,17 @@ let _permissionToCognitoGroups = (
|
|
|
215
218
|
| AllowAuthenticated | AllowAnonymous => None
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
// Both directive forms are defined once in the runtime-pure AppSync_SdlDecorate
|
|
222
|
+
// so this deploy path and the bundled AdminEventCollector Lambda's reactive push
|
|
223
|
+
// cannot drift. The Cognito-only arm carries the rationale for why it is
|
|
224
|
+
// `@aws_cognito_user_pools(...)` and never `@aws_auth(...)`.
|
|
225
|
+
let _formatGroupsDirective = AppSync_SdlDecorate.formatCognitoGroupsDirective
|
|
222
226
|
|
|
223
227
|
// Multi-auth directive for a field that must accept BOTH Cognito and IAM.
|
|
224
228
|
// `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps the
|
|
225
229
|
// field open to any authenticated Cognito user. `@aws_iam` admits the
|
|
226
230
|
// deploy-time SigV4 system caller. See the dual-auth note above.
|
|
227
|
-
let _formatDualAuthDirective =
|
|
228
|
-
let cognito = switch groups {
|
|
229
|
-
| Some(g) =>
|
|
230
|
-
let quoted = g->Array.map(x => `"${x}"`)->Array.join(", ")
|
|
231
|
-
`@aws_cognito_user_pools(cognito_groups: [${quoted}])`
|
|
232
|
-
| None => `@aws_cognito_user_pools`
|
|
233
|
-
}
|
|
234
|
-
`${cognito} @aws_iam`
|
|
235
|
-
}
|
|
231
|
+
let _formatDualAuthDirective = AppSync_SdlDecorate.formatDualAuthDirective
|
|
236
232
|
|
|
237
233
|
// ── Type-level dual-auth ─────────────────────────────────────────────────────
|
|
238
234
|
// On a multi-auth API, object TYPES without auth directives are accessible only
|
|
@@ -359,6 +355,12 @@ let injectAwsAuth = (
|
|
|
359
355
|
}
|
|
360
356
|
})
|
|
361
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
|
+
|
|
362
364
|
let augmentedMutations = parts.mutations->Array.map(field => {
|
|
363
365
|
let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
|
|
364
366
|
let groups = mutationAuthMap->Dict.get(fieldName)
|
|
@@ -367,7 +369,7 @@ let injectAwsAuth = (
|
|
|
367
369
|
} else {
|
|
368
370
|
switch groups {
|
|
369
371
|
| Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
|
|
370
|
-
| None => field
|
|
372
|
+
| None => `${field}\n ${openDirective}`
|
|
371
373
|
}
|
|
372
374
|
}
|
|
373
375
|
})
|
|
@@ -383,11 +385,23 @@ let injectAwsAuth = (
|
|
|
383
385
|
} else {
|
|
384
386
|
switch groups {
|
|
385
387
|
| Some(groups) => `${field} ${_formatGroupsDirective(groups)}`
|
|
386
|
-
| None => field
|
|
388
|
+
| None => `${field} ${openDirective}`
|
|
387
389
|
}
|
|
388
390
|
}
|
|
389
391
|
})
|
|
390
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
|
+
|
|
391
405
|
// Type-level dual-auth for every type the callable entries' responses reach —
|
|
392
406
|
// the node type, its Connection/Edge wrappers, and nested state types all
|
|
393
407
|
// share the entry's returnTypeName prefix.
|
|
@@ -404,6 +418,7 @@ let injectAwsAuth = (
|
|
|
404
418
|
types: augmentedTypes,
|
|
405
419
|
mutations: augmentedMutations,
|
|
406
420
|
queries: augmentedQueries,
|
|
421
|
+
subscriptions: augmentedSubscriptions,
|
|
407
422
|
})
|
|
408
423
|
}
|
|
409
424
|
|
|
@@ -448,6 +463,9 @@ let stitchStandaloneWithAwsDirectives = (
|
|
|
448
463
|
ReventlessCore.GraphQL_Stitcher.stitchStandalone(~fragment)
|
|
449
464
|
->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
|
|
450
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
|
|
451
469
|
}
|
|
452
470
|
|
|
453
471
|
// ── Provider implementation ────────────────────────────────────────────────
|
|
@@ -126,22 +126,6 @@ function _permissionToCognitoGroups(permission) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
function _formatGroupsDirective(groups) {
|
|
130
|
-
let quoted = groups.map(g => `"` + g + `"`).join(", ");
|
|
131
|
-
return `@aws_auth(cognito_groups: [` + quoted + `])`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function _formatDualAuthDirective(groups) {
|
|
135
|
-
let cognito;
|
|
136
|
-
if (groups !== undefined) {
|
|
137
|
-
let quoted = groups.map(x => `"` + x + `"`).join(", ");
|
|
138
|
-
cognito = `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
|
|
139
|
-
} else {
|
|
140
|
-
cognito = `@aws_cognito_user_pools`;
|
|
141
|
-
}
|
|
142
|
-
return cognito + ` @aws_iam`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
129
|
function _typeDeclName(decl) {
|
|
146
130
|
if (!decl.startsWith("type ")) {
|
|
147
131
|
return;
|
|
@@ -228,11 +212,11 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
228
212
|
let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
|
|
229
213
|
let groups = mutationAuthMap[fieldName];
|
|
230
214
|
if (Stdlib_Option.getOr(iamFields[fieldName], false)) {
|
|
231
|
-
return field + `\n ` +
|
|
215
|
+
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatDualAuthDirective(groups);
|
|
232
216
|
} else if (groups !== undefined) {
|
|
233
|
-
return field + `\n ` +
|
|
217
|
+
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
|
|
234
218
|
} else {
|
|
235
|
-
return field;
|
|
219
|
+
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
|
|
236
220
|
}
|
|
237
221
|
});
|
|
238
222
|
let augmentedQueries = parts.queries.map(field => {
|
|
@@ -240,11 +224,20 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
240
224
|
let groups = queryAuthMap[fieldName];
|
|
241
225
|
let isIam = Stdlib_Option.getOr(iamFields[fieldName], false) || iamQueryFieldPrefixes.some(p => fieldName.startsWith(p));
|
|
242
226
|
if (isIam) {
|
|
243
|
-
return field + ` ` +
|
|
227
|
+
return field + ` ` + AppSync_SdlDecorate$ReventlessAws.formatDualAuthDirective(groups);
|
|
244
228
|
} else if (groups !== undefined) {
|
|
245
|
-
return field + ` ` +
|
|
229
|
+
return field + ` ` + AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective(groups);
|
|
230
|
+
} else {
|
|
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);
|
|
246
239
|
} else {
|
|
247
|
-
return field;
|
|
240
|
+
return field + `\n ` + AppSync_SdlDecorate$ReventlessAws.cognitoOpenDirective;
|
|
248
241
|
}
|
|
249
242
|
});
|
|
250
243
|
let augmentedTypes = parts.types.map(decl => {
|
|
@@ -259,7 +252,7 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
259
252
|
types: augmentedTypes,
|
|
260
253
|
mutations: augmentedMutations,
|
|
261
254
|
queries: augmentedQueries,
|
|
262
|
-
subscriptions:
|
|
255
|
+
subscriptions: augmentedSubscriptions,
|
|
263
256
|
subscriptionSources: parts.subscriptionSources
|
|
264
257
|
});
|
|
265
258
|
}
|
|
@@ -271,7 +264,7 @@ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
|
|
|
271
264
|
|
|
272
265
|
function stitchStandaloneWithAwsDirectives(fragment) {
|
|
273
266
|
let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
|
|
274
|
-
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)));
|
|
275
268
|
}
|
|
276
269
|
|
|
277
270
|
function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
|
|
@@ -346,6 +339,10 @@ function generateFragment(mutationEntries, queryEntries) {
|
|
|
346
339
|
return injectAwsAuth(fragment, mutationEntries, queryEntries);
|
|
347
340
|
}
|
|
348
341
|
|
|
342
|
+
let _formatGroupsDirective = AppSync_SdlDecorate$ReventlessAws.formatCognitoGroupsDirective;
|
|
343
|
+
|
|
344
|
+
let _formatDualAuthDirective = AppSync_SdlDecorate$ReventlessAws.formatDualAuthDirective;
|
|
345
|
+
|
|
349
346
|
let stampSharedIamTypes = AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes;
|
|
350
347
|
|
|
351
348
|
let primaryAuthenticationType = "AMAZON_COGNITO_USER_POOLS";
|
|
@@ -67,26 +67,55 @@ let injectAwsSubscribe = (
|
|
|
67
67
|
// the deploy path without dragging Pulumi into the runtime graph. AppSync_Adapter
|
|
68
68
|
// delegates its `injectAwsAuthAll` / `stampSharedIamTypes` to these definitions.
|
|
69
69
|
|
|
70
|
+
// Cognito group gate. `@aws_cognito_user_pools(cognito_groups: [...])` — NOT the
|
|
71
|
+
// single-mode `@aws_auth(cognito_groups: [...])`.
|
|
72
|
+
//
|
|
73
|
+
// `@aws_auth` is honoured only on an API whose sole authorization mode is
|
|
74
|
+
// AMAZON_COGNITO_USER_POOLS. This adapter always configures AWS_IAM as an
|
|
75
|
+
// additional provider (see AppSync_Adapter's api args — the server-to-server
|
|
76
|
+
// lambdas need it), so every API we provision is multi-auth and the service
|
|
77
|
+
// **silently ignores** `@aws_auth`: the schema deploys, the directive shows up
|
|
78
|
+
// in the deployed SDL, and it gates nothing. What is left is
|
|
79
|
+
// `userPoolConfig.defaultAction: ALLOW`, which admits every authenticated
|
|
80
|
+
// Cognito user — a group gate that fails open.
|
|
81
|
+
//
|
|
82
|
+
// Confirmed by observation against a deployed API: a user in no groups read
|
|
83
|
+
// every `@aws_auth`-gated field and reached a gated mutation's handler. See
|
|
84
|
+
// docs/plans/appsync-group-authorization-unenforced.md.
|
|
85
|
+
let formatCognitoGroupsDirective = (groups: array<string>): string => {
|
|
86
|
+
let quoted = groups->Array.map(g => `"${g}"`)->Array.join(", ")
|
|
87
|
+
`@aws_cognito_user_pools(cognito_groups: [${quoted}])`
|
|
88
|
+
}
|
|
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
|
+
|
|
70
102
|
// Multi-auth directive for a field/type that must accept BOTH Cognito and IAM.
|
|
71
103
|
// `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps it
|
|
72
104
|
// open to any authenticated Cognito user. `@aws_iam` admits the deploy-time
|
|
73
105
|
// SigV4 system caller.
|
|
74
106
|
let formatDualAuthDirective = (groups: option<array<string>>): string => {
|
|
75
107
|
let cognito = switch groups {
|
|
76
|
-
| Some(g) =>
|
|
77
|
-
|
|
78
|
-
`@aws_cognito_user_pools(cognito_groups: [${quoted}])`
|
|
79
|
-
| None => `@aws_cognito_user_pools`
|
|
108
|
+
| Some(g) => formatCognitoGroupsDirective(g)
|
|
109
|
+
| None => cognitoOpenDirective
|
|
80
110
|
}
|
|
81
111
|
`${cognito} @aws_iam`
|
|
82
112
|
}
|
|
83
113
|
|
|
84
|
-
// Injects
|
|
85
|
-
//
|
|
86
|
-
// deploy-time IAM
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// the SigV4 system caller. Subscriptions are never IAM-marked.
|
|
114
|
+
// Injects the Cognito group gate on ALL mutation, query, and subscription fields
|
|
115
|
+
// in a fragment. `~iamFieldNames` opts the named mutation/query fields into
|
|
116
|
+
// deploy-time IAM as well, appending the `@aws_iam` arm. Subscriptions are never
|
|
117
|
+
// IAM-marked (the deploy caller does not subscribe) but still carry the Cognito
|
|
118
|
+
// gate — a subscription is a read.
|
|
90
119
|
let injectAwsAuthAll = (
|
|
91
120
|
fragment: Reventless.Plugin.apiSchemaFragment,
|
|
92
121
|
~group: string,
|
|
@@ -95,19 +124,18 @@ let injectAwsAuthAll = (
|
|
|
95
124
|
let parts = ReventlessCore.GraphQL_Stitcher.decode(fragment)
|
|
96
125
|
let isIam = (field: string): bool =>
|
|
97
126
|
iamFieldNames->Array.includes(ReventlessCore.GraphQL_Stitcher.extractLeadingName(field))
|
|
127
|
+
let cognitoOnly = formatCognitoGroupsDirective([group])
|
|
98
128
|
|
|
99
129
|
let augmentedMutations = parts.mutations->Array.map(field =>
|
|
100
130
|
isIam(field)
|
|
101
131
|
? `${field}\n ${formatDualAuthDirective(Some([group]))}`
|
|
102
|
-
: `${field}\n
|
|
132
|
+
: `${field}\n ${cognitoOnly}`
|
|
103
133
|
)
|
|
104
134
|
let augmentedQueries = parts.queries->Array.map(field =>
|
|
105
|
-
isIam(field)
|
|
106
|
-
? `${field} ${formatDualAuthDirective(Some([group]))}`
|
|
107
|
-
: `${field} @aws_auth(cognito_groups: ["${group}"])`
|
|
135
|
+
isIam(field) ? `${field} ${formatDualAuthDirective(Some([group]))}` : `${field} ${cognitoOnly}`
|
|
108
136
|
)
|
|
109
137
|
let augmentedSubscriptions = parts.subscriptions->Array.map(field =>
|
|
110
|
-
`${field}\n
|
|
138
|
+
`${field}\n ${cognitoOnly}`
|
|
111
139
|
)
|
|
112
140
|
|
|
113
141
|
ReventlessCore.GraphQL_Stitcher.encode({
|
|
@@ -134,6 +162,38 @@ let stampSharedIamTypes = (sdl: string): string =>
|
|
|
134
162
|
acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
|
|
135
163
|
)
|
|
136
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
|
+
|
|
137
197
|
// ── Merged-API canonical stamping ─────────────────────────────────────────────
|
|
138
198
|
// Under AppSync Merged APIs the admin source API owns the shared traversal
|
|
139
199
|
// types; `@canonical` makes its definition win over every plugin source's copy
|
|
@@ -37,35 +37,37 @@ function injectAwsSubscribe(sdl, sources) {
|
|
|
37
37
|
return before + decorated + after;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function formatCognitoGroupsDirective(groups) {
|
|
41
|
+
let quoted = groups.map(g => `"` + g + `"`).join(", ");
|
|
42
|
+
return `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let cognitoOpenDirective = "@aws_cognito_user_pools";
|
|
46
|
+
|
|
40
47
|
function formatDualAuthDirective(groups) {
|
|
41
|
-
let cognito;
|
|
42
|
-
if (groups !== undefined) {
|
|
43
|
-
let quoted = groups.map(x => `"` + x + `"`).join(", ");
|
|
44
|
-
cognito = `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
|
|
45
|
-
} else {
|
|
46
|
-
cognito = `@aws_cognito_user_pools`;
|
|
47
|
-
}
|
|
48
|
+
let cognito = groups !== undefined ? formatCognitoGroupsDirective(groups) : cognitoOpenDirective;
|
|
48
49
|
return cognito + ` @aws_iam`;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
|
|
52
53
|
let iamFieldNames = iamFieldNamesOpt !== undefined ? iamFieldNamesOpt : [];
|
|
53
54
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
|
|
55
|
+
let cognitoOnly = formatCognitoGroupsDirective([group]);
|
|
54
56
|
let augmentedMutations = parts.mutations.map(field => {
|
|
55
57
|
if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
|
|
56
58
|
return field + `\n ` + formatDualAuthDirective([group]);
|
|
57
59
|
} else {
|
|
58
|
-
return field + `\n
|
|
60
|
+
return field + `\n ` + cognitoOnly;
|
|
59
61
|
}
|
|
60
62
|
});
|
|
61
63
|
let augmentedQueries = parts.queries.map(field => {
|
|
62
64
|
if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
|
|
63
65
|
return field + ` ` + formatDualAuthDirective([group]);
|
|
64
66
|
} else {
|
|
65
|
-
return field + `
|
|
67
|
+
return field + ` ` + cognitoOnly;
|
|
66
68
|
}
|
|
67
69
|
});
|
|
68
|
-
let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n
|
|
70
|
+
let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n ` + cognitoOnly);
|
|
69
71
|
return GraphQL_Stitcher$ReventlessCore.encode({
|
|
70
72
|
types: parts.types,
|
|
71
73
|
mutations: augmentedMutations,
|
|
@@ -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",
|
|
@@ -125,10 +141,13 @@ function stampCanonicalTypes(sdl) {
|
|
|
125
141
|
|
|
126
142
|
export {
|
|
127
143
|
injectAwsSubscribe,
|
|
144
|
+
formatCognitoGroupsDirective,
|
|
145
|
+
cognitoOpenDirective,
|
|
128
146
|
formatDualAuthDirective,
|
|
129
147
|
injectAwsAuthAll,
|
|
130
148
|
sharedIamTypeNames,
|
|
131
149
|
stampSharedIamTypes,
|
|
150
|
+
stampAllTypesCognito,
|
|
132
151
|
canonicalTypeNames,
|
|
133
152
|
stampCanonicalTypes,
|
|
134
153
|
}
|
|
@@ -25,19 +25,19 @@ let decodeFragment = (fragment: Reventless.Plugin.apiSchemaFragment) =>
|
|
|
25
25
|
describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
26
26
|
let baseFragment = ReventlessCore.Platform_AdminApi.baseFragment(~cloner=true)
|
|
27
27
|
|
|
28
|
-
testSync("adds
|
|
28
|
+
testSync("adds the Cognito group directive to all mutation fields", () => {
|
|
29
29
|
let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
|
|
30
30
|
let parts = decodeFragment(augmented)
|
|
31
31
|
parts.mutations->Array.forEach(field => {
|
|
32
|
-
expect(field)->toContain(`@
|
|
32
|
+
expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
33
33
|
})
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
testSync("adds
|
|
36
|
+
testSync("adds the Cognito group directive to all query fields", () => {
|
|
37
37
|
let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
|
|
38
38
|
let parts = decodeFragment(augmented)
|
|
39
39
|
parts.queries->Array.forEach(field => {
|
|
40
|
-
expect(field)->toContain(`@
|
|
40
|
+
expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
41
41
|
})
|
|
42
42
|
})
|
|
43
43
|
|
|
@@ -61,15 +61,15 @@ describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
61
61
|
// admin base. That caused AppSync to reject the auto-generated resolver for
|
|
62
62
|
// `Subscription.onPlatform_Plugin_Activate` (and Deactivate) with "Type not
|
|
63
63
|
// found" at deploy time. The subscription fields must survive the round-trip
|
|
64
|
-
// and pick up the same
|
|
65
|
-
testSync("preserves subscription fields with
|
|
64
|
+
// and pick up the same Cognito group directive as mutations/queries.
|
|
65
|
+
testSync("preserves subscription fields with the Cognito group directive", () => {
|
|
66
66
|
let original = decodeFragment(baseFragment)
|
|
67
67
|
let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
|
|
68
68
|
let parts = decodeFragment(augmented)
|
|
69
69
|
expect(parts.subscriptions->Array.length)->toBe(original.subscriptions->Array.length)
|
|
70
70
|
expect(parts.subscriptions->Array.length)->toBeGreaterThan(0)
|
|
71
71
|
parts.subscriptions->Array.forEach(field => {
|
|
72
|
-
expect(field)->toContain(`@
|
|
72
|
+
expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
73
73
|
})
|
|
74
74
|
})
|
|
75
75
|
|
|
@@ -80,6 +80,21 @@ describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
80
80
|
expect(joined)->toContain("onPlatform_Plugin_Activate")
|
|
81
81
|
expect(joined)->toContain("onPlatform_Plugin_Deactivate")
|
|
82
82
|
})
|
|
83
|
+
|
|
84
|
+
// Regression: the admin base was stamped with the single-mode
|
|
85
|
+
// `@aws_auth(cognito_groups: [...])`, which AppSync IGNORES on a multi-auth API
|
|
86
|
+
// — and every API this adapter provisions is multi-auth. A deployed probe found
|
|
87
|
+
// a Cognito user in no groups reading every admin field. The directive must be
|
|
88
|
+
// `@aws_cognito_user_pools(...)`, the form the service actually enforces.
|
|
89
|
+
// See docs/plans/appsync-group-authorization-unenforced.md.
|
|
90
|
+
testSync("never emits the single-mode @aws_auth form on any field", () => {
|
|
91
|
+
let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
|
|
92
|
+
let parts = decodeFragment(augmented)
|
|
93
|
+
let everyField =
|
|
94
|
+
Array.concatMany(parts.mutations, [parts.queries, parts.subscriptions])->Array.join("\n")
|
|
95
|
+
expect(everyField)->not_->toContain("@aws_auth")
|
|
96
|
+
expect(everyField)->toContain("@aws_cognito_user_pools")
|
|
97
|
+
})
|
|
83
98
|
})
|
|
84
99
|
|
|
85
100
|
describe("AppSync_Adapter.injectAwsAuth", () => {
|
|
@@ -97,12 +112,12 @@ describe("AppSync_Adapter.injectAwsAuth", () => {
|
|
|
97
112
|
)
|
|
98
113
|
let parts = decodeFragment(augmented)
|
|
99
114
|
|
|
100
|
-
// Query entries have authorization → should
|
|
115
|
+
// Query entries have authorization → should carry the directive
|
|
101
116
|
let queryWithAuth =
|
|
102
117
|
queryEntries->Array.some(entry => entry.authorization->Option.isSome)
|
|
103
118
|
if queryWithAuth {
|
|
104
119
|
let hasAuthQuery = parts.queries->Array.some(field =>
|
|
105
|
-
field->String.includes("@
|
|
120
|
+
field->String.includes("@aws_cognito_user_pools")
|
|
106
121
|
)
|
|
107
122
|
expect(hasAuthQuery)->toBe(true)
|
|
108
123
|
}
|
|
@@ -124,11 +139,11 @@ describe("AppSync_Adapter.generateFragment", () => {
|
|
|
124
139
|
})
|
|
125
140
|
})
|
|
126
141
|
|
|
127
|
-
// ── Stage E2: spec-level permission →
|
|
142
|
+
// ── Stage E2: spec-level permission → Cognito group directive ───────────────
|
|
128
143
|
//
|
|
129
144
|
// Verifies that `injectAwsAuth` lifts the `Authorization.permission` values
|
|
130
145
|
// (set by Plugin_Builder / Dcb_Builder from `@@reventless.authorize` /
|
|
131
|
-
// `@authorize` PPX annotations) into `@
|
|
146
|
+
// `@authorize` PPX annotations) into `@aws_cognito_user_pools(cognito_groups: [...])`
|
|
132
147
|
// directives on the corresponding SDL fields.
|
|
133
148
|
|
|
134
149
|
describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () => {
|
|
@@ -182,7 +197,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
182
197
|
)
|
|
183
198
|
let parts = decodeFragment(aug)
|
|
184
199
|
let m = parts.mutations->Array.getUnsafe(0)
|
|
185
|
-
expect(m)->toContain(`@
|
|
200
|
+
expect(m)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
186
201
|
})
|
|
187
202
|
|
|
188
203
|
testSync("AllowGroups multi-group emits comma-separated groups", () => {
|
|
@@ -199,11 +214,11 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
199
214
|
)
|
|
200
215
|
let parts = decodeFragment(aug)
|
|
201
216
|
expect(parts.mutations->Array.getUnsafe(0))->toContain(
|
|
202
|
-
`@
|
|
217
|
+
`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`,
|
|
203
218
|
)
|
|
204
219
|
})
|
|
205
220
|
|
|
206
|
-
testSync("AllowAuthenticated emits
|
|
221
|
+
testSync("AllowAuthenticated emits the group-less Cognito directive", () => {
|
|
207
222
|
let fp = Dict.fromArray([(
|
|
208
223
|
"p_Add",
|
|
209
224
|
Reventless.Authorization.AllowAuthenticated,
|
|
@@ -216,7 +231,11 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
216
231
|
~queryEntries=[],
|
|
217
232
|
)
|
|
218
233
|
let parts = decodeFragment(aug)
|
|
219
|
-
|
|
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")
|
|
220
239
|
})
|
|
221
240
|
|
|
222
241
|
testSync("DenyAll emits sentinel __deny_all__ group", () => {
|
|
@@ -258,7 +277,9 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
258
277
|
| None => JsError.throwWithMessage("missing archive field")
|
|
259
278
|
}
|
|
260
279
|
switch add {
|
|
261
|
-
| Some(f) =>
|
|
280
|
+
| Some(f) =>
|
|
281
|
+
expect(f)->toContain("@aws_cognito_user_pools")
|
|
282
|
+
expect(f)->not_->toContain("cognito_groups")
|
|
262
283
|
| None => JsError.throwWithMessage("missing add field")
|
|
263
284
|
}
|
|
264
285
|
})
|
|
@@ -323,7 +344,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
323
344
|
expect(m)->not_->toContain(`"Admin"`)
|
|
324
345
|
})
|
|
325
346
|
|
|
326
|
-
testSync("AllowAuthenticated on a field overrides the legacy authorization (
|
|
347
|
+
testSync("AllowAuthenticated on a field overrides the legacy authorization (drops the group)", () => {
|
|
327
348
|
let fp = Dict.fromArray([(
|
|
328
349
|
"p_X",
|
|
329
350
|
Reventless.Authorization.AllowAuthenticated,
|
|
@@ -344,7 +365,11 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
344
365
|
~queryEntries=[],
|
|
345
366
|
)
|
|
346
367
|
let parts = decodeFragment(aug)
|
|
347
|
-
|
|
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")
|
|
348
373
|
})
|
|
349
374
|
})
|
|
350
375
|
|
|
@@ -397,7 +422,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
|
397
422
|
expect(m)->not_->toContain("@aws_auth")
|
|
398
423
|
})
|
|
399
424
|
|
|
400
|
-
testSync("a non-systemCallable sibling
|
|
425
|
+
testSync("a non-systemCallable sibling stays Cognito-only (no @aws_iam)", () => {
|
|
401
426
|
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
402
427
|
fieldNames: ["p_Sync", "p_Other"],
|
|
403
428
|
commandSchema: S.unknown,
|
|
@@ -420,7 +445,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
|
420
445
|
let parts = decodeFragment(aug)
|
|
421
446
|
switch parts.mutations->Array.find(f => f->String.includes("p_Other")) {
|
|
422
447
|
| Some(other) =>
|
|
423
|
-
expect(other)->toContain(`@
|
|
448
|
+
expect(other)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
424
449
|
expect(other)->not_->toContain("@aws_iam")
|
|
425
450
|
| None => JsError.throwWithMessage("missing p_Other field")
|
|
426
451
|
}
|
|
@@ -588,13 +613,13 @@ describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
|
|
|
588
613
|
expect(field)->toContain("@aws_iam")
|
|
589
614
|
expect(field)->not_->toContain("@aws_auth")
|
|
590
615
|
} else {
|
|
591
|
-
expect(field)->toContain(`@
|
|
616
|
+
expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
592
617
|
expect(field)->not_->toContain("@aws_iam")
|
|
593
618
|
}
|
|
594
619
|
})
|
|
595
620
|
})
|
|
596
621
|
|
|
597
|
-
testSync("default (no ~iamFieldNames) leaves every field
|
|
622
|
+
testSync("default (no ~iamFieldNames) leaves every field Cognito-only", () => {
|
|
598
623
|
let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
|
|
599
624
|
let parts = decodeFragment(augmented)
|
|
600
625
|
parts.mutations->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
|
|
@@ -836,3 +861,56 @@ describe("AppSync_Adapter.waitForMergeSuccess", () => {
|
|
|
836
861
|
expect(message.contents)->toContain("MERGE_IN_PROGRESS")
|
|
837
862
|
})
|
|
838
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
|
+
})
|
|
@@ -29,18 +29,18 @@ let decodeFragment = GraphQL_Stitcher$ReventlessCore.decode;
|
|
|
29
29
|
|
|
30
30
|
globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
31
31
|
let baseFragment = Platform_AdminApi$ReventlessCore.baseFragment(true);
|
|
32
|
-
globalThis.test("adds
|
|
32
|
+
globalThis.test("adds the Cognito group directive to all mutation fields", () => {
|
|
33
33
|
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
34
34
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
35
35
|
parts.mutations.forEach(field => {
|
|
36
|
-
globalThis.expect(field).toContain(`@
|
|
36
|
+
globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
-
globalThis.test("adds
|
|
39
|
+
globalThis.test("adds the Cognito group directive to all query fields", () => {
|
|
40
40
|
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
41
41
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
42
42
|
parts.queries.forEach(field => {
|
|
43
|
-
globalThis.expect(field).toContain(`@
|
|
43
|
+
globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
46
|
globalThis.test("preserves type definitions unchanged", () => {
|
|
@@ -56,14 +56,14 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
56
56
|
globalThis.expect(field).toContain(`cognito_groups: ["SuperAdmin"]`);
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
|
-
globalThis.test("preserves subscription fields with
|
|
59
|
+
globalThis.test("preserves subscription fields with the Cognito group directive", () => {
|
|
60
60
|
let original = GraphQL_Stitcher$ReventlessCore.decode(baseFragment);
|
|
61
61
|
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
62
62
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
63
63
|
globalThis.expect(parts.subscriptions.length).toBe(original.subscriptions.length);
|
|
64
64
|
globalThis.expect(parts.subscriptions.length).toBeGreaterThan(0);
|
|
65
65
|
parts.subscriptions.forEach(field => {
|
|
66
|
-
globalThis.expect(field).toContain(`@
|
|
66
|
+
globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
69
|
globalThis.test("admin Plugin aggregate subscription fields survive the round-trip", () => {
|
|
@@ -73,6 +73,13 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll", () => {
|
|
|
73
73
|
globalThis.expect(joined).toContain("onPlatform_Plugin_Activate");
|
|
74
74
|
globalThis.expect(joined).toContain("onPlatform_Plugin_Deactivate");
|
|
75
75
|
});
|
|
76
|
+
globalThis.test("never emits the single-mode @aws_auth form on any field", () => {
|
|
77
|
+
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
78
|
+
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
79
|
+
let everyField = parts.mutations.concat(parts.queries, parts.subscriptions).join("\n");
|
|
80
|
+
globalThis.expect(everyField).not.toContain("@aws_auth");
|
|
81
|
+
globalThis.expect(everyField).toContain("@aws_cognito_user_pools");
|
|
82
|
+
});
|
|
76
83
|
});
|
|
77
84
|
|
|
78
85
|
globalThis.describe("AppSync_Adapter.injectAwsAuth", () => {
|
|
@@ -85,7 +92,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth", () => {
|
|
|
85
92
|
if (!queryWithAuth) {
|
|
86
93
|
return;
|
|
87
94
|
}
|
|
88
|
-
let hasAuthQuery = parts.queries.some(field => field.includes("@
|
|
95
|
+
let hasAuthQuery = parts.queries.some(field => field.includes("@aws_cognito_user_pools"));
|
|
89
96
|
globalThis.expect(hasAuthQuery).toBe(true);
|
|
90
97
|
});
|
|
91
98
|
});
|
|
@@ -135,7 +142,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
135
142
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
136
143
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
137
144
|
let m = parts.mutations[0];
|
|
138
|
-
globalThis.expect(m).toContain(`@
|
|
145
|
+
globalThis.expect(m).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
139
146
|
});
|
|
140
147
|
globalThis.test("AllowGroups multi-group emits comma-separated groups", () => {
|
|
141
148
|
let fp = Object.fromEntries([[
|
|
@@ -152,9 +159,9 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
152
159
|
let frag = makeFragment(["p_X(id: ID!): String"], []);
|
|
153
160
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
154
161
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
155
|
-
globalThis.expect(parts.mutations[0]).toContain(`@
|
|
162
|
+
globalThis.expect(parts.mutations[0]).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin", "Editor"])`);
|
|
156
163
|
});
|
|
157
|
-
globalThis.test("AllowAuthenticated emits
|
|
164
|
+
globalThis.test("AllowAuthenticated emits the group-less Cognito directive", () => {
|
|
158
165
|
let fp = Object.fromEntries([[
|
|
159
166
|
"p_Add",
|
|
160
167
|
"AllowAuthenticated"
|
|
@@ -163,7 +170,9 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
163
170
|
let frag = makeFragment(["p_Add(id: ID!): String"], []);
|
|
164
171
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
165
172
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
166
|
-
|
|
173
|
+
let m = parts.mutations[0];
|
|
174
|
+
globalThis.expect(m).toContain("@aws_cognito_user_pools");
|
|
175
|
+
globalThis.expect(m).not.toContain("cognito_groups");
|
|
167
176
|
});
|
|
168
177
|
globalThis.test("DenyAll emits sentinel __deny_all__ group", () => {
|
|
169
178
|
let fp = Object.fromEntries([[
|
|
@@ -208,7 +217,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
208
217
|
Stdlib_JsError.throwWithMessage("missing archive field");
|
|
209
218
|
}
|
|
210
219
|
if (add !== undefined) {
|
|
211
|
-
globalThis.expect(add).
|
|
220
|
+
globalThis.expect(add).toContain("@aws_cognito_user_pools");
|
|
221
|
+
globalThis.expect(add).not.toContain("cognito_groups");
|
|
212
222
|
return;
|
|
213
223
|
} else {
|
|
214
224
|
return Stdlib_JsError.throwWithMessage("missing add field");
|
|
@@ -262,7 +272,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
262
272
|
globalThis.expect(m).toContain(`cognito_groups: ["Manager"]`);
|
|
263
273
|
globalThis.expect(m).not.toContain(`"Admin"`);
|
|
264
274
|
});
|
|
265
|
-
globalThis.test("AllowAuthenticated on a field overrides the legacy authorization (
|
|
275
|
+
globalThis.test("AllowAuthenticated on a field overrides the legacy authorization (drops the group)", () => {
|
|
266
276
|
let fp = Object.fromEntries([[
|
|
267
277
|
"p_X",
|
|
268
278
|
"AllowAuthenticated"
|
|
@@ -282,7 +292,9 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
282
292
|
let frag = makeFragment(["p_X(id: ID!): String"], []);
|
|
283
293
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
284
294
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
285
|
-
|
|
295
|
+
let m = parts.mutations[0];
|
|
296
|
+
globalThis.expect(m).toContain("@aws_cognito_user_pools");
|
|
297
|
+
globalThis.expect(m).not.toContain("cognito_groups");
|
|
286
298
|
});
|
|
287
299
|
});
|
|
288
300
|
|
|
@@ -337,7 +349,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
|
|
|
337
349
|
globalThis.expect(m).not.toContain("cognito_groups");
|
|
338
350
|
globalThis.expect(m).not.toContain("@aws_auth");
|
|
339
351
|
});
|
|
340
|
-
globalThis.test("a non-systemCallable sibling
|
|
352
|
+
globalThis.test("a non-systemCallable sibling stays Cognito-only (no @aws_iam)", () => {
|
|
341
353
|
let entry_fieldNames = [
|
|
342
354
|
"p_Sync",
|
|
343
355
|
"p_Other"
|
|
@@ -391,7 +403,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
|
|
|
391
403
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(aug);
|
|
392
404
|
let other = parts.mutations.find(f => f.includes("p_Other"));
|
|
393
405
|
if (other !== undefined) {
|
|
394
|
-
globalThis.expect(other).toContain(`@
|
|
406
|
+
globalThis.expect(other).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
395
407
|
globalThis.expect(other).not.toContain("@aws_iam");
|
|
396
408
|
return;
|
|
397
409
|
} else {
|
|
@@ -548,12 +560,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () =>
|
|
|
548
560
|
globalThis.expect(field).toContain("@aws_iam");
|
|
549
561
|
globalThis.expect(field).not.toContain("@aws_auth");
|
|
550
562
|
} else {
|
|
551
|
-
globalThis.expect(field).toContain(`@
|
|
563
|
+
globalThis.expect(field).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
552
564
|
globalThis.expect(field).not.toContain("@aws_iam");
|
|
553
565
|
}
|
|
554
566
|
});
|
|
555
567
|
});
|
|
556
|
-
globalThis.test("default (no ~iamFieldNames) leaves every field
|
|
568
|
+
globalThis.test("default (no ~iamFieldNames) leaves every field Cognito-only", () => {
|
|
557
569
|
let augmented = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
558
570
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(augmented);
|
|
559
571
|
parts.mutations.forEach(field => {
|
|
@@ -739,6 +751,51 @@ globalThis.describe("AppSync_Adapter.waitForMergeSuccess", () => {
|
|
|
739
751
|
});
|
|
740
752
|
});
|
|
741
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
|
+
|
|
742
799
|
export {
|
|
743
800
|
decodeFragment,
|
|
744
801
|
}
|
|
@@ -31,7 +31,7 @@ type Mutation {
|
|
|
31
31
|
type Subscription {
|
|
32
32
|
onCatalog_AddProduct(id: ID): CommandResult
|
|
33
33
|
onUIFragmentChange: UIFragmentChangeEvent
|
|
34
|
-
@
|
|
34
|
+
@aws_cognito_user_pools(cognito_groups: ["Admin"])
|
|
35
35
|
onCatalogEventLog_eventAppended: CatalogEventLogEvent
|
|
36
36
|
}`
|
|
37
37
|
|
|
@@ -43,13 +43,13 @@ describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
|
|
|
43
43
|
)
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
testSync("appends the many-mutations fan-in on a no-arg field, keeping
|
|
46
|
+
testSync("appends the many-mutations fan-in on a no-arg field, keeping the group directive", () => {
|
|
47
47
|
let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
|
|
48
48
|
expect(sdl)->toContain(
|
|
49
49
|
`onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`,
|
|
50
50
|
)
|
|
51
51
|
// The auth directive line appended by injectAwsAuthAll survives.
|
|
52
|
-
expect(sdl)->toContain(`@
|
|
52
|
+
expect(sdl)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
testSync("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
|
|
@@ -113,7 +113,7 @@ describe("AppSync_SdlDecorate.injectAwsAuthAll", () => {
|
|
|
113
113
|
expect(parts.mutations->Array.getUnsafe(0))->toContain("@aws_iam")
|
|
114
114
|
// A query NOT in iamFieldNames stays Cognito-only.
|
|
115
115
|
let other = queryField("Other_Query")
|
|
116
|
-
expect(other)->toContain(`@
|
|
116
|
+
expect(other)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
117
117
|
expect(other->String.includes("@aws_iam"))->toBe(false)
|
|
118
118
|
})
|
|
119
119
|
})
|
|
@@ -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
|
+
})
|
|
@@ -35,7 +35,7 @@ type Mutation {
|
|
|
35
35
|
type Subscription {
|
|
36
36
|
onCatalog_AddProduct(id: ID): CommandResult
|
|
37
37
|
onUIFragmentChange: UIFragmentChangeEvent
|
|
38
|
-
@
|
|
38
|
+
@aws_cognito_user_pools(cognito_groups: ["Admin"])
|
|
39
39
|
onCatalogEventLog_eventAppended: CatalogEventLogEvent
|
|
40
40
|
}`;
|
|
41
41
|
|
|
@@ -44,10 +44,10 @@ globalThis.describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
|
|
|
44
44
|
let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
|
|
45
45
|
globalThis.expect(sdl).toContain(`onCatalog_AddProduct(id: ID): CommandResult\n @aws_subscribe(mutations: ["Catalog_AddProduct"])`);
|
|
46
46
|
});
|
|
47
|
-
globalThis.test("appends the many-mutations fan-in on a no-arg field, keeping
|
|
47
|
+
globalThis.test("appends the many-mutations fan-in on a no-arg field, keeping the group directive", () => {
|
|
48
48
|
let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
|
|
49
49
|
globalThis.expect(sdl).toContain(`onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`);
|
|
50
|
-
globalThis.expect(sdl).toContain(`@
|
|
50
|
+
globalThis.expect(sdl).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
51
51
|
});
|
|
52
52
|
globalThis.test("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
|
|
53
53
|
let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
|
|
@@ -100,7 +100,7 @@ globalThis.describe("AppSync_SdlDecorate.injectAwsAuthAll", () => {
|
|
|
100
100
|
globalThis.expect(queryField("Platform_ApiFragments")).toContain("@aws_iam");
|
|
101
101
|
globalThis.expect(parts.mutations[0]).toContain("@aws_iam");
|
|
102
102
|
let other = queryField("Other_Query");
|
|
103
|
-
globalThis.expect(other).toContain(`@
|
|
103
|
+
globalThis.expect(other).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`);
|
|
104
104
|
globalThis.expect(other.includes("@aws_iam")).toBe(false);
|
|
105
105
|
});
|
|
106
106
|
});
|
|
@@ -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,
|