Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ What's changed since pre-release v1.48.0-B0228:
[#3907](https://github.com/Azure/PSRule.Rules.Azure/issues/3907)
- Fixed cross-scope `existing` resource reference via symbolic name resolving to a malformed mock, breaking `concat()`/`map()` during pre-flight expansion.
[#3920](https://github.com/Azure/PSRule.Rules.Azure/issues/3920)
- Fixed `uniqueString()` and `guid()` failing on the `principalId` or `clientId` of an `existing` user-assigned identity during expansion.
[#3972](https://github.com/Azure/PSRule.Rules.Azure/issues/3972)
- Engineering:
- Bump YamlDotNet to 11.2.5.

Expand Down
6 changes: 6 additions & 0 deletions src/PSRule.Rules.Azure/Arm/Mocks/Mock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,24 @@ internal sealed class Mock
private const string RESOURCE_TYPE_VIRTUAL_NETWORK = "Microsoft.Network/virtualNetworks";
private const string RESOURCE_TYPE_SUBNET = "Microsoft.Network/virtualNetworks/subnets";
private const string RESOURCE_TYPE_IPAM_POOL = "Microsoft.Network/networkManagers/ipamPools";
private const string RESOURCE_TYPE_USER_ASSIGNED_IDENTITY = "Microsoft.ManagedIdentity/userAssignedIdentities";
private const string PATH_VIRTUAL_NETWORK_ADDRESS_PREFIXES = "addressSpace.addressPrefixes";
private const string PATH_SUBNET_ADDRESS_PREFIX = "addressPrefix";
private const string PATH_SUBNET_ADDRESS_PREFIXES = "addressPrefixes";
private const string PATH_PRINCIPAL_ID = "principalId";
private const string PATH_CLIENT_ID = "clientId";
private const string PLACEHOLDER_VIRTUAL_NETWORK_CIDR = "192.0.2.0/24";
private const string PLACEHOLDER_SUBNET_CIDR = "192.0.2.0/28";
private const string PLACEHOLDER_GUID = "ffffffff-ffff-ffff-ffff-ffffffffffff";

private static readonly MockPlaceholderDescriptor[] _ResourcePlaceholders =
[
new(RESOURCE_TYPE_VIRTUAL_NETWORK, PATH_VIRTUAL_NETWORK_ADDRESS_PREFIXES, MockPlaceholderKind.Array, [PLACEHOLDER_VIRTUAL_NETWORK_CIDR]),
new(RESOURCE_TYPE_SUBNET, PATH_SUBNET_ADDRESS_PREFIX, MockPlaceholderKind.String, [PLACEHOLDER_SUBNET_CIDR]),
new(RESOURCE_TYPE_SUBNET, PATH_SUBNET_ADDRESS_PREFIXES, MockPlaceholderKind.Array, [PLACEHOLDER_SUBNET_CIDR]),
new(RESOURCE_TYPE_IPAM_POOL, PATH_SUBNET_ADDRESS_PREFIXES, MockPlaceholderKind.Array, [PLACEHOLDER_VIRTUAL_NETWORK_CIDR]),
new(RESOURCE_TYPE_USER_ASSIGNED_IDENTITY, PATH_PRINCIPAL_ID, MockPlaceholderKind.String, [PLACEHOLDER_GUID]),
new(RESOURCE_TYPE_USER_ASSIGNED_IDENTITY, PATH_CLIENT_ID, MockPlaceholderKind.String, [PLACEHOLDER_GUID]),
];

private enum MockPlaceholderKind
Expand Down
16 changes: 16 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Arm/Expressions/FunctionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,22 @@ public void ReferenceUsesConcreteNetworkAddressProperties()
Assert.Equal("203.0.113.0/24", actual["addressSpace"]["addressPrefixes"][0].Value<string>());
}

[Fact]
[Trait(TRAIT, TRAIT_RESOURCE)]
public void ReferenceWithIdentityPlaceholders()
{
var context = GetContext();
var identityId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-001";

var identity = Functions.Reference(context, [identityId]) as Mock.MockObject;
Assert.NotNull(identity);
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", identity["principalId"].Value<string>());
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", identity["clientId"].Value<string>());
Assert.Equal(JTokenType.String, identity["principalId"].Type);
Assert.NotNull(Functions.UniqueString(context, ["vault", identity["principalId"]]));
Assert.NotNull(Functions.Guid(context, [identityId, identity["principalId"]]));
}

[Fact]
[Trait(TRAIT, TRAIT_RESOURCE)]
public void References()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public void ProcessTemplate_WhenConditionalExistingReference_IgnoresExpand()

actual = resources[2];
Assert.Equal("Microsoft.Authorization/roleAssignments", actual["type"].Value<string>());
Assert.Equal("02041802-66a9-0a85-7330-8186e16422c7", actual["name"].Value<string>());
Assert.Equal("2f5ec487-2ecb-1424-fd53-71891b2f5eac", actual["name"].Value<string>());
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@
<None Update="Tests.Bicep.45.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Tests.Bicep.46.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Bicep\SymbolicNameTestCases\Tests.Bicep.1.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Expand Down
18 changes: 18 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1444,4 +1444,22 @@ public void ProcessTemplate_WhenExistingResourceReferencedByResourceId_ShouldRet
Assert.True(templateContext.RootDeployment.TryOutput("combinedAddressPrefixes", out JObject combinedAddressPrefixesOutput));
Assert.Equal(JTokenType.Array, combinedAddressPrefixesOutput["value"].Type);
}

/// <summary>
/// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3972
/// </summary>
[Fact]
public void ProcessTemplate_WhenExistingResourcePropertyPassedToModuleAndHashed_ShouldExpand()
{
var resources = ProcessTemplate(GetSourcePath("Tests.Bicep.46.json"), null, out _);

var deployment = resources.FirstOrDefault(r => r["type"].Value<string>() == "Microsoft.Resources/deployments" && r["name"].Value<string>().StartsWith("ra-", System.StringComparison.Ordinal));
Assert.NotNull(deployment);
Assert.Matches("^ra-[a-z0-9]{13}$", deployment["name"].Value<string>());

var assignment = resources.FirstOrDefault(r => r["type"].Value<string>() == "Microsoft.Authorization/roleAssignments");
Assert.NotNull(assignment);
Assert.True(System.Guid.TryParse(assignment["name"].Value<string>(), out _));
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", assignment["properties"]["principalId"].Value<string>());
}
}
24 changes: 24 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Tests.Bicep.46.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3972

