Skip to content

Commit 3896444

Browse files
committed
feat(cli-internal): escape hatch for unmapped cognito auth actions
resolveAuthAccess() silently dropped cognito-idp actions that didn't match GROUPED_AUTH_PERMISSIONS or AUTH_ACTION_MAPPING. Functions lost permissions they had in Gen1 (e.g., CreateGroup, DeleteGroup, UpdateGroup, Describe*). Add a third resolution tier: unmapped actions now generate addToRolePolicy(new aws_iam.PolicyStatement({...})) escape hatches in backend.ts, following the existing Kinesis grant pattern. Also removes AUTH_TRIGGER_ACTION_MAPPING and resolveAuthTriggerAccess() introduced in the previous commit. The auth trigger path (extractAuthTriggerCfnPermissions) now feeds into the same resolveAuthAccess, and trigger-specific actions like GetGroup/CreateGroup naturally fall through to the escape hatch since they don't match any group or individual mapping. Changes: - resolveAuthAccess returns { permissions, unmapped } - ResolvedFunction gains unmappedAuthActions field - New contributeAuthEscapeHatch() generates addToRolePolicy - Remove AUTH_TRIGGER_ACTION_MAPPING + resolveAuthTriggerAccess - Updated snapshots: fitness-tracker (Describe*), media-vault (CreateGroup/DeleteGroup/UpdateGroup), store-locator (GetGroup/CreateGroup) All 12 generate snapshot tests and 135 test suites pass. --- Prompt: implement escape hatch tier for unmapped cognito auth actions in resolveAuthAccess, remove AUTH_TRIGGER_ACTION_MAPPING, update snapshots
1 parent ef60c18 commit 3896444

4 files changed

Lines changed: 190 additions & 35 deletions

File tree

  • amplify-migration-apps
    • fitness-tracker/_snapshot.post.generate/amplify
    • media-vault/_snapshot.post.generate/amplify
    • store-locator/_snapshot.post.generate/amplify
  • packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function

amplify-migration-apps/fitness-tracker/_snapshot.post.generate/amplify/backend.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from 'aws-cdk-lib/aws-apigateway';
1313
import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
1414
import { defineBackend } from '@aws-amplify/backend';
15-
import { Duration, Stack } from 'aws-cdk-lib';
15+
import { Duration, aws_iam, Stack } from 'aws-cdk-lib';
1616

