Public/ServiceEM/New-EntraOpsServiceEMAssignmentPolicy.ps1

<#
.SYNOPSIS
    Creates assignment policies for each service access package.

.DESCRIPTION
    Creates one assignment policy per access package in the catalog. Policies
    define requestor settings (self-add/remove enabled), reviewer settings
    (quarterly reviews, WorkloadPlane-Admins as reviewer of the WorkloadPlane-Users
    policies, ControlPlane-Admins as reviewer of the ManagementPlane-Admins policies, ManagementPlane-Admins
    as reviewer of all other policies, CatalogPlane-Members as last fallback reviewer), and expiration settings.
    Requests for privileged access packages are only approved by the same or a higher tier; CatalogPlane-Members
    only approve their own access package. Reviewers are configurable per policy in
    ServiceEM.AccessReviews.Policies.<Policy> (Group, SelfReview, SpecificReviewers or Manager). Assignments of all policies expire after 365 days
    by default, configurable per policy in ServiceEM.AssignmentPolicies.<Policy>.Expiration.

    Idempotent: existing policies with matching display names are reused.

.PARAMETER ServiceName
    Name of the service. Used in verbose logging and policy lookups.

.PARAMETER ServiceCatalogId
    Object ID of the Entitlement Management catalog.

.PARAMETER ServiceGroups
    Entra group objects. Used to resolve the CatalogPlane-Members and
    ManagementPlane-Admins groups for reviewer assignments.

.PARAMETER ServicePackages
    Access package objects returned by New-EntraOpsServiceEMAccessPackage.

.PARAMETER WorkloadPlaneAdminId
    Optional object ID of the workload plane admin (user). If set, the initial admin policy used for the
    admin's assignment (Initial Management Admin Policy, or Initial Workload Admin Policy if the service has no
    ManagementPlane-Admins package) only allows this user as target instead of all member users. Entra ID ignores
    "all member users" policies for users in scope of another policy with specific targets of the same package.

.PARAMETER logPrefix
    Text prepended to verbose messages. Defaults to the function name.