param identityResourceGroup string = 'rg-identity'
param identityName string = 'id-issue-3972'

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
name: identityName
scope: resourceGroup(identityResourceGroup)
}

module child './Tests.Bicep.46.child.bicep' = {
name: 'child'
params: {
grants: [
{
name: 'vault'
principalId: identity.properties.principalId
}
]
}
}
13 changes: 13 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Tests.Bicep.46.child.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

param grants array

module assignment './Tests.Bicep.46.grant.bicep' = [
for grant in grants: {
name: 'ra-${uniqueString(grant.name, grant.principalId)}'
params: {
principalId: grant.principalId
}
}
]
15 changes: 15 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Tests.Bicep.46.grant.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

param principalId string

resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, principalId, '4633458b-17de-408a-b874-0445c86b69e6')
properties: {
principalId: principalId
principalType: 'ServicePrincipal'
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
}
}

output principalId string = principalId
116 changes: 116 additions & 0 deletions tests/PSRule.Rules.Azure.Tests/Tests.Bicep.46.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.48.1.52935",
"templateHash": "6919797167586006276"
}
},
"parameters": {
"identityResourceGroup": {
"type": "string",
"defaultValue": "rg-identity"
},
"identityName": {
"type": "string",
"defaultValue": "id-issue-3972"
}
},
"resources": [
{
"type": "Microsoft.Resources/deployments",
"apiVersion": "2025-04-01",
"name": "child",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"parameters": {
"grants": {
"value": [
{
"name": "vault",
"principalId": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('identityResourceGroup')), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('identityName')), '2023-01-31').principalId]"
}
]
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.48.1.52935",
"templateHash": "5082234139662032612"
}
},
"parameters": {
"grants": {
"type": "array"
}
},
"resources": [
{
"copy": {
"name": "assignment",
"count": "[length(parameters('grants'))]"
},
"type": "Microsoft.Resources/deployments",
"apiVersion": "2025-04-01",
"name": "[format('ra-{0}', uniqueString(parameters('grants')[copyIndex()].name, parameters('grants')[copyIndex()].principalId))]",
"properties": {
"expressionEvaluationOptions": {
"scope": "inner"
},
"mode": "Incremental",
"parameters": {
"principalId": {
"value": "[parameters('grants')[copyIndex()].principalId]"
}
},
"template": {
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_generator": {
"name": "bicep",
"version": "0.48.1.52935",
"templateHash": "3551328931435798587"
}
},
"parameters": {
"principalId": {
"type": "string"
}
},
"resources": [
{
"type": "Microsoft.Authorization/roleAssignments",
"apiVersion": "2022-04-01",
"name": "[guid(resourceGroup().id, parameters('principalId'), '4633458b-17de-408a-b874-0445c86b69e6')]",
"properties": {
"principalId": "[parameters('principalId')]",
"principalType": "ServicePrincipal",
"roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]"
}
}
],
"outputs": {
"principalId": {
"type": "string",
"value": "[parameters('principalId')]"
}
}
}
}
}
]
}
}
}
]
}