Skip to content

[BUG] Expansion fails with "Hash must be finalized before the hash value is retrieved" when using an object output from a cross-scope module #3909

Description

@oWretch

Existing rule

N/A

Description of the issue

When a Bicep module is deployed to a different scope using the scope property (for example a nested management group), the resource ID recorded for the nested deployment does not account for the explicit scope.

DeploymentVisitor.GetDeploymentScope resolves the subscriptionId, resourceGroup and managementGroup properties of a deployment resource, but ignores the scope property. As a result the nested deployment is registered under its real scope, while the symbol resolves to the parent context scope. These do not match, so TemplateContext.TryGetResource fails to find the deployment.

reference() then falls back to a synthetic mock, and a chain such as reference('customRoles').outputs.policyReader.value.id resolves to an empty value instead of the expected string. When that empty value is passed to guid() as the last argument, expansion fails with:

Hash must be finalized before the hash value is retrieved

There are two defects here:

  1. The explicit scope property of a deployment resource is not used when calculating the resource ID, so cross-scope module outputs do not resolve.
  2. ExpressionHelpers.GetUnique only calls TransformFinalBlock when the final argument can be converted to a string. If it cannot, the hash is never finalized and reading HashAlgorithm.Hash throws. An argument that cannot be converted in any other position is silently skipped instead, which would produce a duplicate name rather than an error.

Error messages

Hash must be finalized before the hash value is retrieved

Inner stack trace:

at System.Security.Cryptography.HashAlgorithm.get_Hash()
at PSRule.Rules.Azure.Arm.Expressions.ExpressionHelpers.GetUnique(Object[] args)
at PSRule.Rules.Azure.Arm.Expressions.Functions.Guid(ITemplateContext context, Object[] args)

Reproduction

The module deployed with scope set to a child management group is required to reproduce the issue. Without scope the same template expands correctly.

main.bicep:

targetScope = 'managementGroup'

resource intermediateRoot 'Microsoft.Management/managementGroups@2023-04-01' = {
  scope: tenant()
  name: 'mg-intermediate-root'
  properties: {
    displayName: 'Intermediate Root'
  }
}

module customRoles './child.bicep' = {
  name: 'customRoles'
  scope: intermediateRoot
}

module roleAssignments './assignments.bicep' = {
  name: 'roleAssignments'
  scope: intermediateRoot
  params: {
    roleAssignments: [
      {
        principalId: '00000000-0000-0000-0000-000000000001'
        roleDefinitionId: customRoles.outputs.policyReader.id
      }
      {
        principalId: '00000000-0000-0000-0000-000000000002'
        roleDefinitionId: customRoles.outputs.policyReader.id
      }
    ]
  }
}

child.bicep:

targetScope = 'managementGroup'

resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' = {
  name: guid('policyReader', managementGroup().id)
  properties: {
    roleName: 'Policy Reader'
    description: 'Read only access to policy.'
    type: 'CustomRole'
    permissions: [
      {
        actions: [
          'Microsoft.Authorization/policyAssignments/read'
        ]
      }
    ]
    assignableScopes: [
      managementGroup().id
    ]
  }
}

output policyReader object = {
  id: roleDefinition.id
  name: roleDefinition.name
}

assignments.bicep:

targetScope = 'managementGroup'

type roleAssignmentInput = {
  principalId: string
  roleDefinitionId: string
}

param roleAssignments roleAssignmentInput[]

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
  for assignment in roleAssignments: {
    name: guid(managementGroup().id, assignment.principalId, assignment.roleDefinitionId)
    properties: {
      principalId: assignment.principalId
      principalType: 'ServicePrincipal'
      roleDefinitionId: assignment.roleDefinitionId
    }
  }
]

Expansion of main.bicep fails. Reducing the name to guid(assignment.roleDefinitionId) also fails, which isolates the failure to that argument. Removing scope: intermediateRoot from the modules allows expansion to succeed.

The template requires symbolicNameCodegen to be enabled in bicepconfig.json to produce the reference('customRoles') form.

Version of PSRule

2.9.0

Version of PSRule for Azure

1.48.0

Additional context

Note that fixing only the hash finalization is not sufficient, and would be worse than the current behaviour. The unresolved argument would be silently skipped, so every role assignment sharing a principal would collapse to the same guid() value and expansion would emit duplicate resource names instead of an error. The scope resolution needs to be fixed so the value resolves correctly.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions