Skip to content

fix: Fail on generated IAM role logical ID collisions - #4007

Open
licjun wants to merge 2 commits into
aws:developfrom
licjun:fix/generated-role-logical-id-collision
Open

licjun wants to merge 2 commits into
aws:developfrom
licjun:fix/generated-role-logical-id-collision

Conversation

@licjun

@licjun licjun commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Issue #, if available

N/A

Description of changes

Generated resources are merged into the output template with dict.update(), and verify_unique_logical_id only checks them against resources written in the input template. When two generators produce an IAM role with the same logical ID, the later role silently overwrites the earlier one and a resource ends up with another resource's permissions.

Example: Function NetOperator generates NetOperatorRole (<Id>Role), and NetworkConnector Net generates NetOperatorRole (<Id>OperatorRole). Today the transform succeeds and the function's role is the NetworkConnector operator role.

Changes:

  • verify_logical_id.py: add GeneratedLogicalIdTracker, which records which source generated each logical ID and reports a conflict when a different source generates the same one. Which resources are checked is a should_check parameter (default is_iam_role), so the scope can be widened later without changing the tracker or its callers.
  • translator.py:
    • Use the tracker in the main SAM resource loop.
    • Also check the CodeDeploy service role (CodeDeployServiceRole), which is added after the loop and is not generated by any single SAM resource. It is recorded under a dedicated source, CODEDEPLOY_SERVICE_ROLE_SOURCE = "AWS::CodeDeploy::ServiceRole", which can never equal a SAM logical ID. This catches a collision with any function's execution role, including the function that has the DeploymentPreference itself.
    • A CodeDeploy role collision is reported as InvalidResourceException on the colliding function: It generates an IAM role with id [CodeDeployServiceRole], which is reserved for the CodeDeploy service role generated for DeploymentPreference. Please use a different id for this resource.
  • exceptions.py: DuplicateLogicalIdException takes an optional conflicting_logical_id so the message names both resources. The existing message is unchanged when it is not passed.

Why only IAM roles: SAM never intentionally generates the same IAM role from two different generators (every generated role name derives from its own resource's logical ID; CodeDeployServiceRole is generated once). Other types are intentionally shared across SAM resources under one logical ID (ServerlessUsagePlan / ServerlessApiKey / ServerlessUsagePlanKey, ApiGatewayDomainName<hash>, RecordSetGroup<hash>), so checking all types would need an allowlist and risk breaking existing templates.

No existing logical IDs change. Templates that currently hit this collision will now fail to transform instead of silently getting the wrong role.

Description of how you validated changes

  • New transform error tests:
    • error_generated_role_logical_id_collision (Function + NetworkConnector)
    • error_codedeploy_role_logical_id_collision (Function CodeDeployService + a different function with DeploymentPreference)
    • error_codedeploy_role_logical_id_self_collision (single Function CodeDeployService that has the DeploymentPreference)
  • New unit tests in tests/translator/test_verify_logical_id.py for the tracker, including a widened scope.
  • pytest tests/translator: 2428 passed.
  • bin/run_cfn_lint.sh: exit 0.
  • ruff check: passes.

Checklist

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

licjun added 2 commits October 6, 2026 12:55
Generated resources are merged into the output template with
dict.update(), and verify_unique_logical_id only checks them against
resources written in the input template. When two SAM resources
generate an IAM role with the same logical ID (e.g. Function
"NetOperator" -> "NetOperatorRole" and NetworkConnector "Net" ->
"NetOperatorRole"), the later role silently overwrites the earlier one
and a resource ends up with another resource's permissions.

Add GeneratedLogicalIdTracker to record which SAM resource generated
each logical ID, and raise DuplicateLogicalIdException when a different
SAM resource generates the same one. The check also covers the
CodeDeploy service role, which is added after the main loop.

The scope is limited to AWS::IAM::Role for now, because other resource
types (shared usage plans, custom domains, Route53 record set groups)
are intentionally generated by multiple SAM resources. The scope is a
parameter of the tracker so it can be widened later.
The CodeDeploy service role was recorded in GeneratedLogicalIdTracker
under the first function with a DeploymentPreference. When that same
function is named "CodeDeployService", its execution role
"CodeDeployServiceRole" and the CodeDeploy service role share both the
logical ID and the recorded source, so the tracker reported no conflict
and the execution role was silently overwritten.

Record the CodeDeploy service role under a dedicated source that no
SAM logical ID can match, and report the collision as an invalid
resource with a message explaining that the role id is reserved for
the CodeDeploy service role.
@licjun
licjun requested a review from a team as a code owner October 6, 2026 19:57

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants