开始从暂停状态恢复时,请先将最关键的集合恢复联机,以便最高优先级的成员资格规则先于其他所有规则恢复生效。
Overview
此 PowerShell 示例会恢复对按 ID 指定的关键组和管理单元的动态成员身份规则的处理。 在从暂停恢复时先使用此示例:在批量重新激活非关键集合之前恢复最关键的集合。 该脚本在两个阶段中运行,以便可以恢复组、管理单元或两者。
重要注意事项
从 PowerShell 5.1(x64)或更高版本运行脚本。
安装 Microsoft Graph PowerShell 模块:
Install-Module Microsoft.Graph -Scope CurrentUser使用可管理目标集合的帐户登录。 群组阶段需要 Groups Administrator Microsoft Entra 角色,并请求
Group.ReadWrite.AllMicrosoft Graph 权限范围。 管理单元阶段需要 Privileged Role Administrator Microsoft Entra 角色,并请求AdministrativeUnit.ReadWrite.All权限范围。 该脚本仅请求你所运行阶段所需的作用域。该脚本分两个阶段运行:首先处理具有动态成员资格的组,然后处理具有动态成员资格的管理单元。 在每个阶段开始时,脚本会要求确认。 输入
yes以运行该阶段,或跳过其他任何步骤。 这样,您就可以在一次运行中仅以组为目标、仅以管理单元为目标,或同时以两者为目标。在大型租户中,此脚本可能会触发 Microsoft Graph 的节流机制。 该脚本内置了重试机制,因此预计运行时间会更长,而不是出现失败。 除非出现明确的错误提示,否则不要在脚本运行期间取消它。
一次恢复多个集合可以创建处理积压工作。 仅使用此示例恢复最高优先级组和管理单元,然后通过再次运行此示例或在Microsoft Entra 管理中心中重新激活其余组和管理单元。
暂停后至少等待 12 小时才能恢复处理,以便服务可以恢复。
在生产环境中运行脚本之前,请验证测试环境中的所有步骤。
示例脚本
# DISCLAIMER:
# Copyright (c) Microsoft Corporation. All rights reserved. This
# script is made available to you without any express, implied or
# statutory warranty, not even the implied warranty of
# merchantability or fitness for a particular purpose, or the
# warranty of title or non-infringement. The entire risk of the
# use or the results from the use of this script remains with you.
#
# Usage: powershell.exe .\UnpauseSpecificCritical.ps1
# This script allows you to unpause specific critical dynamic membership collections (groups and administrative units).
# It can be helpful when you need to mitigate ongoing issues with your dynamic membership collections.
# This function checks if you are already connected to Microsoft Graph.
# If yes,
# It disconnects to fetch your current information. Then, it prompts you to confirm the your current information.
# If you confirm, it reconnects to Microsoft Graph with Group.ReadWrite.All and AdministrativeUnit.ReadWrite.All permissions.
# If you don't confirm, it will not reconnect and will prompt you to connect manually using `Connect-MgGraph`.
# If not,
# It attempts to connect to Microsoft Graph with Group.ReadWrite.All and AdministrativeUnit.ReadWrite.All permissions.
# If it fails to connect, it informs you that the Microsoft.Graph module might not be installed and provides the command to install it.
# Microsoft Graph API version used for all requests in this script.
# Change to "beta" if you need to call beta endpoints. Default: "v1.0".
$graphApiVersion = "v1.0"
function ConnectToGraph {
param (
[string]$environment,
[string[]]$scopes
)
# Check if already connected to Microsoft Graph
if (Get-MgContext) {
# Disconnect to fetch your current information
$accountInfo = Disconnect-MgGraph
Write-Host "MAKE SURE THE BELOW ACCOUNT/CLIENT APPLICATION HAS THE RIGHT SET OF PERMISSIONS TO UNPAUSE DYNAMIC MEMBERSHIP COLLECTIONS (GROUPS AND ADMINISTRATIVE UNITS)" -ForegroundColor Yellow
Write-Host "Confirm the account: $($accountInfo.Account), TenantId: $($accountInfo.TenantId), and ClientId: $($accountInfo.ClientId)" -ForegroundColor Yellow
$input = Read-Host "Type 'yes' to confirm: "
if ($input.Trim().ToLower() -eq "yes") {
# Reconnect with the scopes required by the phases the operator selected.
Connect-MgGraph -Environment $environment -ClientId 'YOUR_CLIENT_ID' -TenantId 'YOUR_TENANT_ID' -Scopes $scopes
} else {
# Inform you to reconnect manually
Write-Host "Information not confirmed. Either re-run the script to confirm again or call <Connect-MgGraph> to log in using a different account." -ForegroundColor Yellow
exit 1
}
} else {
# Attempt to connect with the scopes required by the phases the operator selected.
Connect-MgGraph -Environment $environment -ClientId 'YOUR_CLIENT_ID' -TenantId 'YOUR_TENANT_ID' -Scopes $scopes
if (Get-MgContext) {
# Recursive call to confirm your information
ConnectToGraph -environment $environment -scopes $scopes
} else {
# Inform you to install Microsoft.Graph module if not connected
Write-Host "If the Microsoft.Graph module is not installed, you need to install it to run this script." -ForegroundColor Yellow
Write-Host "Run <Install-Module Microsoft.Graph -Scope CurrentUser> as an administrator." -ForegroundColor Yellow
exit 1
}
}
}
# This function unpauses a specific critical group with dynamic membership.
# It handles any errors, including throttling, and retries if necessary.
function UnpauseGroup {
param (
[string] $uri, [string] $groupId
)
$invokeArgs = @{
Uri = $uri
Method = 'PATCH'
Headers = @{ ConsistencyLevel = 'eventual' }
Body = $unpauseDGjson
}
try {
# Make PATCH request to unpause the group
Invoke-MgGraphRequest @invokeArgs
Write-Host "Successfully unpaused group with dynamic membership. Id: $groupId" -ForegroundColor Green
return $true
} catch {
# Handle errors and throttling
$errorMessage = $_.Exception.Message
$statusCode = $_.Exception.Response.StatusCode
if ($statusCode -eq 429) {
try {
# Handle throttling if status code is 429 by sleeping then retrying once
HandleThrottling -ErrorRecord $_
Invoke-MgGraphRequest @invokeArgs
Write-Host "Throttling mitigated. Successfully unpaused group with dynamic membership. Id: $groupId" -ForegroundColor Green
return $true
} catch {
# Handle failure after throttling mitigation
$errorMessage = $_.Exception.Message
Write-Host "Failed to unpause group with dynamic membership. Id: $groupId. Error message: $errorMessage" -ForegroundColor Red
return $false
}
} else {
# Handle other errors
Write-Host "Failed to unpause group with dynamic membership. Id: $groupId. Error message: $errorMessage" -ForegroundColor Red
return $false
}
}
}
# This function unpauses a specific critical administrative unit with dynamic membership.
# It handles any errors, including throttling, and retries if necessary.
function UnpauseAdministrativeUnit {
param (
[string] $uri, [string] $auId
)
$invokeArgs = @{
Uri = $uri
Method = 'PATCH'
Headers = @{ ConsistencyLevel = 'eventual' }
Body = $unpauseDGjson
}
try {
# Make PATCH request to unpause the administrative unit
Invoke-MgGraphRequest @invokeArgs
Write-Host "Successfully unpaused administrative unit with dynamic membership. Id: $auId" -ForegroundColor Green
return $true
} catch {
# Handle errors and throttling
$errorMessage = $_.Exception.Message
$statusCode = $_.Exception.Response.StatusCode
if ($statusCode -eq 429) {
try {
# Handle throttling if status code is 429 by sleeping then retrying once
HandleThrottling -ErrorRecord $_
Invoke-MgGraphRequest @invokeArgs
Write-Host "Throttling mitigated. Successfully unpaused administrative unit with dynamic membership. Id: $auId" -ForegroundColor Green
return $true
} catch {
# Handle failure after throttling mitigation
$errorMessage = $_.Exception.Message
Write-Host "Failed to unpause administrative unit with dynamic membership. Id: $auId. Error message: $errorMessage" -ForegroundColor Red
return $false
}
} else {
# Handle other errors
Write-Host "Failed to unpause administrative unit with dynamic membership. Id: $auId. Error message: $errorMessage" -ForegroundColor Red
return $false
}
}
}
# Function to validate GUID
function Is-Guid {
param (
[string]$Guid
)
return [guid]::TryParse($Guid, [ref]([guid]::Empty))
}
# Helper that prompts you for a comma-separated list of IDs of a given kind, validates them as GUIDs,
# echoes them back for confirmation, and returns the validated list.
# Returns $null if the user enters no valid IDs or declines to confirm.
function PromptForIdList {
param (
[string] $kindLabel
)
Write-Host "Enter the $kindLabel IDs (separated by comma if multiple):" -ForegroundColor Yellow
$inputIds = Read-Host
if ([string]::IsNullOrWhiteSpace($inputIds)) {
Write-Host "No IDs entered." -ForegroundColor Red
return $null
}
# Extracting individual IDs
$idList = $inputIds -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
# Validate each ID and remove invalid ones
$validIdList = @()
$invalidIds = @()
foreach ($id in $idList) {
if (Is-Guid $id) {
$validIdList += $id
} else {
$invalidIds += $id
}
}
if ($invalidIds.Count -gt 0) {
Write-Host "The following IDs are not valid GUIDs and will be removed:" -ForegroundColor Red
foreach ($invalidId in $invalidIds) {
Write-Host $invalidId
}
}
if ($validIdList.Count -eq 0) {
Write-Host "No valid IDs entered. Please re-run the script and provide IDs in GUID format." -ForegroundColor Red
return $null
}
Write-Host "Confirm that you entered $($validIdList.count) valid $kindLabel ID(s), as displayed here:" -ForegroundColor Yellow
foreach ($id in $validIdList) {
Write-Host $id
}
$confirm = Read-Host "Type 'yes' to confirm: "
if ($confirm.Trim().ToLower() -ne "yes") {
return $null
}
Write-Host "You have confirmed the entry of valid $kindLabel IDs." -ForegroundColor Green
return $validIdList
}
# Unpauses each of the supplied critical groups (by ID) if it is a dynamic membership group currently in "Paused" state.
# Returns a hashtable with Success and Failure counts.
function UnpauseSpecificCriticalDynamicMembershipGroups {
param (
[string] $graphEndpoint,
[string[]] $groupIdList
)
$successCount = 0
$failureCount = 0
foreach ($groupId in $groupIdList) {
# Fetch the group with the id given by you.
$uri = "$graphEndpoint/$graphApiVersion/groups/$groupId"
try {
$group = Invoke-MgGraphRequest -Method GET -Uri $uri
} catch {
# Branch on status so 429 retries, 404 reports as not-found, and other
# errors are not silently mislabelled as "Could not find".
$errorMessage = $_.Exception.Message
$statusCode = $null
try { $statusCode = [int]$_.Exception.Response.StatusCode } catch {}
if ($statusCode -eq 429) {
try {
HandleThrottling -ErrorRecord $_
$group = Invoke-MgGraphRequest -Method GET -Uri $uri
} catch {
$errorMessage = $_.Exception.Message
Write-Host "Failed to fetch the group with Id: $($groupId) after throttling mitigation. Error message: $($errorMessage)" -ForegroundColor Red
$failureCount++
continue
}
} elseif ($statusCode -eq 404) {
Write-Host "Could not find the group with Id: $($groupId). Error message: $($errorMessage)" -ForegroundColor Red
$failureCount++
continue
} else {
$statusText = if ($statusCode) { "$statusCode" } else { "Unknown" }
Write-Host "Failed to fetch the group with Id: $($groupId). Status: $statusText. Error message: $($errorMessage)" -ForegroundColor Red
$failureCount++
continue
}
}
$isDynamic = $group.groupTypes -contains "DynamicMembership"
$isPaused = $group.membershipRuleProcessingState -ceq "Paused"
# Unpause this group if it has a dynamic membership rule and is currently paused.
if ($isDynamic -and $isPaused) {
$result = UnpauseGroup -uri $uri -groupId $group.id
if ($result) {
$successCount++
} else {
$failureCount++
}
} else {
if (-not $isDynamic) {
Write-Host "Group skipped because it was found to be not a group with dynamic membership. Id: $($group.Id)" -ForegroundColor Yellow
continue
}
Write-Host "Group skipped because it was found to be in $($group.membershipRuleProcessingState) state. Id: $($group.Id)" -ForegroundColor Yellow
}
}
return @{ Success = $successCount; Failure = $failureCount }
}
# Unpauses each of the supplied critical administrative units (by ID) if it is a dynamic membership AU currently in "Paused" state.
# Returns a hashtable with Success and Failure counts.
function UnpauseSpecificCriticalDynamicMembershipAdministrativeUnits {
param (
[string] $graphEndpoint,
[string[]] $auIdList
)
$successCount = 0
$failureCount = 0
foreach ($auId in $auIdList) {
# Fetch the administrative unit with the id given by you.
$uri = "$graphEndpoint/$graphApiVersion/directory/administrativeUnits/$auId"
try {
$au = Invoke-MgGraphRequest -Method GET -Uri $uri
} catch {
$errorMessage = $_.Exception.Message
$statusCode = $null
try { $statusCode = [int]$_.Exception.Response.StatusCode } catch {}
if ($statusCode -eq 429) {
try {
HandleThrottling -ErrorRecord $_
$au = Invoke-MgGraphRequest -Method GET -Uri $uri
} catch {
$errorMessage = $_.Exception.Message
Write-Host "Failed to fetch the administrative unit with Id: $($auId) after throttling mitigation. Error message: $($errorMessage)" -ForegroundColor Red
$failureCount++
continue
}
} elseif ($statusCode -eq 404) {
Write-Host "Could not find the administrative unit with Id: $($auId). Error message: $($errorMessage)" -ForegroundColor Red
$failureCount++
continue
} else {
$statusText = if ($statusCode) { "$statusCode" } else { "Unknown" }
Write-Host "Failed to fetch the administrative unit with Id: $($auId). Status: $statusText. Error message: $($errorMessage)" -ForegroundColor Red
$failureCount++
continue
}
}
$isDynamic = $au.membershipType -ceq "Dynamic"
$isPaused = $au.membershipRuleProcessingState -ceq "Paused"
# Unpause this administrative unit if it has a dynamic membership rule and is currently paused.
if ($isDynamic -and $isPaused) {
$result = UnpauseAdministrativeUnit -uri $uri -auId $au.id
if ($result) {
$successCount++
} else {
$failureCount++
}
} else {
if (-not $isDynamic) {
Write-Host "Administrative unit skipped because it was found to be not an administrative unit with dynamic membership. Id: $($au.Id)" -ForegroundColor Yellow
continue
}
Write-Host "Administrative unit skipped because it was found to be in $($au.membershipRuleProcessingState) state. Id: $($au.Id)" -ForegroundColor Yellow
}
}
return @{ Success = $successCount; Failure = $failureCount }
}
# Internal function to handle throttling and retry requests
# This function handles throttling by sleeping for the duration indicated by the Retry-After
# header (or a default of 60 seconds). The caller is responsible for retrying the request.
function HandleThrottling {
param (
[System.Management.Automation.ErrorRecord]$ErrorRecord
)
# Throttling occurred, extract Retry-After header if available
$retryAfter = $ErrorRecord.Exception.Response.Headers.'Retry-After'
if ($retryAfter) {
Write-Host "Throttling detected. Waiting for $retryAfter seconds before retrying..."
Start-Sleep -Seconds $retryAfter
} else {
# If Retry-After header is not available, wait for a default time
Write-Host "Throttling detected. Waiting for default time i.e. 60 seconds before retrying..."
Start-Sleep -Seconds 60 # Wait for 60 seconds by default
}
}
# Function to prompt you to select the environment for determining the Microsoft Graph endpoint.
# This function helps you choose the correct environment and returns the corresponding endpoint.
function GetEnvironmentAndEndpoint {
# Prompt you to select the environment
try {
$environmentChoice = Read-Host "Please select the environment (default is 'Global'): `nOptions: Global, USGov, USGovDoD, China"
} catch {
$environmentChoice = 'Global'
}
# Normalize the input to lower case
$environmentChoice = $environmentChoice.Trim().ToLower()
# Set the default environment to global
$selectedEnvironment = "Global"
# Map your choice to the corresponding environment
switch ($environmentChoice) {
"usgov" { $selectedEnvironment = "USGov" }
"usgovdod" { $selectedEnvironment = "USGovDoD" }
"china" { $selectedEnvironment = "China" }
default { $selectedEnvironment = "Global" }
}
# Dictionary to map environment names to their endpoints
$endpoints = @{
"Global" = "https://microsoftgraph.chinacloudapi.cn"
"USGov" = "https://graph.microsoft.us"
"USGovDoD" = "https://dod-graph.microsoft.us"
"China" = "https://microsoftgraph.chinacloudapi.cn"
}
$graphEndpoint = $endpoints[$selectedEnvironment]
Write-Host "Environment Selected: $($selectedEnvironment). It maps to the graph endpoint: $($graphEndpoint)" -ForegroundColor Green
# Return the selected environment and graph endpoint
return @{ "SelectedEnvironment" = $selectedEnvironment; "GraphEndpoint" = $graphEndpoint }
}
#Running the script:
#Prompt you to confirm if you want to run the unpause specific critical flow.
Write-Host "DO YOU WANT TO UNPAUSE SPECIFIC CRITICAL DYNAMIC MEMBERSHIP COLLECTIONS (GROUPS AND/OR ADMINISTRATIVE UNITS)?" -ForegroundColor Yellow
Write-Host "NOTE: If you are running this script as part of a mitigation exercise recommended by Microsoft, we strongly recommend performing this operation for BOTH groups and administrative units with dynamic membership (i.e. answer 'yes' to both phases below)." -ForegroundColor Cyan
Write-Host "NOTE THAT IF YOU UNPAUSE A HIGH NUMBER OF DYNAMIC MEMBERSHIP COLLECTIONS, YOU MIGHT SEE A MASSIVE BACKLOG OF PROCESSING." -ForegroundColor Yellow
Write-Host "IT IS HIGHLY RECOMMENDED THAT YOU MANUALLY UNPAUSE COLLECTIONS ON THE AZURE PORTAL BASED ON THEIR PRIORITY." -ForegroundColor Yellow
$input = Read-Host "Type 'yes' to confirm: "
#Global variable for JSON change.
$global:unpauseDGjson = '{"membershipRuleProcessingState":"On"}'
# Start the unpause specific critical flow if confirmed.
if (($input.Trim().ToLower() -eq "yes")) {
$result = GetEnvironmentAndEndpoint
$selectedEnvironment = $result.SelectedEnvironment
$graphEndpoint = $result.GraphEndpoint
# Determine which phases the operator wants to run BEFORE connecting to Microsoft Graph,
# so we only request the scopes needed for the selected phases. This avoids requiring
# AdministrativeUnit.ReadWrite.All consent when the operator only wants to run the groups phase.
Write-Host "" -ForegroundColor Yellow
Write-Host "Before connecting to Microsoft Graph, please choose which phases to include in this run." -ForegroundColor Yellow
Write-Host "Only the scopes required by the selected phases will be requested." -ForegroundColor Yellow
Write-Host "" -ForegroundColor Yellow
Write-Host "Include the groups phase (unpause specific critical groups with dynamic membership by ID)?" -ForegroundColor Yellow
Write-Host "(Type 'yes' to include, anything else to skip.)" -ForegroundColor Yellow
$groupsPhaseInput = Read-Host "Type 'yes' to confirm: "
$runGroupsPhase = $groupsPhaseInput.Trim().ToLower() -eq "yes"
Write-Host "" -ForegroundColor Yellow
Write-Host "Include the administrative units phase (unpause specific critical administrative units with dynamic membership by ID)?" -ForegroundColor Yellow
Write-Host "(Type 'yes' to include, anything else to skip.)" -ForegroundColor Yellow
$ausPhaseInput = Read-Host "Type 'yes' to confirm: "
$runAusPhase = $ausPhaseInput.Trim().ToLower() -eq "yes"
# Build the scopes list based on the selected phases.
$selectedScopes = @()
if ($runGroupsPhase) { $selectedScopes += "Group.ReadWrite.All" }
if ($runAusPhase) { $selectedScopes += "AdministrativeUnit.ReadWrite.All" }
if ($selectedScopes.Count -eq 0) {
Write-Host "" -ForegroundColor Yellow
Write-Host "No phases selected. Exiting without connecting to Microsoft Graph." -ForegroundColor Yellow
exit 0
}
try {
# Show the operator which scopes will be requested so they know what consent to expect.
Write-Host "" -ForegroundColor Yellow
Write-Host "Requesting the following Microsoft Graph scope(s) for this run: $($selectedScopes -join ', ')" -ForegroundColor Yellow
# Connect to Microsoft Graph requesting only the scopes needed for the selected phases.
ConnectToGraph -environment $selectedEnvironment -scopes $selectedScopes
# Track per-phase results so a combined summary can be printed at the end.
$groupResult = $null
$auResult = $null
# Phase 1: groups with dynamic membership
if ($runGroupsPhase) {
Write-Host "" -ForegroundColor Yellow
Write-Host "PHASE 1 OF 2: GROUPS WITH DYNAMIC MEMBERSHIP" -ForegroundColor Yellow
$groupIds = PromptForIdList -kindLabel "group"
if ($groupIds -eq $null) {
Write-Host "Phase 1 cancelled by user. No groups with dynamic membership were unpaused." -ForegroundColor Yellow
} else {
$groupResult = UnpauseSpecificCriticalDynamicMembershipGroups -graphEndpoint $graphEndpoint -groupIdList $groupIds
}
} else {
Write-Host "" -ForegroundColor Yellow
Write-Host "Phase 1 skipped. No groups with dynamic membership were unpaused." -ForegroundColor Yellow
}
# Phase 2: administrative units with dynamic membership
if ($runAusPhase) {
Write-Host "" -ForegroundColor Yellow
Write-Host "PHASE 2 OF 2: ADMINISTRATIVE UNITS WITH DYNAMIC MEMBERSHIP" -ForegroundColor Yellow
$auIds = PromptForIdList -kindLabel "administrative unit"
if ($auIds -eq $null) {
Write-Host "Phase 2 cancelled by user. No administrative units with dynamic membership were unpaused." -ForegroundColor Yellow
} else {
$auResult = UnpauseSpecificCriticalDynamicMembershipAdministrativeUnits -graphEndpoint $graphEndpoint -auIdList $auIds
}
} else {
Write-Host "" -ForegroundColor Yellow
Write-Host "Phase 2 skipped. No administrative units with dynamic membership were unpaused." -ForegroundColor Yellow
}
# Combined summary
Write-Host "" -ForegroundColor Yellow
Write-Host "UnpauseSpecificCritical Operation Complete." -ForegroundColor Yellow
if ($groupResult) {
Write-Host "Groups with dynamic membership - Successfully Unpaused: $($groupResult.Success), Failed: $($groupResult.Failure)" -ForegroundColor Yellow
} else {
Write-Host "Groups with dynamic membership - Phase skipped or cancelled." -ForegroundColor Yellow
}
if ($auResult) {
Write-Host "Administrative units with dynamic membership - Successfully Unpaused: $($auResult.Success), Failed: $($auResult.Failure)" -ForegroundColor Yellow
} else {
Write-Host "Administrative units with dynamic membership - Phase skipped or cancelled." -ForegroundColor Yellow
}
} catch {
# Handle any errors during the process
$errorMessage = $_.Exception.Message
Write-Host "UnpauseSpecificCritical operation failed. Error message: $($errorMessage)" -ForegroundColor Red
}
# Inform you that the input was not accepted.
} else {
Write-Host "UnpauseSpecificCritical script terminated. Please re-run the script and input 'yes' to run the UnpauseSpecificCritical script." -ForegroundColor Red
}