1717
const backend = defineBackend({
1818
auth,
@@ -83,6 +83,12 @@ backend.admin.addEnvironment(
8383
'AUTH_FITNESSTRACKER33F5545533F55455_USERPOOLID',
8484
backend.auth.resources.userPool.userPoolId
8585
);
86+
backend.admin.resources.lambda.addToRolePolicy(
87+
new aws_iam.PolicyStatement({
88+
actions: ['cognito-idp:Describe*'],
89+
resources: [backend.auth.resources.userPool.userPoolArn],
90+
})
91+
);
8692
const cfnGraphqlApi = backend.data.resources.cfnResources.cfnGraphqlApi;
8793
cfnGraphqlApi.additionalAuthenticationProviders = [
8894
{

amplify-migration-apps/media-vault/_snapshot.post.generate/amplify/backend.ts

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -109,27 +109,109 @@ backend.addusertogroup.addEnvironment(
109109
'AUTH_MEDIAVAULT1F08412D_USERPOOLID',
110110
backend.auth.resources.userPool.userPoolId
111111
);
112-
backend.removeuserfromgroup.resources.cfnResources.cfnFunction.functionName = `removeuserfromgroup-${branchName}`;
113-
backend.removeuserfromgroup.addEnvironment(
114-
'AUTH_MEDIAVAULT1F08412D_USERPOOLID',
115-
backend.auth.resources.userPool.userPoolId
116-
);
117112
backend.addusertogroup.resources.lambda.addToRolePolicy(
118113
new aws_iam.PolicyStatement({
119114
actions: [
120-
"cognito-idp:CreateGroup",
121-
"cognito-idp:DeleteGroup",
122-
"cognito-idp:UpdateGroup",
115+
'cognito-idp:ConfirmSignUp',
116+
'cognito-idp:CreateUserImportJob',
117+
'cognito-idp:AdminLinkProviderForUser',
118+
'cognito-idp:CreateIdentityProvider',
119+
'cognito-idp:SetUICustomization',
120+
'cognito-idp:SignUp',
121+
'cognito-idp:SetRiskConfiguration',
122+
'cognito-idp:StartUserImportJob',
123+
'cognito-idp:AssociateSoftwareToken',
124+
'cognito-idp:CreateResourceServer',
125+
'cognito-idp:RespondToAuthChallenge',
126+
'cognito-idp:CreateUserPoolClient',
127+
'cognito-idp:GlobalSignOut',
128+
'cognito-idp:AddCustomAttributes',
129+
'cognito-idp:CreateGroup',
130+
'cognito-idp:CreateUserPool',
131+
'cognito-idp:CreateUserPoolDomain',
132+
'cognito-idp:StopUserImportJob',
133+
'cognito-idp:InitiateAuth',
134+
'cognito-idp:ConfirmForgotPassword',
135+
'cognito-idp:VerifySoftwareToken',
136+
'cognito-idp:AdminDisableProviderForUser',
137+
'cognito-idp:SetUserPoolMfaConfig',
138+
'cognito-idp:ChangePassword',
139+
'cognito-idp:ConfirmDevice',
140+
'cognito-idp:ResendConfirmationCode',
141+
'cognito-idp:Describe*',
142+
'cognito-idp:ForgotPassword',
143+
'cognito-idp:UpdateAuthEventFeedback',
144+
'cognito-idp:UpdateResourceServer',
145+
'cognito-idp:UpdateUserPoolClient',
146+
'cognito-idp:UpdateUserPoolDomain',
147+
'cognito-idp:UpdateIdentityProvider',
148+
'cognito-idp:UpdateGroup',
149+
'cognito-idp:UpdateDeviceStatus',
150+
'cognito-idp:UpdateUserPool',
151+
'cognito-idp:DeleteUserPoolDomain',
152+
'cognito-idp:DeleteResourceServer',
153+
'cognito-idp:DeleteGroup',
154+
'cognito-idp:DeleteUserPoolClient',
155+
'cognito-idp:DeleteUserAttributes',
156+
'cognito-idp:DeleteUserPool',
157+
'cognito-idp:DeleteIdentityProvider',
158+
'cognito-idp:DeleteUser',
123159
],
124160
resources: [backend.auth.resources.userPool.userPoolArn],
125161
})
126162
);
163+
backend.removeuserfromgroup.resources.cfnResources.cfnFunction.functionName = `removeuserfromgroup-${branchName}`;
164+
backend.removeuserfromgroup.addEnvironment(
165+
'AUTH_MEDIAVAULT1F08412D_USERPOOLID',
166+
backend.auth.resources.userPool.userPoolId
167+
);
127168
backend.removeuserfromgroup.resources.lambda.addToRolePolicy(
128169
new aws_iam.PolicyStatement({
129170
actions: [
130-
"cognito-idp:CreateGroup",
131-
"cognito-idp:DeleteGroup",
132-
"cognito-idp:UpdateGroup",
171+
'cognito-idp:ConfirmSignUp',
172+
'cognito-idp:CreateUserImportJob',
173+
'cognito-idp:AdminLinkProviderForUser',
174+
'cognito-idp:CreateIdentityProvider',
175+
'cognito-idp:SetUICustomization',
176+
'cognito-idp:SignUp',
177+
'cognito-idp:SetRiskConfiguration',
178+
'cognito-idp:StartUserImportJob',
179+
'cognito-idp:AssociateSoftwareToken',
180+
'cognito-idp:CreateResourceServer',
181+
'cognito-idp:RespondToAuthChallenge',
182+
'cognito-idp:CreateUserPoolClient',
183+
'cognito-idp:GlobalSignOut',
184+
'cognito-idp:AddCustomAttributes',
185+
'cognito-idp:CreateGroup',
186+
'cognito-idp:CreateUserPool',
187+
'cognito-idp:CreateUserPoolDomain',
188+
'cognito-idp:StopUserImportJob',
189+
'cognito-idp:InitiateAuth',
190+
'cognito-idp:ConfirmForgotPassword',
191+
'cognito-idp:VerifySoftwareToken',
192+
'cognito-idp:AdminDisableProviderForUser',
193+
'cognito-idp:SetUserPoolMfaConfig',
194+
'cognito-idp:ChangePassword',
195+
'cognito-idp:ConfirmDevice',
196+
'cognito-idp:ResendConfirmationCode',
197+
'cognito-idp:Describe*',
198+
'cognito-idp:ForgotPassword',
199+
'cognito-idp:UpdateAuthEventFeedback',
200+
'cognito-idp:UpdateResourceServer',
201+
'cognito-idp:UpdateUserPoolClient',
202+
'cognito-idp:UpdateUserPoolDomain',
203+
'cognito-idp:UpdateIdentityProvider',
204+
'cognito-idp:UpdateGroup',
205+
'cognito-idp:UpdateDeviceStatus',
206+
'cognito-idp:UpdateUserPool',
207+
'cognito-idp:DeleteUserPoolDomain',
208+
'cognito-idp:DeleteResourceServer',
209+
'cognito-idp:DeleteGroup',
210+
'cognito-idp:DeleteUserPoolClient',
211+
'cognito-idp:DeleteUserAttributes',
212+
'cognito-idp:DeleteUserPool',
213+
'cognito-idp:DeleteIdentityProvider',
214+
'cognito-idp:DeleteUser',
133215
],
134216
resources: [backend.auth.resources.userPool.userPoolArn],
135217
})

amplify-migration-apps/store-locator/_snapshot.post.generate/amplify/backend.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { PolicyStatement } from 'aws-cdk-lib/aws-iam';
21
import { auth } from './auth/resource';
32
import { storelocator41a9495f41a9495fPostConfirmation } from './auth/storelocator41a9495f41a9495fPostConfirmation/resource';
43
import { defineGeo } from './geo/resource';
@@ -33,13 +32,9 @@ userPool.addClient('NativeAppClient', {
3332
});
3433
const branchName = process.env.AWS_BRANCH ?? 'sandbox';
3534
backend.storelocator41a9495f41a9495fPostConfirmation.resources.cfnResources.cfnFunction.functionName = `storelocator41a9495f41a9495fPostConfirmation-${branchName}`;
36-
3735
backend.storelocator41a9495f41a9495fPostConfirmation.resources.lambda.addToRolePolicy(
38-
new aws_iam.PolicyStatement({
39-
actions: [
40-
"cognito-idp:GetGroup",
41-
"cognito-idp:CreateGroup"
42-
],
36+
new aws_iam.PolicyStatement({
37+
actions: ['cognito-idp:GetGroup', 'cognito-idp:CreateGroup'],
4338
resources: [backend.auth.resources.userPool.userPoolArn],
4439
})
4540
);

packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.generator.ts

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ interface ResolvedFunction {
4444
readonly kinesisActions: readonly string[];
4545
readonly graphqlApiPermissions: { readonly hasMutation: boolean; readonly hasQuery: boolean };
4646
readonly authAccess: AuthPermissions;
47+
readonly unmappedAuthActions: readonly string[];
4748
}
4849

4950
/**
@@ -131,6 +132,7 @@ export class FunctionGenerator implements Planner {
131132
await this.generateResource(func);
132133
this.contributeOverrides(func);
133134
this.contributeGrants(func);
135+
this.contributeAuthEscapeHatch(func);
134136
if (triggerModels.length > 0) {
135137
this.contributeDynamoTrigger(func.resourceName, triggerModels);
136138
}
@@ -173,11 +175,18 @@ export class FunctionGenerator implements Planner {
173175
const { retained, escapeHatches } = classifyEnvVars(config.Environment?.Variables ?? {});
174176

175177
// Extract DynamoDB/Kinesis actions and GraphQL API permissions from the function's CloudFormation template
176-
const { dynamoActions, kinesisActions, graphqlApiPermissions, authAccess: cfnAuthAccess } = this.extractCfnPermissions();
178+
const {
179+
dynamoActions,
180+
kinesisActions,
181+
graphqlApiPermissions,
182+
authAccess: cfnAuthAccess,
183+
unmappedAuthActions: cfnUnmapped,
184+
} = this.extractCfnPermissions();
177185

178186
// For auth trigger functions, also extract permissions from the auth-trigger CFN template.
179-
const triggerAuthAccess = this.extractAuthTriggerCfnPermissions();
187+
const { permissions: triggerAuthAccess, unmapped: triggerUnmapped } = this.extractAuthTriggerCfnPermissions();
180188
const authAccess = { ...cfnAuthAccess, ...triggerAuthAccess };
189+
const unmappedAuthActions = [...new Set([...cfnUnmapped, ...triggerUnmapped])];
181190

182191
return {
183192
resourceName: this.resource.resourceName,
@@ -194,6 +203,7 @@ export class FunctionGenerator implements Planner {
194203
kinesisActions,
195204
graphqlApiPermissions,
196205
authAccess,
206+
unmappedAuthActions,
197207
};
198208
}
199209

@@ -257,6 +267,62 @@ export class FunctionGenerator implements Planner {
257267
}
258268
}
259269

270+
/** Emits `addToRolePolicy` in backend.ts for cognito-idp actions that don't map to any Gen2 auth permission. */
271+
private contributeAuthEscapeHatch(func: ResolvedFunction): void {
272+
if (func.unmappedAuthActions.length === 0) return;
273+
274+
this.backendGenerator.addImport('aws-cdk-lib', ['aws_iam']);
275+
276+
const lambdaRef = factory.createPropertyAccessExpression(
277+
factory.createPropertyAccessExpression(
278+
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier(func.resourceName)),
279+
factory.createIdentifier('resources'),
280+
),
281+
factory.createIdentifier('lambda'),
282+
);
283+
284+
const policyStatement = factory.createNewExpression(
285+
factory.createPropertyAccessExpression(factory.createIdentifier('aws_iam'), factory.createIdentifier('PolicyStatement')),
286+
undefined,
287+
[
288+
factory.createObjectLiteralExpression(
289+
[
290+
factory.createPropertyAssignment(
291+
'actions',
292+
factory.createArrayLiteralExpression(func.unmappedAuthActions.map((a) => factory.createStringLiteral(a))),
293+
),
294+
factory.createPropertyAssignment(
295+
'resources',
296+
factory.createArrayLiteralExpression([
297+
factory.createPropertyAccessExpression(
298+
factory.createPropertyAccessExpression(
299+
factory.createPropertyAccessExpression(
300+
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('auth')),
301+
factory.createIdentifier('resources'),
302+
),
303+
factory.createIdentifier('userPool'),
304+
),
305+
factory.createIdentifier('userPoolArn'),
306+
),
307+
]),
308+
),
309+
],
310+
true,
311+
),
312+
],
313+
);
314+
315+
this.backendGenerator.addStatement(
316+
factory.createExpressionStatement(
317+
factory.createCallExpression(
318+
factory.createPropertyAccessExpression(lambdaRef, factory.createIdentifier('addToRolePolicy')),
319+
undefined,
320+
[policyStatement],
321+
),
322+
),
323+
);
324+
}
325+
260326
private contributeAuthTrigger(): void {
261327
if (!this.authGenerator || this.category !== 'auth') return;
262328
const authResourceName = this.gen1App.singleResourceName('auth', 'Cognito');
@@ -544,13 +610,20 @@ export class FunctionGenerator implements Planner {
544610
kinesisActions: string[];
545611
graphqlApiPermissions: { hasMutation: boolean; hasQuery: boolean };
546612
authAccess: AuthPermissions;
613+
unmappedAuthActions: string[];
547614
} {
548615
const templatePath = `function/${this.resource.resourceName}/${this.resource.resourceName}-cloudformation-template.json`;
549616
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped CloudFormation template
550617
const template = this.gen1App.json(templatePath);
551618
const policy = template.Resources?.AmplifyResourcesPolicy;
552619
if (!policy || policy.Type !== 'AWS::IAM::Policy') {
553-
return { dynamoActions: [], kinesisActions: [], graphqlApiPermissions: { hasMutation: false, hasQuery: false }, authAccess: {} };
620+
return {
621+
dynamoActions: [],
622+
kinesisActions: [],
623+
graphqlApiPermissions: { hasMutation: false, hasQuery: false },
624+
authAccess: {},
625+
unmappedAuthActions: [],
626+
};
554627
}
555628

556629
const statements = policy.Properties?.PolicyDocument?.Statement ?? [];
@@ -589,8 +662,8 @@ export class FunctionGenerator implements Planner {
589662
}
590663
}
591664

592-
const authAccess = resolveAuthAccess(cognitoActions);
593-
return { dynamoActions, kinesisActions, graphqlApiPermissions: { hasMutation, hasQuery }, authAccess };
665+
const { permissions: authAccess, unmapped: unmappedAuthActions } = resolveAuthAccess(cognitoActions);
666+
return { dynamoActions, kinesisActions, graphqlApiPermissions: { hasMutation, hasQuery }, authAccess, unmappedAuthActions };
594667
}
595668

596669
/**
@@ -601,12 +674,12 @@ export class FunctionGenerator implements Planner {
601674
* This method reads that template and extracts cognito-idp actions from IAM policies
602675
* that reference this function.
603676
*/
604-
private extractAuthTriggerCfnPermissions(): AuthPermissions {
605-
if (this.category !== 'auth') return {};
677+
private extractAuthTriggerCfnPermissions(): { permissions: AuthPermissions; unmapped: string[] } {
678+
if (this.category !== 'auth') return { permissions: {}, unmapped: [] };
606679

607680
const authResourceName = this.gen1App.singleResourceName('auth', 'Cognito');
608681
const templatePath = `auth/${authResourceName}/build/auth-trigger-cloudformation-template.json`;
609-
if (!this.gen1App.fileExists(templatePath)) return {};
682+
if (!this.gen1App.fileExists(templatePath)) return { permissions: {}, unmapped: [] };
610683

611684
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped CloudFormation template
612685
const template = this.gen1App.json(templatePath);
@@ -1077,14 +1150,10 @@ const AUTH_ACTION_MAPPING: Readonly<Record<string, keyof AuthPermissions>> = {
10771150
'cognito-idp:UpdateUserAttributes': 'updateUserAttributes',
10781151
'cognito-idp:SetUserMFAPreference': 'setUserMfaPreference',
10791152
'cognito-idp:SetUserSettings': 'setUserSettings',
1080-
'cognito-idp:GetGroup': 'manageGroups',
1081-
'cognito-idp:CreateGroup': 'manageGroups',
1082-
'cognito-idp:DeleteGroup': 'manageGroups',
1083-
'cognito-idp:UpdateGroup': 'manageGroups',
10841153
};
10851154

1086-
function resolveAuthAccess(cognitoActions: string[]): AuthPermissions {
1087-
if (cognitoActions.length === 0) return {};
1155+
function resolveAuthAccess(cognitoActions: string[]): { permissions: AuthPermissions; unmapped: string[] } {
1156+
if (cognitoActions.length === 0) return { permissions: {}, unmapped: [] };
10881157
const result: Record<string, boolean> = {};
10891158
const covered = new Set<string>();
10901159

@@ -1096,12 +1165,15 @@ function resolveAuthAccess(cognitoActions: string[]): AuthPermissions {
10961165
}
10971166

10981167
for (const action of cognitoActions) {
1099-
if (!covered.has(action) && AUTH_ACTION_MAPPING[action]) {
1168+
if (covered.has(action)) continue;
1169+
if (AUTH_ACTION_MAPPING[action]) {
11001170
result[AUTH_ACTION_MAPPING[action]] = true;
1171+
covered.add(action);
11011172
}
11021173
}
11031174

1104-
return result as AuthPermissions;
1175+
const unmapped = cognitoActions.filter((a) => !covered.has(a));
1176+
return { permissions: result as AuthPermissions, unmapped };
11051177
}
11061178

11071179
// ── Auth trigger suffix mapping ───────────────────────────────────

0 commit comments

Comments
 (0)