分批恢复具有动态成员资格的非关键组和管理单元

在关键集合恢复联机后,以可控的批次恢复其余非关键集合,以避免动态成员身份处理压力过大。

Overview

此 PowerShell 示例可恢复对已暂停的非关键组和管理单元的动态成员身份规则的处理,每次运行最多处理 100 个组和 100 个管理单元。 在恢复关键集合,并且自暂停以来已等待至少 12 小时后,运行此示例。 分批处理可防止在许多集合同时恢复处理时服务因负载过高而不堪重负。 该脚本分两个阶段执行:先处理最多 100 个组,然后再处理最多 100 个管理单元。

重要注意事项

  • 从 PowerShell 5.1(x64)或更高版本运行脚本。

  • 安装 Microsoft Graph PowerShell 模块:

    Install-Module Microsoft.Graph -Scope CurrentUser
    
  • 使用可管理目标集合的帐户登录。 群组阶段需要 Groups Administrator Microsoft Entra 角色,并请求 Group.ReadWrite.All Microsoft Graph 权限范围。 管理单元阶段需要 Privileged Role Administrator Microsoft Entra 角色,并请求 AdministrativeUnit.ReadWrite.All 权限范围。 该脚本仅请求你所运行阶段所需的作用域。

  • 该脚本分两个阶段运行:首先处理具有动态成员资格的组,然后处理具有动态成员资格的管理单元。 在每个阶段开始时,脚本会要求确认。 输入 yes 以运行该阶段,或跳过其他任何步骤。 这样,您就可以在一次运行中仅以组为目标、仅以管理单元为目标,或同时以两者为目标。

  • 在大型租户中,此脚本可能会触发 Microsoft Graph 的节流机制。 该脚本内置了重试机制,因此预计运行时间会更长,而不是出现失败。 除非出现明确的错误提示,否则不要在脚本运行期间取消它。

  • 首先使用 resume-specific-critical sample 恢复关键集合。

  • 该脚本最多处理 100 个组,每个运行最多 100 个管理单元。 重复运行它,直到恢复所有非关键集合。

  • 在生产环境中运行脚本之前,请验证测试环境中的所有步骤。

示例脚本

# 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 .\UnpauseNonCritical.ps1
# This script allows you to unpause non critical dynamic membership collections (groups and administrative units), 100 at a time per phase.
# It can be helpful when you need to mitigate ongoing issues with your collections that have dynamic membership rules.

# 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 fetches a single page of items (groups or administrative units) from Microsoft Graph.
# It returns the items and the next page token if available.
function PageableFetchFromGraph {
    param (
        [string] $uri,
        [hashtable] $headers = @{},
        [string] $kindLabel = "item"
    )
    # Save the request args so we can retry the same call after throttling.
    $invokeArgs = @{
        Method = 'GET'
        Uri    = $uri
    }
    if ($headers.Count -gt 0) {
        $invokeArgs.Headers = $headers
    }
    try {
        # Make GET request to fetch items. AU queries require additional advanced query option headers.
        $response = Invoke-MgGraphRequest @invokeArgs
        $items = $response.value
        $nextPage = $response.'@odata.nextLink'
        Write-Host "Found $($items.count) $kindLabel(s) with dynamic membership." -ForegroundColor Green
        return $items, $nextPage
    } catch {
        # Handle any errors during fetch, including throttling
        $errorMessage = $_.Exception.Message
        $statusCode = $null
        try { $statusCode = [int]$_.Exception.Response.StatusCode } catch {}
        if ($statusCode -eq 429) {
            try {
                # Sleep then retry the same request once after throttling
                HandleThrottling -ErrorRecord $_
                $response = Invoke-MgGraphRequest @invokeArgs
                $items = $response.value
                $nextPage = $response.'@odata.nextLink'
                Write-Host "Throttling mitigated. Found $($items.count) $kindLabel(s) with dynamic membership." -ForegroundColor Green
                return $items, $nextPage
            } catch {
                $errorMessage = $_.Exception.Message
                Write-Host "Failed to fetch $kindLabel(s) with dynamic membership after throttling mitigation. URI: $uri" -ForegroundColor Red
                Write-Host "Error message: $($errorMessage)" -ForegroundColor Red
                return $null
            }
        }
        $responseBody = $null
        try {
            if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                $responseBody = $_.ErrorDetails.Message
            } elseif ($_.Exception.Response) {
                $stream = $_.Exception.Response.GetResponseStream()
                if ($stream) {
                    $reader = New-Object System.IO.StreamReader($stream)
                    $responseBody = $reader.ReadToEnd()
                }
            }
        } catch {}
        Write-Host "Failed to fetch $kindLabel(s) with dynamic membership. URI: $uri" -ForegroundColor Red
        Write-Host "Error message: $($errorMessage)" -ForegroundColor Red
        if ($responseBody) {
            Write-Host "Response body: $responseBody" -ForegroundColor Red
        }
        return $null
    }
}

