diff --git a/.changes/unreleased/changed-20251223-145359.yaml b/.changes/unreleased/changed-20251223-145359.yaml new file mode 100644 index 000000000..f91884cf3 --- /dev/null +++ b/.changes/unreleased/changed-20251223-145359.yaml @@ -0,0 +1,5 @@ +kind: changed +body: Extended powerplatform_enterprise_policy resource to support Identity policy type +time: 2025-12-23T14:53:59.263648543Z +custom: + Issue: "917" diff --git a/.terraformrc b/.terraformrc index 55101451c..0df19ad7a 100644 --- a/.terraformrc +++ b/.terraformrc @@ -2,6 +2,9 @@ provider_installation { # If GOBIN env is different from /go/bin, change it to the GOBIN value. # If GOBIN is not set, and GOPATH is different from /go/bin, change it to ${GOPATH}/bin + # to add additional local binaries read this doc: https://developer.hashicorp.com/terraform/cli/config/config-file#locations + # and use: + # `export TF_CLI_CONFIG_FILE=/workspaces/terraform-provider-power-platform/.terraformrc` dev_overrides { "registry.terraform.io/microsoft/power-platform" = "${GOPATH}/bin" } diff --git a/docs/resources/enterprise_policy.md b/docs/resources/enterprise_policy.md index 67119a15b..bff7d804a 100644 --- a/docs/resources/enterprise_policy.md +++ b/docs/resources/enterprise_policy.md @@ -76,6 +76,23 @@ resource "powerplatform_managed_environment" "managed_development" { suppress_validation_emails = true } +// module that creates all azure resources required for the identity policy and the policy itself +module "identity" { + source = "./identity" + + should_register_provider = false + + //Entra Object ID for administrator user that will configure Azure Synapse Link using Managed Identity + policy_reader_object_id = "00000000-0000-0000-0000-000000000000" + + environment_id = powerplatform_environment.example_environment.id + + resource_group_name = "rg_example_identity" + resource_group_location = local.europe_location[0].azure_regions[0] + + enterprise_policy_name = "ep_example_identity_policy" + enterprise_policy_location = local.europe_location[0].name +} // module that creates all azure resources required for the network injection policy and the policy itself module "network_injection" { @@ -90,6 +107,8 @@ module "network_injection" { vnet_locations = local.europe_location[0].azure_regions enterprise_policy_name = "ep_example_network_injection_policy" enterprise_policy_location = local.europe_location[0].name +// let's wait for first policy to be executed + depends_on = [module.identity] } // module that creates all azure resources required for the encryption policy and the policy itself @@ -100,14 +119,13 @@ module "encryption" { environment_id = powerplatform_environment.example_environment.id - resource_group_name = "rg_example_encryption_policy8" + resource_group_name = "rg_example_encryption_policy" resource_group_location = local.europe_location[0].azure_regions[0] - enterprise_policy_name = "ep_example_encryption_policy8" + enterprise_policy_name = "ep_example_encryption_policy" enterprise_policy_location = "europe" - keyvault_name = "kv-ep-example8" - - // let's wait for first policy to be executed - depends_on = [powerplatform_enterprise_policy.network_injection] + keyvault_name = "kv-ep-example" + // let's wait for second policy to be executed + depends_on = [module.network_injection] } ``` @@ -117,7 +135,7 @@ module "encryption" { ### Required - `environment_id` (String) Environment id -- `policy_type` (String) Policy type [NetworkInjection, Encryption] +- `policy_type` (String) Policy type [NetworkInjection, Encryption, Identity] - `system_id` (String) Policy SystemId value in following format `/regions//providers/Microsoft.PowerPlatform/enterprisePolicies/` ### Optional diff --git a/examples/resources/powerplatform_enterprise_policy/encryption/main.tf b/examples/resources/powerplatform_enterprise_policy/encryption/main.tf index 46da48506..e0842ca32 100644 --- a/examples/resources/powerplatform_enterprise_policy/encryption/main.tf +++ b/examples/resources/powerplatform_enterprise_policy/encryption/main.tf @@ -1,11 +1,17 @@ terraform { required_version = "> 1.7.0" required_providers { + powerplatform = { + source = "microsoft/power-platform" + version = "~>4.0" + } azapi = { - source = "azure/azapi" + source = "azure/azapi" + version = "~>2.0" } azurerm = { - source = "hashicorp/azurerm" + source = "hashicorp/azurerm" + version = "~>4.8" } } } diff --git a/examples/resources/powerplatform_enterprise_policy/identity/main.tf b/examples/resources/powerplatform_enterprise_policy/identity/main.tf new file mode 100644 index 000000000..54f2cf99e --- /dev/null +++ b/examples/resources/powerplatform_enterprise_policy/identity/main.tf @@ -0,0 +1,124 @@ +terraform { + required_version = "> 1.7.0" + required_providers { + powerplatform = { + source = "microsoft/power-platform" + version = "~>4.0" + } + azapi = { + source = "azure/azapi" + version = "~>2.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~>4.8" + } + } +} + + +variable "environment_id" { + description = "The ID of the environment" + type = string + validation { + condition = length(var.environment_id) > 0 + error_message = "The environment ID must not be empty" + } +} + +variable "should_register_provider" { + description = "A flag to determine if the PowerPlatform provider should be registered in the subscription" + type = bool + default = true +} + +variable "resource_group_name" { + description = "The name of the resource group" + type = string + validation { + condition = length(var.resource_group_name) > 0 + error_message = "The resource group name must not be empty" + } +} + +variable "resource_group_location" { + description = "The location of the resource group" + type = string + validation { + condition = length(var.resource_group_location) > 0 + error_message = "The resource group location must not be empty" + } +} + +variable "enterprise_policy_name" { + description = "The name of the enterprise policy" + type = string + validation { + condition = length(var.enterprise_policy_name) > 0 + error_message = "The enterprise policy name must not be empty" + } +} + +variable "enterprise_policy_location" { + description = "The location of the enterprise policy" + type = string + validation { + condition = length(var.enterprise_policy_location) > 0 + error_message = "The enterprise policy location must not be empty" + } +} + +variable "policy_reader_object_id" { + description = "The object ID of the user to assign the Reader role to (needed to finish the setup of the Azure Synapse Link to Dataverse)" + type = string + validation { + condition = length(var.policy_reader_object_id) > 0 + error_message = "The policy reader object ID must not be empty" + } +} + +resource "azurerm_resource_group" "resource_group" { + name = var.resource_group_name + location = var.resource_group_location +} + +resource "azurerm_resource_provider_registration" "provider_registration" { + count = var.should_register_provider ? 1 : 0 + name = "Microsoft.PowerPlatform" +} + +resource "azurerm_role_assignment" "policy_reader" { + scope = azapi_resource.powerplatform_policy.id + role_definition_name = "Reader" + principal_id = var.policy_reader_object_id +} + +resource "azapi_resource" "powerplatform_policy" { + schema_validation_enabled = false + + type = "Microsoft.PowerPlatform/enterprisePolicies@2020-10-30-preview" + name = var.enterprise_policy_name + location = var.enterprise_policy_location + parent_id = azurerm_resource_group.resource_group.id + body = { + identity = { + type = "SystemAssigned" + } + kind = "Identity" + } +} + +resource "powerplatform_enterprise_policy" "identity_policy" { + environment_id = var.environment_id + system_id = azapi_resource.powerplatform_policy.output.properties.systemId + policy_type = "Identity" +} + +output "enterprise_policy_system_id" { + value = azapi_resource.powerplatform_policy.output.properties.systemId +} + +output "enterprise_policy_id" { + value = azapi_resource.powerplatform_policy.output.id +} + diff --git a/examples/resources/powerplatform_enterprise_policy/network_injection/main.tf b/examples/resources/powerplatform_enterprise_policy/network_injection/main.tf index 87652e5e2..0e03f4147 100644 --- a/examples/resources/powerplatform_enterprise_policy/network_injection/main.tf +++ b/examples/resources/powerplatform_enterprise_policy/network_injection/main.tf @@ -1,13 +1,17 @@ terraform { required_version = "> 1.7.0" required_providers { + powerplatform = { + source = "microsoft/power-platform" + version = "~>4.0" + } azapi = { source = "azure/azapi" - version = "~>2.2.0" + version = "~>2.0" } azurerm = { source = "hashicorp/azurerm" - version = "~>4.16.0" + version = "~>4.8" } } } diff --git a/examples/resources/powerplatform_enterprise_policy/resource.tf b/examples/resources/powerplatform_enterprise_policy/resource.tf index cb2c80e9a..af851e821 100644 --- a/examples/resources/powerplatform_enterprise_policy/resource.tf +++ b/examples/resources/powerplatform_enterprise_policy/resource.tf @@ -61,6 +61,23 @@ resource "powerplatform_managed_environment" "managed_development" { suppress_validation_emails = true } +// module that creates all azure resources required for the identity policy and the policy itself +module "identity" { + source = "./identity" + + should_register_provider = false + + //Entra Object ID for administrator user that will configure Azure Synapse Link using Managed Identity + policy_reader_object_id = "00000000-0000-0000-0000-000000000000" + + environment_id = powerplatform_environment.example_environment.id + + resource_group_name = "rg_example_identity" + resource_group_location = local.europe_location[0].azure_regions[0] + + enterprise_policy_name = "ep_example_identity_policy" + enterprise_policy_location = local.europe_location[0].name +} // module that creates all azure resources required for the network injection policy and the policy itself module "network_injection" { @@ -75,6 +92,8 @@ module "network_injection" { vnet_locations = local.europe_location[0].azure_regions enterprise_policy_name = "ep_example_network_injection_policy" enterprise_policy_location = local.europe_location[0].name +// let's wait for first policy to be executed + depends_on = [module.identity] } // module that creates all azure resources required for the encryption policy and the policy itself @@ -85,12 +104,11 @@ module "encryption" { environment_id = powerplatform_environment.example_environment.id - resource_group_name = "rg_example_encryption_policy8" + resource_group_name = "rg_example_encryption_policy" resource_group_location = local.europe_location[0].azure_regions[0] - enterprise_policy_name = "ep_example_encryption_policy8" + enterprise_policy_name = "ep_example_encryption_policy" enterprise_policy_location = "europe" - keyvault_name = "kv-ep-example8" - - // let's wait for first policy to be executed - depends_on = [powerplatform_enterprise_policy.network_injection] + keyvault_name = "kv-ep-example" + // let's wait for second policy to be executed + depends_on = [module.network_injection] } diff --git a/internal/services/enterprise_policy/dto.go b/internal/services/enterprise_policy/dto.go index 1ca3ef4d9..fff1c05c1 100644 --- a/internal/services/enterprise_policy/dto.go +++ b/internal/services/enterprise_policy/dto.go @@ -6,6 +6,7 @@ package enterprise_policy const ( NETWORK_INJECTION_POLICY_TYPE = "NetworkInjection" ENCRYPTION_POLICY_TYPE = "Encryption" + IDENTITY_POLICY_TYPE = "Identity" ) type linkEnterprosePolicyDto struct { diff --git a/internal/services/enterprise_policy/resource_enterprise_policy.go b/internal/services/enterprise_policy/resource_enterprise_policy.go index 9206affeb..d07511aa1 100644 --- a/internal/services/enterprise_policy/resource_enterprise_policy.go +++ b/internal/services/enterprise_policy/resource_enterprise_policy.go @@ -98,10 +98,10 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp }, }, "policy_type": schema.StringAttribute{ - MarkdownDescription: fmt.Sprintf("Policy type [%s, %s]", NETWORK_INJECTION_POLICY_TYPE, ENCRYPTION_POLICY_TYPE), + MarkdownDescription: fmt.Sprintf("Policy type [%s, %s, %s]", NETWORK_INJECTION_POLICY_TYPE, ENCRYPTION_POLICY_TYPE, IDENTITY_POLICY_TYPE), Required: true, Validators: []validator.String{ - stringvalidator.OneOf(NETWORK_INJECTION_POLICY_TYPE, ENCRYPTION_POLICY_TYPE), + stringvalidator.OneOf(NETWORK_INJECTION_POLICY_TYPE, ENCRYPTION_POLICY_TYPE, IDENTITY_POLICY_TYPE), }, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), @@ -162,6 +162,8 @@ func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *res state.SystemId = types.StringValue(env.Properties.EnterprisePolicies.Vnets.SystemId) } else if state.PolicyType.ValueString() == ENCRYPTION_POLICY_TYPE && env.Properties.EnterprisePolicies.CustomerManagedKeys != nil { state.SystemId = types.StringValue(env.Properties.EnterprisePolicies.CustomerManagedKeys.SystemId) + } else if state.PolicyType.ValueString() == IDENTITY_POLICY_TYPE && env.Properties.EnterprisePolicies.Identity != nil { + state.SystemId = types.StringValue(env.Properties.EnterprisePolicies.Identity.SystemId) } } else { state.SystemId = types.StringNull() diff --git a/internal/services/environment/dto.go b/internal/services/environment/dto.go index 8794d2a38..750d9e315 100644 --- a/internal/services/environment/dto.go +++ b/internal/services/environment/dto.go @@ -91,6 +91,7 @@ type UsedByDto struct { type EnvironmentEnterprisePoliciesDto struct { Vnets *EnterprisePolicyDto `json:"vnets,omitempty"` CustomerManagedKeys *EnterprisePolicyDto `json:"customerManagedKeys,omitempty"` + Identity *EnterprisePolicyDto `json:"identity,omitempty"` } type EnterprisePolicyDto struct { diff --git a/internal/services/environment/models.go b/internal/services/environment/models.go index 9ee165b20..8bfab0b7f 100644 --- a/internal/services/environment/models.go +++ b/internal/services/environment/models.go @@ -382,6 +382,26 @@ func convertEnterprisePolicyModelFromDto(environmentDto EnvironmentDto, model *S }, } if environmentDto.Properties.EnterprisePolicies != nil { + if environmentDto.Properties.EnterprisePolicies.Identity != nil { + model.EnterprisePolicies = types.SetValueMust(enterprisePolicyAttrType, []attr.Value{ + types.ObjectValueMust( + map[string]attr.Type{ + "type": types.StringType, + "id": types.StringType, + "location": types.StringType, + "system_id": types.StringType, + "status": types.StringType, + }, + map[string]attr.Value{ + "type": types.StringValue("Identity"), + "id": types.StringValue(environmentDto.Properties.EnterprisePolicies.Identity.Id), + "location": types.StringValue(environmentDto.Properties.EnterprisePolicies.Identity.Location), + "system_id": types.StringValue(environmentDto.Properties.EnterprisePolicies.Identity.SystemId), + "status": types.StringValue(environmentDto.Properties.EnterprisePolicies.Identity.LinkStatus), + }, + ), + }) + } if environmentDto.Properties.EnterprisePolicies.Vnets != nil { model.EnterprisePolicies = types.SetValueMust(enterprisePolicyAttrType, []attr.Value{ types.ObjectValueMust(