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 # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjcwNTQxMDhaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCKKxL3lOKEMyppdeq5NR9C85WG+BLAComH # QRbWUcQgULOHzofEITQy7V+UoWz4AsswgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAianqM/IW56KbNP6WJVd43W7x3M4FpU4AH5ZHJmbmkgNrbX8R # g4EgunS1LnS1oLDLuxreIdVtxzifAJgqDFAd+tjYmMkgz7GXts0Vw7yYchtm/JU5 # WdLkphJ+3IAsyeY/OuCe/wNFQLIxtN8ozwGO4hPYz4/m//BV79jOp5p9/h/JfhTK # M4/xbFG9A8kvBa8TaFfzc6ffaBkVptTmJxW6K1M94Noqy//i8jOVLTEdIr0BbrXL # bt3tIFbeZt0GE2ZoPTAjFlFzQRmMrvML+kIVcJbHUQkUcVwwLpoM54T/obQUnSlS # k1sIJaxVQ8h9+zIifRPbGRPN6hVBMs07Kc89V90iKfe6O980wnOD/afb8vYjY1zh # b+QANDHUFSdePIghZs3B1MwqhVee4EPBrz2WHkWu/Ppht/cur20BzroGEwWe7i8m # fBvLGq0XPD71bsh3/ooQZl3E6IP2whiPR7BnZYUPC4+ZuMY1GbwXBJWL+OamWFOR # Is90bF5HyV4xPlInid8dF20+O12xa5gd4U7g5iP2V25Y3+IBWLLr17QN84SwQgEm # yT+VTz7AZdus9NsYD+bP28CGU2Y3tjkpS3oF3c1Li4EDFA59PcXh96/jiuyeJuK8 # xYppXVmInP4yAp/XknIeA09K/FiHgUnG2JqZ0zeNIH5mX4nHnpjxumvmrP0= # SIG # End signature block |