Skip to content

Commit ba1885e

Browse files
authored
Merge pull request #81 from Azure/policymg
feat: add management group scope support for Policy and Role constructs
2 parents d59dae0 + 2f79d61 commit ba1885e

File tree

15 files changed

+467
-96
lines changed

15 files changed

+467
-96
lines changed

API.md

Lines changed: 104 additions & 76 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSE

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/azure-policyassignment/README.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This module provides a unified, version-aware implementation for managing Azure
44

55
## Overview
66

7-
Azure Policy Assignments apply policy definitions to specific scopes (subscription, resource group, or resource) and provide parameter values for policy enforcement. Policy assignments can configure enforcement modes, managed identities for remediation, and custom non-compliance messages.
7+
Azure Policy Assignments apply policy definitions to specific scopes (management group, subscription, resource group, or resource) and provide parameter values for policy enforcement. Policy assignments can configure enforcement modes, managed identities for remediation, and custom non-compliance messages.
88

99
## Key Features
1010

@@ -13,7 +13,7 @@ Azure Policy Assignments apply policy definitions to specific scopes (subscripti
1313
- **Schema-Driven Validation**: Built-in validation based on Azure API schemas
1414
- **Type-Safe**: Full TypeScript support with comprehensive interfaces
1515
- **JSII Compatible**: Can be used from multiple programming languages
16-
- **Flexible Scoping**: Support for subscription, resource group, and resource-level assignments
16+
- **Flexible Scoping**: Support for management group, subscription, resource group, and resource-level assignments
1717
- **Enforcement Modes**: Control whether policies are enforced or audited
1818
- **Managed Identity Support**: Enable remediation for deployIfNotExists and modify policies
1919
- **Scope Exclusions**: Exclude specific scopes from policy evaluation
@@ -285,6 +285,14 @@ console.log("Enforcement Mode:", assignment.enforcementMode);
285285

286286
Policy assignments can be applied at different organizational levels:
287287

288+
#### Management Group Scope
289+
290+
```typescript
291+
scope: "/providers/Microsoft.Management/managementGroups/my-mg";
292+
```
293+
294+
Applies to all subscriptions and resources within the management group hierarchy. This is the highest level scope and is ideal for organization-wide policies.
295+
288296
#### Subscription Scope
289297

290298
```typescript
@@ -390,6 +398,40 @@ Policy Assignment constructs expose the following outputs:
390398

391399
## Examples
392400

401+
### Assign Policy at Management Group Level
402+
403+
```typescript
404+
// Apply an organization-wide policy at management group scope
405+
const mgPolicyDefinition = new PolicyDefinition(this, "org-policy", {
406+
name: "require-resource-tags",
407+
parentId: "/providers/Microsoft.Management/managementGroups/my-mg",
408+
displayName: "Require Resource Tags",
409+
policyRule: {
410+
if: {
411+
field: "tags['CostCenter']",
412+
exists: "false",
413+
},
414+
then: {
415+
effect: "deny",
416+
},
417+
},
418+
});
419+
420+
const mgAssignment = new PolicyAssignment(this, "mg-tag-assignment", {
421+
name: "require-tags-org-wide",
422+
displayName: "Require Tags Across Organization",
423+
description: "Enforces required tags across all subscriptions in the management group",
424+
policyDefinitionId: mgPolicyDefinition.id,
425+
scope: "/providers/Microsoft.Management/managementGroups/my-mg",
426+
nonComplianceMessages: [
427+
{
428+
message:
429+
"All resources must have a CostCenter tag for billing and cost allocation purposes.",
430+
},
431+
],
432+
});
433+
```
434+
393435
### Assign Tag Policy at Subscription Level
394436

395437
```typescript

src/azure-policyassignment/lib/policy-assignment-schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const COMMON_PROPERTIES: { [key: string]: PropertyDefinition } = {
6363
dataType: PropertyType.STRING,
6464
required: true,
6565
description:
66-
"The scope at which the policy assignment is applied (subscription, resource group, or resource)",
66+
"The scope at which the policy assignment is applied (management group, subscription, resource group, or resource)",
6767
validation: [
6868
{
6969
ruleType: ValidationRuleType.REQUIRED,

src/azure-policyassignment/lib/policy-assignment.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
*
44
* This class provides a version-aware implementation for managing Azure Policy Assignments
55
* using the AZAPI provider. Policy assignments apply policy definitions to specific scopes
6-
* (subscription, resource group, or resource) and can provide parameter values and
7-
* enforcement settings.
6+
* (management group, subscription, resource group, or resource) and can provide parameter
7+
* values and enforcement settings.
88
*
99
* Supported API Versions:
1010
* - 2022-06-01 (Active, Latest)
@@ -89,9 +89,10 @@ export interface PolicyAssignmentProps extends AzapiResourceProps {
8989

9090
/**
9191
* The scope at which the policy assignment is applied
92-
* Can be a subscription, resource group, or resource
92+
* Can be a management group, subscription, resource group, or resource
9393
* Required property
9494
*
95+
* @example "/providers/Microsoft.Management/managementGroups/my-mg"
9596
* @example "/subscriptions/00000000-0000-0000-0000-000000000000"
9697
* @example "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-name"
9798
*/
@@ -262,8 +263,9 @@ export interface PolicyAssignmentBody {
262263
* Policy Assignments. It automatically handles version resolution, schema validation,
263264
* and property transformation.
264265
*
265-
* Note: Policy assignments can be deployed at subscription, resource group, or resource scope.
266-
* Like policy definitions, they do not have a location property as they are not region-specific.
266+
* Note: Policy assignments can be deployed at management group, subscription, resource group,
267+
* or resource scope. Like policy definitions, they do not have a location property as they
268+
* are not region-specific.
267269
*
268270
* @example
269271
* // Basic policy assignment:
@@ -302,6 +304,16 @@ export interface PolicyAssignmentBody {
302304
* }
303305
* });
304306
*
307+
* @example
308+
* // Policy assignment at management group scope:
309+
* const mgAssignment = new PolicyAssignment(this, "mgAssignment", {
310+
* name: "mg-policy-assignment",
311+
* policyDefinitionId: "/providers/Microsoft.Authorization/policyDefinitions/policy-id",
312+
* scope: "/providers/Microsoft.Management/managementGroups/my-mg",
313+
* displayName: "Management Group Policy",
314+
* description: "Applies policy across the entire management group hierarchy"
315+
* });
316+
*
305317
* @stability stable
306318
*/
307319
export class PolicyAssignment extends AzapiResource {

src/azure-policyassignment/test/policy-assignment.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,19 @@ describe("PolicyAssignment - Unified Implementation", () => {
793793
});
794794

795795
describe("Scope Configurations", () => {
796+
it("should support management group scope", () => {
797+
const assignment = new PolicyAssignment(stack, "ManagementGroupScope", {
798+
name: "mg-assignment",
799+
policyDefinitionId:
800+
"/providers/Microsoft.Authorization/policyDefinitions/test-policy",
801+
scope: "/providers/Microsoft.Management/managementGroups/my-mg",
802+
});
803+
804+
expect(assignment.assignmentScope).toContain(
805+
"/providers/Microsoft.Management/managementGroups/",
806+
);
807+
});
808+
796809
it("should support subscription scope", () => {
797810
const assignment = new PolicyAssignment(stack, "SubscriptionScope", {
798811
name: "subscription-assignment",

src/azure-policydefinition/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,32 @@ Policy Definition constructs expose the following outputs:
344344

345345
## Examples
346346

347+
### Policy Definition at Management Group Scope
348+
349+
```typescript
350+
// Create a policy definition at management group scope
351+
// This makes the policy available to all subscriptions under the management group
352+
new PolicyDefinition(this, "mg-policy", {
353+
name: "org-wide-tag-policy",
354+
parentId: "/providers/Microsoft.Management/managementGroups/my-mg",
355+
displayName: "Organization-Wide Tag Policy",
356+
description: "Enforces required tags across all subscriptions in the organization",
357+
policyRule: {
358+
if: {
359+
field: "tags['CostCenter']",
360+
exists: "false",
361+
},
362+
then: {
363+
effect: "deny",
364+
},
365+
},
366+
metadata: {
367+
category: "Tags",
368+
version: "1.0.0",
369+
},
370+
});
371+
```
372+
347373
### Require Specific Resource Locations
348374

349375
```typescript

src/azure-policydefinition/lib/policy-definition.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,17 @@ export interface PolicyDefinitionProps extends AzapiResourceProps {
112112
*/
113113
readonly metadata?: any;
114114

115+
/**
116+
* The parent scope where the policy definition should be created
117+
* Can be a management group or subscription scope
118+
* If not specified, defaults to subscription scope
119+
*
120+
* @example "/providers/Microsoft.Management/managementGroups/my-mg"
121+
* @example "/subscriptions/00000000-0000-0000-0000-000000000000"
122+
* @defaultValue Subscription scope (auto-detected from client config)
123+
*/
124+
readonly parentId?: string;
125+
115126
/**
116127
* The lifecycle rules to ignore changes
117128
* @example ["metadata"]
@@ -223,6 +234,24 @@ export interface PolicyDefinitionBody {
223234
* }
224235
* });
225236
*
237+
* @example
238+
* // Policy definition at management group scope:
239+
* const mgPolicyDefinition = new PolicyDefinition(this, "mgPolicy", {
240+
* name: "mg-require-tag-policy",
241+
* parentId: "/providers/Microsoft.Management/managementGroups/my-mg",
242+
* displayName: "Management Group Tag Policy",
243+
* description: "Enforces tags across the management group hierarchy",
244+
* policyRule: {
245+
* if: {
246+
* field: "tags['CostCenter']",
247+
* exists: "false"
248+
* },
249+
* then: {
250+
* effect: "deny"
251+
* }
252+
* }
253+
* });
254+
*
226255
* @stability stable
227256
*/
228257
export class PolicyDefinition extends AzapiResource {
@@ -368,6 +397,22 @@ export class PolicyDefinition extends AzapiResource {
368397
};
369398
}
370399

400+
/**
401+
* Overrides parent ID resolution to use parentId from props if provided
402+
* Policy definitions can be deployed at subscription or management group scope
403+
*/
404+
protected resolveParentId(props: any): string {
405+
const typedProps = props as PolicyDefinitionProps;
406+
407+
// If explicit parentId is provided, use it (management group or subscription)
408+
if (typedProps.parentId) {
409+
return typedProps.parentId;
410+
}
411+
412+
// Otherwise, fall back to default subscription scope
413+
return super.resolveParentId(props);
414+
}
415+
371416
// =============================================================================
372417
// PUBLIC METHODS FOR POLICY DEFINITION OPERATIONS
373418
// =============================================================================

src/azure-policydefinition/test/policy-definition.spec.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,4 +1062,106 @@ describe("PolicyDefinition - Unified Implementation", () => {
10621062
expect(resourceProviderPolicy.policyMode).toBe("Microsoft.KeyVault.Data");
10631063
});
10641064
});
1065+
1066+
describe("Management Group Scope Support", () => {
1067+
it("should support management group scope via parentId property", () => {
1068+
const mgPolicyDefinition = new PolicyDefinition(
1069+
stack,
1070+
"MgPolicyDefinition",
1071+
{
1072+
name: "mg-policy",
1073+
parentId: "/providers/Microsoft.Management/managementGroups/my-mg",
1074+
displayName: "Management Group Policy",
1075+
description: "Policy deployed at management group scope",
1076+
policyRule: {
1077+
if: {
1078+
field: "tags['CostCenter']",
1079+
exists: "false",
1080+
},
1081+
then: {
1082+
effect: "deny",
1083+
},
1084+
},
1085+
},
1086+
);
1087+
1088+
expect(mgPolicyDefinition).toBeDefined();
1089+
expect(mgPolicyDefinition.props.parentId).toBe(
1090+
"/providers/Microsoft.Management/managementGroups/my-mg",
1091+
);
1092+
1093+
// Verify the policy was created successfully
1094+
const synthesized = Testing.synth(stack);
1095+
expect(synthesized).toBeDefined();
1096+
1097+
const stackConfig = JSON.parse(synthesized);
1098+
const azapiResource = Object.values(
1099+
stackConfig.resource.azapi_resource,
1100+
)[0] as any;
1101+
1102+
expect(azapiResource.parent_id).toBe(
1103+
"/providers/Microsoft.Management/managementGroups/my-mg",
1104+
);
1105+
});
1106+
1107+
it("should default to subscription scope when parentId is not provided", () => {
1108+
new PolicyDefinition(stack, "SubPolicyDefinition", {
1109+
name: "sub-policy",
1110+
displayName: "Subscription Policy",
1111+
policyRule: {
1112+
if: {
1113+
field: "type",
1114+
equals: "Microsoft.Compute/virtualMachines",
1115+
},
1116+
then: {
1117+
effect: "audit",
1118+
},
1119+
},
1120+
});
1121+
1122+
const synthesized = Testing.synth(stack);
1123+
const stackConfig = JSON.parse(synthesized);
1124+
const azapiResource = Object.values(
1125+
stackConfig.resource.azapi_resource,
1126+
)[0] as any;
1127+
1128+
// Should default to subscription scope
1129+
expect(azapiResource.parent_id).toContain("/subscriptions/");
1130+
});
1131+
1132+
it("should support explicit subscription scope via parentId", () => {
1133+
const subPolicyDefinition = new PolicyDefinition(
1134+
stack,
1135+
"ExplicitSubPolicy",
1136+
{
1137+
name: "explicit-sub-policy",
1138+
parentId: "/subscriptions/00000000-0000-0000-0000-000000000000",
1139+
displayName: "Explicit Subscription Policy",
1140+
policyRule: {
1141+
if: {
1142+
field: "location",
1143+
equals: "eastus",
1144+
},
1145+
then: {
1146+
effect: "deny",
1147+
},
1148+
},
1149+
},
1150+
);
1151+
1152+
expect(subPolicyDefinition.props.parentId).toBe(
1153+
"/subscriptions/00000000-0000-0000-0000-000000000000",
1154+
);
1155+
1156+
const synthesized = Testing.synth(stack);
1157+
const stackConfig = JSON.parse(synthesized);
1158+
const azapiResource = Object.values(
1159+
stackConfig.resource.azapi_resource,
1160+
)[0] as any;
1161+
1162+
expect(azapiResource.parent_id).toBe(
1163+
"/subscriptions/00000000-0000-0000-0000-000000000000",
1164+
);
1165+
});
1166+
});
10651167
});

0 commit comments

Comments
 (0)