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
@@ -1,5 +1,6 @@
import { FunctionDefinition } from '../../core/migration-pipeline';
import { FunctionConfiguration } from '@aws-sdk/client-lambda';
import { AuthAccess } from '../../generators/functions/index';
import { analyzeApiPermissionsFromCfn } from '../../codegen-head/api-cfn-access';
import assert from 'node:assert';

Expand Down Expand Up @@ -44,6 +45,7 @@
functionSchedules: FunctionSchedule[],
functionCategoryMap: Map<string, string>,
meta: AmplifyMetaWithFunction,
functionAuthAccess?: Map<string, AuthAccess>,
): FunctionDefinition[] => {
const funcDefList: FunctionDefinition[] = [];

Expand Down Expand Up @@ -105,7 +107,11 @@
funcDef.resourceName = functionRecordInMeta[0];
funcDef.schedule = functionSchedules.find((schedule) => schedule.functionName === functionName)?.scheduleExpression;

// Analyze CFN template for API permissions
// Add auth access configuration if available
if (functionAuthAccess?.has(functionRecordInMeta[0])) {
funcDef.authAccess = functionAuthAccess.get(functionRecordInMeta[0]) };
Comment thread Dismissed

// Analyze CFN template for API permissions
if (funcDef.resourceName) {
funcDef.apiPermissions = analyzeApiPermissionsFromCfn(funcDef.resourceName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DescribeRuleCommand, CloudWatchEventsClient } from '@aws-sdk/client-clo
import * as path from 'path';
import { StateManager, $TSMeta, JSONUtilities } from '@aws-amplify/amplify-cli-core';
import { BackendDownloader } from './backend_downloader';
import { AuthAccessAnalyzer } from './auth_access_analyzer';

/**
* Configuration interface for Amplify Auth category resources.
Expand Down Expand Up @@ -66,6 +67,7 @@ export class AppFunctionsDefinitionFetcher {
private backendEnvironmentResolver: BackendEnvironmentResolver,
private stateManager: StateManager,
private ccbFetcher: BackendDownloader,
private authAnalyzer: AuthAccessAnalyzer,
) {}

/**
Expand Down Expand Up @@ -202,11 +204,15 @@ export class AppFunctionsDefinitionFetcher {
// Wait for all schedule fetching operations to complete
const functionSchedules = await Promise.all(getFunctionSchedulePromises);

// Get auth access from auth analyzer for function definitions
const functionAuthAccess = await this.authAnalyzer.getFunctionAuthAccess();

// Build comprehensive function definitions by combining:
// - Live AWS Lambda configurations (runtime, memory, timeout, etc.)
// - CloudWatch schedule expressions (for scheduled functions)
// - Trigger category mappings (auth, storage, etc.)
// - Original Amplify project metadata
return getFunctionDefinition(functionConfigurations, functionSchedules, functionCategoryMap, meta);
// - CloudFormation templates for auth access parsing
return getFunctionDefinition(functionConfigurations, functionSchedules, functionCategoryMap, meta, functionAuthAccess);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { BackendEnvironmentResolver } from './backend_environment_selector';
import { BackendDownloader } from './backend_downloader';
import { JSONUtilities, $TSMeta } from '@aws-amplify/amplify-cli-core';
import { fileOrDirectoryExists } from './directory_exists';
import { AuthAccess } from '../generators/functions/index';
import path from 'node:path';
import fs from 'node:fs/promises';
import assert from 'node:assert';

// Define grouped permissions and their required actions
Comment thread
iliapolo marked this conversation as resolved.
const GROUPED_PERMISSIONS = {
manageUsers: [
'cognito-idp:AdminConfirmSignUp',
'cognito-idp:AdminCreateUser',
'cognito-idp:AdminDeleteUser',
'cognito-idp:AdminDeleteUserAttributes',
'cognito-idp:AdminDisableUser',
'cognito-idp:AdminEnableUser',
'cognito-idp:AdminGetUser',
'cognito-idp:AdminListGroupsForUser',
'cognito-idp:AdminRespondToAuthChallenge',
'cognito-idp:AdminSetUserMFAPreference',
'cognito-idp:AdminSetUserSettings',
'cognito-idp:AdminUpdateUserAttributes',
'cognito-idp:AdminUserGlobalSignOut',
],
manageGroupMembership: ['cognito-idp:AdminAddUserToGroup', 'cognito-idp:AdminRemoveUserFromGroup'],
manageGroups: [
'cognito-idp:GetGroup',
'cognito-idp:ListGroups',
'cognito-idp:CreateGroup',
'cognito-idp:DeleteGroup',
'cognito-idp:UpdateGroup',
],
manageUserDevices: [
'cognito-idp:AdminForgetDevice',
'cognito-idp:AdminGetDevice',
'cognito-idp:AdminListDevices',
'cognito-idp:AdminUpdateDeviceStatus',
],
managePasswordRecovery: ['cognito-idp:AdminResetUserPassword', 'cognito-idp:AdminSetUserPassword'],
};

const AUTH_ACTION_MAPPING: Record<string, keyof AuthAccess> = {
// Individual permissions only - no conflicts with grouped permissions
'cognito-idp:AdminAddUserToGroup': 'addUserToGroup',
'cognito-idp:AdminCreateUser': 'createUser',
'cognito-idp:AdminDeleteUser': 'deleteUser',
'cognito-idp:AdminDeleteUserAttributes': 'deleteUserAttributes',
'cognito-idp:AdminDisableUser': 'disableUser',
'cognito-idp:AdminEnableUser': 'enableUser',
'cognito-idp:AdminForgetDevice': 'forgetDevice',
'cognito-idp:AdminGetDevice': 'getDevice',
'cognito-idp:AdminGetUser': 'getUser',
'cognito-idp:AdminListDevices': 'listDevices',
'cognito-idp:AdminListGroupsForUser': 'listGroupsForUser',
'cognito-idp:AdminRemoveUserFromGroup': 'removeUserFromGroup',
'cognito-idp:AdminResetUserPassword': 'resetUserPassword',
'cognito-idp:AdminSetUserMFAPreference': 'setUserMfaPreference',
'cognito-idp:AdminSetUserPassword': 'setUserPassword',
'cognito-idp:AdminSetUserSettings': 'setUserSettings',
'cognito-idp:AdminUpdateDeviceStatus': 'updateDeviceStatus',
'cognito-idp:AdminUpdateUserAttributes': 'updateUserAttributes',
'cognito-idp:ListUsers': 'listUsers',
'cognito-idp:ListUsersInGroup': 'listUsersInGroup',

// Actions that don't have individual permissions - map to grouped
'cognito-idp:AdminConfirmSignUp': 'manageUsers',
'cognito-idp:AdminRespondToAuthChallenge': 'manageUsers',
'cognito-idp:AdminUserGlobalSignOut': 'manageUsers',
'cognito-idp:AdminInitiateAuth': 'manageUsers',
'cognito-idp:AdminUpdateAuthEventFeedback': 'manageUsers',

// Other actions without individual permissions
'cognito-idp:ForgetDevice': 'forgetDevice',
'cognito-idp:VerifyUserAttribute': 'updateUserAttributes',
'cognito-idp:UpdateUserAttributes': 'updateUserAttributes',
'cognito-idp:SetUserMFAPreference': 'setUserMfaPreference',
'cognito-idp:SetUserSettings': 'setUserSettings',
};

function extractCognitoActionsFromPolicy(amplifyResourcesPolicy: any): string[] {
const actions: string[] = [];

const policyDocument = amplifyResourcesPolicy.Properties?.PolicyDocument;
const statements = Array.isArray(policyDocument?.Statement) ? policyDocument.Statement : [policyDocument?.Statement].filter(Boolean);

for (const statement of statements) {
const statementActions = Array.isArray(statement.Action) ? statement.Action : [statement.Action];

for (const action of statementActions) {
if (typeof action === 'string' && action.startsWith('cognito-idp:')) {
if (!actions.includes(action)) {
actions.push(action);
}
}
}
}

return actions;
}

export function parseAuthAccessFromTemplate(templateContent: string): AuthAccess {
const authAccess: AuthAccess = {};

const cfnTemplate = JSON.parse(templateContent);

// Check only AmplifyResourcesPolicy for consistency with other parsers
const amplifyResourcesPolicy = cfnTemplate.Resources?.AmplifyResourcesPolicy;

if (!amplifyResourcesPolicy || amplifyResourcesPolicy.Type !== 'AWS::IAM::Policy') {
return {};
}

const cognitoActions = extractCognitoActionsFromPolicy(amplifyResourcesPolicy);
const coveredActions = new Set<string>();

// First, check for complete grouped permissions
Object.entries(GROUPED_PERMISSIONS).forEach(([groupedPermission, requiredActions]) => {
const hasAllActions = requiredActions.every((action) => cognitoActions.includes(action));
if (hasAllActions) {
authAccess[groupedPermission as keyof AuthAccess] = true;
// Mark these actions as covered by the group permission
requiredActions.forEach((action) => coveredActions.add(action));
}
});

// Then, map remaining individual actions to individual permissions
cognitoActions.forEach((action) => {
if (!coveredActions.has(action)) {
const permission = AUTH_ACTION_MAPPING[action];
if (permission) {
authAccess[permission] = true;
}
}
});

return authAccess;
}

/**
* Combined auth access analyzer that handles both template fetching and parsing.
* Provides centralized functionality for auth-related CloudFormation analysis.
*/
export class AuthAccessAnalyzer {
constructor(private backendEnvironmentResolver: BackendEnvironmentResolver, private ccbFetcher: BackendDownloader) {}

/**
* Fetches CloudFormation templates for all functions in the project.
* @returns Map of function names to their CloudFormation template content
*/
async getFunctionTemplates(): Promise<Map<string, string>> {
const backendEnvironment = await this.backendEnvironmentResolver.selectBackendEnvironment();
assert(backendEnvironment?.deploymentArtifacts);

const currentCloudBackendDirectory = await this.ccbFetcher.getCurrentCloudBackend(backendEnvironment.deploymentArtifacts);
const amplifyMetaPath = path.join(currentCloudBackendDirectory, 'amplify-meta.json');

const meta = JSONUtilities.readJson<$TSMeta>(amplifyMetaPath, { throwIfNotExist: true });
const functions = meta?.function ?? {};

const functionTemplates = new Map<string, string>();
for (const functionName of Object.keys(functions)) {
const templatePath = path.join(
currentCloudBackendDirectory,
'function',
functionName,
`${functionName}-cloudformation-template.json`,
);
if (await fileOrDirectoryExists(templatePath)) {
const templateContent = await fs.readFile(templatePath, 'utf8');
functionTemplates.set(functionName, templateContent);
}
}

return functionTemplates;
}

/**
* Analyzes auth access for all functions by fetching templates and parsing them.
* @returns Map of function names to their auth access permissions
*/
async getFunctionAuthAccess(): Promise<Map<string, AuthAccess>> {
const templates = await this.getFunctionTemplates();
const authAccessMap = new Map<string, AuthAccess>();

for (const [functionName, templateContent] of templates) {
const authAccess = parseAuthAccessFromTemplate(templateContent);
if (Object.keys(authAccess).length > 0) {
authAccessMap.set(functionName, authAccess);
}
}

return authAccessMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { AuthTriggerConnection } from '../adapters/auth/index';
import { DataDefinitionFetcher } from './data_definition_fetcher';
import { AmplifyStackParser } from './amplify_stack_parser';
import { AppFunctionsDefinitionFetcher } from './app_functions_definition_fetcher';
import { AuthAccessAnalyzer } from './auth_access_analyzer';
// import { TemplateGenerator, ResourceMapping } from '@aws-amplify/migrate-template-gen'; // Package not available
import { printer } from './printer';
import { format } from './format';
Expand Down Expand Up @@ -542,6 +543,8 @@ export async function prepare(logger: Logger, appId: string, envName: string, re
const amplifyStackParser = new AmplifyStackParser(cloudFormationClient);
const ccbFetcher = new BackendDownloader(s3Client);

const authAnalyzer = new AuthAccessAnalyzer(backendEnvironmentResolver, ccbFetcher);

await generateGen2Code({
outputDirectory: TEMP_GEN_2_OUTPUT_DIR,
storageDefinitionFetcher: new AppStorageDefinitionFetcher(backendEnvironmentResolver, new BackendDownloader(s3Client), s3Client),
Expand All @@ -560,6 +563,7 @@ export async function prepare(logger: Logger, appId: string, envName: string, re
backendEnvironmentResolver,
stateManager,
ccbFetcher,
authAnalyzer,
),
analyticsDefinitionFetcher: new AppAnalyticsDefinitionFetcher(backendEnvironmentResolver, stateManager),
analytics: new AppAnalytics(appId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,11 +410,27 @@ export const createGen2Renderer = ({

// Process authentication configuration - create amplify/auth/resource.ts
if (auth) {
// Create function category map for correct import paths
const functionCategories = new Map<string, string>();
if (functions) {
functions.forEach((func) => {
if (func.resourceName && func.category) {
functionCategories.set(func.resourceName, func.category);
}
});
}

renderers.push(new EnsureDirectory(path.join(outputDir, 'amplify', 'auth')));
renderers.push(
new TypescriptNodeArrayRenderer(
async () => renderAuthNode(auth),
(content) => fileWriter(content, path.join(outputDir, 'amplify', 'auth', 'resource.ts')),
async () => renderAuthNode(auth, functions, functionCategories),
async (content) => {
Comment thread
iliapolo marked this conversation as resolved.
// Remove unused parameter and add type annotation
let cleanedContent = content.replace(/\(allow, _unused\)/g, '(allow: any)');
// Add trailing comma after access array
cleanedContent = cleanedContent.replace(/(access: \(allow: any\) => \[[\s\S]*?\n {4}\])/g, '$1,');
return fileWriter(cleanedContent, path.join(outputDir, 'amplify', 'auth', 'resource.ts'));
},
),
);
// Configure auth parameters for backend synthesis
Expand Down
Loading
Loading