Skip to content

Commit

Permalink
Fixes tenant scope resource ID #3237 (#3246)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Feb 7, 2025
1 parent 4f32a0c commit ca8ce6c
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 14 deletions.
6 changes: 6 additions & 0 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

## Unreleased

What's changed since v1.41.0:

- Bug fixes:
- Fixed incorrect generation of resource ID for tenant scoped deployments by @BernieWhite.
[#3237](https://github.com/Azure/PSRule.Rules.Azure/issues/3237)

## v1.41.0

What's changed since v1.40.0:
Expand Down
14 changes: 14 additions & 0 deletions src/PSRule.Rules.Azure/Common/ResourceHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ internal static bool TryResourceGroup(string resourceId, out string? subscriptio
TryConsumeResourceGroupPart(idParts, ref i, out resourceGroupName);
}

/// <summary>
/// Get the name of the management group from the specified resource Id.
/// </summary>
internal static bool TryManagementGroup(string resourceId, out string? managementGroup)
{
managementGroup = null;
if (string.IsNullOrEmpty(resourceId))
return false;

var idParts = resourceId.Split(SLASH_C);
var i = 0;
return TryConsumeManagementGroupPart(idParts, ref i, out managementGroup);
}

/// <summary>
/// Combines Id fragments to form a resource Id.
/// </summary>
Expand Down
40 changes: 26 additions & 14 deletions src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal abstract class TemplateVisitor : ResourceManagerVisitor
private const string PROPERTY_TYPE = "type";
private const string PROPERTY_PROPERTIES = "properties";
private const string PROPERTY_TEMPLATE = "template";
private const string PROPERTY_TEMPLATELINK = "templateLink";
private const string PROPERTY_TEMPLATE_LINK = "templateLink";
private const string PROPERTY_LOCATION = "location";
private const string PROPERTY_COPY = "copy";
private const string PROPERTY_NAME = "name";
Expand All @@ -49,15 +49,15 @@ internal abstract class TemplateVisitor : ResourceManagerVisitor
private const string PROPERTY_MODE = "mode";
private const string PROPERTY_DEFAULTVALUE = "defaultValue";
private const string PROPERTY_SECRETNAME = "secretName";
private const string PROPERTY_PROVISIONINGSTATE = "provisioningState";
private const string PROPERTY_PROVISIONING_STATE = "provisioningState";
private const string PROPERTY_ID = "id";
private const string PROPERTY_URI = "uri";
private const string PROPERTY_TEMPLATEHASH = "templateHash";
private const string PROPERTY_EXPRESSIONEVALUATIONOPTIONS = "expressionEvaluationOptions";
private const string PROPERTY_SCOPE = "scope";
private const string PROPERTY_RESOURCEGROUP = "resourceGroup";
private const string PROPERTY_SUBSCRIPTIONID = "subscriptionId";
private const string PROPERTY_MANAGEMENTGROUP = "managementGroup";
private const string PROPERTY_RESOURCE_GROUP = "resourceGroup";
private const string PROPERTY_SUBSCRIPTION_ID = "subscriptionId";
private const string PROPERTY_MANAGEMENT_GROUP = "managementGroup";
private const string PROPERTY_NAMESPACE = "namespace";
private const string PROPERTY_MEMBERS = "members";
private const string PROPERTY_OUTPUT = "output";
Expand Down Expand Up @@ -481,7 +481,7 @@ internal void EnterDeployment(string deploymentName, JObject template, bool isNe
TryObjectProperty(template, PROPERTY_METADATA, out var metadata);
TryStringProperty(template, PROPERTY_SCHEMA, out var schema);
var scope = GetDeploymentScope(schema, out var deploymentScope);
var id = string.Concat(scope, "/providers/", RESOURCE_TYPE_DEPLOYMENT, "/", deploymentName);
var id = string.Concat(scope == "/" ? string.Empty : scope, "/providers/", RESOURCE_TYPE_DEPLOYMENT, "/", deploymentName);
var location = ResourceGroup.Location;

var templateLink = new JObject
Expand All @@ -493,10 +493,10 @@ internal void EnterDeployment(string deploymentName, JObject template, bool isNe
var properties = new JObject
{
{ PROPERTY_TEMPLATE, template.CloneTemplateJToken() },
{ PROPERTY_TEMPLATELINK, templateLink },
{ PROPERTY_TEMPLATE_LINK, templateLink },
{ PROPERTY_PARAMETERS, _Parameters },
{ PROPERTY_MODE, "Incremental" },
{ PROPERTY_PROVISIONINGSTATE, "Accepted" },
{ PROPERTY_PROVISIONING_STATE, "Accepted" },
{ PROPERTY_TEMPLATEHASH, templateHash },
{ PROPERTY_OUTPUTS, new JObject() }
};
Expand Down Expand Up @@ -673,7 +673,7 @@ internal bool TryParameterDefault(string parameterName, ParameterType type, out
return true;
}
break;
};
}
return false;
}

Expand Down Expand Up @@ -1406,14 +1406,14 @@ private static DeploymentScope GetDeploymentScope(TemplateContext context, JObje
}