.EXAMPLE
    New-EntraOpsServiceEMAssignmentPolicy `
        -ServiceName "MyService" `
        -ServiceCatalogId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" `
        -ServiceGroups $groups `
        -ServicePackages $packages

    Creates assignment policies for all non-Unified access packages in the
    MyService catalog, wiring up reviewers from the CatalogPlane-Members and
    ManagementPlane-Admins groups.

#>

function New-EntraOpsServiceEMAssignmentPolicy {
    [OutputType([psobject[]])]
    [cmdletbinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ServiceName,

        [Parameter(Mandatory)]
        [string]$ServiceCatalogId,

        [Parameter(Mandatory)]
        [psobject[]]$ServiceGroups,

        [Parameter(Mandatory)]
        [psobject[]]$ServicePackages,

        [string]$WorkloadPlaneAdminId,

        [string]$logPrefix = "[$($MyInvocation.MyCommand)]"
    )

    begin {
        $policies = @()
        $encodedServiceCatalogId = ConvertTo-EntraOpsODataStringLiteral -Value $ServiceCatalogId
        $assignmentPolicyUri = "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies?`$filter=catalog/id eq '$encodedServiceCatalogId'&`$expand=accessPackage,catalog"
        try{
            Write-Verbose "$logPrefix Looking up Assignment Policy"
            # -ThrowOnFailure: a failed lookup must not look like "no policies" and create every policy again
            $policies += Invoke-EntraOpsMsGraphQuery -Method GET -Uri $assignmentPolicyUri -OutputType PSObject -DisableCache -ThrowOnFailure
        }catch{
            throw "$logPrefix Failed to look up the assignment policies of catalog ${ServiceCatalogId}; no policy was created: $($_.Exception.Message)"
        }
        $catalogPlaneMembersGroupId = ($ServiceGroups|Where-Object{$_.DisplayName -like "*CatalogPlane-Members"}).Id
        # Delegated (tenant-wide or other landing zone) groups are injected by name into ServiceGroups
        $mgmtApproverGroupId = ($ServiceGroups | Where-Object { $_.DisplayName -like "*ManagementPlane-Admins" } | Select-Object -First 1).Id
        $controlPlaneApproverId = ($ServiceGroups | Where-Object { $_.DisplayName -like "*ControlPlane-Admins" } | Select-Object -First 1).Id
        $wlUsersApproverId = ($ServiceGroups | Where-Object { $_.DisplayName -like "*WorkloadPlane-Admins" } | Select-Object -First 1).Id
        if (-not $wlUsersApproverId) { $wlUsersApproverId = $mgmtApproverGroupId }
        # Approver of the initial WorkloadPlane-Members requests; services without CatalogPlane group (custom roles)
        # use their WorkloadPlane-Admins or ManagementPlane-Admins group
        $membershipApproverGroupId = if ($catalogPlaneMembersGroupId) { $catalogPlaneMembersGroupId } else { $wlUsersApproverId }
        # Same package choice as New-EntraOpsServiceEMAssignment for the admin's initial assignment
        $initialAdminPolicyName = if ($ServicePackages | Where-Object { $_.DisplayName -like "*ManagementPlane-Admins" }) { 'Initial Management Admin Policy' } else { 'Initial Workload Admin Policy' }
        $setInitialAdminTarget = {
            param($params)
            if (-not [string]::IsNullOrWhiteSpace($WorkloadPlaneAdminId) -and $params.displayName -eq $initialAdminPolicyName) {
                Write-Verbose "$logPrefix Restricting $($params.displayName) to workload plane admin $WorkloadPlaneAdminId"
                $params.allowedTargetScope = 'specificDirectoryUsers'
                $params.specificAllowedTargets = @(
                    @{
                        '@odata.type' = '#microsoft.graph.singleUser'
                        userId        = $WorkloadPlaneAdminId
                    }
                )
            }
        }

        # Defaults can be overridden in ServiceEM.AssignmentPolicies and ServiceEM.AccessReviews of EntraOpsConfig
        $serviceEMConfig = if ($null -ne $Global:EntraOpsConfig) { $Global:EntraOpsConfig.ServiceEM }
        $defaultExpiration = 'P365D'
        $getPolicySetting = {
            param([string]$PolicyKey, [string]$Setting, $Default)
            $value = $serviceEMConfig.AssignmentPolicies.$PolicyKey.$Setting
            if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) { return $Default }
            return $value
        }
        $getExpiration = {
            param([string]$PolicyKey, [string]$Default)
            $value = [string](& $getPolicySetting $PolicyKey 'Expiration' $Default)
            if ($value -eq 'noExpiration') { return @{ type = 'noExpiration' } }
            if ($value -notmatch '^P(?=\d|T\d)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$') {
                throw "Invalid ServiceEM.AssignmentPolicies.$PolicyKey.Expiration '$value' in EntraOpsConfig. Use 'noExpiration' or an ISO 8601 duration such as 'P90D'."
            }
            return @{ type = 'afterDuration'; duration = $value }
        }
        $getApprovalTimeout = {
            param([string]$PolicyKey, [string]$Default)
            $value = [string](& $getPolicySetting $PolicyKey 'ApprovalTimeout' $Default)
            if ($value -notmatch '^P\d+D$') {
                throw "Invalid ServiceEM.AssignmentPolicies.$PolicyKey.ApprovalTimeout '$value' in EntraOpsConfig. Use a duration in days such as 'P2D'."
            }
            return $value
        }
        # Users may extend expiring assignments of the standard request policies; extensions always need approval
        $setExtension = {
            param([hashtable]$Params, [string]$PolicyKey)
            $allowExtension = (& $getPolicySetting $PolicyKey 'AllowExtension' $true) -and $Params.expiration.type -ne 'noExpiration'
            $Params.requestorSettings = $Params.requestorSettings.Clone()
            $Params.requestorSettings.enableTargetsToSelfUpdateAccess = $allowExtension
            $Params.requestApprovalSettings = $Params.requestApprovalSettings.Clone()
            $Params.requestApprovalSettings.isApprovalRequiredForUpdate = $allowExtension
        }
        $accessReviewConfig = $serviceEMConfig.AccessReviews
        $enableAccessReviews = if ($null -ne $accessReviewConfig.EnableAccessReviews) { [bool]$accessReviewConfig.EnableAccessReviews } else { $true }
        $reviewIntervalInMonths = if ($null -ne $accessReviewConfig.RecurrenceIntervalInMonths) { [int]$accessReviewConfig.RecurrenceIntervalInMonths } else { 3 }
        $reviewStartAfterDays = if ($null -ne $accessReviewConfig.StartAfterDays) { [int]$accessReviewConfig.StartAfterDays } else { 4 }
        $reviewDuration = if (-not [string]::IsNullOrWhiteSpace([string]$accessReviewConfig.ReviewDuration)) { [string]$accessReviewConfig.ReviewDuration } else { 'P25D' }
        if ($reviewIntervalInMonths -lt 1 -or $reviewIntervalInMonths -gt 12) {
            throw "Invalid ServiceEM.AccessReviews.RecurrenceIntervalInMonths '$reviewIntervalInMonths' in EntraOpsConfig. Use a value from 1 to 12."
        }
        if ($reviewStartAfterDays -lt 0) {
            throw "Invalid ServiceEM.AccessReviews.StartAfterDays '$reviewStartAfterDays' in EntraOpsConfig. Use 0 or a positive number of days."
        }
        if ($reviewDuration -notmatch '^P\d+D$') {
            throw "Invalid ServiceEM.AccessReviews.ReviewDuration '$reviewDuration' in EntraOpsConfig. Use a duration in days such as 'P25D'."
        }
        $guidPattern = '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$'
        $reviewerTiers = @('WorkloadPlane-Admins', 'ManagementPlane-Admins', 'ControlPlane-Admins')
        # Group reviewers are service group name suffixes (e.g. WorkloadPlane-Admins) or group object IDs
        $resolveReviewerGroups = {
            param([string]$PolicyKey, [string[]]$Values)
            foreach ($value in $Values) {
                if ($value -match $guidPattern) {
                    @{ "@odata.type" = "#microsoft.graph.groupMembers"; groupId = $value }
                    continue
                }
                # A missing admin group falls back to the same or a higher tier only
                $candidates = if ($value -in $reviewerTiers) { $reviewerTiers[$reviewerTiers.IndexOf($value)..($reviewerTiers.Count - 1)] } else { @($value) }
                $groupId = $null
                foreach ($candidate in $candidates) {
                    $groupId = ($ServiceGroups | Where-Object { $_.DisplayName -like "*$candidate" } | Select-Object -First 1).Id
                    if ($groupId) { break }
                }
                if ($groupId) {
                    @{ "@odata.type" = "#microsoft.graph.groupMembers"; groupId = $groupId }
                } else {
                    Write-Verbose "$logPrefix No reviewer group '$value' or a higher tier found for $PolicyKey"
                }
            }
        }
        $reviewSettingsByPolicy = @{}
        foreach ($reviewPolicyKey in 'BaselinePolicy', 'WorkloadPlaneUsers', 'WorkloadPlaneAdmins', 'ManagementPlaneAdmins', 'InitialWorkloadMembership', 'InitialManagementAdmins', 'InitialWorkloadUsers', 'InitialWorkloadAdmins', 'InitialCatalogMembers') {
            if (-not $enableAccessReviews) { break }
            $reviewerConfig = $accessReviewConfig.Policies.$reviewPolicyKey
            $reviewerType = if (-not [string]::IsNullOrWhiteSpace([string]$reviewerConfig.ReviewerType)) { [string]$reviewerConfig.ReviewerType } else { 'Group' }
            $reviewers = @($reviewerConfig.Reviewers | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } | ForEach-Object { ([string]$_).Trim() })
            $defaultReviewerGroup = switch ($reviewPolicyKey) {
                { $_ -in 'WorkloadPlaneUsers', 'InitialWorkloadUsers' } { 'WorkloadPlane-Admins'; break }
                { $_ -in 'ManagementPlaneAdmins', 'InitialManagementAdmins' } { 'ControlPlane-Admins'; break }
                default { 'ManagementPlane-Admins' }
            }
            $reviewerGroups = if ($reviewers.Count -gt 0) { $reviewers } else { @($defaultReviewerGroup) }
            $reviewSettings = @{
                isEnabled = $true
                expirationBehavior = "keepAccess"
                isRecommendationEnabled = $true
                isReviewerJustificationRequired = $true
                isSelfReview = $false
                schedule = @{
                    startDateTime = (Get-Date).AddDays($reviewStartAfterDays)
                    expiration = @{
                        duration = $reviewDuration
                        type = "afterDuration"
                    }
                    recurrence = @{
                        pattern = @{
                            type = "absoluteMonthly"
                            interval = $reviewIntervalInMonths
                            month = 0
                            dayOfMonth = 0
                        }
                        range = @{
                            type = "noEnd"
                        }
                    }
                }
            }
            switch ($reviewerType) {
                'Group' {
                    $reviewSettings.primaryReviewers = @(& $resolveReviewerGroups $reviewPolicyKey $reviewerGroups)
                    if ($reviewSettings.primaryReviewers.Count -eq 0) { $reviewSettings = $null }
                }
                'SelfReview' {
                    $reviewSettings.isSelfReview = $true
                    $reviewSettings.primaryReviewers = @()
                }
                'Manager' {
                    $reviewSettings.primaryReviewers = @(@{ "@odata.type" = "#microsoft.graph.requestorManager"; managerLevel = 1 })
                    $reviewSettings.fallbackReviewers = @(& $resolveReviewerGroups $reviewPolicyKey $reviewerGroups)
                }
                'SpecificReviewers' {
                    if ($reviewers.Count -eq 0) {
                        throw "ServiceEM.AccessReviews.Policies.$reviewPolicyKey.Reviewers in EntraOpsConfig needs at least one user object ID or UPN for ReviewerType 'SpecificReviewers'."
                    }
                    $reviewSettings.primaryReviewers = @(foreach ($reviewer in $reviewers) {
                            $userId = if ($reviewer -match $guidPattern) { $reviewer } else {
                                (Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/v1.0/users/$([uri]::EscapeDataString($reviewer))?`$select=id" -OutputType PSObject).Id
                            }
                            if (-not $userId) {
                                throw "Reviewer '$reviewer' in ServiceEM.AccessReviews.Policies.$reviewPolicyKey.Reviewers of EntraOpsConfig not found."
                            }
                            @{ "@odata.type" = "#microsoft.graph.singleUser"; userId = $userId }
                        })
                }
                default {
                    throw "Invalid ServiceEM.AccessReviews.Policies.$reviewPolicyKey.ReviewerType '$reviewerType' in EntraOpsConfig. Use 'Group', 'SelfReview', 'SpecificReviewers' or 'Manager'."
                }
            }
            $reviewSettingsByPolicy[$reviewPolicyKey] = $reviewSettings
        }
        $setReviewSettings = {
            param([hashtable]$Params, [string]$PolicyKey)
            if (-not $enableAccessReviews) { return }
            if ($reviewSettingsByPolicy[$PolicyKey]) {
                $Params.reviewSettings = $reviewSettingsByPolicy[$PolicyKey]
            } else {
                Write-Warning "$logPrefix '$($Params.displayName)' is created without access review: no reviewer group of the configured or a higher tier in this scope."
            }
        }
        $wpUsersRequestorScope = [string](& $getPolicySetting 'WorkloadPlaneUsers' 'RequestorScope' 'AllMemberUsers')
        if ($wpUsersRequestorScope -notin @('AllMemberUsers', 'CatalogPlaneMembers')) {
            throw "Invalid ServiceEM.AssignmentPolicies.WorkloadPlaneUsers.RequestorScope '$wpUsersRequestorScope' in EntraOpsConfig. Use 'AllMemberUsers' or 'CatalogPlaneMembers'."
        }
        foreach ($extensionPolicyKey in 'BaselinePolicy', 'WorkloadPlaneUsers', 'WorkloadPlaneAdmins', 'ManagementPlaneAdmins') {
            $allowExtensionValue = & $getPolicySetting $extensionPolicyKey 'AllowExtension' $true
            if ($allowExtensionValue -isnot [bool]) {
                throw "Invalid ServiceEM.AssignmentPolicies.$extensionPolicyKey.AllowExtension '$allowExtensionValue' in EntraOpsConfig. Use true or false."
            }
        }
        $policyParams = @{
            requestorSettings = @{
                enableTargetsToSelfAddAccess = $true
                enableTargetsToSelfUpdateAccess = $false
                enableTargetsToSelfRemoveAccess = $true
                allowCustomAssignmentSchedule = $false
                enableOnBehalfRequestorsToAddAccess = $false
                enableOnBehalfRequestorsToUpdateAccess = $false
                enableOnBehalfRequestorsToRemoveAccess = $false
            }
            accessPackage = @{
                id = ""
            }
        }
        $baselinePolicyParams = @{
            displayName = "Baseline Policy"
            description = "The baseline policy for $ServiceName access packages."
            allowedTargetScope = "specificDirectoryUsers"
            specificAllowedTargets = @(
                @{
                    "@odata.type" = "#microsoft.graph.groupMembers"
                    groupId = $(($ServiceGroups|Where-Object{$_.DisplayName -like "*CatalogPlane-Members"}).Id)
                }
            )
            expiration = & $getExpiration 'BaselinePolicy' $defaultExpiration
            requestApprovalSettings = @{
                isApprovalRequiredForAdd = $true
                isApprovalRequiredForUpdate = $false
                stages = @(
                    @{
                        durationBeforeAutomaticDenial = & $getApprovalTimeout 'BaselinePolicy' 'P2D'
                        isApproverJustificationRequired = $true
                        isEscalationEnabled = $false
                        durationBeforeEscalation = "PT0S"
                        primaryApprovers = @(
                            @{
                                "@odata.type" = "#microsoft.graph.groupMembers"
                                groupId = $(($ServiceGroups|Where-Object{$_.DisplayName -like "*CatalogPlane-Members"}).Id)
                            }
                        )
                    }
                )
            }
        }
        $initialPolicyParams = @{
            displayName = "Initial Membership Policy"
            description = "The initial membership policy for $ServiceName."
            allowedTargetScope = "allMemberUsers"
            expiration = & $getExpiration 'InitialWorkloadMembership' $defaultExpiration
            requestApprovalSettings = @{
                isApprovalRequiredForAdd = $true
                isApprovalRequiredForUpdate = $false
                stages = @(
                    @{
                        durationBeforeAutomaticDenial = "P7D"
                        isApproverJustificationRequired = $true
                        isEscalationEnabled = $false
                        durationBeforeEscalation = "PT0S"
                        primaryApprovers = @(
                            @{
                                "@odata.type" = "#microsoft.graph.requestorManager"
                                managerLevel = 1
                            }
                        )
                        fallbackPrimaryApprovers = @(
                            @{
                                "@odata.type" = "#microsoft.graph.groupMembers"
                                groupId = $membershipApproverGroupId
                            }
                        )
                    },
                    @{
                        durationBeforeAutomaticDenial = "P14D"
                        isApproverJustificationRequired = $true
                        isEscalationEnabled = $false
                        durationBeforeEscalation = "PT0S"
                        primaryApprovers = @(
                            @{
                                "@odata.type" = "#microsoft.graph.groupMembers"
                                groupId = $membershipApproverGroupId
                            }
                        )
                    }
                )
            }
        }
        $workloadPlanePolicyParams = @{
            displayName = "Workload Plane Policy"
            description = "The Workload Plane Policy for $ServiceName access packages."
            allowedTargetScope = "specificDirectoryUsers"
            specificAllowedTargets = @(
                @{
                    "@odata.type" = "#microsoft.graph.groupMembers"
                    groupId = $(
                        $wlpMembersId = ($ServiceGroups|Where-Object{$_.DisplayName -like "*WorkloadPlane-Members"}).Id
                        if($wlpMembersId){ $wlpMembersId } else { ($ServiceGroups|Where-Object{$_.DisplayName -like "*CatalogPlane-Members"}).Id }
                    )
                }
            )
            expiration = & $getExpiration 'WorkloadPlaneAdmins' $defaultExpiration
            requestApprovalSettings = @{
                isApprovalRequiredForAdd = $true
                isApprovalRequiredForUpdate = $false
                stages = @(
                    @{
                        durationBeforeAutomaticDenial = & $getApprovalTimeout 'WorkloadPlaneAdmins' 'P2D'
                        isApproverJustificationRequired = $true
                        isEscalationEnabled = $false
                        durationBeforeEscalation = "PT0S"
                        primaryApprovers = @(
                            @{
                                "@odata.type" = "#microsoft.graph.groupMembers"
                                groupId = $mgmtApproverGroupId
                            }
                        )
                    }
                )
            }
        }

        # Admin-only direct assignment policies for the initial assignments made by New-EntraOpsServiceEMAssignment
        $adminOnlyRequestorSettings = @{
            enableTargetsToSelfAddAccess           = $false
            enableTargetsToSelfUpdateAccess        = $false
            enableTargetsToSelfRemoveAccess        = $false
            allowCustomAssignmentSchedule          = $false
            enableOnBehalfRequestorsToAddAccess    = $false
            enableOnBehalfRequestorsToUpdateAccess = $false
            enableOnBehalfRequestorsToRemoveAccess = $false
        }
        $initialDirectPolicies = @(
            [pscustomobject]@{ PackageFilter = "*WorkloadPlane-Users";  DisplayName = "Initial Workload Users Policy"; ConfigKey = "InitialWorkloadUsers" }
            [pscustomobject]@{ PackageFilter = "*WorkloadPlane-Admins"; DisplayName = "Initial Workload Admin Policy"; ConfigKey = "InitialWorkloadAdmins" }
            [pscustomobject]@{ PackageFilter = "*CatalogPlane-Members"; DisplayName = "Initial Catalog Members Policy"; ConfigKey = "InitialCatalogMembers" }
        )
        foreach ($initialDirectPolicy in $initialDirectPolicies) {
            $initialDirectPolicy | Add-Member -NotePropertyName Expiration -NotePropertyValue (& $getExpiration $initialDirectPolicy.ConfigKey $defaultExpiration)
        }
        if (-not $enableAccessReviews) {
            Write-Verbose "$logPrefix Access reviews disabled in ServiceEM.AccessReviews"
        }
    }

    process {
        Write-Verbose "$logPrefix Beginning EM Assignment Policy"

        $policyFailures = [System.Collections.Generic.List[string]]::new()
        foreach($package in $ServicePackages){
            $policyParams.accessPackage.id = $package.Id
            # Check the main policy by name: a missing one is created on re-run even when secondary policies exist
            $secondaryPolicyNames = @('Management Plane Policy') + @($initialDirectPolicies | Where-Object { $package.DisplayName -like $_.PackageFilter } | ForEach-Object { $_.DisplayName })
            $hasMainPolicy = @($policies | Where-Object { $_.AccessPackage.Id -eq $package.Id -and $_.DisplayName -notin $secondaryPolicyNames }).Count -gt 0
            if(-not $hasMainPolicy){
                try{
                    Write-Verbose "$logPrefix Assigning Policy for Access Package ID: $($package.Id)"
                    if($package.DisplayName -like "*WorkloadPlane-Members"){
                        if (-not $membershipApproverGroupId) {
                            throw "No CatalogPlane-Members, WorkloadPlane-Admins or ManagementPlane-Admins group in this scope approves the requests."
                        }
                        $params = $policyParams + $initialPolicyParams
                        $params.displayName = "Initial Workload Membership Policy"
                        & $setReviewSettings $params 'InitialWorkloadMembership'
                        $policies += Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies" -Body ($params | ConvertTo-Json -Depth 20) -OutputType PSObject -ThrowOnFailure
                    }elseif($package.displayName -like "*ManagementPlane-Admins"){
                        # Admin-only policy for the initial assignment of the workload plane admin; requests are disabled
                        $params = $initialPolicyParams.Clone()
                        $params.displayName = "Initial Management Admin Policy"
                        $params.requestApprovalSettings = @{
                            isApprovalRequiredForAdd = $false  # No approval needed for adminAdd
                            isApprovalRequiredForUpdate = $false
                        }
                        $params.expiration = & $getExpiration 'InitialManagementAdmins' $defaultExpiration
                        $params = $policyParams + $params
                        $params.requestorSettings = $adminOnlyRequestorSettings
                        & $setInitialAdminTarget $params
                        & $setReviewSettings $params 'InitialManagementAdmins'
                        $policies += Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies" -Body ($params | ConvertTo-Json -Depth 20) -OutputType PSObject -ThrowOnFailure
                    } elseif ($package.displayName -like "*WorkloadPlane-Admins" -and -not $mgmtApproverGroupId) {
                        Write-Verbose "$logPrefix Skipping Workload Plane Policy — no ManagementPlane-Admins approver for this scope"
                    }elseif($package.displayName -like "*WorkloadPlane-Admins"){
                        $params = $policyParams + $workloadPlanePolicyParams
                        & $setExtension $params 'WorkloadPlaneAdmins'
                        & $setReviewSettings $params 'WorkloadPlaneAdmins'
                        $policies += Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies" -Body ($params | ConvertTo-Json -Depth 20) -OutputType PSObject -ThrowOnFailure
                    } elseif ($package.displayName -like "*WorkloadPlane-Users" -and -not $wlUsersApproverId) {
                        Write-Verbose "$logPrefix Skipping Workload Plane Users Policy — no WorkloadPlane-Admins or ManagementPlane-Admins approver for this scope"
                    }elseif($package.displayName -like "*WorkloadPlane-Users"){
                        $params = $baselinePolicyParams.Clone()
                        $params.displayName = "Workload Plane Users Policy"
                        $params.expiration = & $getExpiration 'WorkloadPlaneUsers' $defaultExpiration
                        if ($wpUsersRequestorScope -eq 'AllMemberUsers') {
                            $params.allowedTargetScope = "allMemberUsers"
                            $params.Remove('specificAllowedTargets')
                        }
                        $params.requestApprovalSettings = @{
                            isApprovalRequiredForAdd = $true
                            isApprovalRequiredForUpdate = $false
                            stages = @(
                                @{
                                    durationBeforeAutomaticDenial = & $getApprovalTimeout 'WorkloadPlaneUsers' 'P2D'
                                    isApproverJustificationRequired = $true
                                    isEscalationEnabled = $false
                                    durationBeforeEscalation = "PT0S"
                                    primaryApprovers = @(
                                        @{
                                            "@odata.type" = "#microsoft.graph.groupMembers"
                                            groupId       = $wlUsersApproverId
                                        }
                                    )
                                }
                            )
                        }
                        $params = $policyParams + $params
                        & $setExtension $params 'WorkloadPlaneUsers'
                        & $setReviewSettings $params 'WorkloadPlaneUsers'
                        $policies += Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies" -Body ($params | ConvertTo-Json -Depth 20) -OutputType PSObject -ThrowOnFailure
                    }else{
                        if (-not $catalogPlaneMembersGroupId) {
                            throw "The Baseline Policy requires a CatalogPlane-Members group in this scope as requestor and approver group."
                        }
                        $params = $policyParams + $baselinePolicyParams
                        & $setExtension $params 'BaselinePolicy'
                        & $setReviewSettings $params 'BaselinePolicy'
                        $policies += Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies" -Body ($params | ConvertTo-Json -Depth 20) -OutputType PSObject -ThrowOnFailure
                    }
                } catch {
                    Write-Warning "$logPrefix Failed to create assignment policy for package '$($package.DisplayName)' (ID: $($package.Id)). Error: $_"
                    $policyFailures.Add("assignment policy for '$($package.DisplayName)': $($_.Exception.Message)")
                }
            }

            # Self-service request of ManagementPlane-Admins by the administrator group, approved by ControlPlane-Admins.
            # Checked per policy name so existing landing zones get the policy on re-run.
            if ($package.DisplayName -like "*ManagementPlane-Admins" -and -not ($policies | Where-Object { $_.AccessPackage.Id -eq $package.Id -and $_.DisplayName -eq "Management Plane Policy" })) {
                if (-not $controlPlaneApproverId -or -not $catalogPlaneMembersGroupId) {
                    Write-Verbose "$logPrefix Skipping Management Plane Policy — no ControlPlane-Admins approver or CatalogPlane-Members requestor group for this scope"
                } else {
                    $params = $policyParams + @{
                        displayName             = "Management Plane Policy"
                        description             = "The Management Plane Policy for $ServiceName ManagementPlane-Admins access package."
                        allowedTargetScope      = "specificDirectoryUsers"
                        specificAllowedTargets  = @(
                            @{
                                "@odata.type" = "#microsoft.graph.groupMembers"
                                groupId       = $catalogPlaneMembersGroupId
                            }
                        )
                        expiration              = & $getExpiration 'ManagementPlaneAdmins' $defaultExpiration
                        requestApprovalSettings = @{
                            isApprovalRequiredForAdd    = $true
                            isApprovalRequiredForUpdate = $false
                            stages                      = @(
                                @{
                                    durationBeforeAutomaticDenial   = & $getApprovalTimeout 'ManagementPlaneAdmins' 'P1D'
                                    isApproverJustificationRequired = $true
                                    isEscalationEnabled             = $false
                                    durationBeforeEscalation        = "PT0S"
                                    primaryApprovers                = @(
                                        @{
                                            "@odata.type" = "#microsoft.graph.groupMembers"
                                            groupId       = $controlPlaneApproverId
                                        }
                                    )
                                }
                            )
                        }
                    }
                    & $setExtension $params 'ManagementPlaneAdmins'
                    & $setReviewSettings $params 'ManagementPlaneAdmins'
                    try {
                        Write-Verbose "$logPrefix Creating Management Plane Policy for Access Package ID: $($package.Id)"
                        $policies += Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies" -Body ($params | ConvertTo-Json -Depth 20) -OutputType PSObject -ThrowOnFailure
                    } catch {
                        Write-Warning "$logPrefix Failed to create Management Plane Policy for package '$($package.DisplayName)'. ManagementPlane-Admins can't be requested until it exists; re-run the deployment. Error: $_"
                        $policyFailures.Add("Management Plane Policy for '$($package.DisplayName)': $($_.Exception.Message)")
                    }
                }
            }

            # Checked per policy name so existing landing zones get the initial policy on re-run
            $initialDirectPolicy = $initialDirectPolicies | Where-Object { $package.DisplayName -like $_.PackageFilter } | Select-Object -First 1
            if ($initialDirectPolicy -and -not ($policies | Where-Object { $_.AccessPackage.Id -eq $package.Id -and $_.DisplayName -eq $initialDirectPolicy.DisplayName })) {
                $params = $policyParams + @{
                    displayName             = $initialDirectPolicy.DisplayName
                    description             = "Administrator direct assignments for the initial assignments of $ServiceName (no requests, no approval)."
                    allowedTargetScope      = "allMemberUsers"
                    expiration              = $initialDirectPolicy.Expiration
                    requestApprovalSettings = @{
                        isApprovalRequiredForAdd    = $false
                        isApprovalRequiredForUpdate = $false
                    }
                }
                $params.requestorSettings = $adminOnlyRequestorSettings
                & $setInitialAdminTarget $params
                & $setReviewSettings $params $initialDirectPolicy.ConfigKey
                try {
                    Write-Verbose "$logPrefix Creating $($initialDirectPolicy.DisplayName) for Access Package ID: $($package.Id)"
                    $policies += Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/assignmentPolicies" -Body ($params | ConvertTo-Json -Depth 20) -OutputType PSObject -ThrowOnFailure
                } catch {
                    Write-Warning "$logPrefix Failed to create $($initialDirectPolicy.DisplayName) for package '$($package.DisplayName)'. Error: $_"
                    $policyFailures.Add("$($initialDirectPolicy.DisplayName) for '$($package.DisplayName)': $($_.Exception.Message)")
                }
            }
        }
        if ($policyFailures.Count -gt 0) {
            throw "$logPrefix Failed to create $($policyFailures.Count) assignment policy(ies); re-run the deployment after resolving: $($policyFailures -join '; ')"
        }
    }

    end {
        $expectedIds = @($policies | Where-Object { $_.id } | Select-Object -ExpandProperty id)
        $check = @{}
        $confirmed = Wait-EntraOpsServiceEMCondition -Activity "Assignment policies" -logPrefix $logPrefix -Condition {
            $check.Policies = Invoke-EntraOpsMsGraphQuery -Method GET -Uri $assignmentPolicyUri -OutputType PSObject -DisableCache
            $actualIds = @($check.Policies | Where-Object { $_.id } | Select-Object -ExpandProperty id)
            # The catalog may hold further policies (e.g. added manually or by another deployment)
            @($expectedIds | Where-Object { $_ -notin $actualIds }).Count -eq 0
        }
        if(-not $confirmed){
            throw "Assignment policy consistency with Entra not achieved"
        }
        return [psobject[]]$check.Policies
    }
}
# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBlhn6+YOQzgMae
# QjYN6ljc5BpuvikMofiQzTINb8xjbaCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK
# ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy
# dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l
# dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG
# A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl
# BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb
# Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC
# 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va
# qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ
# 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet
# /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe
# GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV
# WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl
# s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb
# MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc
# noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl
# jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA
# AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8
# +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud
# DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w
# bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v
# c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np
# dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG
# CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM
# BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1
# BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR
# mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl
# lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs
# NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv
# n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggZrMIIEU6ADAgECAhBX5gfP8LabvnxC
# XoItEBa/MA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB
# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp
# Z25pbmcgMjAyMSBDQTAeFw0yNjEwMDYwODI4MjNaFw0yNzEwMDYwODI4MjJaMIGQ
# MQswCQYDVQQGEwJERTEdMBsGA1UECAwUUmhpbmVsYW5kLVBhbGF0aW5hdGUxEjAQ
# BgNVBAcMCUxhaG5zdGVpbjEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVy
# MS4wLAYDVQQDDCVPcGVuIFNvdXJjZSBEZXZlbG9wZXIgVGhvbWFzIE5hdW5oZWlt
# MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAphp2ljSmVsRB6gvkEq7A
# DUMENpKZiDFoAfF1ktD1nCwaVyI+bJ0ROjHwAn4iJgS+vlBDPOUIw2GLHkiqUhOg
# HSwzg6gj45GNmt6zxsKP0zPlhMB/fb0zmz7/F7e3yfKCXLfhbHuk0Es9gJ+8Wmet
# 9StVwwSE/K0MpW06GKA9nPWyE4N/ADwigDIj9rsTKJR4cve9w8G1b3zohpUc/qMr
# onab+HVw5r9b3sOBnnaXC6zvrX4fovRz1ztwfWjRjILLoFG6mW/TuK5F2ICD3Rhl
# 9R2rJWWWhzTDMsQlzvX+/SaM74q3OFpIVNdhvtmWU9pKUBRAyYMijcDqKEFGSzrc
# 8JFmGMpP5TuroWF+N21d3px2eARvW1mV0DCfFAH+Og20SH/gVW6/ltQQEkH/z/Zt
# 49ARyJrgESLOQtlMCIYSoKISdkk4ml4VUS1E/PcbzXEYj3a7m6DOxcd3WuY03UT9
# cnk3p0LMN6LeLYAZgVY+HRloZlH+kW2SkOHNuWgtXvZhAgMBAAGjggF4MIIBdDAM
# BgNVHRMBAf8EAjAAMD0GA1UdHwQ2MDQwMqAwoC6GLGh0dHA6Ly9jY3NjYTIwMjEu
# Y3JsLmNlcnR1bS5wbC9jY3NjYTIwMjEuY3JsMHMGCCsGAQUFBwEBBGcwZTAsBggr
# BgEFBQcwAYYgaHR0cDovL2Njc2NhMjAyMS5vY3NwLWNlcnR1bS5jb20wNQYIKwYB
# BQUHMAKGKWh0dHA6Ly9yZXBvc2l0b3J5LmNlcnR1bS5wbC9jY3NjYTIwMjEuY2Vy
# MB8GA1UdIwQYMBaAFN10XUwA23ufoHTKsW73PMAywHDNMB0GA1UdDgQWBBTBvGyH
# MXj1dMbVnxuof9oP4PY7cDBLBgNVHSAERDBCMAgGBmeBDAEEATA2BgsqhGgBhvZ3
# AgUBBDAnMCUGCCsGAQUFBwIBFhlodHRwczovL3d3dy5jZXJ0dW0ucGwvQ1BTMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF
# AAOCAgEAkC165c7/r6y3VeEEpNXas9bQs6ZpKPdJffHkf9xoPi4kWvAIWGc4ocG1
# DJLb18RXiDYShDmT5sb/6I08FeipKi2DNJkuOq6yOTJ3S7+hNFIZCyd6i7WKO8ea
# P5I+FAbyJP4+5coZoMg45bUa9JoocaJCm+5n7WiCfN7/EQpClW5Trm4+ZMbp7EMN
# F1uLJgGCMnN0SsoUarp7PHlqupMxMC4Uhhe+lAP2ytcSIU81L97/qjrADbLWqICk
# 9il5iGM+aZndguq/cmfH9rJj4GRZHWnG6WwnzYD9+KgYlRI6em+/9dq+IRiXe2Bs
# thUuIaFW7U2gWw8tBTEYm1UBLHdJx+L1gjJCLs5KT/wy5wU32wZxNDqPUQejUC1x
# GvU3cKqmLgRq+yNf3gFVfzqIoggyM3U9wQvnk7nBtCkpjXS2hdx4yxzcmrlWsDHr
# z6+6/jeyQySzbYUlFmKnc4n9Mw2soHOajEua5poFARVRS5KgUBuiiQ21YFNYzWBo
# qGuECaaAI/C5rGxapMJ3dAOBnHw6NLqBet+ZXBJEccX5y89MLlOQu+jDmJP8HieR
# qDLlwfhVZnw4RY8Rus43NZBwhNlVuOUXqocHKmJhKNlekda3qsJj93eQBQOxKJcc
# w5O08q9jukpH2+yzFAj+7ll33DuNUgWptogc4Awtbx3glzcEvJQwggaCMIIEaqAD
# AgECAhAo8HfBHDa9/l90MkdwJy4DMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYT
# AlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMT
# G0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQTAeFw0yNjAzMTEwNzM0NTRaFw0z
# NjAyMjcwNzM0NTRaMFAxCzAJBgNVBAYTAlBMMSEwHwYDVQQKDBhBc3NlY28gRGF0
# YSBTeXN0ZW1zIFMuQS4xHjAcBgNVBAMMFUNlcnR1bSBUaW1lc3RhbXAgMjAyNjCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALi9zfBw+xI12hi2F9uNQBNO
# STkTIi13fuh3DiWybI5392bBfJw6tO7zDs7UqP/rejVf4xFTU3XNOPsuOFkP5SPF
# snWwn3VJL+NhNhsuujvYsQtaQnvNo1wk46reTgws1L8OFEdKaxgyqT4zOQx81mLw
# NZSqXNU9Rrb9oAJWJF4Mydz+C1ebu0D0+vgM3tqAQ8TU203fqgKCPqIdHloaFqH9
# eyR+8tnpmy2hnnN59ZOtmsuHeRwf5iTOhlhf0d/qqquLOSIGPgGVL1Hj5XBUox2O
# E7tQRr+l/xxNbL6OuwBO4Aa2EbuxYg37cpXv5i2/lTL4NASB/duf9W4fjA3Ro+GP
# +bC54n2OYiM3Cq8z92ejx8F5HWO2e4/75eC95B9tMw/eq7109GDyW2+2ut5Bn0o8
# MXJTERkyystWbqcnUkomA97qfiDCwvpWuOMdfTOksXe9S+xDxOJaCbiyTivgPulx
# cT+tfQTRXBr5UwtPvc+INnafOEilzVHA11tv+pHcylryxBc08cUihf/ttqkZS0bm
# x3VxQay8uohaTp0//ECA9vYGW51SKdJMJhXvKyIdBLy8WYNchTt8yRITv9Adxg9J
# qGujh+TnP1bW3aQjFtlEl7SzgNeI8OVA/CJAWWUuRT3Au9ToOpHy/flrrknhN4he
# Ew7/6ILMv4HSsj0YZ4otAgMBAAGjggFQMIIBTDB1BggrBgEFBQcBAQRpMGcwOwYI
# KwYBBQUHMAKGL2h0dHA6Ly9zdWJjYS5yZXBvc2l0b3J5LmNlcnR1bS5wbC9jdHNj
# YTIwMjEuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMB8GA1UdIwQYMBaAFL5UAi+/QGxzQ86sCSVOnkNEGu7gMAwGA1UdEwEB/wQC
# MAAwOQYDVR0fBDIwMDAuoCygKoYoaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0ucGwv
# Y3RzY2EyMDIxLmNybDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8E
# BAMCB4AwIgYDVR0gBBswGTAIBgZngQwBBAIwDQYLKoRoAYb2dwIFAQswHQYDVR0O
# BBYEFCM5aiirmhKnpKfyH/lFmr+N/oIDMA0GCSqGSIb3DQEBDAUAA4ICAQBm/pOb
# rAveTqYFPCSthxNVwBHTW4TfuBQY2KGCdMlH6ALjVPYf2XARNGMTXScR0JQ2/c2L
# xLpoHHhRwxFf3d933DhFS165rYj6iPSQKm41rc9AMpTCjMdmai1aK9o870pOzTzL
# RhWFVIxm+5Qf+t4V5z14tPoUsAWdfRKD1Zzvzfe5AHlE9rxOZcGROiULxs3Edf7Z
# J8RZI0jzD9juj2oEX2XS7BVKuNCK1W0h1mjapv028yqo8UtZh5/1Ib48Yyp88BDY
# Shh5PSGUUuS5tYXVfcduIPBe3PdNTYRYdT5VNxkwLgjMzIcr/6SH2fejFJDjnCxj
# vXLuarErJg/IdI+hXdGIBDVOGv1zrkF4ktoQvFZIp5DIAYh0GwyUhc87qqn5UaYT
# j2+iyiBDSNh7aGCrkuAnqg7Qh7frhX+QhMGeJ9SpLpnG76qN85hLyjvYR6gRYiLT
# j8G4fxjqV400/buPw0rBo9yJZyq/8cGdCOLPOYf9qpefTcrZKeO9pg+UJHx8UABb
# aTqizRxm8sUSEycccCCFhIit3AI/3wGuSOADeiA676NcQ7+dsP04YvzvdN7pTZNy
# g672WpBDJdg0eItHl4yomvNeIOWh1qs4QqOW6TI8Ocsi4uSDTLorxLUDnBTwKQzz
# pxd1YUctEgRh32R0J9FkFkeEruJU94RljFr1uDCCBrkwggShoAMCAQICEQCZo4AK
# JlU7ZavcboSms+o5MA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAG
# A1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVt
# IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3Rl
# ZCBOZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjE4WhcNMzYwNTE4MDUzMjE4WjBW
# MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu
# MSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQCdI88EMCM7wUYs5zNzPmNdenW6vlxNur3r
# Lfi+5OZ+U3iZIB+AspO+CC/bj+taJUbMbFP1gQBJUzDUCPx7BNLgid1TyztVLn52
# NKgxxu8gpyTr6EjWyGzKU/gnIu+bHAse1LCitX3CaOE13rbuHbtrxF2tPU8f253Q
# gX6eO8yTbGps1Mg+yda3DcTsOYOhSYNCJiL+5wnjZ9weoGRtvFgMHtJg6i671OPX
# IciiHO4Lwo2p9xh/tnj+JmCQEn5QU0NxzrOiRna4kjFaA9ZcwSaG7WAxeC/xoZSx
# F1oK1UPZtKVt+yrsGKqWONoK6f5EmBOAVEK2y4ATDSkb34UD7JA32f+Rm0wsr5aj
# zftDhA5mBipVZDjHpwzv8bTKzCDUSUuUmPo1govD0RwFcTtMXcfJtm1i+P2UNXad
# PyYVKRxKQATHN3imsfBiNRdN5kiVVeqP55piqgxOkyt+HkwIA4gbmSc3hD8ke66t
# 9MjlcNg73rZZlrLHsAIV/nJ0mmgSjBI/TthoGJDydekOQ2tQD2Dup/+sKQptalDl
# ui59SerVSJg8gAeV7N/ia4mrGoiez+SqV3olVfxyLFt3o/OQOnBmjhKUANoKLYlK
# mUpKEFI0PfoT8Q1W/y6s9LTI6ekbi0igEbFUIBE8KDUGfIwnisEkBw5KcBZ3XwnH
# mfznwlKo8QIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU
# 3XRdTADbe5+gdMqxbvc8wDLAcM0wHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61Pgc
# ptE6Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMDAGA1Ud
# HwQpMCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYI
# KwYBBQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0
# dW0uY29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwv
# Y3RuY2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0
# cDovL3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQB1iFgP5Y9Q
# KJpTnxDsQ/z0O23JmoZifZdEOEmQvo/79PQg9nLF/GJe6ZiUBEyDBHMtFRK0mXj3
# Qv3gL0sYXe+PPMfwmreJHvgFGWQ7XwnfMh2YIpBrkvJnjwh8gIlNlUl4KENTK5DL
# qsYPEtRQCw7R6p4s2EtWyDDr/M58iY2UBEqfUU/ujR9NuPyKk0bEcEi62JGxauFY
# zZ/yld13fHaZskIoq2XazjaD0pQkcQiIueL0HKiohS6XgZuUtCKA7S6CHttZEsOb
# QJ1j2s0urIDdqF7xaXFVaTHKtAuMfwi0jXtF3JJphrJfc+FFILgCbX/uYBPBlbBI
# P4Ht4xxk2GmfzMn7oxPITpigQFJFWuzTMUUgdRHTxaTSKRJ/6Uh7ki/pFjf9sUAS
# WgxT69QF9Ki4JF5nBIujxZ2sOU9e1HSCJwOfK07t5nnzbs1LbHuAIGJsRJiQ6HX/
# DW1XFOlXY1rc9HufFhWU+7Uk+hFkJsfzqBz3pRO+5aI6u5abI4Qws4YaeJH7H7M8
# X/YNoaArZbV4Ql+jarKsE0+8XvC4DJB+IVcvC9Ydqahi09mjQse4fxfef0L7E3hh
# o2O3bLDM6v60rIRUCi2fJT2/IRU5ohgyTch4GuYWefSBsp5NPJh4QRTP9DC3gc5Q
# EKtbrTY0Ka87Web7/zScvLmvQBm8JDFpDjCCBrkwggShoAMCAQICEQDn/2nHOzXO
# S5Em2HR8aKWHMA0GCSqGSIb3DQEBDAUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UE
# ChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl
# cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBO
# ZXR3b3JrIENBIDIwHhcNMjEwNTE5MDUzMjA3WhcNMzYwNTE4MDUzMjA3WjBWMQsw
# CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQw
# IgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0EwggIiMA0GCSqGSIb3
# DQEBAQUAA4ICDwAwggIKAoICAQDpEh8ENe25XXrFppVBvoplf0530W0lddNmjtv4
# YSh/f7eDQKFaIqc7tHj7ox+u8vIsJZlroakUeMS3i3T8aJRC+eQs4FF0GqvkM6+W
# ZO8kmzZfxmZaBYmMLs8FktgFYCzywmXeQ1fEExflee2OpbHVk665eXRHjH7MYZIz
# Nnjl2m8Hy8ulB9mR8wL/W0v0pjKNT6G0sfrx1kk+3OGosFUb7yWNnVkWKU4qSxLv
# 16kJ6oVJ4BSbZ4xMak6JLeB8szrK9vwGDpvGDnKCUMYL3NuviwH1x4gZG0JAXU3x
# 2pOAz91JWKJSAmRy/l0s0l5bEYKolg+DMqVhlOANd8Yh5mkQWaMEvBRE/kAGzIqg
# WhwzN2OsKIVtO8mf5sPWSrvyplSABAYa13rMYnzwfg08nljZHghquCJYCa/xHK9a
# cev9UD7Y+usr15d7mrszzxhF1JOr1Mpup2chNSBlyOObhlSO16rwrffVrg/SzaKf
# SndS5swRhr8bnDqNJY9TNyEYvBYpgF95K7p0g4LguR4A++Z1nFIHWVY5v0fNVZmg
# zxD9uVo/gta3onGOQj3JCxgYx0KrCXu4yc9QiVwTFLWbNdHFSjBCt5/8Q9pLuRhV
# ocdCunhcHudMS1CGQ/Rn0+7P+fzMgWdRKfEOh/hjLrnQ8BdJiYrZNxvIOhM2aa3z
# EDHNwwIDAQABo4IBVTCCAVEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUvlQC
# L79AbHNDzqwJJU6eQ0Qa7uAwHwYDVR0jBBgwFoAUtqFUOQLDoD+Oirz61PgcptE6
# Dv0wDgYDVR0PAQH/BAQDAgEGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMDAGA1UdHwQp
# MCcwJaAjoCGGH2h0dHA6Ly9jcmwuY2VydHVtLnBsL2N0bmNhMi5jcmwwbAYIKwYB
# BQUHAQEEYDBeMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0u
# Y29tMDIGCCsGAQUFBzAChiZodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ru
# Y2EyLmNlcjA5BgNVHSAEMjAwMC4GBFUdIAAwJjAkBggrBgEFBQcCARYYaHR0cDov
# L3d3dy5jZXJ0dW0ucGwvQ1BTMA0GCSqGSIb3DQEBDAUAA4ICAQC4k1l3yUwV/ZQH
# CKCneqAs8EGTnwEUJLdDpokN/dMhKjK0rR5qX8nIIHzxpQR3TAw2IRw1Uxsr2Pli
# G3bCFqSdQTUbfaTq6V3vBzEebDru9QFjqlKnxCF2h1jhLNFFplbPJiW+JSnJTh1f
# KEqEdKdxgl9rVTvlxfEJ7exOn25MGbd/wGPwuSmMxRJVO0wnqgS7kmoJjNF9zqee
# hFSDDP8ZVkWg4EZ2tIS0M3uZmByRr+1Lkwjjt8AtW83mVnZTyTsOb+FNfwJY7DS4
# FmWhkRbgcHRetreoTirPOr/ozyDKhT8MTSTf6Lttg6s6T/u08mDWw6HK04ZRDfQ9
# sb77QV8mKgO44WGP31vXnVKoWVJpFBjPvjL8/Zck/5wXX2iqjOaLStFOR/IQki+E
# hn4zlcgVm22ZVCBPF+l8nAwUUShCtKuSU7GmZLKCmmxQMkSiWILTm8EtVD6AxnJh
# oq8EnhjEEyUoflkeRF2WhFiVQOmWTwZRr44IxWGkNJC6tTorW5rl2Zl+2e9JLPYf
# 3pStAPMDoPKIjVXd6NW2+fZrNUBeDo2eOa5Fn7Brs/HLQff5Xgris5MeUbdVgDrF
# 8uxO6cLPvZPo63j62SsNg55pTWk9fUIF9iPoRbb4QurjoY/woI1RAOKtYtTic6aA
# Jq3u83RIPpGXBSJKwx4KJAOZnCDCtTGCBqIwggaeAgEBMGowVjELMAkGA1UEBhMC
# UEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMb
# Q2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBAhBX5gfP8LabvnxCXoItEBa/MA0G
# CWCGSAFlAwQCAQUAoIGEMBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZI
# hvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcC
# ARUwLwYJKoZIhvcNAQkEMSIEIPzzJNeegputIWK+JXocHVQnY/ZkPy3dY4+Rjvyi
# tMyhMA0GCSqGSIb3DQEBAQUABIIBgIIlxiQgROTl5G/CIM6J+uDw98HiL2DoQmL7
# RZEKxexRL3Ur9xXe0YI2oa5LQ2Sly4uRh15frxysLuqBw0hdyAr6/csKDTwj4dy5
# wlgPZHj/77mOoRQbC5NDiHRz2hYeJEqzN5JM1C2Wy4jsr/JzuFrw/dxKh9FD9v/6
# DviLxmNAYqQuxg6c2kiBQWsFnhJAjT4EwIvRnXU5DTedZFvizJhStDTmErONhW6f
# +ghg06DG3Asyjmh3MeL5muKjMmv9floSqiYky3Whm5XFZK2jyRJBWUT3dId/Os4X
# 5BYO4Xs1RMwBXEP38N0f8MRk6sKKQEWh9UTS6wgShSYxJzJTiJJSuDgW0kL6hQqe
# MoCu6TqF2+HKF41ksaFecrQUaCmna4mvzL6In0oobfoknFE/WX5WvmH1jOoMdZv8
# 0dCHKGpFT5KmXZVC3sLc0XSJKP7AuoMEcCCSdUpBzt4Hyg82gP/g9iZJO5gbCb0Y
# Lu10vwZDRxWL1eGbZOA6pbDRTva1PqGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTQwMzZaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCKKxL3lOKEMyppdeq5NR9C85WG+BLAComH
# QRbWUcQgULOHzofEITQy7V+UoWz4AsswgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAqcVZQOEZxV7cY6EjULflooJW3ytJsBJrblf1FavK9feCDV4a
# y6V3F2IDjf5ja3ntVO0kH68g+gtTCx5ASne6Vi67Kx4UWf5FzLVy694htqpSozby
# kpUX7kq6WOuP0YmZiwiPbv6LyAQ0dC4mWNJnVhS3q3ppOis/5gqbpVrhS6Bt1iSK
# 5TkRCDm6BuMb4p0cN/N8cJwSkQ+FAg6e3sYOzykdN9fDNry3fUaO/B41dJ1U7jFx
# Lr1aZODBOyMga7J3K7sqg0Z6tS790KJ0OKqePo8ER17vjIofcJKtvZOUo9lsLKbB
# 1u2n5BaPzjIMAsn8pZxHm1A2yKv/qn9a3vo4eCMa3ocyOnkcrTm5bTR0hgFuZEvv
# nU29QVfzSE0+qmQ7WBzWjI26NiZydbDNV8EUok/La7lhpYcEbUnjveCkCCOV5AS4
# FYBdqdD4vPhHlAjM71IS/UXm8/c0soG6BeP9b9/zctqDHFxwntOIzJl4cQp1esoe
# 3ypP85HVbW6Sy8jbaDZ5KVHp16/BSwcfN4bG5Ko0U/QL0yR1jvAnjipIyiZfEqHX
# gEucFzDGwW6yqlo0sH28ZuEJ/RM5q5/OaqIeyJYneKPSUYWN1lMa3ZBdnqRX4i5V
# zGIRzhbApyehJ7E91ErTk9P2C50ijTjtV8p+V/vLLgW2+lGpVYq1K1DCZps=
# SIG # End signature block