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:
- The explicit
scope property of a deployment resource is not used when calculating the resource ID, so cross-scope module outputs do not resolve.
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.
Existing rule
N/A
Description of the issue
When a Bicep module is deployed to a different scope using the
scopeproperty (for example a nested management group), the resource ID recorded for the nested deployment does not account for the explicit scope.DeploymentVisitor.GetDeploymentScoperesolves thesubscriptionId,resourceGroupandmanagementGroupproperties of a deployment resource, but ignores thescopeproperty. 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, soTemplateContext.TryGetResourcefails to find the deployment.reference()then falls back to a synthetic mock, and a chain such asreference('customRoles').outputs.policyReader.value.idresolves to an empty value instead of the expected string. When that empty value is passed toguid()as the last argument, expansion fails with:There are two defects here:
scopeproperty of a deployment resource is not used when calculating the resource ID, so cross-scope module outputs do not resolve.ExpressionHelpers.GetUniqueonly callsTransformFinalBlockwhen the final argument can be converted to a string. If it cannot, the hash is never finalized and readingHashAlgorithm.Hashthrows. 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
Inner stack trace:
Reproduction
The module deployed with
scopeset to a child management group is required to reproduce the issue. Withoutscopethe same template expands correctly.main.bicep:child.bicep:assignments.bicep:Expansion of
main.bicepfails. Reducing the name toguid(assignment.roleDefinitionId)also fails, which isolates the failure to that argument. Removingscope: intermediateRootfrom the modules allows expansion to succeed.The template requires
symbolicNameCodegento be enabled inbicepconfig.jsonto produce thereference('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.