# This function unpauses a specific group with dynamic membership.
# It handles any errors, including throttling, and retries if necessary.
function UnpauseGroup {
    param (
        [string] $uri, [string] $groupId
    )
    $invokeArgs = @{
        Uri     = "$uri/$groupId"
        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 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/$auId"
        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
        }
    }
}

# Unpauses up to 100 non-critical paused groups with dynamic membership in a single page.
# Returns a hashtable with Success and Failure counts, or $null if the fetch failed.
function UnpauseNonCriticalDynamicMembershipGroups {
    param (
        [string] $graphEndpoint
    )
    # Fetch up to 100 paused groups with dynamic membership (single page; cap at 100 by design).
    # The groupTypes/any(...) lambda filter requires advanced query options (ConsistencyLevel: eventual and $count=true).
    # The filter value must be pre-encoded because Invoke-MgGraphRequest double-encodes
    # URLs containing unescaped (, ), or : characters.
    $url = "$graphEndpoint/$graphApiVersion/groups"
    $filterValue = "groupTypes/any(c:c eq 'DynamicMembership') and membershipRuleProcessingState eq 'Paused'"
    $encodedFilter = [uri]::EscapeDataString($filterValue)
    $uri = "$url`?`$filter=$encodedFilter&`$count=true&`$top=100"
    $advancedHeaders = @{ ConsistencyLevel = 'eventual' }

    $successCount = 0
    $failureCount = 0

    $groupsData = PageableFetchFromGraph -uri $uri -headers $advancedHeaders -kindLabel "group"
    if ($groupsData -eq $null) {
        Write-Host "Phase 1 terminated due to fetch failure." -ForegroundColor Red
        return $null
    }
    $dynamicGroups = $groupsData[0]

    foreach ($group in $dynamicGroups) {
        if ($group.membershipRuleProcessingState -ceq "Paused") {
            $result = UnpauseGroup -uri $url -groupId $group.id
            if ($result) {
                $successCount++
            } else {
                $failureCount++
            }
        } else {
            # Skip groups that are not in "Paused" state
            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 up to 100 non-critical paused administrative units with dynamic membership in a single page.
# Returns a hashtable with Success and Failure counts, or $null if the fetch failed.
function UnpauseNonCriticalDynamicMembershipAdministrativeUnits {
    param (
        [string] $graphEndpoint
    )
    # Fetch up to 100 paused administrative units with dynamic membership (single page; cap at 100 by design).
    # AU dynamic membership queries require advanced query options ($count + ConsistencyLevel: eventual).
    $url = "$graphEndpoint/$graphApiVersion/directory/administrativeUnits"
    $filter = "?`$filter=membershipType eq 'Dynamic' and membershipRuleProcessingState eq 'Paused'"
    $countAndTop = "&`$count=true&`$top=100"
    $uri = "$url$filter$countAndTop"
    $headers = @{ ConsistencyLevel = 'eventual' }

    $successCount = 0
    $failureCount = 0

    $ausData = PageableFetchFromGraph -uri $uri -headers $headers -kindLabel "administrative unit"
    if ($ausData -eq $null) {
        Write-Host "Phase 2 terminated due to fetch failure." -ForegroundColor Red
        return $null
    }
    $dynamicAUs = $ausData[0]

    foreach ($au in $dynamicAUs) {
        if ($au.membershipRuleProcessingState -ceq "Paused") {
            $result = UnpauseAdministrativeUnit -uri $url -auId $au.id
            if ($result) {
                $successCount++
            } else {
                $failureCount++
            }
        } else {
            # Skip administrative units that are not in "Paused" state
            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 non-critical flow.
Write-Host "DO YOU WANT TO UNPAUSE NON-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
Write-Host "YOU CAN ONLY UNPAUSE UP TO 100 GROUPS AND 100 ADMINISTRATIVE UNITS PER RUN OF THIS SCRIPT." -ForegroundColor Yellow
Write-Host "PLEASE MAKE SURE YOU HAVE ALREADY UNPAUSED THE CRITICAL COLLECTIONS. THIS SCRIPT IS RECOMMENDED TO BE USED FOR NON-CRITICAL COLLECTIONS ONLY." -ForegroundColor Yellow
$input = Read-Host "Type 'yes' to confirm: "

#Global variable for JSON change.
$global:unpauseDGjson = '{"membershipRuleProcessingState":"On"}'

# Start the unpause non-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 up to 100 non-critical paused groups with dynamic membership)?" -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 up to 100 non-critical paused administrative units with dynamic membership)?" -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.
        # Status values: "Skipped" (user declined), "FetchFailed" (LIST call failed), "Completed".
        $groupResult = $null
        $auResult = $null
        $groupPhaseStatus = "Skipped"
        $auPhaseStatus = "Skipped"

        # Phase 1: groups with dynamic membership
        if ($runGroupsPhase) {
            Write-Host "" -ForegroundColor Yellow
            Write-Host "PHASE 1 OF 2: GROUPS WITH DYNAMIC MEMBERSHIP" -ForegroundColor Yellow
            $groupResult = UnpauseNonCriticalDynamicMembershipGroups -graphEndpoint $graphEndpoint
            if ($null -eq $groupResult) { $groupPhaseStatus = "FetchFailed" } else { $groupPhaseStatus = "Completed" }
        } 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
            $auResult = UnpauseNonCriticalDynamicMembershipAdministrativeUnits -graphEndpoint $graphEndpoint
            if ($null -eq $auResult) { $auPhaseStatus = "FetchFailed" } else { $auPhaseStatus = "Completed" }
        } 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 "UnpauseNonCritical Operation Complete." -ForegroundColor Yellow
        if ($groupPhaseStatus -eq "Completed") {
            Write-Host "Groups with dynamic membership - Successfully Unpaused: $($groupResult.Success), Failed: $($groupResult.Failure)" -ForegroundColor Yellow
        } elseif ($groupPhaseStatus -eq "FetchFailed") {
            Write-Host "Groups with dynamic membership - Phase failed: could not list groups with dynamic membership. See error above." -ForegroundColor Red
        } else {
            Write-Host "Groups with dynamic membership - Phase skipped." -ForegroundColor Yellow
        }
        if ($auPhaseStatus -eq "Completed") {
            Write-Host "Administrative units with dynamic membership - Successfully Unpaused: $($auResult.Success), Failed: $($auResult.Failure)" -ForegroundColor Yellow
        } elseif ($auPhaseStatus -eq "FetchFailed") {
            Write-Host "Administrative units with dynamic membership - Phase failed: could not list administrative units with dynamic membership. See error above." -ForegroundColor Red
        } else {
            Write-Host "Administrative units with dynamic membership - Phase skipped." -ForegroundColor Yellow
        }
    } catch {
        # Handle any errors during the process
        $errorMessage = $_.Exception.Message
        Write-Host "UnpauseNonCritical operation failed. Error message: $($errorMessage)" -ForegroundColor Red
    }
# Inform you that the input was not accepted.
} else {
    Write-Host "UnpauseNonCritical script terminated. Please re-run the script and input 'yes' to run the UnpauseNonCritical script." -ForegroundColor Red
}

后续步骤