From 7c10bd909b5206a4057bd93bb49cbfbb193bfcac Mon Sep 17 00:00:00 2001 From: w3lld1 <42353747+w3lld1@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:13:57 +0200 Subject: [PATCH 1/3] fix(amplify-category-storage): make IAM policy names environment-specific --- .../s3-stack-builder.test.ts | 15 +++--- .../s3-stack-transform.test.ts | 51 +++++++++++++++++++ .../cdk-stack-builder/s3-stack-transform.ts | 15 +++--- 3 files changed, 67 insertions(+), 14 deletions(-) create mode 100644 packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts index 8e0a10a0405..3ea7edb0c89 100644 --- a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts @@ -25,6 +25,7 @@ const mockContext = { }, }), getUserPoolGroupList: () => [], + getEnvInfo: () => ({ envName: 'mockenv' }), // eslint-disable-next-line getResourceStatus: () => { return { allResources: S3MockDataBuilder.getMockGetAllResourcesNoExistingLambdas() }; @@ -91,13 +92,13 @@ describe('Test S3 transform generates correct CFN template', () => { unauthRoleName: { Ref: 'UnauthRoleName' }, authRoleName: { Ref: 'AuthRoleName' }, triggerFunction: mockTriggerFunction, - s3PrivatePolicy: `Private_policy_${shortId}`, - s3ProtectedPolicy: `Protected_policy_${shortId}`, - s3PublicPolicy: `Public_policy_${shortId}`, - s3ReadPolicy: `read_policy_${shortId}`, - s3UploadsPolicy: `Uploads_policy_${shortId}`, - authPolicyName: `s3_amplify_${shortId}`, - unauthPolicyName: `s3_amplify_${shortId}`, + s3PrivatePolicy: `Private_policy_${shortId}_mockenv`, + s3ProtectedPolicy: `Protected_policy_${shortId}_mockenv`, + s3PublicPolicy: `Public_policy_${shortId}_mockenv`, + s3ReadPolicy: `read_policy_${shortId}_mockenv`, + s3UploadsPolicy: `Uploads_policy_${shortId}_mockenv`, + authPolicyName: `s3_amplify_${shortId}_mockenv`, + unauthPolicyName: `s3_amplify_${shortId}_mockenv`, AuthenticatedAllowList: 'ALLOW', GuestAllowList: 'ALLOW', s3PermissionsAuthenticatedPrivate: 's3:PutObject,s3:GetObject,s3:DeleteObject', diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts new file mode 100644 index 00000000000..cdc92eafbaf --- /dev/null +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts @@ -0,0 +1,51 @@ +import { $TSContext } from '@aws-amplify/amplify-cli-core'; +import { AmplifyS3ResourceStackTransform } from '../../../../provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform'; +import { S3UserInputs } from '../../../../provider-utils/awscloudformation/service-walkthrough-types/s3-user-input-types'; +import { S3InputState } from '../../../../provider-utils/awscloudformation/service-walkthroughs/s3-user-input-state'; + +jest.mock('@aws-amplify/amplify-cli-core', () => ({ + pathManager: { + getBackendDirPath: jest.fn().mockReturnValue('mockbackendpath'), + }, +})); + +jest.mock('../../../../provider-utils/awscloudformation/service-walkthroughs/s3-user-input-state'); + +describe('AmplifyS3ResourceStackTransform', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('includes the environment name in IAM policy names', () => { + const userInput: S3UserInputs = { + resourceName: 'storage', + bucketName: 'bucket', + policyUUID: 'abc123', + storageAccess: undefined, + guestAccess: [], + authAccess: [], + }; + const context = { + amplify: { + getEnvInfo: jest.fn().mockReturnValue({ envName: 'prod' }), + }, + } as unknown as $TSContext; + + jest.spyOn(S3InputState.prototype, 'getCliInputPayload').mockReturnValue(userInput); + jest.spyOn(S3InputState.prototype, 'getUserInput').mockReturnValue(userInput); + jest.spyOn(S3InputState, 'getCfnPermissionsFromInputPermissions').mockReturnValue([]); + + const transform = new AmplifyS3ResourceStackTransform('storage', context); + transform.generateCfnInputParameters(); + + expect(transform.getCFNInputParams()).toMatchObject({ + s3PrivatePolicy: 'Private_policy_abc123_prod', + s3ProtectedPolicy: 'Protected_policy_abc123_prod', + s3PublicPolicy: 'Public_policy_abc123_prod', + s3ReadPolicy: 'read_policy_abc123_prod', + s3UploadsPolicy: 'Uploads_policy_abc123_prod', + authPolicyName: 's3_amplify_abc123_prod', + unauthPolicyName: 's3_amplify_abc123_prod', + }); + }); +}); diff --git a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts index 36d666baf35..7052a7aea2e 100644 --- a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts +++ b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts @@ -112,13 +112,14 @@ export class AmplifyS3ResourceStackTransform { if (userInput.adminTriggerFunction?.triggerFunction && userInput.adminTriggerFunction.triggerFunction !== 'NONE') { this.cfnInputParams.adminTriggerFunction = userInput.adminTriggerFunction.triggerFunction; } - this.cfnInputParams.s3PrivatePolicy = `Private_policy_${userInput.policyUUID}`; - this.cfnInputParams.s3ProtectedPolicy = `Protected_policy_${userInput.policyUUID}`; - this.cfnInputParams.s3PublicPolicy = `Public_policy_${userInput.policyUUID}`; - this.cfnInputParams.s3ReadPolicy = `read_policy_${userInput.policyUUID}`; - this.cfnInputParams.s3UploadsPolicy = `Uploads_policy_${userInput.policyUUID}`; - this.cfnInputParams.authPolicyName = `s3_amplify_${userInput.policyUUID}`; - this.cfnInputParams.unauthPolicyName = `s3_amplify_${userInput.policyUUID}`; + const policyNameSuffix = `${userInput.policyUUID}_${this.context.amplify.getEnvInfo().envName}`; + this.cfnInputParams.s3PrivatePolicy = `Private_policy_${policyNameSuffix}`; + this.cfnInputParams.s3ProtectedPolicy = `Protected_policy_${policyNameSuffix}`; + this.cfnInputParams.s3PublicPolicy = `Public_policy_${policyNameSuffix}`; + this.cfnInputParams.s3ReadPolicy = `read_policy_${policyNameSuffix}`; + this.cfnInputParams.s3UploadsPolicy = `Uploads_policy_${policyNameSuffix}`; + this.cfnInputParams.authPolicyName = `s3_amplify_${policyNameSuffix}`; + this.cfnInputParams.unauthPolicyName = `s3_amplify_${policyNameSuffix}`; this.cfnInputParams.AuthenticatedAllowList = this._getAuthGuestListPermission(S3PermissionType.READ, userInput.authAccess); this.cfnInputParams.GuestAllowList = this._getAuthGuestListPermission(S3PermissionType.READ, userInput.guestAccess); this.cfnInputParams.s3PermissionsAuthenticatedPrivate = this._getPublicPrivatePermissions( From 3f565c8859643059880db13e9ba7b108c8b41992 Mon Sep 17 00:00:00 2001 From: Sharonya Jain Date: Fri, 21 Aug 2026 15:10:39 +0200 Subject: [PATCH 2/3] fix(amplify-category-storage): guard against missing env name in S3 policy names The env-specific policy-name suffix read `getEnvInfo().envName` directly. When env info is absent or blank (uninitialized env / headless / export paths) the suffix silently became `..._undefined`, which puts every such deployment back on one shared policy name and re-collides -- the exact bug this change fixes -- or threw an opaque error on a path that previously succeeded. Read the env name once and validate it: fail fast with a clear `EnvironmentNotInitializedError` when it is missing or empty, so names never degrade to a shared `_undefined` suffix. Adds a regression test covering the missing-envName case alongside the existing happy-path test. --- .../s3-stack-transform.test.ts | 31 +++++++++++++++++++ .../cdk-stack-builder/s3-stack-transform.ts | 15 ++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts index cdc92eafbaf..84df677e8cb 100644 --- a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts @@ -7,6 +7,12 @@ jest.mock('@aws-amplify/amplify-cli-core', () => ({ pathManager: { getBackendDirPath: jest.fn().mockReturnValue('mockbackendpath'), }, + AmplifyError: class AmplifyError extends Error { + constructor(code: string, options: { message: string }) { + super(options.message); + this.name = code; + } + }, })); jest.mock('../../../../provider-utils/awscloudformation/service-walkthroughs/s3-user-input-state'); @@ -48,4 +54,29 @@ describe('AmplifyS3ResourceStackTransform', () => { unauthPolicyName: 's3_amplify_abc123_prod', }); }); + + it('throws when the current environment name cannot be determined', () => { + const userInput: S3UserInputs = { + resourceName: 'storage', + bucketName: 'bucket', + policyUUID: 'abc123', + storageAccess: undefined, + guestAccess: [], + authAccess: [], + }; + // Env info carries no envName (uninitialized env / headless path). + const context = { + amplify: { + getEnvInfo: jest.fn().mockReturnValue({}), + }, + } as unknown as $TSContext; + + jest.spyOn(S3InputState.prototype, 'getCliInputPayload').mockReturnValue(userInput); + jest.spyOn(S3InputState.prototype, 'getUserInput').mockReturnValue(userInput); + jest.spyOn(S3InputState, 'getCfnPermissionsFromInputPermissions').mockReturnValue([]); + + const transform = new AmplifyS3ResourceStackTransform('storage', context); + + expect(() => transform.generateCfnInputParameters()).toThrow(/environment name/); + }); }); diff --git a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts index 7052a7aea2e..fc77930c6ef 100644 --- a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts +++ b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts @@ -112,7 +112,20 @@ export class AmplifyS3ResourceStackTransform { if (userInput.adminTriggerFunction?.triggerFunction && userInput.adminTriggerFunction.triggerFunction !== 'NONE') { this.cfnInputParams.adminTriggerFunction = userInput.adminTriggerFunction.triggerFunction; } - const policyNameSuffix = `${userInput.policyUUID}_${this.context.amplify.getEnvInfo().envName}`; + // Policy names must be unique per environment. When multiple environments share the + // same IAM role (e.g. an imported Identity Pool with shared auth/unauth roles), reusing + // the same policyUUID across environments produces identically named inline policies on + // the same role, which CloudFormation's single-stack ownership enforcement rejects with + // "Policy resource was already managed by another stack". The environment name is read + // once and validated so the suffix never silently degrades to a shared "_undefined". + const envName = this.context.amplify.getEnvInfo()?.envName; + if (typeof envName !== 'string' || envName.length === 0) { + throw new AmplifyError('EnvironmentNotInitializedError', { + message: 'Cannot determine the current Amplify environment name while generating S3 IAM policy names.', + resolution: `Run 'amplify init' or 'amplify env checkout ' in the root of your app directory to select an environment, then try again.`, + }); + } + const policyNameSuffix = `${userInput.policyUUID}_${envName}`; this.cfnInputParams.s3PrivatePolicy = `Private_policy_${policyNameSuffix}`; this.cfnInputParams.s3ProtectedPolicy = `Protected_policy_${policyNameSuffix}`; this.cfnInputParams.s3PublicPolicy = `Public_policy_${policyNameSuffix}`; From b9e589cc48df0ec85e9429fd8cc7480fa7e79d2c Mon Sep 17 00:00:00 2001 From: Sharonya Jain Date: Mon, 24 Aug 2026 15:08:04 +0200 Subject: [PATCH 3/3] fix(amplify-category-storage): only env-scope S3 policy names for imported auth Appending the environment name to every S3 IAM policy name renamed the inline policy for all already-deployed storage apps. PolicyName is replace-on-update for AWS::IAM::Policy, so the next push after upgrading would force a policy replacement on every existing environment -- churn and risk well beyond the shared-role collision this fixes, and it hit apps that never had the problem. The collision only occurs when the auth/unauth IAM roles are shared across environments, which is the imported Cognito Identity Pool case. Gate the `_${envName}` suffix on imported auth (detected from project meta): managed-auth apps keep the legacy env-agnostic name and see no policy replacement on upgrade, while imported-auth environments -- whose deploys are already failing -- get the unique per-env name. The missing-envName fail-fast now applies only on the imported path. Adds a regression test asserting managed auth keeps the legacy names, alongside the imported happy-path and imported missing-envName tests. --- .../s3-stack-builder.test.ts | 15 ++-- .../s3-stack-transform.test.ts | 89 +++++++++++-------- .../cdk-stack-builder/s3-stack-transform.ts | 45 +++++++--- 3 files changed, 89 insertions(+), 60 deletions(-) diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts index 3ea7edb0c89..8e0a10a0405 100644 --- a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-builder.test.ts @@ -25,7 +25,6 @@ const mockContext = { }, }), getUserPoolGroupList: () => [], - getEnvInfo: () => ({ envName: 'mockenv' }), // eslint-disable-next-line getResourceStatus: () => { return { allResources: S3MockDataBuilder.getMockGetAllResourcesNoExistingLambdas() }; @@ -92,13 +91,13 @@ describe('Test S3 transform generates correct CFN template', () => { unauthRoleName: { Ref: 'UnauthRoleName' }, authRoleName: { Ref: 'AuthRoleName' }, triggerFunction: mockTriggerFunction, - s3PrivatePolicy: `Private_policy_${shortId}_mockenv`, - s3ProtectedPolicy: `Protected_policy_${shortId}_mockenv`, - s3PublicPolicy: `Public_policy_${shortId}_mockenv`, - s3ReadPolicy: `read_policy_${shortId}_mockenv`, - s3UploadsPolicy: `Uploads_policy_${shortId}_mockenv`, - authPolicyName: `s3_amplify_${shortId}_mockenv`, - unauthPolicyName: `s3_amplify_${shortId}_mockenv`, + s3PrivatePolicy: `Private_policy_${shortId}`, + s3ProtectedPolicy: `Protected_policy_${shortId}`, + s3PublicPolicy: `Public_policy_${shortId}`, + s3ReadPolicy: `read_policy_${shortId}`, + s3UploadsPolicy: `Uploads_policy_${shortId}`, + authPolicyName: `s3_amplify_${shortId}`, + unauthPolicyName: `s3_amplify_${shortId}`, AuthenticatedAllowList: 'ALLOW', GuestAllowList: 'ALLOW', s3PermissionsAuthenticatedPrivate: 's3:PutObject,s3:GetObject,s3:DeleteObject', diff --git a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts index 84df677e8cb..e912ca335bc 100644 --- a/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts +++ b/packages/amplify-category-storage/src/__tests__/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.test.ts @@ -17,31 +17,40 @@ jest.mock('@aws-amplify/amplify-cli-core', () => ({ jest.mock('../../../../provider-utils/awscloudformation/service-walkthroughs/s3-user-input-state'); +const userInput: S3UserInputs = { + resourceName: 'storage', + bucketName: 'bucket', + policyUUID: 'abc123', + storageAccess: undefined, + guestAccess: [], + authAccess: [], +}; + +const importedAuthMeta = { auth: { cognitoauth: { service: 'Cognito', serviceType: 'imported' } } }; +const managedAuthMeta = { auth: { cognitoauth: { service: 'Cognito', serviceType: 'managed' } } }; + +const buildContext = (meta: unknown, envName: string): $TSContext => + ({ + amplify: { + getProjectMeta: jest.fn().mockReturnValue(meta), + getEnvInfo: jest.fn().mockReturnValue({ envName }), + }, + } as unknown as $TSContext); + +const stubInputState = (): void => { + jest.spyOn(S3InputState.prototype, 'getCliInputPayload').mockReturnValue(userInput); + jest.spyOn(S3InputState.prototype, 'getUserInput').mockReturnValue(userInput); + jest.spyOn(S3InputState, 'getCfnPermissionsFromInputPermissions').mockReturnValue([]); +}; + describe('AmplifyS3ResourceStackTransform', () => { afterEach(() => { jest.clearAllMocks(); }); - it('includes the environment name in IAM policy names', () => { - const userInput: S3UserInputs = { - resourceName: 'storage', - bucketName: 'bucket', - policyUUID: 'abc123', - storageAccess: undefined, - guestAccess: [], - authAccess: [], - }; - const context = { - amplify: { - getEnvInfo: jest.fn().mockReturnValue({ envName: 'prod' }), - }, - } as unknown as $TSContext; - - jest.spyOn(S3InputState.prototype, 'getCliInputPayload').mockReturnValue(userInput); - jest.spyOn(S3InputState.prototype, 'getUserInput').mockReturnValue(userInput); - jest.spyOn(S3InputState, 'getCfnPermissionsFromInputPermissions').mockReturnValue([]); - - const transform = new AmplifyS3ResourceStackTransform('storage', context); + it('appends the environment name to IAM policy names when auth is imported', () => { + stubInputState(); + const transform = new AmplifyS3ResourceStackTransform('storage', buildContext(importedAuthMeta, 'prod')); transform.generateCfnInputParameters(); expect(transform.getCFNInputParams()).toMatchObject({ @@ -55,27 +64,29 @@ describe('AmplifyS3ResourceStackTransform', () => { }); }); - it('throws when the current environment name cannot be determined', () => { - const userInput: S3UserInputs = { - resourceName: 'storage', - bucketName: 'bucket', - policyUUID: 'abc123', - storageAccess: undefined, - guestAccess: [], - authAccess: [], - }; - // Env info carries no envName (uninitialized env / headless path). - const context = { - amplify: { - getEnvInfo: jest.fn().mockReturnValue({}), - }, - } as unknown as $TSContext; + it('keeps the legacy (env-agnostic) IAM policy names when auth is not imported', () => { + // Managed auth gives each environment its own roles, so names never collide. Keeping the + // legacy name avoids forcing a policy replacement on every existing storage app on upgrade. + stubInputState(); + const transform = new AmplifyS3ResourceStackTransform('storage', buildContext(managedAuthMeta, 'prod')); + transform.generateCfnInputParameters(); - jest.spyOn(S3InputState.prototype, 'getCliInputPayload').mockReturnValue(userInput); - jest.spyOn(S3InputState.prototype, 'getUserInput').mockReturnValue(userInput); - jest.spyOn(S3InputState, 'getCfnPermissionsFromInputPermissions').mockReturnValue([]); + expect(transform.getCFNInputParams()).toMatchObject({ + s3PrivatePolicy: 'Private_policy_abc123', + s3ProtectedPolicy: 'Protected_policy_abc123', + s3PublicPolicy: 'Public_policy_abc123', + s3ReadPolicy: 'read_policy_abc123', + s3UploadsPolicy: 'Uploads_policy_abc123', + authPolicyName: 's3_amplify_abc123', + unauthPolicyName: 's3_amplify_abc123', + }); + }); - const transform = new AmplifyS3ResourceStackTransform('storage', context); + it('throws when auth is imported but the current environment name cannot be determined', () => { + // Imported auth needs the env suffix; a blank envName would otherwise degrade to a + // shared "_undefined" name and re-collide, so we fail fast instead. + stubInputState(); + const transform = new AmplifyS3ResourceStackTransform('storage', buildContext(importedAuthMeta, '')); expect(() => transform.generateCfnInputParameters()).toThrow(/environment name/); }); diff --git a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts index fc77930c6ef..baeb7bff3b9 100644 --- a/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts +++ b/packages/amplify-category-storage/src/provider-utils/awscloudformation/cdk-stack-builder/s3-stack-transform.ts @@ -112,20 +112,39 @@ export class AmplifyS3ResourceStackTransform { if (userInput.adminTriggerFunction?.triggerFunction && userInput.adminTriggerFunction.triggerFunction !== 'NONE') { this.cfnInputParams.adminTriggerFunction = userInput.adminTriggerFunction.triggerFunction; } - // Policy names must be unique per environment. When multiple environments share the - // same IAM role (e.g. an imported Identity Pool with shared auth/unauth roles), reusing - // the same policyUUID across environments produces identically named inline policies on - // the same role, which CloudFormation's single-stack ownership enforcement rejects with - // "Policy resource was already managed by another stack". The environment name is read - // once and validated so the suffix never silently degrades to a shared "_undefined". - const envName = this.context.amplify.getEnvInfo()?.envName; - if (typeof envName !== 'string' || envName.length === 0) { - throw new AmplifyError('EnvironmentNotInitializedError', { - message: 'Cannot determine the current Amplify environment name while generating S3 IAM policy names.', - resolution: `Run 'amplify init' or 'amplify env checkout ' in the root of your app directory to select an environment, then try again.`, - }); + // Policy names carry the app-wide policyUUID. That alone is only insufficient when the + // authenticated/unauthenticated IAM roles are SHARED across environments, which happens + // when auth is an imported Cognito Identity Pool: two environments' storage stacks then + // try to manage identically named inline policies on the same role, and CloudFormation's + // single-stack ownership enforcement rejects the deploy ("Policy resource was already + // managed by another stack"). Appending the environment name keeps them distinct. + // + // For the default (managed) auth case each environment owns its own auth roles, so the + // identical policy names land on different roles and never collide. We deliberately keep + // the legacy `${policyUUID}` name there: a PolicyName change is replace-on-update for + // AWS::IAM::Policy, so suffixing unconditionally would force a policy replacement on + // EVERY existing storage app's next push -- churn (and risk) far beyond the shared-role + // bug this fixes. Gating on imported auth limits the rename to the environments that + // actually need it (and whose deploys are already failing). + const authResources = this.context.amplify.getProjectMeta?.()?.auth ?? {}; + const hasImportedAuth = Object.values(authResources).some( + (authResource: $TSAny) => authResource?.service === 'Cognito' && authResource?.serviceType === 'imported', + ); + let policyNameSuffix = userInput.policyUUID; + if (hasImportedAuth) { + // Imported auth shares roles across environments, so the env name is required to keep + // policy names distinct. generateCfnInputParameters only runs for a checked-out + // environment (push / add / update / override / export), so envName is expected here; + // fail fast with a clear error rather than silently producing a shared "_undefined". + const envName = this.context.amplify.getEnvInfo()?.envName; + if (typeof envName !== 'string' || envName.length === 0) { + throw new AmplifyError('EnvironmentNotInitializedError', { + message: 'Cannot determine the current Amplify environment name while generating S3 IAM policy names for imported auth.', + resolution: `Run 'amplify init' or 'amplify env checkout ' in the root of your app directory to select an environment, then try again.`, + }); + } + policyNameSuffix = `${userInput.policyUUID}_${envName}`; } - const policyNameSuffix = `${userInput.policyUUID}_${envName}`; this.cfnInputParams.s3PrivatePolicy = `Private_policy_${policyNameSuffix}`; this.cfnInputParams.s3ProtectedPolicy = `Protected_policy_${policyNameSuffix}`; this.cfnInputParams.s3PublicPolicy = `Public_policy_${policyNameSuffix}`;