Skip to content

Commit 105f99c

Browse files
authored
feat(gen2-migration): generate IAM auth grant for Gen1 AppSync API (#14778)
feat(cli-internal): generate IAM auth grant for Gen1 AppSync API When a Gen1 AppSync API uses AWS_IAM authentication, the generate step now automatically adds an IAM policy granting the Gen2 authenticated user role appsync:GraphQL access to the Gen1 API. This is required post-refactor because the identity pool moves to the Gen2 stack with a new auth role that has no access to the Gen1 API. The DataGenerator detects AWS_IAM as default or additional auth mode and contributes the policy statement to backend.ts via BackendGenerator. The Gen1 API ID is hardcoded as a string literal since it references an external resource. Removes the manual addGen1AppSyncPolicy() post-generate step from the product-catalog app. All 12 generate snapshot tests pass. --- Prompt: Implement codegen for IAM auth grant when Gen1 AppSync API uses AWS_IAM authentication mode. The generate step should detect AWS_IAM and add the policy statement granting the Gen2 auth role access to the Gen1 API.
1 parent 2afcd18 commit 105f99c

3 files changed

Lines changed: 106 additions & 54 deletions

File tree

amplify-migration-apps/product-catalog/_snapshot.post.generate/amplify/backend.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { storage } from './storage/resource';
44
import { S3Trigger1ef46783 } from './storage/S3Trigger1ef46783/resource';
55
import { lowstockproducts } from './function/lowstockproducts/resource';
66
import { defineBackend } from '@aws-amplify/backend';
7-
import { Duration } from 'aws-cdk-lib';
7+
import { Duration, aws_iam } from 'aws-cdk-lib';
88

99
const backend = defineBackend({
1010
auth,
@@ -49,6 +49,15 @@ cfnGraphqlApi.additionalAuthenticationProviders = [
4949
},
5050
},
5151
];
52+
backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(
53+
new aws_iam.PolicyStatement({
54+
effect: aws_iam.Effect.ALLOW,
55+
actions: ['appsync:GraphQL'],
56+
resources: [
57+
`arn:aws:appsync:${backend.data.stack.region}:${backend.data.stack.account}:apis/3oy6oxkj6ffojmc2upd52ftdsq/*`,
58+
],
59+
})
60+
);
5261
const branchName = process.env.AWS_BRANCH ?? 'sandbox';
5362
backend.S3Trigger1ef46783.resources.cfnResources.cfnFunction.functionName = `S3Trigger1ef46783-${branchName}`;
5463
backend.S3Trigger1ef46783.addEnvironment(

amplify-migration-apps/product-catalog/migration/post-generate.ts

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@
1010
* 4. Update lowstockproducts/resource.ts to use secret() instead of hardcoded SSM path
1111
* 5. Convert S3Trigger function from CommonJS to ESM
1212
* 6. Update frontend import from amplifyconfiguration.json to amplify_outputs.json
13-
* 7. Add IAM policy to backend.ts for authenticated user to access Gen1 AppSync API
14-
* 8. Resolve the Gen1 AppSync API ID and replace the placeholder in backend.ts
1513
*/
1614

1715
import { execSync } from 'child_process';
1816
import fs from 'fs/promises';
1917
import fsSync from 'fs';
2018
import path from 'path';
21-
import { AppSyncClient, paginateListGraphqlApis } from '@aws-sdk/client-appsync';
2219

2320
function resolveTargetBranch(): string {
2421
if (process.env.AWS_BRANCH) {
@@ -120,62 +117,12 @@ async function updateFrontendConfig(appPath: string): Promise<void> {
120117
await fs.writeFile(mainPath, updated, 'utf-8');
121118
}
122119

123-
/**
124-
* Look up the Gen1 AppSync API ID by querying all APIs and finding
125-
* the one tagged with "user:Application" matching the app name.
126-
*/
127-
async function resolveGen1AppSyncApiId(appName: string): Promise<string> {
128-
const client = new AppSyncClient({});
129-
130-
for await (const page of paginateListGraphqlApis({ client }, {})) {
131-
for (const api of page.graphqlApis ?? []) {
132-
if (api.tags?.['user:Application'] === appName) {
133-
return api.apiId!;
134-
}
135-
}
136-
}
137-
138-
throw new Error(`No AppSync API found with tag user:Application=${appName}`);
139-
}
140-
141-
async function addGen1AppSyncPolicy(appPath: string, appName: string): Promise<void> {
142-
const apiId = await resolveGen1AppSyncApiId(appName);
143-
144-
const backendPath = path.join(appPath, 'amplify', 'backend.ts');
145-
const content = await fs.readFile(backendPath, 'utf-8');
146-
147-
// Add aws_iam to the Duration import from aws-cdk-lib
148-
let updated = content.replace(
149-
/import\s*\{([^}]*)\bDuration\b([^}]*)\}\s*from\s*["']aws-cdk-lib["']/,
150-
(match, before, after) => {
151-
if (match.includes('aws_iam')) return match;
152-
return `import {${before}Duration${after}, aws_iam } from "aws-cdk-lib"`;
153-
},
154-
);
155-
156-
const policyBlock = `
157-
backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(new aws_iam.PolicyStatement({
158-
effect: aws_iam.Effect.ALLOW,
159-
actions: ['appsync:GraphQL'],
160-
resources: [\`arn:aws:appsync:\${backend.data.stack.region}:\${backend.data.stack.account}:apis/${apiId}/*\`]
161-
}))
162-
`;
163-
164-
updated = updated.trimEnd() + '\n' + policyBlock;
165-
166-
await fs.writeFile(backendPath, updated, 'utf-8');
167-
}
168-
169120
export async function postGenerate(appPath: string): Promise<void> {
170-
const packageJson = JSON.parse(await fs.readFile(path.join(appPath, 'package.json'), 'utf-8'));
171-
const appName = packageJson.name as string;
172-
173121
await updateBranchName(appPath);
174122
await convertLowstockproductsToESM(appPath);
175123
await updateLowstockproductsResource(appPath);
176124
await convertS3TriggerToESM(appPath);
177125
await updateFrontendConfig(appPath);
178-
await addGen1AppSyncPolicy(appPath, appName);
179126
}
180127

181128
async function main(): Promise<void> {

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ export class DataGenerator implements Planner {
8686
if (additionalAuthProviders && additionalAuthProviders.length > 0 && hasAuth) {
8787
this.contributeAdditionalAuthProviders(additionalAuthProviders);
8888
}
89+
90+
// Grant the Gen2 authenticated user IAM role access to the Gen1 AppSync API
91+
if (hasAuth) {
92+
this.contributeIamAuthGrant(apiId, authorizationModes, additionalAuthProviders);
93+
}
8994
},
9095
},
9196
];
@@ -178,6 +183,97 @@ export class DataGenerator implements Planner {
178183
);
179184
this.backendGenerator.addStatement(assignment);
180185
}
186+
187+
/**
188+
* Grants the Gen2 authenticated user IAM role access to the Gen1 AppSync API.
189+
*
190+
* Post-refactor, the identity pool moves to the Gen2 stack with a new AuthRole.
191+
* If the Gen1 API uses AWS_IAM auth, the new role needs an explicit policy to
192+
* call appsync:GraphQL on the Gen1 API during the transition period.
193+
*/
194+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped authConfig from amplify-meta.json
195+
private contributeIamAuthGrant(apiId: string, authorizationModes: any, additionalAuthProviders?: Array<Record<string, unknown>>): void {
196+
const defaultAuthType = authorizationModes?.defaultAuthentication?.authenticationType;
197+
const hasIamDefault = defaultAuthType === 'AWS_IAM';
198+
const hasIamAdditional = additionalAuthProviders?.some((p) => p.authenticationType === 'AWS_IAM') ?? false;
199+
200+
if (!hasIamDefault && !hasIamAdditional) return;
201+
202+
this.backendGenerator.addImport('aws-cdk-lib', ['aws_iam']);
203+
204+
// backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(
205+
// new aws_iam.PolicyStatement({ effect: aws_iam.Effect.ALLOW, actions: ['appsync:GraphQL'],
206+
// resources: [`arn:aws:appsync:${backend.data.stack.region}:${backend.data.stack.account}:apis/<apiId>/*`] })
207+
// )
208+
const policyStatement = factory.createNewExpression(
209+
factory.createPropertyAccessExpression(factory.createIdentifier('aws_iam'), factory.createIdentifier('PolicyStatement')),
210+
undefined,
211+
[
212+
factory.createObjectLiteralExpression(
213+
[
214+
factory.createPropertyAssignment(
215+
'effect',
216+
factory.createPropertyAccessExpression(
217+
factory.createPropertyAccessExpression(factory.createIdentifier('aws_iam'), factory.createIdentifier('Effect')),
218+
factory.createIdentifier('ALLOW'),
219+
),
220+
),
221+
factory.createPropertyAssignment(
222+
'actions',
223+
factory.createArrayLiteralExpression([factory.createStringLiteral('appsync:GraphQL')]),
224+
),
225+
factory.createPropertyAssignment(
226+
'resources',
227+
factory.createArrayLiteralExpression([
228+
factory.createTemplateExpression(factory.createTemplateHead('arn:aws:appsync:'), [
229+
factory.createTemplateSpan(
230+
factory.createPropertyAccessExpression(
231+
factory.createPropertyAccessExpression(
232+
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('data')),
233+
factory.createIdentifier('stack'),
234+
),
235+
factory.createIdentifier('region'),
236+
),
237+
factory.createTemplateMiddle(':'),
238+
),
239+
factory.createTemplateSpan(
240+
factory.createPropertyAccessExpression(
241+
factory.createPropertyAccessExpression(
242+
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('data')),
243+
factory.createIdentifier('stack'),
244+
),
245+
factory.createIdentifier('account'),
246+
),
247+
factory.createTemplateTail(`:apis/${apiId}/*`),
248+
),
249+
]),
250+
]),
251+
),
252+
],
253+
true,
254+
),
255+
],
256+
);
257+
258+
const addToPrincipalPolicy = factory.createExpressionStatement(
259+
factory.createCallExpression(
260+
factory.createPropertyAccessExpression(
261+
factory.createPropertyAccessExpression(
262+
factory.createPropertyAccessExpression(
263+
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('auth')),
264+
factory.createIdentifier('resources'),
265+
),
266+
factory.createIdentifier('authenticatedUserIamRole'),
267+
),
268+
factory.createIdentifier('addToPrincipalPolicy'),
269+
),
270+
undefined,
271+
[policyStatement],
272+
),
273+
);
274+
275+
this.backendGenerator.addStatement(addToPrincipalPolicy);
276+
}
181277
}
182278

183279
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped JSON from AppSync logConfig

0 commit comments

Comments
 (0)