Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { storage } from './storage/resource';
import { S3Trigger1ef46783 } from './storage/S3Trigger1ef46783/resource';
import { lowstockproducts } from './function/lowstockproducts/resource';
import { defineBackend } from '@aws-amplify/backend';
import { Duration } from 'aws-cdk-lib';
import { Duration, aws_iam } from 'aws-cdk-lib';

const backend = defineBackend({
auth,
Expand Down Expand Up @@ -49,6 +49,15 @@ cfnGraphqlApi.additionalAuthenticationProviders = [
},
},
];
backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(
new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
actions: ['appsync:GraphQL'],
resources: [
`arn:aws:appsync:${backend.data.stack.region}:${backend.data.stack.account}:apis/3oy6oxkj6ffojmc2upd52ftdsq/*`,
],
})
);
const branchName = process.env.AWS_BRANCH ?? 'sandbox';
backend.S3Trigger1ef46783.resources.cfnResources.cfnFunction.functionName = `S3Trigger1ef46783-${branchName}`;
backend.S3Trigger1ef46783.addEnvironment(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@
* 4. Update lowstockproducts/resource.ts to use secret() instead of hardcoded SSM path
* 5. Convert S3Trigger function from CommonJS to ESM
* 6. Update frontend import from amplifyconfiguration.json to amplify_outputs.json
* 7. Add IAM policy to backend.ts for authenticated user to access Gen1 AppSync API
* 8. Resolve the Gen1 AppSync API ID and replace the placeholder in backend.ts
*/

import { execSync } from 'child_process';
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import { AppSyncClient, paginateListGraphqlApis } from '@aws-sdk/client-appsync';

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

/**
* Look up the Gen1 AppSync API ID by querying all APIs and finding
* the one tagged with "user:Application" matching the app name.
*/
async function resolveGen1AppSyncApiId(appName: string): Promise<string> {
const client = new AppSyncClient({});

for await (const page of paginateListGraphqlApis({ client }, {})) {
for (const api of page.graphqlApis ?? []) {
if (api.tags?.['user:Application'] === appName) {
return api.apiId!;
}
}
}

throw new Error(`No AppSync API found with tag user:Application=${appName}`);
}

async function addGen1AppSyncPolicy(appPath: string, appName: string): Promise<void> {
const apiId = await resolveGen1AppSyncApiId(appName);

const backendPath = path.join(appPath, 'amplify', 'backend.ts');
const content = await fs.readFile(backendPath, 'utf-8');

// Add aws_iam to the Duration import from aws-cdk-lib
let updated = content.replace(
/import\s*\{([^}]*)\bDuration\b([^}]*)\}\s*from\s*["']aws-cdk-lib["']/,
(match, before, after) => {
if (match.includes('aws_iam')) return match;
return `import {${before}Duration${after}, aws_iam } from "aws-cdk-lib"`;
},
);

const policyBlock = `
backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
actions: ['appsync:GraphQL'],
resources: [\`arn:aws:appsync:\${backend.data.stack.region}:\${backend.data.stack.account}:apis/${apiId}/*\`]
}))
`;

updated = updated.trimEnd() + '\n' + policyBlock;

await fs.writeFile(backendPath, updated, 'utf-8');
}

export async function postGenerate(appPath: string): Promise<void> {
const packageJson = JSON.parse(await fs.readFile(path.join(appPath, 'package.json'), 'utf-8'));
const appName = packageJson.name as string;

await updateBranchName(appPath);
await convertLowstockproductsToESM(appPath);
await updateLowstockproductsResource(appPath);
await convertS3TriggerToESM(appPath);
await updateFrontendConfig(appPath);
await addGen1AppSyncPolicy(appPath, appName);
}

async function main(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export class DataGenerator implements Planner {
if (additionalAuthProviders && additionalAuthProviders.length > 0 && hasAuth) {
this.contributeAdditionalAuthProviders(additionalAuthProviders);
}

// Grant the Gen2 authenticated user IAM role access to the Gen1 AppSync API
if (hasAuth) {
this.contributeIamAuthGrant(apiId, authorizationModes, additionalAuthProviders);
}
},
},
];
Expand Down Expand Up @@ -178,6 +183,97 @@ export class DataGenerator implements Planner {
);
this.backendGenerator.addStatement(assignment);
}

/**
* Grants the Gen2 authenticated user IAM role access to the Gen1 AppSync API.
*
* Post-refactor, the identity pool moves to the Gen2 stack with a new AuthRole.
* If the Gen1 API uses AWS_IAM auth, the new role needs an explicit policy to
* call appsync:GraphQL on the Gen1 API during the transition period.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped authConfig from amplify-meta.json
private contributeIamAuthGrant(apiId: string, authorizationModes: any, additionalAuthProviders?: Array<Record<string, unknown>>): void {
const defaultAuthType = authorizationModes?.defaultAuthentication?.authenticationType;
const hasIamDefault = defaultAuthType === 'AWS_IAM';
const hasIamAdditional = additionalAuthProviders?.some((p) => p.authenticationType === 'AWS_IAM') ?? false;

if (!hasIamDefault && !hasIamAdditional) return;

this.backendGenerator.addImport('aws-cdk-lib', ['aws_iam']);

// backend.auth.resources.authenticatedUserIamRole.addToPrincipalPolicy(
// new aws_iam.PolicyStatement({ effect: aws_iam.Effect.ALLOW, actions: ['appsync:GraphQL'],
// resources: [`arn:aws:appsync:${backend.data.stack.region}:${backend.data.stack.account}:apis/<apiId>/*`] })
// )
const policyStatement = factory.createNewExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('aws_iam'), factory.createIdentifier('PolicyStatement')),
undefined,
[
factory.createObjectLiteralExpression(
[
factory.createPropertyAssignment(
'effect',
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('aws_iam'), factory.createIdentifier('Effect')),
factory.createIdentifier('ALLOW'),
),
),
factory.createPropertyAssignment(
'actions',
factory.createArrayLiteralExpression([factory.createStringLiteral('appsync:GraphQL')]),
),
factory.createPropertyAssignment(
'resources',
factory.createArrayLiteralExpression([
factory.createTemplateExpression(factory.createTemplateHead('arn:aws:appsync:'), [
factory.createTemplateSpan(
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('data')),
factory.createIdentifier('stack'),
),
factory.createIdentifier('region'),
),
factory.createTemplateMiddle(':'),
),
factory.createTemplateSpan(
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('data')),
factory.createIdentifier('stack'),
),
factory.createIdentifier('account'),
),
factory.createTemplateTail(`:apis/${apiId}/*`),
),
]),
]),
),
],
true,
),
],
);

const addToPrincipalPolicy = factory.createExpressionStatement(
factory.createCallExpression(
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(
factory.createPropertyAccessExpression(factory.createIdentifier('backend'), factory.createIdentifier('auth')),
factory.createIdentifier('resources'),
),
factory.createIdentifier('authenticatedUserIamRole'),
),
factory.createIdentifier('addToPrincipalPolicy'),
),
undefined,
[policyStatement],
),
);

this.backendGenerator.addStatement(addToPrincipalPolicy);
}
}

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