Skip to content
Open
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 @@ -21,4 +21,12 @@ export const auth = defineAuth({
multifactor: {
mode: 'OFF',
},
access: (allow: any) => [
allow
.resource(storelocator41a9495f41a9495fPostConfirmation)
.to(['addUserToGroup']),
allow
.resource(storelocator41a9495f41a9495fPostConfirmation)
.to(['manageGroups']),
],
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
* 1. Update frontend import from amplifyconfiguration.json to amplify_outputs.json
* 2. Convert PostConfirmation trigger index.js from CommonJS to ESM
* 3. Convert PostConfirmation trigger add-to-group.js from CommonJS to ESM
* 4. Add auth resource access for the PostConfirmation trigger
* 5. Update PostConfirmation resource.ts (memoryMB, resourceGroupName)
* 4. Update PostConfirmation resource.ts (memoryMB, resourceGroupName)
*/

import fs from 'fs/promises';
Expand Down Expand Up @@ -77,24 +76,6 @@ async function convertAddToGroupToESM(appPath: string, dirName: string): Promise
await fs.writeFile(filePath, updated, 'utf-8');
}

async function addAuthResourceAccess(appPath: string, dirName: string): Promise<void> {
const resourcePath = path.join(appPath, 'amplify', 'auth', 'resource.ts');

const content = await fs.readFile(resourcePath, 'utf-8');

// Find the variable name from the import statement
const importMatch = content.match(/import\s*\{\s*(\w+)\s*\}\s*from\s*['"]\.\//);
const fnName = importMatch ? importMatch[1] : dirName;

// Add access block after the triggers block
const updated = content.replace(
/(triggers:\s*\{[^}]*\},?)/,
`$1\n access: (allow) => [\n allow.resource(${fnName}).to([\n "addUserToGroup",\n "manageGroups",\n ]),\n ],`,
);

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

async function updatePostConfirmationResource(appPath: string, dirName: string): Promise<void> {
const resourcePath = path.join(appPath, 'amplify', 'auth', dirName, 'resource.ts');
let content = await fs.readFile(resourcePath, 'utf-8');
Expand All @@ -118,7 +99,6 @@ export async function postGenerate(appPath: string): Promise<void> {
await updateFrontendConfig(appPath);
await convertIndexToESM(appPath, dirName);
await convertAddToGroupToESM(appPath, dirName);
await addAuthResourceAccess(appPath, dirName);
await updatePostConfirmationResource(appPath, dirName);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,11 @@ export class AuthRenderer {
}

for (const func of functionsWithAuthAccess) {
namedImports[`../function/${func.resourceName}/resource`] = new Set([func.resourceName]);
// Skip adding import if the function is already imported (e.g., by addLambdaTriggers for auth triggers).
const alreadyImported = Object.values(namedImports).some((names) => names.has(func.resourceName));
if (!alreadyImported) {
namedImports[`../function/${func.resourceName}/resource`] = new Set([func.resourceName]);
}
}

const accessRules: ts.Expression[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,11 @@ export class FunctionGenerator implements Planner {
const { retained, escapeHatches } = classifyEnvVars(config.Environment?.Variables ?? {});

// Extract DynamoDB/Kinesis actions and GraphQL API permissions from the function's CloudFormation template
const { dynamoActions, kinesisActions, graphqlApiPermissions, authAccess } = this.extractCfnPermissions();
const { dynamoActions, kinesisActions, graphqlApiPermissions, authAccess: cfnAuthAccess } = this.extractCfnPermissions();

// For auth trigger functions, also extract permissions from the auth-trigger CFN template.
const triggerAuthAccess = this.extractAuthTriggerCfnPermissions();
const authAccess = { ...cfnAuthAccess, ...triggerAuthAccess };

return {
resourceName: this.resource.resourceName,
Expand Down Expand Up @@ -589,6 +593,47 @@ export class FunctionGenerator implements Planner {
return { dynamoActions, kinesisActions, graphqlApiPermissions: { hasMutation, hasQuery }, authAccess };
}

/**
* Extracts auth permissions from the auth-trigger CFN template for auth trigger functions.
*
* Gen1 auth trigger IAM permissions live in a separate nested stack
* (`auth-trigger-cloudformation-template.json`), not in the function's own template.
* This method reads that template and extracts cognito-idp actions from IAM policies
* that reference this function.
*/
private extractAuthTriggerCfnPermissions(): AuthPermissions {
if (this.category !== 'auth') return {};

const authResourceName = this.gen1App.singleResourceName('auth', 'Cognito');
const templatePath = `auth/${authResourceName}/build/auth-trigger-cloudformation-template.json`;
if (!this.gen1App.fileExists(templatePath)) return {};

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped CloudFormation template
const template = this.gen1App.json(templatePath);
const resources = template.Resources ?? {};
const cognitoActions: string[] = [];

for (const [logicalId, resource] of Object.entries(resources)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped CloudFormation resource
const res = resource as any;
if (res.Type !== 'AWS::IAM::Policy') continue;
// Match policies whose logical ID contains this function's resource name.
if (!logicalId.includes(this.resource.resourceName)) continue;

const statements = res.Properties?.PolicyDocument?.Statement ?? [];
for (const stmt of statements) {
const actions = Array.isArray(stmt.Action) ? stmt.Action : [stmt.Action];
for (const action of actions) {
if (typeof action === 'string' && action.startsWith('cognito-idp:') && !cognitoActions.includes(action)) {
cognitoActions.push(action);
}
}
}
}

return resolveAuthTriggerAccess(cognitoActions);
}

/**
* Generates grant statements for this function accessing standalone
* DynamoDB tables (STORAGE_ env vars).
Expand Down Expand Up @@ -1055,6 +1100,35 @@ function resolveAuthAccess(cognitoActions: string[]): AuthPermissions {
return result as AuthPermissions;
}

/**
* Maps cognito-idp IAM actions from auth-trigger CFN templates to Gen2 auth permissions.
*
* Auth trigger policies (e.g., "Add User To Group") use actions like `GetGroup` and
* `CreateGroup` that aren't in the standard `AUTH_ACTION_MAPPING` (which covers actions
* from function-level `AmplifyResourcesPolicy`). This function extends the base mapping
* with trigger-specific actions that map to `manageGroups`.
*/
const AUTH_TRIGGER_ACTION_MAPPING: Readonly<Record<string, keyof AuthPermissions>> = {
...AUTH_ACTION_MAPPING,
'cognito-idp:GetGroup': 'manageGroups',
'cognito-idp:CreateGroup': 'manageGroups',
'cognito-idp:DeleteGroup': 'manageGroups',
'cognito-idp:UpdateGroup': 'manageGroups',
};

function resolveAuthTriggerAccess(cognitoActions: string[]): AuthPermissions {
if (cognitoActions.length === 0) return {};
const result: Record<string, boolean> = {};

for (const action of cognitoActions) {
if (AUTH_TRIGGER_ACTION_MAPPING[action]) {
result[AUTH_TRIGGER_ACTION_MAPPING[action]] = true;
}
}

return result as AuthPermissions;
}

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

const TRIGGER_SUFFIX_TO_EVENT: Readonly<Record<string, AuthTriggerEvent>> = {
Expand Down
Loading