// Handle special case for cross-scope deployments which may have an alternative subscription or resource group set.
subscriptionId = ResolveDeploymentScopeProperty(context, resource, PROPERTY_SUBSCRIPTIONID, contextValue:
subscriptionId = ResolveDeploymentScopeProperty(context, resource, PROPERTY_SUBSCRIPTION_ID, contextValue:
context.Deployment.DeploymentScope == DeploymentScope.Subscription ||
context.Deployment.DeploymentScope == DeploymentScope.ResourceGroup ? context.Subscription.SubscriptionId : null);

resourceGroupName = ResolveDeploymentScopeProperty(context, resource, PROPERTY_RESOURCEGROUP, contextValue:
resourceGroupName = ResolveDeploymentScopeProperty(context, resource, PROPERTY_RESOURCE_GROUP, contextValue:
context.Deployment.DeploymentScope == DeploymentScope.ResourceGroup ? context.ResourceGroup.Name : null);

managementGroup = ResolveDeploymentScopeProperty(context, resource, PROPERTY_MANAGEMENTGROUP, contextValue:
managementGroup = ResolveDeploymentScopeProperty(context, resource, PROPERTY_MANAGEMENT_GROUP, contextValue:
context.Deployment.DeploymentScope == DeploymentScope.ManagementGroup ? context.ManagementGroup.Name : null);

// Update the deployment scope.
Expand Down Expand Up @@ -1541,20 +1541,32 @@ private TemplateContext GetDeploymentContext(TemplateContext context, string dep
var resourceGroup = new ResourceGroupOption(context.ResourceGroup);
var tenant = new TenantOption(context.Tenant);
var managementGroup = new ManagementGroupOption(context.ManagementGroup);
if (TryStringProperty(resource, PROPERTY_SUBSCRIPTIONID, out var subscriptionId))
if (TryStringProperty(resource, PROPERTY_SUBSCRIPTION_ID, out var subscriptionId))
{
var targetSubscriptionId = ExpandString(context, subscriptionId);
if (!string.IsNullOrEmpty(subscriptionId))
subscription.SubscriptionId = targetSubscriptionId;
}

if (TryStringProperty(resource, PROPERTY_RESOURCEGROUP, out var resourceGroupName))
if (TryStringProperty(resource, PROPERTY_RESOURCE_GROUP, out var resourceGroupName))
{
var targetResourceGroup = ExpandString(context, resourceGroupName);
if (!string.IsNullOrEmpty(targetResourceGroup))
resourceGroup.Name = targetResourceGroup;
}

if (TryStringProperty(resource, PROPERTY_SCOPE, out var scopeId) && ResourceHelper.ResourceIdComponents(scopeId, out _, out var managementGroupName, out subscriptionId, out resourceGroupName, out _, out _))
{
if (!string.IsNullOrEmpty(managementGroupName))
managementGroup.Name = managementGroupName;

if (!string.IsNullOrEmpty(subscriptionId))
subscription.SubscriptionId = subscriptionId;

if (!string.IsNullOrEmpty(resourceGroupName))
resourceGroup.Name = resourceGroupName;
}

resourceGroup.SubscriptionId = subscription.SubscriptionId;
TryObjectProperty(template, PROPERTY_PARAMETERS, out var templateParameters);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,20 @@ public void ProcessTemplate_WhenResourceGroupFromSubscriptionScope_ShouldReturnC
var property = actual["identity"]["userAssignedIdentities"].Value<JObject>().Properties().FirstOrDefault();
Assert.Equal("/subscriptions/ffffffff-ffff-ffff-ffff-ffffffffffff/resourceGroups/rg-1/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-1", property.Name);
}

[Fact]
public void ProcessTemplate_WhenTenantScope_ShouldReturnCorrectIdentifiers()
{
var resources = ProcessTemplate(GetSourcePath("Bicep/ScopeTestCases/Tests.Bicep.3.json"), null, out _);

Assert.NotNull(resources);

var actual = resources.Where(r => r["name"].Value<string>() == "mg-test").FirstOrDefault();
Assert.Equal("Microsoft.Management/managementGroups", actual["type"].Value<string>());
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-test", actual["id"].Value<string>());

actual = resources.Where(r => r["name"].Value<string>() == "mg-test/00000000-0000-0000-0000-000000000000").FirstOrDefault();
Assert.Equal("Microsoft.Management/managementGroups/subscriptions", actual["type"].Value<string>());
Assert.Equal("/providers/Microsoft.Management/managementGroups/mg-test/subscriptions/00000000-0000-0000-0000-000000000000", actual["id"].Value<string>());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

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

// Based on AVM subscription placement module.

targetScope = 'tenant'

module mg './Tests.Bicep.3.child1.bicep' = {
name: 'mg'
scope: tenant()
}

module child './Tests.Bicep.3.child2.bicep' = {
name: 'child'
params: {
managementGroups: [
{
id: mg.outputs.managementGroupId
subscriptionId: mg.outputs.subscriptionId
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

targetScope = 'tenant'

resource managementGroup 'Microsoft.Management/managementGroups@2023-04-01' = {
name: 'mg-test'
properties: {
displayName: 'Test Management Group'
details: {
parent: {
id: ''
}
}
}
}

output managementGroupId string = managementGroup.id
output subscriptionId string = '00000000-0000-0000-0000-000000000000'
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

targetScope = 'tenant'

param managementGroups managementGroup[]

type managementGroup = {
id: string
subscriptionId: string
}

module customSubscriptionPlacement './Tests.Bicep.3.child3.bicep' = [
for (place, index) in managementGroups: {
name: 'placement-${uniqueString(place.id)}${index}'
params: {
managementGroupId: place.id
subscriptionId: place.subscriptionId
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

targetScope = 'tenant'

param managementGroupId string
param subscriptionId string

resource customSubscriptionPlacement 'Microsoft.Management/managementGroups/subscriptions@2023-04-01' = {
name: '${last(split(managementGroupId, '/'))}/${subscriptionId}'
}
Loading

0 comments on commit ca8ce6c

Please sign in to comment.