Avancerede connectorpolitikker (ACP) styrer connectorforbruget med en streng allowlist, der blokerer connectors som standard. Ud over Power Platform Administration-oplevelsen kan du administrere ACP med kode ved hjælp af Power Platform-API'en og administrations-SDK'erne. Det er nyttigt at automatisere ACP, når du standardiserer styring på tværs af mange miljøgrupper, replikerer en oprindelig politik mellem grupper eller administrerer politikker som en del af en udrulningspipeline.
I dette selvstudium lærer du, hvordan du kan:
-
Godkend ved hjælp af Power Platform API.
-
Forstå ACP-politikskabelonen.
-
Opret en politik, og føj den til en miljøgruppe.
-
Aktivér en enkelt handling for en forbindelseskomponent.
-
Anvend eller opdater en politik på et enkelt miljø.
-
Kopiér en politik fra én miljøgruppe til en anden.
-
Fjern ACP fra en miljøgruppe.
Avancerede connectorpolitikker vises via handlingerne governance/ruleBasedPolicies i Power Platform-API'en. En politik indeholder et eller flere regelsæt. regelsættet med id'et ConnectorManagement indeholder listen over tilladte ACP-connectors. Alle eksempler i artiklen bruger API-version 2024-10-01.
Forudsætninger
En appregistrering, der er konfigureret til Power Platform API. Bemærk appregistreringens program-id (klient) og mappe-id (lejer).
Tilladelse til at administrere politikker for styring. For tjenesteprincipaler skal du tildele en rolle som RBAC, der kan skrive ressourcer, f.eks. Bidragyder til Power Platform eller Ejer af Power Platform. Du kan få flere oplysninger under Selvstudium: Tildel roller til tjenesteprincipaler.
I SDK-eksemplerne skal du installere det SDK, der sendes månedligt, i det offentlige galleri:
dotnet add package Microsoft.PowerPlatform.Management
pip install powerplatform-management
Alle eksempler godkendes med klient-id'et for din appregistrering efter vejledningen i Godkendelse. Følgende eksempler logger på interaktivt som den aktuelle bruger. Hvis du vil køre uden brugerinput som en tjenesteprincipal, skal du se flowet for fortrolige klienter i artiklen Godkendelse og tildele tjenesteprincipalen en RBAC-rolle.
# 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)
Trin 2. Forstå ACP-politikkens struktur
En avanceret connectorpolitik er en regelbaseret politik, der indeholder et regelsæt med id'et ConnectorManagement. Dette regelsæt har en version, og dets inputs rummer en AllowedConnectorList, hvor hver post tillader en forbindelse og angiver, hvordan dens handlinger og forbindelsestyper styres:
{
"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"
}
]
}
}
]
}
Vær opmærksom på følgende semantik:
- En connector, der ikke er i
AllowedConnectorList , er blokeret (standardafvis).
- Hvert element sætter
AllowedActionsMode.
AllAllowed tillader alle handlinger på connectoren.
SomeAllowed begrænser connectoren til de handlinger, der er angivet i postens AllowedActions matrix.
Trin 4 viser, hvordan du tilføjer en handling og angiver denne tilstand.
-
AllowedConnectionTypesMode styrer, hvilke forbindelsestyper der er tilladt, og følger det samme AllAllowed mønster.
- Medtag regelsættets
version , når du opretter eller opdaterer en politik. Læs den fra en eksisterende politik, og bevar den værdi, som tjenesten returnerer.
Tip!
Den nøjagtige værdi af AllowedConnector er connectorens ressource-id. Den mest pålidelige måde at lære figuren for forbindelser, der allerede findes i din lejer, er at læse en eksisterende politik først (trin 4 viser, hvordan) eller bruge connectorkataloget (beskrevet næste), og derefter afspejle denne figur, når du opretter eller opdaterer politikker.
Find connector- og handlings-id'er med connectorkataloget
Hvis du vil finde ud af, hvilke connectors og handlinger du kan tillade, skal du bruge API'en til connectorkataloget. Den viser de connectors, der er tilgængelige i et miljø, sammen med de id'er, du placerer i AllowedConnector og AllowedActions.
Note
Handlingerne i forbindelseskataloget kræver et miljø-id i stienog en OData $filter , der angiver det samme miljø , $filter=environment eq '<environmentId>'f.eks. . Begge er påkrævet.
$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
Brug connectorens id (den fulde ressourcesti, f.eks /providers/Microsoft.PowerApps/apis/shared_office365. ) som AllowedConnector værdien, og connectorens handlings-id'er som værdierne i AllowedActions. Du kan få adgang til det samme katalog via connectivity navneområdet for administrator-SDK'erne.
Trin 3. Opret en politik, og føj den til en miljøgruppe
Tilføjelse af ACP til en miljøgruppe er en handling i to dele: Opret politikken, og tildel den derefter til gruppen. Oprettelsesopkaldet returnerer den nye politik id, som du bruger i tildelingsopkaldet.
Hvis du vil tildele politikken til hele gruppen, skal du sende en tildelingsanmodning med en tom brødtekst ({}). Alle miljøer i gruppen nedarver politikken og forbliver synkroniseret med den.
$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}")
Trin 4. Aktivér en individuel forbindelseshandling
Hvis du kun vil tillade bestemte handlinger på en connector, skal du angive den AllowedActionsMode til SomeAllowed og angive de tilladte handlinger i AllowedActions. I dette eksempel føjes en handling, f.eks. en skjult handling, der ikke kan vælges i Administration, til en connectors allowlist og angiver connectoren til SomeAllowed. Læs politikken, opdater connectorposten, og send det opdaterede regelsæt tilbage ved hjælp af programrettelsen. Patch opdaterer en regel, der er angivet af id, og lader politikkens andre regelsæt være urørte.
$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}")
Trin 5. Anvend eller opdater en politik på et enkelt miljø
Du kan målrette en politik mod et enkelt miljø i stedet for en miljøgruppe. Denne fremgangsmåde er nyttig til miljøer med høj risiko, pilotmiljøer eller regulerede miljøer. Tildel politikken til miljøet, og brug det samme programrettelsesmønster fra trin 4 til at redigere den senere. Hvert miljø understøtter én effektiv AVS-politik.
$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}")
Trin 6. Kopiér en politik fra én miljøgruppe til en anden
Når du replikerer en oprindelig plan for styring til en anden gruppe, skal du vælge, hvor meget der skal kopieres, ved hjælp af flaget CopyAllRules :
-
CopyAllRules = true: Opret en ny politik ud fra alle kildegruppens regelsæt, og tildel den til målgruppen. Målgruppens styring bliver en uafhængig kopi af kilden.
-
CopyAllRules = false: Udtræk kun regelsættet ConnectorManagement fra kildepolitikken, og flet det til målgruppens eksisterende politik. Programrettelseshandlingen tilføjer eller opdaterer den regel, der er angivet af id'et, så målgruppen bevarer sine andre regler.
$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}")
Trin 6b. Kopiér en politik fra én miljøgruppe til en anden, herunder connectorhandlinger
Når du replikerer en oprindelig plan for styring til en anden gruppe, skal du vælge, hvor meget der skal kopieres, ved hjælp af flaget CopyAllRules :
-
CopyAllRules = true: Opret en ny politik ud fra alle kildegruppens regelsæt, og tildel den til målgruppen. Målgruppens styring bliver en uafhængig kopi af kilden.
-
CopyAllRules = false: Udtræk kun regelsættet ConnectorManagement fra kildepolitikken, og flet det til målgruppens eksisterende politik. Programrettelseshandlingen tilføjer eller opdaterer den regel, der er angivet af id'et, så målgruppen bevarer sine andre regler.
$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())
Trin 7. Fjern ACP fra en miljøgruppe
Selvom en gruppe har en aktiv AVS-regel, stemmer alle miljøer i gruppen overens med gruppens politik. Den måde, du fjerner håndhævelse på, afhænger af, om du vil have, at disse miljøer bevarer deres aktuelle konfiguration eller rydder ACP helt:
-
Fjern reglen fra gruppens politik for at forhindre gruppen i at administrere AVS. Brug handlingen
removeRule til at fjerne regelsættet ConnectorManagement fra gruppens politik. Miljøerne bevarer deres senest anvendte ACP-konfiguration, men de holdes ikke længere synkroniseret med gruppen. Du kan administrere hvert miljø individuelt og lade dem divergere.
-
Fjern ACP fra gruppen og fra alle miljøer for at slå ACP fra overalt. Fjern reglen fra gruppens politik, gå derefter gennem gruppens miljøer, og fjern også regelsættet
ConnectorManagement fra hvert miljøs politik.
Note
Hvis du fjerner reglen fra en gruppes politik, fjernes ACP ikke automatisk fra de miljøer, der har arvet den. Disse miljøer bevarer deres senest anvendte konfiguration for at undgå et hul i håndhævelse. Hvis du vil rydde ACP overalt, skal du fjerne det fra hvert miljø, som vist i eksemplet med løkken. Du kan få flere oplysninger under Avancerede connectorpolitikker.
Fjern reglen fra gruppens politik
I følgende eksempel fjernes regelsættet ConnectorManagement fra en politik ved hjælp af handlingen 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}")
Fjern ACP fra alle miljøer i gruppen
Hvis du vil slå ACP fra på tværs af alle miljøer i en gruppe, skal du først fjerne reglen fra gruppens politik (forrige eksempel) og derefter gentage fjernelsen for hvert miljøs egen politik. Læs den politik, der er tildelt hvert miljø, fra dets miljøtilknytning, og kald derefter removeRule på den politik. Angiv de miljø-id'er, der tilhører gruppen, eller optæl dem ved hjælp af API'erne til miljøstyring.
# 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"
}
}
Det samme kald pr. miljø removeRule fungerer sammen med de C# og Python SDK'er, der er vist tidligere. Ombryd opkaldet i en løkke over gruppens miljø-id'er.
Relateret indhold
Avancerede connectorpolitikker
Regelbaserede politikker – REST API-reference
Godkendelse
Selvstudium: Tildel roller til tjenesteprincipaler
Oversigt over programmering og udvidelse