I criteri dei connettori avanzati (ACP) regolano l'utilizzo del connettore con un elenco di indirizzi consentiti rigoroso che blocca i connettori per impostazione predefinita. Oltre all'esperienza dell'interfaccia di amministrazione di Power Platform , è possibile gestire ACP con il codice usando l'API Power Platform e gli SDK di amministrazione (admin). L'automazione di ACP è utile quando si standardizza la governance in molti gruppi di ambiente, replicare criteri di base tra gruppi o gestire i criteri come parte di una pipeline di distribuzione.
In questa esercitazione, scopri come:
-
Autenticazione tramite API Power Platform .
-
Comprendere la forma dei criteri ACP.
-
Creare un criterio e aggiungerlo a un gruppo di ambiente.
-
Abilitare una singola azione del connettore.
-
Applicare o aggiornare un criterio in un singolo ambiente.
-
Copiare un criterio da un gruppo di ambiente a un altro.
-
Rimuovere ACP da un gruppo di ambiente.
I criteri avanzati del connettore vengono esposti tramite le governance/ruleBasedPolicies operazioni dell'API Power Platform. Un criterio contiene uno o più set di regole; il set di regole con l'ID ConnectorManagement contiene l'elenco di elementi consentiti del connettore ACP. Tutti gli esempi nell'articolo usano la versione 2024-10-01dell'API .
Prerequisiti
Registrazione dell'app configurata per l'API Power Platform. Prendi nota degli ID applicazione (client) e ID directory (tenant) della registrazione dell'app.
Autorizzazione per gestire i criteri di governance. Per le entità servizio, assegna un ruolo RBAC che consenta di scrivere nelle risorse, ad esempio Collaboratore Power Platform o Proprietario Power Platform. Per altre informazioni, vedi Esercitazione: assegnare ruoli alle entità servizio.
Per gli esempi dell'SDK, installa l'SDK pubblicato ogni mese nella sua raccolta pubblica:
dotnet add package Microsoft.PowerPlatform.Management
pip install powerplatform-management
Tutti gli esempi eseguono l'autenticazione con l'ID client della registrazione dell'app, seguendo le indicazioni riportate in Autenticazione. Gli esempi seguenti accedono in modo interattivo come utente corrente. Per eseguire un flusso senza intervento dell'utente come entità servizio, vedi il flusso del client riservato nell'articolo Autenticazione e assegna all'entità servizio un ruolo RBAC.
# Requires the MSAL.PS module: Install-Module MSAL.PS -Scope CurrentUser
Import-Module "MSAL.PS"
$clientId = "<application (client) ID of your app registration>"
$apiBaseUrl = "https://api.powerplatform.com"
$apiVersion = "2024-10-01"
# Sign in interactively and request a token for the Power Platform API
$auth = Get-MsalToken -ClientId $clientId -Scope "https://api.powerplatform.com/.default" -Interactive
$headers = @{ Authorization = "Bearer $($auth.AccessToken)" }
using Microsoft.PowerPlatform.Management;
using Microsoft.PowerPlatform.Management.Models;
// Create an interactive client using your app registration's client ID.
// A browser window opens for sign-in.
var factory = new ServiceClientFactory();
var client = factory.Create("YOUR_CLIENT_ID");
import asyncio
from azure.identity import InteractiveBrowserCredential
from kiota_authentication_azure.azure_identity_authentication_provider import (
AzureIdentityAuthenticationProvider,
)
from kiota_http.httpx_request_adapter import HttpxRequestAdapter
from mspp_management.service_client_base import ServiceClientBase
# Sign in interactively using your app registration's client ID.
credential = InteractiveBrowserCredential(client_id="YOUR_CLIENT_ID")
auth_provider = AzureIdentityAuthenticationProvider(
credentials=credential,
scopes=["https://api.powerplatform.com/.default"],
)
adapter = HttpxRequestAdapter(authentication_provider=auth_provider)
client = ServiceClientBase(adapter)
Passaggio 2: Comprendere la forma dei criteri ACP
Un criterio connettore avanzato è un criterio basato su regole che contiene un set di regole con l'ID ConnectorManagement. Questo insieme di regole include un oggetto version e il relativo inputs contiene un AllowedConnectorList, in cui ogni voce consente di specificare un connettore e definisce come vengono gestite le relative azioni e i tipi di connessione:
{
"name": "Contoso ACP baseline",
"ruleSets": [
{
"id": "ConnectorManagement",
"version": "1.0",
"inputs": {
"AllowedConnectorList": [
{
"AllowedConnector": "/providers/Microsoft.PowerApps/apis/shared_office365",
"AllowedActionsMode": "AllAllowed",
"AllowedConnectionTypesMode": "AllAllowed"
},
{
"AllowedConnector": "/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps",
"AllowedActionsMode": "SomeAllowed",
"AllowedActions": ["GetItem", "CreateRecord"],
"AllowedConnectionTypesMode": "AllAllowed"
}
]
}
}
]
}
Tenere presente la semantica seguente:
- Un connettore che non si trova in
AllowedConnectorList è bloccato (negazione predefinita).
- Ogni voce imposta
AllowedActionsMode.
AllAllowed consente ogni azione sul connettore.
SomeAllowed limita il connettore alle azioni elencate nell'array AllowedActions della voce.
Il passaggio 4 illustra come aggiungere un'azione e impostare questa modalità.
-
AllowedConnectionTypesMode gestisce i tipi di connessione consentiti e segue lo stesso AllAllowed modello.
- Includi il
version del set di regole quando crei o aggiorni un criterio. Leggilo da un criterio esistente e mantieni il valore restituito dal servizio.
Tip
Il valore esatto di AllowedConnector è l'identificatore della risorsa del connettore. Il modo più affidabile per apprendere la forma per i connettori già presenti nel tenant consiste nel leggere prima un criterio esistente (passaggio 4 mostra come) o usare il catalogo dei connettori (descritto di seguito), quindi eseguire il mirroring di tale forma quando si creano o aggiornano i criteri.
Trova gli ID del connettore e dell'azione con il catalogo dei connettori
Per individuare i connettori e le azioni che è possibile consentire, usare l'API Catalogo connettori. Elenca i connettori disponibili in un ambiente, insieme agli identificatori inseriti in AllowedConnector e AllowedActions.
Note
Le operazioni del catalogo dei connettori richiedono un ID ambiente nel percorsoe un OData $filter che specifica lo stesso ambiente, $filter=environment eq '<environmentId>'ad esempio . Entrambi sono obbligatori.
$environmentId = "<environment ID>"
$filter = [uri]::EscapeDataString("environment eq '$environmentId'")
# List connectors available in the environment
$connectors = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/connectivity/environments/$environmentId/connectors?`$filter=$filter&api-version=$apiVersion" `
-Headers $headers
$connectors.value | Select-Object name, @{ n = "displayName"; e = { $_.properties.displayName } }
# Get a single connector by ID (the connector's name, such as shared_office365)
$connectorId = "shared_office365"
$connector = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/connectivity/environments/$environmentId/connectors/$connectorId?`$filter=$filter&api-version=$apiVersion" `
-Headers $headers
$connector.id # full resource path to use as AllowedConnector
Usare il id del connettore (il suo percorso completo della risorsa, ad esempio /providers/Microsoft.PowerApps/apis/shared_office365) come valore di AllowedConnector e gli ID delle operazioni del connettore come i valori in AllowedActions. Puoi accedere allo stesso catalogo tramite il namespace connectivity degli SDK di amministrazione.
Passaggio 3. Creare un criterio e aggiungerlo a un gruppo di ambiente
L'aggiunta di ACP a un gruppo di ambiente è un'operazione in due parti: creare i criteri, quindi assegnarli al gruppo. La chiamata di creazione restituisce il nuovo criterio id, che viene usato nella chiamata di assegnazione.
Per assegnare il criterio all'intero gruppo, invia una richiesta di assegnazione con corpo vuoto ({}). Ogni ambiente del gruppo eredita i criteri e rimane sincronizzato con esso.
$environmentGroupId = "<environment group ID>"
# 1. Create the policy with a ConnectorManagement rule set
$policyBody = @{
name = "Contoso ACP baseline"
ruleSets = @(
@{
id = "ConnectorManagement"
version = "1.0"
inputs = @{
AllowedConnectorList = @(
@{
AllowedConnector = "/providers/Microsoft.PowerApps/apis/shared_office365"
AllowedActionsMode = "AllAllowed"
AllowedConnectionTypesMode = "AllAllowed"
}
)
}
}
)
} | ConvertTo-Json -Depth 10
$policy = Invoke-RestMethod -Method Post `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body $policyBody
Write-Host "Created policy $($policy.id)"
# 2. Assign the policy to the environment group (empty body = whole group)
Invoke-RestMethod -Method Post `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$($policy.id)/environmentGroups/$environmentGroupId/assignments?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body "{}"
Write-Host "Assigned policy $($policy.id) to group $environmentGroupId"
var environmentGroupId = "<environment group ID>";
// 1. Create the policy with a ConnectorManagement rule set
var ruleSet = new RuleSet { Id = "ConnectorManagement", Version = "1.0", Inputs = new RuleSet_inputs() };
ruleSet.Inputs.AdditionalData["AllowedConnectorList"] = new List<object>
{
new Dictionary<string, object>
{
["AllowedConnector"] = "/providers/Microsoft.PowerApps/apis/shared_office365",
["AllowedActionsMode"] = "AllAllowed",
["AllowedConnectionTypesMode"] = "AllAllowed"
}
};
var policyRequest = new PolicyRequest
{
Name = "Contoso ACP baseline",
RuleSets = new List<RuleSet> { ruleSet }
};
var policy = await client.Governance.RuleBasedPolicies.PostAsync(policyRequest);
Console.WriteLine($"Created policy {policy.Id}");
// 2. Assign the policy to the environment group (empty request = whole group)
await client.Governance.RuleBasedPolicies[policy.Id]
.EnvironmentGroups[environmentGroupId]
.Assignments.PostAsync(new PolicyAssignmentRequest());
Console.WriteLine($"Assigned policy {policy.Id} to group {environmentGroupId}");
from mspp_management.models.policy_request import PolicyRequest
from mspp_management.models.rule_set import RuleSet
from mspp_management.models.rule_set_inputs import RuleSet_inputs
from mspp_management.models.policy_assignment_request import PolicyAssignmentRequest
async def create_and_assign(client, environment_group_id: str):
# 1. Create the policy with a ConnectorManagement rule set
rule_set = RuleSet()
rule_set.id = "ConnectorManagement"
rule_set.version = "1.0"
rule_set.inputs = RuleSet_inputs()
rule_set.inputs.additional_data = {
"AllowedConnectorList": [
{
"AllowedConnector": "/providers/Microsoft.PowerApps/apis/shared_office365",
"AllowedActionsMode": "AllAllowed",
"AllowedConnectionTypesMode": "AllAllowed",
}
]
}
policy_request = PolicyRequest()
policy_request.name = "Contoso ACP baseline"
policy_request.rule_sets = [rule_set]
policy = await client.governance.rule_based_policies.post(policy_request)
print(f"Created policy {policy.id}")
# 2. Assign the policy to the environment group (empty request = whole group)
await (
client.governance.rule_based_policies
.by_policy_id(policy.id)
.environment_groups.by_group_id(environment_group_id)
.assignments.post(PolicyAssignmentRequest())
)
print(f"Assigned policy {policy.id} to group {environment_group_id}")
Passaggio 4: Abilitare un'azione del singolo connettore
Per consentire solo azioni specifiche per un connettore, impostare AllowedActionsMode su SomeAllowed ed elencare le azioni consentite in AllowedActions. Questo esempio aggiunge un'azione, ad esempio un'azione nascosta che non è selezionabile nell'interfaccia di amministrazione, all'elenco di elementi consentiti di un connettore e imposta il connettore su SomeAllowed. Leggi il criterio, aggiorna la voce del connettore e rinvia il set di regole aggiornato utilizzando patch. Patch aggiorna un set di regole in base all'ID e lascia invariati gli altri set di regole del criterio.
$policyId = "<policy ID>"
$connectorId = "shared_commondataserviceforapps" # last segment of AllowedConnector
$actionToAdd = "aibuilderpredict_customprompt"
# 1. Read the current policy
$policy = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$policyId`?api-version=$apiVersion" `
-Headers $headers
# 2. Find the ConnectorManagement rule set and the connector entry
$ruleSet = $policy.ruleSets | Where-Object { $_.id -eq "ConnectorManagement" }
$entry = $ruleSet.inputs.AllowedConnectorList |
Where-Object { ($_.AllowedConnector -split "/")[-1] -eq $connectorId }
# 3. Restrict the connector to specific actions: add the action and set SomeAllowed
if ($entry) {
$actions = @()
if ($entry.PSObject.Properties.Name -contains "AllowedActions") { $actions = @($entry.AllowedActions) }
if ($actions -notcontains $actionToAdd) { $actions += $actionToAdd }
$entry | Add-Member -NotePropertyName AllowedActions -NotePropertyValue $actions -Force
$entry.AllowedActionsMode = "SomeAllowed"
# 4. Patch only the modified rule set back to the policy
$patchBody = @{ name = $policy.name; ruleSets = @($ruleSet) } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Patch `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$policyId`?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body $patchBody
Write-Host "Set '$connectorId' to SomeAllowed with '$actionToAdd' in policy $policyId"
}
var policyId = "<policy ID>";
var connectorId = "shared_commondataserviceforapps"; // last segment of AllowedConnector
var actionToAdd = "aibuilderpredict_customprompt";
// 1. Read the current policy
var policy = await client.Governance.RuleBasedPolicies[policyId].GetAsync();
// 2. Find the ConnectorManagement rule set and the connector entry
var ruleSet = policy.RuleSets.First(r => r.Id == "ConnectorManagement");
var connectorList = (List<object>)ruleSet.Inputs.AdditionalData["AllowedConnectorList"];
var entry = connectorList
.Cast<Dictionary<string, object>>()
.First(e => ((string)e["AllowedConnector"]).Split('/').Last() == connectorId);
// 3. Restrict the connector to specific actions: add the action and set SomeAllowed
if (!(entry.TryGetValue("AllowedActions", out var actionsObj) && actionsObj is List<object> actions))
{
actions = new List<object>();
}
if (!actions.Contains(actionToAdd)) actions.Add(actionToAdd);
entry["AllowedActions"] = actions;
entry["AllowedActionsMode"] = "SomeAllowed";
// 4. Patch only the modified rule set back to the policy
var patch = new PolicyRequest { Name = policy.Name, RuleSets = new List<RuleSet> { ruleSet } };
await client.Governance.RuleBasedPolicies[policyId].PatchAsync(patch);
Console.WriteLine($"Set '{connectorId}' to SomeAllowed with '{actionToAdd}' in policy {policyId}");
async def enable_action(client, policy_id: str, connector_id: str, action_to_add: str):
# 1. Read the current policy
policy = await client.governance.rule_based_policies.by_policy_id(policy_id).get()
# 2. Find the ConnectorManagement rule set and the connector entry
rule_set = next(r for r in policy.rule_sets if r.id == "ConnectorManagement")
connector_list = rule_set.inputs.additional_data["AllowedConnectorList"]
entry = next(
e for e in connector_list
if e["AllowedConnector"].split("/")[-1] == connector_id
)
# 3. Restrict the connector to specific actions: add the action and set SomeAllowed
actions = entry.get("AllowedActions") or []
if action_to_add not in actions:
actions.append(action_to_add)
entry["AllowedActions"] = actions
entry["AllowedActionsMode"] = "SomeAllowed"
# 4. Patch only the modified rule set back to the policy
from mspp_management.models.policy_request import PolicyRequest
patch = PolicyRequest()
patch.name = policy.name
patch.rule_sets = [rule_set]
await client.governance.rule_based_policies.by_policy_id(policy_id).patch(patch)
print(f"Set '{connector_id}' to SomeAllowed with '{action_to_add}' in policy {policy_id}")
Passaggio 5. Applicare o aggiornare un criterio in un singolo ambiente
È possibile applicare un criterio a un singolo ambiente anziché a un gruppo di ambienti. Questo approccio è utile per ambienti ad alto rischio, pilota o regolamentati. Assegna il criterio all'ambiente e usa lo stesso schema di patch del Passaggio 4 per modificarlo successivamente. Ogni ambiente supporta una politica ACP efficace.
$policyId = "<policy ID>"
$environmentId = "<environment ID>"
# Assign the policy directly to the environment
Invoke-RestMethod -Method Post `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$policyId/environments/$environmentId/assignments?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body "{}"
Write-Host "Assigned policy $policyId to environment $environmentId"
var policyId = "<policy ID>";
var environmentId = "<environment ID>";
// Assign the policy directly to the environment
await client.Governance.RuleBasedPolicies[policyId]
.Environments[environmentId]
.Assignments.PostAsync(new PolicyAssignmentRequest());
Console.WriteLine($"Assigned policy {policyId} to environment {environmentId}");
async def assign_to_environment(client, policy_id: str, environment_id: str):
from mspp_management.models.policy_assignment_request import PolicyAssignmentRequest
await (
client.governance.rule_based_policies
.by_policy_id(policy_id)
.environments.by_environment_id(environment_id)
.assignments.post(PolicyAssignmentRequest())
)
print(f"Assigned policy {policy_id} to environment {environment_id}")
Passaggio 6. Copiare un criterio da un gruppo di ambiente a un altro
Quando si replica una baseline di governance in un altro gruppo, scegliere quanto copiare usando il CopyAllRules flag :
-
CopyAllRules = true: creare un nuovo criterio da tutti i set di regole del gruppo di origine e assegnarlo al gruppo di destinazione. La governance del gruppo di destinazione diventa una copia indipendente di quella del gruppo di origine.
-
CopyAllRules = false: estrarre solo il ConnectorManagement set di regole dai criteri di origine e unirlo nei criteri esistenti del gruppo di destinazione. L'operazione patch aggiunge o aggiorna il set di regole in base all'ID, quindi il gruppo di destinazione mantiene le altre regole.
$sourceGroupId = "<source environment group ID>"
$targetGroupId = "<target environment group ID>"
$CopyAllRules = $true
# 1. Find and read the policy assigned to the source group
$sourceAssignments = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/environmentGroups/$sourceGroupId/assignments?api-version=$apiVersion" `
-Headers $headers
$sourcePolicyId = $sourceAssignments.value[0].policyId
$source = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$sourcePolicyId`?api-version=$apiVersion" `
-Headers $headers
if ($CopyAllRules) {
# 2a. Copy ALL rule sets into a new policy and assign it to the target group
$copyBody = @{ name = "$($source.name) (copy)"; ruleSets = $source.ruleSets } | ConvertTo-Json -Depth 20
$copy = Invoke-RestMethod -Method Post `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body $copyBody
Invoke-RestMethod -Method Post `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$($copy.id)/environmentGroups/$targetGroupId/assignments?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body "{}"
Write-Host "Copied all rules to policy $($copy.id) and assigned it to group $targetGroupId"
}
else {
# 2b. Merge ONLY the ConnectorManagement rule into the target group's existing policy
$sourceCm = $source.ruleSets | Where-Object { $_.id -eq "ConnectorManagement" }
$targetAssignments = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/environmentGroups/$targetGroupId/assignments?api-version=$apiVersion" `
-Headers $headers
$targetPolicyId = $targetAssignments.value[0].policyId
$targetPolicy = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$targetPolicyId`?api-version=$apiVersion" `
-Headers $headers
# Patch adds or updates the ConnectorManagement rule set by ID, keeping the target's other rules
$patchBody = @{ name = $targetPolicy.name; ruleSets = @($sourceCm) } | ConvertTo-Json -Depth 20
Invoke-RestMethod -Method Patch `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$targetPolicyId`?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body $patchBody
Write-Host "Merged the ConnectorManagement rule into target policy $targetPolicyId"
}
var sourceGroupId = "<source environment group ID>";
var targetGroupId = "<target environment group ID>";
var copyAllRules = true;
// 1. Find and read the policy assigned to the source group
var sourceAssignments = await client.Governance.RuleBasedPolicies
.EnvironmentGroups[sourceGroupId].Assignments.GetAsync();
var sourcePolicyId = sourceAssignments.Value.First().PolicyId;
var source = await client.Governance.RuleBasedPolicies[sourcePolicyId].GetAsync();
if (copyAllRules)
{
// 2a. Copy ALL rule sets into a new policy and assign it to the target group
var copyRequest = new PolicyRequest { Name = $"{source.Name} (copy)", RuleSets = source.RuleSets };
var copy = await client.Governance.RuleBasedPolicies.PostAsync(copyRequest);
await client.Governance.RuleBasedPolicies[copy.Id]
.EnvironmentGroups[targetGroupId]
.Assignments.PostAsync(new PolicyAssignmentRequest());
Console.WriteLine($"Copied all rules to policy {copy.Id} and assigned it to group {targetGroupId}");
}
else
{
// 2b. Merge ONLY the ConnectorManagement rule into the target group's existing policy
var sourceCm = source.RuleSets.First(r => r.Id == "ConnectorManagement");
var targetAssignments = await client.Governance.RuleBasedPolicies
.EnvironmentGroups[targetGroupId].Assignments.GetAsync();
var targetPolicyId = targetAssignments.Value.First().PolicyId;
var targetPolicy = await client.Governance.RuleBasedPolicies[targetPolicyId].GetAsync();
// Patch adds or updates the ConnectorManagement rule set by ID, keeping the target's other rules
var patch = new PolicyRequest { Name = targetPolicy.Name, RuleSets = new List<RuleSet> { sourceCm } };
await client.Governance.RuleBasedPolicies[targetPolicyId].PatchAsync(patch);
Console.WriteLine($"Merged the ConnectorManagement rule into target policy {targetPolicyId}");
}
async def copy_policy(client, source_group_id: str, target_group_id: str, copy_all_rules: bool = True):
from mspp_management.models.policy_request import PolicyRequest
from mspp_management.models.policy_assignment_request import PolicyAssignmentRequest
# 1. Find and read the policy assigned to the source group
source_assignments = await (
client.governance.rule_based_policies
.environment_groups.by_environment_group_id(source_group_id)
.assignments.get()
)
source_policy_id = source_assignments.value[0].policy_id
source = await client.governance.rule_based_policies.by_policy_id(source_policy_id).get()
if copy_all_rules:
# 2a. Copy ALL rule sets into a new policy and assign it to the target group
copy_request = PolicyRequest()
copy_request.name = f"{source.name} (copy)"
copy_request.rule_sets = source.rule_sets
copy = await client.governance.rule_based_policies.post(copy_request)
await (
client.governance.rule_based_policies
.by_policy_id(copy.id)
.environment_groups.by_group_id(target_group_id)
.assignments.post(PolicyAssignmentRequest())
)
print(f"Copied all rules to policy {copy.id} and assigned it to group {target_group_id}")
else:
# 2b. Merge ONLY the ConnectorManagement rule into the target group's existing policy
source_cm = next(r for r in source.rule_sets if r.id == "ConnectorManagement")
target_assignments = await (
client.governance.rule_based_policies
.environment_groups.by_environment_group_id(target_group_id)
.assignments.get()
)
target_policy_id = target_assignments.value[0].policy_id
target_policy = await client.governance.rule_based_policies.by_policy_id(target_policy_id).get()
# Patch adds or updates the ConnectorManagement rule set by ID, keeping the target's other rules
patch = PolicyRequest()
patch.name = target_policy.name
patch.rule_sets = [source_cm]
await client.governance.rule_based_policies.by_policy_id(target_policy_id).patch(patch)
print(f"Merged the ConnectorManagement rule into target policy {target_policy_id}")
Passaggio 6b. Copiare criteri da un gruppo di ambienti a un altro, incluse le azioni del connettore
Quando si replica una baseline di governance in un altro gruppo, scegliere quanto copiare usando il CopyAllRules flag :
-
CopyAllRules = true: creare un nuovo criterio da tutti i set di regole del gruppo di origine e assegnarlo al gruppo di destinazione. La governance del gruppo di destinazione diventa una copia indipendente di quella del gruppo di origine.
-
CopyAllRules = false: estrarre solo il ConnectorManagement set di regole dai criteri di origine e unirlo nei criteri esistenti del gruppo di destinazione. L'operazione patch aggiunge o aggiorna il set di regole in base all'ID, quindi il gruppo di destinazione mantiene le altre regole.
$tenantId = "<tenant-id>"
$apiBaseUrl = "https://api.powerplatform.com"
$apiVersion = "2024-10-01"
$sourceGroupId = "<source environment group ID>"
$targetGroupId = "<target environment group ID>"
$CopyAllRules = $true
# ---------- Helpers ----------
function Get-Prop($obj, $name) {
if ($null -ne $obj -and $obj.PSObject.Properties.Name -contains $name) { return $obj.$name }
return $null
}
function Get-ConnectorKey($entry) { (([string]$entry.AllowedConnector) -split "/")[-1].ToLowerInvariant() }
function Get-ConnectorManagement($policy) {
$policy.ruleSets | Where-Object { $_.id -eq "ConnectorManagement" } | Select-Object -First 1
}
function Get-ActionSignature($entry) {
$mode = Get-Prop $entry "AllowedActionsMode"
if (-not $mode) { $mode = "AllAllowed" }
if ($mode -ne "SomeAllowed") { return $mode }
$actions = (@(Get-Prop $entry "AllowedActions") | Where-Object { $_ } | Sort-Object) -join "|"
return "$mode::$actions"
}
# Copies AllowedActionsMode/AllowedActions from each source connector to the matching target connector.
# Returns the number of connectors that were changed.
function Sync-ConnectorActions($sourceCm, $targetCm) {
$changed = 0
foreach ($src in @($sourceCm.inputs.AllowedConnectorList)) {
$key = Get-ConnectorKey $src
$tgt = @($targetCm.inputs.AllowedConnectorList) | Where-Object { (Get-ConnectorKey $_) -eq $key } | Select-Object -First 1
if (-not $tgt) { Write-Warning " $key is in the source but not in the target rule - skipped"; continue }
if ((Get-ActionSignature $src) -eq (Get-ActionSignature $tgt)) { continue }
$mode = Get-Prop $src "AllowedActionsMode"
if (-not $mode) { $mode = "AllAllowed" }
$tgt | Add-Member -NotePropertyName AllowedActionsMode -NotePropertyValue $mode -Force
if ($mode -eq "SomeAllowed") {
$actions = [object[]]@(Get-Prop $src "AllowedActions")
$tgt | Add-Member -NotePropertyName AllowedActions -NotePropertyValue $actions -Force
Write-Host " $key -> SomeAllowed ($($actions.Count) allowed): $($actions -join ', ')"
}
else {
$tgt.PSObject.Properties.Remove("AllowedActions")
Write-Host " $key -> $mode"
}
$changed++
}
return $changed
}
if ($MyInvocation.InvocationName -eq ".") { return } # dot-sourced: load helpers only
# ---------- Authenticate (requires the Az.Accounts module: Install-Module Az.Accounts -Scope CurrentUser) ----------
Connect-AzAccount -Tenant $tenantId | Out-Null
$token = (Get-AzAccessToken -ResourceUrl $apiBaseUrl -AsSecureString).Token | ConvertFrom-SecureString -AsPlainText
$headers = @{ Authorization = "Bearer $token" }
function Invoke-PpApi($method, $path, $body) {
$params = @{ Method = $method; Uri = "$apiBaseUrl/$path`?api-version=$apiVersion"; Headers = $headers }
if ($body) { $params.ContentType = "application/json"; $params.Body = $body }
Invoke-RestMethod @params
}
# ---------- 1. Read the source group's policy ----------
$sourceAssignments = Invoke-PpApi Get "governance/ruleBasedPolicies/environmentGroups/$sourceGroupId/assignments"
if (-not $sourceAssignments.value) { throw "No policy is assigned to source group $sourceGroupId" }
$sourcePolicyId = $sourceAssignments.value[0].policyId
$source = Invoke-PpApi Get "governance/ruleBasedPolicies/$sourcePolicyId"
$sourceCm = Get-ConnectorManagement $source
# ---------- 2. Copy the rules (unchanged logic) ----------
# A group can have only one assigned policy, so check the target first
$targetAssignments = Invoke-PpApi Get "governance/ruleBasedPolicies/environmentGroups/$targetGroupId/assignments"
$targetPolicyId = if ($targetAssignments.value) { $targetAssignments.value[0].policyId } else { $null }
if ($targetPolicyId) {
# Target already has a policy: patch the source rule sets into it (patch adds/updates rule sets by ID)
$targetPolicy = Invoke-PpApi Get "governance/ruleBasedPolicies/$targetPolicyId"
$ruleSetsToCopy = if ($CopyAllRules) { @($source.ruleSets) } else { @($sourceCm) }
if (-not $ruleSetsToCopy -or -not $ruleSetsToCopy[0]) { throw "Source policy $sourcePolicyId has no rule sets to copy." }
$patchBody = @{ name = $targetPolicy.name; ruleSets = $ruleSetsToCopy } | ConvertTo-Json -Depth 20
Invoke-PpApi Patch "governance/ruleBasedPolicies/$targetPolicyId" $patchBody | Out-Null
Write-Host "Target group already uses policy $targetPolicyId - updated it with $($ruleSetsToCopy.Count) rule set(s): $(($ruleSetsToCopy.id) -join ', ')"
}
elseif ($CopyAllRules) {
# No policy on the target: copy ALL rule sets into a new policy and assign it
$copyBody = @{ name = "$($source.name) (copy)"; ruleSets = $source.ruleSets } | ConvertTo-Json -Depth 20
$copy = Invoke-PpApi Post "governance/ruleBasedPolicies" $copyBody
Invoke-PpApi Post "governance/ruleBasedPolicies/$($copy.id)/environmentGroups/$targetGroupId/assignments" "{}" | Out-Null
$targetPolicyId = $copy.id
Write-Host "Copied all rules to new policy $targetPolicyId and assigned it to group $targetGroupId"
}
else {
throw "No policy is assigned to target group $targetGroupId. Set `$CopyAllRules = `$true to create one."
}
# ---------- 3. Copy connector actions for each copied connector rule ----------
if (-not $sourceCm) { Write-Host "Source policy has no ConnectorManagement rule set - no connector actions to copy."; return }
$restricted = @($sourceCm.inputs.AllowedConnectorList | Where-Object { (Get-Prop $_ "AllowedActionsMode") -eq "SomeAllowed" })
Write-Host "Source has $($restricted.Count) connector(s) with restricted (partly disabled) actions."
# Re-read what the service actually stored for the target
$target = Invoke-PpApi Get "governance/ruleBasedPolicies/$targetPolicyId"
$targetCm = Get-ConnectorManagement $target
if (-not $targetCm) { throw "Target policy $targetPolicyId has no ConnectorManagement rule set after the copy." }
Write-Host "Syncing connector actions into policy $targetPolicyId :"
$changed = Sync-ConnectorActions $sourceCm $targetCm
if ($changed -gt 0) {
$patchBody = @{ name = $target.name; ruleSets = @($targetCm) } | ConvertTo-Json -Depth 20
Invoke-PpApi Patch "governance/ruleBasedPolicies/$targetPolicyId" $patchBody | Out-Null
Write-Host "Updated actions on $changed connector(s)."
}
else {
Write-Host "Connector actions already match the source - nothing to update."
}
# ---------- 4. Verify ----------
$verifyCm = Get-ConnectorManagement (Invoke-PpApi Get "governance/ruleBasedPolicies/$targetPolicyId")
$mismatches = 0
foreach ($src in @($sourceCm.inputs.AllowedConnectorList)) {
$key = Get-ConnectorKey $src
$tgt = @($verifyCm.inputs.AllowedConnectorList) | Where-Object { (Get-ConnectorKey $_) -eq $key } | Select-Object -First 1
if (-not $tgt -or (Get-ActionSignature $src) -ne (Get-ActionSignature $tgt)) {
Write-Warning "Mismatch for $key - source: $(Get-ActionSignature $src) | target: $(Get-ActionSignature $tgt)"
$mismatches++
}
}
if ($mismatches -eq 0) { Write-Host "Verified: connector actions in $targetPolicyId match the source." }
// Requires the .NET 10 SDK. Run with:
// dotnet run CopyAcpPolicy.cs -- --tenant <tenant-id> --source <source group ID> --target <target group ID> [--connector-only]
#:package Azure.Identity@1.21.0
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;
using Azure.Core;
using Azure.Identity;
// ---------- Settings (can be overridden: --tenant <id> --source <id> --target <id> --connector-only) ----------
var tenantId = "<tenant-id>";
var sourceGroupId = "<source environment group ID>";
var targetGroupId = "<target environment group ID>";
var copyAllRules = true; // false = copy only the ConnectorManagement rule set
const string ApiBaseUrl = "https://api.powerplatform.com";
const string ApiVersion = "2024-10-01";
for (var i = 0; i < args.Length; i++)
{
switch (args[i].ToLowerInvariant())
{
case "--tenant": tenantId = args[++i]; break;
case "--source": sourceGroupId = args[++i]; break;
case "--target": targetGroupId = args[++i]; break;
case "--connector-only": copyAllRules = false; break;
default: Console.Error.WriteLine($"Unknown argument: {args[i]}"); return 1;
}
}
try
{
// ---------- Authenticate (interactive browser sign-in, like Connect-AzAccount) ----------
var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TenantId = tenantId });
var token = await credential.GetTokenAsync(new TokenRequestContext([$"{ApiBaseUrl}/.default"]));
using var http = new HttpClient { BaseAddress = new Uri($"{ApiBaseUrl}/") };
http.DefaultRequestHeaders.Authorization = new("Bearer", token.Token);
var api = new PowerPlatformApi(http, ApiVersion);
// ---------- 1. Read the source group's policy ----------
var sourcePolicyId = await GetAssignedPolicyIdAsync(api, sourceGroupId)
?? throw new InvalidOperationException($"No policy is assigned to source group {sourceGroupId}");
var source = await api.GetAsync($"governance/ruleBasedPolicies/{sourcePolicyId}")
?? throw new InvalidOperationException($"Source policy {sourcePolicyId} returned no content");
var sourceCm = Acp.GetConnectorManagement(source);
// ---------- 2. Copy the rules ----------
// A group can have only one assigned policy, so check the target first
var targetPolicyId = await GetAssignedPolicyIdAsync(api, targetGroupId);
var sourceRuleSets = (source["ruleSets"] as JsonArray)?.OfType<JsonObject>().ToList() ?? [];
if (targetPolicyId is not null)
{
// Target already has a policy: patch the source rule sets into it (patch adds/updates rule sets by ID)
var targetPolicy = await api.GetAsync($"governance/ruleBasedPolicies/{targetPolicyId}");
var ruleSetsToCopy = copyAllRules ? sourceRuleSets : sourceCm is null ? [] : [sourceCm];
if (ruleSetsToCopy.Count == 0)
throw new InvalidOperationException($"Source policy {sourcePolicyId} has no rule sets to copy.");
await api.PatchAsync($"governance/ruleBasedPolicies/{targetPolicyId}", PolicyBody(Acp.GetString(targetPolicy, "name"), ruleSetsToCopy));
Console.WriteLine($"Target group already uses policy {targetPolicyId} - updated it with {ruleSetsToCopy.Count} rule set(s): " +
string.Join(", ", ruleSetsToCopy.Select(r => Acp.GetString(r, "id"))));
}
else if (copyAllRules)
{
// No policy on the target: copy ALL rule sets into a new policy and assign it
var copy = await api.PostAsync("governance/ruleBasedPolicies", PolicyBody($"{Acp.GetString(source, "name")} (copy)", sourceRuleSets));
targetPolicyId = Acp.GetString(copy, "id")
?? throw new InvalidOperationException("Create policy response did not include an id.");
await api.PostAsync($"governance/ruleBasedPolicies/{targetPolicyId}/environmentGroups/{targetGroupId}/assignments", new JsonObject());
Console.WriteLine($"Copied all rules to new policy {targetPolicyId} and assigned it to group {targetGroupId}");
}
else
{
throw new InvalidOperationException($"No policy is assigned to target group {targetGroupId}. Omit --connector-only to create one.");
}
// ---------- 3. Copy connector actions for each copied connector rule ----------
if (sourceCm is null)
{
Console.WriteLine("Source policy has no ConnectorManagement rule set - no connector actions to copy.");
return 0;
}
var restricted = Acp.GetConnectors(sourceCm).Count(e => Acp.GetActionsMode(e) == "SomeAllowed");
Console.WriteLine($"Source has {restricted} connector(s) with restricted (partly disabled) actions.");
// Re-read what the service actually stored for the target
var target = await api.GetAsync($"governance/ruleBasedPolicies/{targetPolicyId}");
var targetCm = Acp.GetConnectorManagement(target)
?? throw new InvalidOperationException($"Target policy {targetPolicyId} has no ConnectorManagement rule set after the copy.");
Console.WriteLine($"Syncing connector actions into policy {targetPolicyId}:");
var changed = Acp.SyncConnectorActions(sourceCm, targetCm);
if (changed > 0)
{
await api.PatchAsync($"governance/ruleBasedPolicies/{targetPolicyId}", PolicyBody(Acp.GetString(target, "name"), [targetCm]));
Console.WriteLine($"Updated actions on {changed} connector(s).");
}
else
{
Console.WriteLine("Connector actions already match the source - nothing to update.");
}
// ---------- 4. Verify ----------
var verifyCm = Acp.GetConnectorManagement(await api.GetAsync($"governance/ruleBasedPolicies/{targetPolicyId}"));
var mismatches = 0;
foreach (var src in Acp.GetConnectors(sourceCm))
{
var key = Acp.GetConnectorKey(src);
var tgt = Acp.FindConnector(verifyCm, key);
if (tgt is null || Acp.GetActionSignature(src) != Acp.GetActionSignature(tgt))
{
Log.Warn($"Mismatch for {key} - source: {Acp.GetActionSignature(src)} | target: {Acp.GetActionSignature(tgt)}");
mismatches++;
}
}
if (mismatches == 0) Console.WriteLine($"Verified: connector actions in {targetPolicyId} match the source.");
return mismatches == 0 ? 0 : 2;
}
catch (Exception ex) when (ex is HttpRequestException or InvalidOperationException or AuthenticationFailedException)
{
Console.Error.WriteLine($"ERROR: {ex.Message}");
return 1;
}
static async Task<string?> GetAssignedPolicyIdAsync(PowerPlatformApi api, string groupId)
{
var assignments = await api.GetAsync($"governance/ruleBasedPolicies/environmentGroups/{groupId}/assignments");
var first = (assignments?["value"] as JsonArray)?.FirstOrDefault();
return Acp.GetString(first, "policyId");
}
// Rule sets are deep-cloned because a JsonNode can only belong to one parent.
static JsonObject PolicyBody(string? name, IEnumerable<JsonObject> ruleSets) => new()
{
["name"] = name,
["ruleSets"] = new JsonArray(ruleSets.Select(r => (JsonNode?)r.DeepClone()).ToArray()),
};
// ---------- Types ----------
/// <summary>Minimal Power Platform API client (equivalent of Invoke-PpApi in the PowerShell version).</summary>
public sealed class PowerPlatformApi(HttpClient http, string apiVersion)
{
public async Task<JsonNode?> SendAsync(HttpMethod method, string path, JsonNode? body = null)
{
using var request = new HttpRequestMessage(method, $"{path}?api-version={apiVersion}");
if (body is not null)
{
request.Content = new StringContent(body.ToJsonString(), Encoding.UTF8);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
using var response = await http.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(
$"{method} {path} failed with {(int)response.StatusCode} {response.ReasonPhrase}: {text}",
null, response.StatusCode);
}
return string.IsNullOrWhiteSpace(text) ? null : JsonNode.Parse(text, Acp.NodeOptions);
}
public Task<JsonNode?> GetAsync(string path) => SendAsync(HttpMethod.Get, path);
public Task<JsonNode?> PostAsync(string path, JsonNode body) => SendAsync(HttpMethod.Post, path, body);
public Task<JsonNode?> PatchAsync(string path, JsonNode body) => SendAsync(HttpMethod.Patch, path, body);
}
/// <summary>Helpers for the ConnectorManagement (advanced connector policy) rule set.</summary>
public static class Acp
{
public const string RuleSetId = "ConnectorManagement";
// Policy JSON is parsed case-insensitively to match PowerShell's property semantics.
public static readonly JsonNodeOptions NodeOptions = new() { PropertyNameCaseInsensitive = true };
public static string? GetString(JsonNode? node, string name) =>
node is JsonObject obj && obj.TryGetPropertyValue(name, out var value) && value is JsonValue v &&
v.TryGetValue<string>(out var s) ? s : null;
public static JsonObject? GetConnectorManagement(JsonNode? policy) =>
(policy?["ruleSets"] as JsonArray)?
.OfType<JsonObject>()
.FirstOrDefault(r => GetString(r, "id") == RuleSetId);
public static IEnumerable<JsonObject> GetConnectors(JsonObject? connectorManagement) =>
(connectorManagement?["inputs"]?["AllowedConnectorList"] as JsonArray)?.OfType<JsonObject>()
?? Enumerable.Empty<JsonObject>();
public static string GetConnectorKey(JsonObject entry) =>
(GetString(entry, "AllowedConnector") ?? "").Split('/').Last().ToLowerInvariant();
public static JsonObject? FindConnector(JsonObject? connectorManagement, string key) =>
GetConnectors(connectorManagement).FirstOrDefault(e => GetConnectorKey(e) == key);
public static string GetActionsMode(JsonObject entry) =>
string.IsNullOrEmpty(GetString(entry, "AllowedActionsMode")) ? "AllAllowed" : GetString(entry, "AllowedActionsMode")!;
public static List<string> GetAllowedActions(JsonObject entry) =>
(entry["AllowedActions"] as JsonArray)?
.Select(a => a?.GetValue<string>())
.Where(a => !string.IsNullOrEmpty(a))
.Select(a => a!)
.ToList()
?? new List<string>();
public static string GetActionSignature(JsonObject? entry)
{
if (entry is null) return "";
var mode = GetActionsMode(entry);
if (mode != "SomeAllowed") return mode;
return $"{mode}::{string.Join("|", GetAllowedActions(entry).OrderBy(a => a, StringComparer.Ordinal))}";
}
/// <summary>
/// Copies AllowedActionsMode/AllowedActions from each source connector to the matching target connector.
/// Returns the number of connectors that were changed.
/// </summary>
public static int SyncConnectorActions(JsonObject sourceCm, JsonObject targetCm)
{
var changed = 0;
foreach (var src in GetConnectors(sourceCm))
{
var key = GetConnectorKey(src);
var tgt = FindConnector(targetCm, key);
if (tgt is null)
{
Log.Warn($" {key} is in the source but not in the target rule - skipped");
continue;
}
if (GetActionSignature(src) == GetActionSignature(tgt)) continue;
var mode = GetActionsMode(src);
tgt["AllowedActionsMode"] = mode;
if (mode == "SomeAllowed")
{
var actions = GetAllowedActions(src);
tgt["AllowedActions"] = new JsonArray(actions.Select(a => (JsonNode?)JsonValue.Create(a)).ToArray());
Console.WriteLine($" {key} -> SomeAllowed ({actions.Count} allowed): {string.Join(", ", actions)}");
}
else
{
tgt.Remove("AllowedActions");
Console.WriteLine($" {key} -> {mode}");
}
changed++;
}
return changed;
}
}
public static class Log
{
public static void Warn(string message)
{
var previous = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"WARNING: {message}");
Console.ForegroundColor = previous;
}
}
# requires-python = ">=3.9"
# dependencies = ["azure-identity"]
"""
Copies rule-based policies (including advanced connector policy actions) from one
Power Platform environment group to another.
Setup: pip install azure-identity
Run: python copy_acp_policy.py --tenant <tenant-id> --source <source group ID> --target <target group ID> [--connector-only]
or: uv run copy_acp_policy.py --tenant ... (installs dependencies automatically)
"""
import argparse
import copy
import json
import sys
import urllib.error
import urllib.request
from azure.identity import InteractiveBrowserCredential
# ---------- Settings (can be overridden on the command line) ----------
TENANT_ID = "<tenant-id>"
SOURCE_GROUP_ID = "<source environment group ID>"
TARGET_GROUP_ID = "<target environment group ID>"
COPY_ALL_RULES = True # False = copy only the ConnectorManagement rule set
API_BASE_URL = "https://api.powerplatform.com"
API_VERSION = "2024-10-01"
RULE_SET_ID = "ConnectorManagement"
# ---------- Helpers ----------
# Property lookups are case-insensitive to match PowerShell's semantics.
def _find_key(obj, name):
if isinstance(obj, dict):
lowered = name.lower()
for key in obj:
if key.lower() == lowered:
return key
return None
def get_prop(obj, name):
key = _find_key(obj, name)
return obj[key] if key is not None else None
def set_prop(obj, name, value):
obj[_find_key(obj, name) or name] = value
def remove_prop(obj, name):
key = _find_key(obj, name)
if key is not None:
del obj[key]
def get_connector_management(policy):
for rule_set in get_prop(policy, "ruleSets") or []:
if get_prop(rule_set, "id") == RULE_SET_ID:
return rule_set
return None
def get_connectors(connector_management):
return get_prop(get_prop(connector_management, "inputs"), "AllowedConnectorList") or []
def get_connector_key(entry):
return str(get_prop(entry, "AllowedConnector") or "").split("/")[-1].lower()
def find_connector(connector_management, key):
return next((e for e in get_connectors(connector_management) if get_connector_key(e) == key), None)
def get_actions_mode(entry):
return get_prop(entry, "AllowedActionsMode") or "AllAllowed"
def get_allowed_actions(entry):
return [a for a in (get_prop(entry, "AllowedActions") or []) if a]
def get_action_signature(entry):
if entry is None:
return ""
mode = get_actions_mode(entry)
if mode != "SomeAllowed":
return mode
return f"{mode}::{'|'.join(sorted(get_allowed_actions(entry)))}"
def warn(message):
print(f"WARNING: {message}", file=sys.stderr)
def sync_connector_actions(source_cm, target_cm):
"""Copies AllowedActionsMode/AllowedActions from each source connector to the matching
target connector. Returns the number of connectors that were changed."""
changed = 0
for src in get_connectors(source_cm):
key = get_connector_key(src)
tgt = find_connector(target_cm, key)
if tgt is None:
warn(f" {key} is in the source but not in the target rule - skipped")
continue
if get_action_signature(src) == get_action_signature(tgt):
continue
mode = get_actions_mode(src)
set_prop(tgt, "AllowedActionsMode", mode)
if mode == "SomeAllowed":
actions = get_allowed_actions(src)
set_prop(tgt, "AllowedActions", list(actions))
print(f" {key} -> SomeAllowed ({len(actions)} allowed): {', '.join(actions)}")
else:
remove_prop(tgt, "AllowedActions")
print(f" {key} -> {mode}")
changed += 1
return changed
def policy_body(name, rule_sets):
return {"name": name, "ruleSets": copy.deepcopy(list(rule_sets))}
# ---------- Power Platform API client (equivalent of Invoke-PpApi) ----------
class PowerPlatformApi:
def __init__(self, token):
self._token = token
def send(self, method, path, body=None):
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(
f"{API_BASE_URL}/{path}?api-version={API_VERSION}", data=data, method=method)
request.add_header("Authorization", f"Bearer {self._token}")
if data is not None:
request.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(request) as response:
text = response.read().decode("utf-8")
except urllib.error.HTTPError as err:
detail = err.read().decode("utf-8", errors="replace")
raise RuntimeError(f"{method} {path} failed with {err.code} {err.reason}: {detail}") from None
return json.loads(text) if text.strip() else None
def get(self, path):
return self.send("GET", path)
def post(self, path, body):
return self.send("POST", path, body)
def patch(self, path, body):
return self.send("PATCH", path, body)
def get_assigned_policy_id(api, group_id):
assignments = api.get(f"governance/ruleBasedPolicies/environmentGroups/{group_id}/assignments")
values = get_prop(assignments, "value") or []
return get_prop(values[0], "policyId") if values else None
# ---------- Main ----------
def run(tenant_id, source_group_id, target_group_id, copy_all_rules):
# Authenticate (interactive browser sign-in, like Connect-AzAccount)
credential = InteractiveBrowserCredential(tenant_id=tenant_id)
token = credential.get_token(f"{API_BASE_URL}/.default").token
api = PowerPlatformApi(token)
# 1. Read the source group's policy
source_policy_id = get_assigned_policy_id(api, source_group_id)
if not source_policy_id:
raise RuntimeError(f"No policy is assigned to source group {source_group_id}")
source = api.get(f"governance/ruleBasedPolicies/{source_policy_id}")
source_cm = get_connector_management(source)
source_rule_sets = get_prop(source, "ruleSets") or []
# 2. Copy the rules. A group can have only one assigned policy, so check the target first.
target_policy_id = get_assigned_policy_id(api, target_group_id)
if target_policy_id:
# Target already has a policy: patch the source rule sets into it (patch adds/updates rule sets by ID)
target_policy = api.get(f"governance/ruleBasedPolicies/{target_policy_id}")
rule_sets_to_copy = source_rule_sets if copy_all_rules else ([source_cm] if source_cm else [])
if not rule_sets_to_copy:
raise RuntimeError(f"Source policy {source_policy_id} has no rule sets to copy.")
api.patch(f"governance/ruleBasedPolicies/{target_policy_id}",
policy_body(get_prop(target_policy, "name"), rule_sets_to_copy))
ids = ", ".join(str(get_prop(r, "id")) for r in rule_sets_to_copy)
print(f"Target group already uses policy {target_policy_id} - updated it with "
f"{len(rule_sets_to_copy)} rule set(s): {ids}")
elif copy_all_rules:
# No policy on the target: copy ALL rule sets into a new policy and assign it
new_policy = api.post("governance/ruleBasedPolicies",
policy_body(f"{get_prop(source, 'name')} (copy)", source_rule_sets))
target_policy_id = get_prop(new_policy, "id")
if not target_policy_id:
raise RuntimeError("Create policy response did not include an id.")
api.post(f"governance/ruleBasedPolicies/{target_policy_id}/environmentGroups/{target_group_id}/assignments", {})
print(f"Copied all rules to new policy {target_policy_id} and assigned it to group {target_group_id}")
else:
raise RuntimeError(f"No policy is assigned to target group {target_group_id}. "
"Omit --connector-only to create one.")
# 3. Copy connector actions for each copied connector rule
if source_cm is None:
print("Source policy has no ConnectorManagement rule set - no connector actions to copy.")
return 0
restricted = sum(1 for e in get_connectors(source_cm) if get_actions_mode(e) == "SomeAllowed")
print(f"Source has {restricted} connector(s) with restricted (partly disabled) actions.")
# Re-read what the service actually stored for the target
target = api.get(f"governance/ruleBasedPolicies/{target_policy_id}")
target_cm = get_connector_management(target)
if target_cm is None:
raise RuntimeError(f"Target policy {target_policy_id} has no ConnectorManagement rule set after the copy.")
print(f"Syncing connector actions into policy {target_policy_id}:")
changed = sync_connector_actions(source_cm, target_cm)
if changed:
api.patch(f"governance/ruleBasedPolicies/{target_policy_id}",
policy_body(get_prop(target, "name"), [target_cm]))
print(f"Updated actions on {changed} connector(s).")
else:
print("Connector actions already match the source - nothing to update.")
# 4. Verify
verify_cm = get_connector_management(api.get(f"governance/ruleBasedPolicies/{target_policy_id}"))
mismatches = 0
for src in get_connectors(source_cm):
key = get_connector_key(src)
tgt = find_connector(verify_cm, key)
if tgt is None or get_action_signature(src) != get_action_signature(tgt):
warn(f"Mismatch for {key} - source: {get_action_signature(src)} | target: {get_action_signature(tgt)}")
mismatches += 1
if mismatches == 0:
print(f"Verified: connector actions in {target_policy_id} match the source.")
return 0 if mismatches == 0 else 2
def main():
parser = argparse.ArgumentParser(description="Copy Power Platform rule-based policies between environment groups.")
parser.add_argument("--tenant", default=TENANT_ID)
parser.add_argument("--source", default=SOURCE_GROUP_ID, help="Source environment group ID")
parser.add_argument("--target", default=TARGET_GROUP_ID, help="Target environment group ID")
parser.add_argument("--connector-only", action="store_true",
help="Copy only the ConnectorManagement rule set")
args = parser.parse_args()
try:
return run(args.tenant, args.source, args.target, COPY_ALL_RULES and not args.connector_only)
except Exception as ex: # noqa: BLE001 - report any failure cleanly
print(f"ERROR: {ex}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())
Passaggio 7. Rimuovere ACP da un gruppo di ambiente
Mentre un gruppo ha una regola ACP attiva, ogni ambiente nel gruppo corrisponde ai criteri del gruppo. Il modo in cui si rimuove l’imposizione dipende dal fatto che si desideri che tali ambienti mantengano la configurazione corrente oppure cancellare completamente ACP:
-
Rimuovere la regola dai criteri del gruppo per impedire al gruppo di gestire ACP. Usare l'operazione
removeRule per rimuovere il ConnectorManagement set di regole dai criteri del gruppo. Gli ambienti mantengono l'ultima configurazione ACP applicata, ma non sono più sincronizzati con il gruppo. È possibile gestire ogni ambiente singolarmente e consentire loro di divergere.
-
Rimuovere ACP dal gruppo e da ogni ambiente per disattivare ACP ovunque. Rimuovi la regola dal criterio del gruppo, quindi passa in rassegna gli ambienti del gruppo e rimuovi anche il set di regole
ConnectorManagement dal criterio di ogni ambiente.
Note
La rimozione della regola dai criteri di un gruppo non cancella automaticamente ACP dagli ambienti che l'hanno ereditata. Questi ambienti mantengono l'ultima configurazione applicata per evitare una lacuna di applicazione. Per rimuovere ACP da tutti gli ambienti, eliminarlo da ciascun ambiente, come mostrato nell'esempio di loop. Per altre informazioni, vedere Criteri dei connettori avanzati.
Rimuovere la regola dai criteri del gruppo
Nell'esempio seguente il ConnectorManagement set di regole viene rimosso da un criterio usando l'operazione removeRule .
$policyId = "<policy ID>"
# Read the policy, then send the rule set to remove
$policy = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$policyId`?api-version=$apiVersion" `
-Headers $headers
$ruleSet = $policy.ruleSets | Where-Object { $_.id -eq "ConnectorManagement" }
$body = @{ name = $policy.name; ruleSets = @($ruleSet) } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Patch `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$policyId/removeRule?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body $body
Write-Host "Removed the ConnectorManagement rule set from policy $policyId"
var policyId = "<policy ID>";
// Remove the ConnectorManagement rule set from the policy
var policy = await client.Governance.RuleBasedPolicies[policyId].GetAsync();
var ruleSet = policy.RuleSets.First(r => r.Id == "ConnectorManagement");
var removeRequest = new PolicyRequest { Name = policy.Name, RuleSets = new List<RuleSet> { ruleSet } };
await client.Governance.RuleBasedPolicies[policyId].RemoveRule.PatchAsync(removeRequest);
Console.WriteLine($"Removed the ConnectorManagement rule set from policy {policyId}");
async def remove_rule(client, policy_id: str):
from mspp_management.models.policy_request import PolicyRequest
# Remove the ConnectorManagement rule set from the policy
policy = await client.governance.rule_based_policies.by_policy_id(policy_id).get()
rule_set = next(r for r in policy.rule_sets if r.id == "ConnectorManagement")
remove_request = PolicyRequest()
remove_request.name = policy.name
remove_request.rule_sets = [rule_set]
await client.governance.rule_based_policies.by_policy_id(policy_id).remove_rule.patch(remove_request)
print(f"Removed the ConnectorManagement rule set from policy {policy_id}")
Rimuovere ACP da ogni ambiente nel gruppo
Per disattivare ACP in tutti gli ambienti di un gruppo, rimuovere prima di tutto la regola dai criteri del gruppo (esempio precedente), quindi ripetere la rimozione per i propri criteri di ogni ambiente. Leggi il criterio assegnato a ciascun ambiente dalla rispettiva assegnazione dell'ambiente, quindi chiama removeRule su tale criterio. Specificare gli ID di ambiente che appartengono al gruppo o enumerarli usando le API di gestione dell'ambiente.
# Environment IDs that belong to the group
$environmentIds = @("<environment ID 1>", "<environment ID 2>")
foreach ($environmentId in $environmentIds) {
# Find the policy currently assigned to the environment
$envAssignments = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/environments/$environmentId/assignments?api-version=$apiVersion" `
-Headers $headers
if (-not $envAssignments.value) { continue }
$envPolicyId = $envAssignments.value[0].policyId
# Remove the ConnectorManagement rule set from that environment's policy
$envPolicy = Invoke-RestMethod -Method Get `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$envPolicyId`?api-version=$apiVersion" `
-Headers $headers
$ruleSet = $envPolicy.ruleSets | Where-Object { $_.id -eq "ConnectorManagement" }
if ($ruleSet) {
$body = @{ name = $envPolicy.name; ruleSets = @($ruleSet) } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Patch `
-Uri "$apiBaseUrl/governance/ruleBasedPolicies/$envPolicyId/removeRule?api-version=$apiVersion" `
-Headers $headers -ContentType "application/json" -Body $body
Write-Host "Removed ACP from environment $environmentId"
}
}
La stessa chiamata per ambiente removeRule funziona con gli SDK C# e Python illustrati in precedenza. Racchiudi la chiamata in un ciclo sugli ID degli ambienti del gruppo.
Contenuti correlati
Criteri dei connettori avanzati
Criteri basati su regole - Informazioni di riferimento sulle API REST
Autenticazione
Esercitazione: Assegnare ruoli alle entità servizio
Panoramica della programmabilità e dell'estendibilità