Public/ServiceEM/New-EntraOpsServiceEMAccessPackageResourceAssignment.ps1
|
<# .SYNOPSIS Assigns Entra security groups as resources inside their matching access packages. .DESCRIPTION For each owned (non-delegated) security group, looks up the corresponding access package by name and registers the group's Member catalog resource role into that package (Eligible Member for the groups in -EligibleMemberGroupIds). Package names are matched using the pattern AP-<ServiceName>-<AccessLevel>-<RoleName>, derived from the group's DisplayName. The Microsoft 365 group (if created) is added to every access package, so all assigned users become members of it. Idempotent: existing resource role scope assignments are detected and skipped to avoid duplicates. .PARAMETER ServiceCatalogId Object ID of the Entitlement Management catalog. .PARAMETER ServiceName Name of the service. Used to derive expected access package names from group display names. .PARAMETER ServiceGroups Entra group objects to register. Delegated groups (IsDelegated=true) are automatically skipped. .PARAMETER ServicePackages Access package objects returned by New-EntraOpsServiceEMAccessPackage. .PARAMETER ServiceCatalogResources Catalog resource objects returned by New-EntraOpsServiceEMCatalogResource. .PARAMETER EligibleMemberGroupIds Object IDs of groups managed by PIM for Groups. Their access package delivers the Eligible Member role (PIM for Groups eligible membership) instead of the active Member role. Requires Microsoft Entra ID Governance or Microsoft Entra Suite licenses. .PARAMETER GroupPrefix Prefix used in group DisplayNames (e.g. "SG"). Defaults to "SG". .PARAMETER GroupNamingDelimiter Delimiter between name segments. Only "-" is supported, see New-EntraOpsServiceEntraGroup. .PARAMETER logPrefix Text prepended to verbose messages. Defaults to the function name. .EXAMPLE New-EntraOpsServiceEMAccessPackageResourceAssignment ` -ServiceCatalogId "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" ` -ServiceName "MyService" ` -ServiceGroups $groups ` -ServicePackages $packages ` -ServiceCatalogResources $resources Maps each security group (e.g. SG-MyService-WorkloadPlane-Members) to its corresponding access package (AP-MyService-WorkloadPlane-Members) as a Member resource role. #> function New-EntraOpsServiceEMAccessPackageResourceAssignment { [OutputType([psobject[]])] [cmdletbinding()] param( [Parameter(Mandatory)] [string]$ServiceCatalogId, [Parameter(Mandatory)] [string]$ServiceName, [Parameter(Mandatory)] [psobject[]]$ServiceGroups, [Parameter(Mandatory)] [psobject[]]$ServicePackages, [Parameter(Mandatory)] [psobject[]]$ServiceCatalogResources, [string[]]$EligibleMemberGroupIds = @(), [string]$GroupPrefix = "SG", [ValidateSet('-')] [string]$GroupNamingDelimiter = "-", [string]$logPrefix = "[$($MyInvocation.MyCommand)]" ) begin { $assignedRoles = @() $assignedRoles += $ServicePackages.ResourceRoleScopes $packageRoles = @() $removedRoles = @() } process { Write-Verbose "$logPrefix Beginning EM Access Package Resource Assignment" Write-Verbose "$logPrefix Processing Access Package Assignments for $(($ServiceGroups|Measure-Object).Count) Groups" $groupPackagePairs = foreach ($group in $ServiceGroups) { # Delegated groups are not catalog resources if ($group.IsDelegated -eq $true) { continue } if ($group.PSObject.Properties.Name -contains 'GroupTypes' -and $group.GroupTypes -contains "Unified") { foreach ($unifiedPackage in $ServicePackages) { [pscustomobject]@{ Group = $group; Package = $unifiedPackage } } continue } # Derive the expected access package name from the group display name. # Groups are named: {Prefix}{Delim}{ServiceName}{Delim}{Plane}{Delim}{Role} # e.g. SG-Contoso-WorkloadPlane-Admins # Packages are named: AP-{ServiceName}-{Plane}-{Role} # e.g. AP-Contoso-WorkloadPlane-Admins $groupServicePrefix = "$(if ($GroupPrefix -match '[-_.]$') { $GroupPrefix } else { "$GroupPrefix$GroupNamingDelimiter" })$ServiceName$GroupNamingDelimiter" if ($group.DisplayName.StartsWith($groupServicePrefix)) { $planePlusRole = $group.DisplayName.Substring($groupServicePrefix.Length) $expectedPackageName = "AP$GroupNamingDelimiter$ServiceName$GroupNamingDelimiter$planePlusRole" $package = $ServicePackages | Where-Object { $_.DisplayName -eq $expectedPackageName } } else { $package = $null } if (-not $package) { Write-Verbose "$logPrefix No matching Access Package found for group '$($group.DisplayName)', skipping" continue } [pscustomobject]@{ Group = $group; Package = $package } } $resourceRolesByResource = @{} $roleScopeFailures = [System.Collections.Generic.List[string]]::new() foreach ($pair in @($groupPackagePairs)) { $group = $pair.Group $package = $pair.Package # Matched by object ID: a display name can be shared or changed (e.g. another GroupPrefix) $resource = $ServiceCatalogResources | Where-Object { $_.OriginSystem -eq "AadGroup" -and ( ($group.Id -and $_.OriginId -eq $group.Id) -or (-not $group.Id -and $_.DisplayName -eq $group.DisplayName)) } | Select-Object -First 1 # A skipped group leaves its access package without access: report it instead of completing successfully if (-not $resource) { $roleScopeFailures.Add("'$($group.DisplayName)' is not a resource of the catalog, so '$($package.DisplayName)' grants no access to it") continue } Write-Verbose "$logPrefix Processing Access Package Resource ID: $($resource.Id)" Write-Verbose "$logPrefix Processing Access Package ID: $($package.Id)" #Get available roles for resource in Catalog #Used to validate if Access Package exists for resource role if (-not $resourceRolesByResource.ContainsKey($resource.Id)) { try { Write-Verbose "$logPrefix Getting Catalog Resource Roles for Resource ID: $($resource.id)" # -ThrowOnFailure: a failed lookup must not look like a resource without roles $resourceRolesByResource[$resource.Id] = Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/v1.0/identityGovernance/entitlementManagement/catalogs/$ServiceCatalogId/resourceRoles?`$filter=originSystem eq 'AadGroup' and resource/id eq '$($resource.id)'&`$expand=resource" -OutputType PSObject -ThrowOnFailure } catch { $roleScopeFailures.Add("catalog resource roles of '$($group.DisplayName)' could not be read: $($_.Exception.Message)") continue } } $resourceRoles = $resourceRolesByResource[$resource.Id] if (-not $resourceRoles) { $roleScopeFailures.Add("the catalog returned no resource roles for '$($group.DisplayName)' (resource not yet indexed?); run the deployment again") continue } Write-Verbose "$logPrefix Found Catalog Resource Roles: $($resourceRoles.OriginId|ConvertTo-Json -Compress)" $eligible = $group.Id -in $EligibleMemberGroupIds # Group roles: Member_<groupId>; EligibleMember_<groupId> only when the group is managed by PIM for Groups $findRole = { param($prefix, $name) $resourceRoles | Where-Object { $_.OriginId -eq "$($prefix)_$($resource.OriginId)" -or $_.DisplayName -eq $name } | Select-Object -First 1 } $memberRole = if ($eligible) { & $findRole 'EligibleMember' 'Eligible Member' } else { & $findRole 'Member' 'Member' } if (-not $memberRole) { if ($eligible) { Write-Warning "$logPrefix Eligible Member role not found for '$($group.DisplayName)' in the catalog (group not yet managed by PIM for Groups, or no Microsoft Entra ID Governance license). No resource role is added to the access package, run the deployment again later." } else { $roleScopeFailures.Add("the Member role of '$($group.DisplayName)' was not found in the catalog (resource not yet indexed?); run the deployment again") } continue } $resourceParams = @{ role = @{ id = $memberRole.Id originId = $memberRole.OriginId originSystem = $resource.OriginSystem resource = @{ id = $resource.Id originId = $resource.OriginId originSystem = $resource.OriginSystem } } scope = @{ originId = $resource.OriginId originSystem = $resource.OriginSystem } } # A package is paired with several groups; roles removed for an earlier group must not return from its stale snapshot $ex = @($package.ResourceRoleScopes | ForEach-Object { "$($_.Role.OriginId)_$($_.Scope.OriginId)" } | Where-Object { $_ -notin $removedRoles }) $packageRoles += $ex if (-not $eligible) { # No active Member next to Eligible Member: it would bypass the PIM activation $eligibleScope = @($package.ResourceRoleScopes | Where-Object { "$($_.Role.OriginId)" -like 'EligibleMember_*' -and $_.Scope.OriginId -eq $resource.OriginId -and "$($_.Role.OriginId)_$($_.Scope.OriginId)" -notin $removedRoles }) if ($eligibleScope.Count -gt 0) { Write-Warning "$logPrefix '$($package.DisplayName)' delivers '$($group.DisplayName)' as Eligible Member (PIM for Groups). The active Member role is not added, so membership still requires PIM activation. Run the deployment with PIM for Groups enabled for this group, or remove the Eligible Member role from the access package to switch to active membership." continue } } $activeRemovalFailed = $false if ($eligible) { # An active Member role (e.g. deployed without PIM for Groups) would bypass the PIM activation $activeRole = & $findRole 'Member' 'Member' foreach ($activeScope in @($package.ResourceRoleScopes | Where-Object { $activeRole -and $_.Role.OriginId -eq $activeRole.OriginId -and $_.Scope.OriginId -eq $resource.OriginId })) { Write-Warning "$logPrefix Replacing the active Member role of '$($group.DisplayName)' in '$($package.DisplayName)' with Eligible Member; assigned users lose their active membership and must activate it with PIM for Groups." try { Invoke-EntraOpsMsGraphQuery -Method DELETE -Uri "/v1.0/identityGovernance/entitlementManagement/accessPackages/$($package.Id)/resourceRoleScopes/$($activeScope.Id)" -OutputType PSObject -ThrowOnFailure | Out-Null $removed = "$($activeScope.Role.OriginId)_$($activeScope.Scope.OriginId)" $removedRoles += $removed $packageRoles = @($packageRoles | Where-Object { $_ -ne $removed }) } catch { $activeRemovalFailed = $true Write-Warning "$logPrefix Failed to remove the active Member role of '$($group.DisplayName)' from '$($package.DisplayName)', Eligible Member is not added; run the deployment again. Error: $($_.Exception.Message)" } } } if ($activeRemovalFailed) { continue } $tb = "$($resourceParams.role.originId)_$($resourceParams.scope.originId)" if ($tb -notin $ex) { try { Write-Verbose "$logPrefix Creating new role assignment" Write-Verbose "$logPrefix Resource Param: $($resourceParams|ConvertTo-Json -Compress)" $postResult = Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/identityGovernance/entitlementManagement/accessPackages/$($package.Id)/resourceRoleScopes" -Body ($resourceParams | ConvertTo-Json -Depth 10) -OutputType PSObject -ThrowOnFailure if ($null -ne $postResult) { $assignedRoles += $postResult $packageRoles += $tb } else { Write-Verbose "$logPrefix Role assignment POST returned null — assignment may not have been created" } } catch { Write-Verbose "$logPrefix Failed to create new role assignment" $roleScopeFailures.Add("$($memberRole.DisplayName) of '$($group.DisplayName)' in '$($package.DisplayName)': $($_.Exception.Message)") } } } if ($roleScopeFailures.Count -gt 0) { throw "$logPrefix Failed to add $($roleScopeFailures.Count) resource role(s) to access packages: $($roleScopeFailures -join '; ')" } } end { # When no role assignments were attempted (e.g. all groups skipped due to timeouts), # skip the consistency wait entirely. if (($packageRoles | Measure-Object).Count -eq 0) { Write-Verbose "$logPrefix No role assignments to verify — skipping consistency check" return [psobject[]]@() } $expectedAssignments = @($packageRoles | Sort-Object -Unique) $check = @{} $confirmed = Wait-EntraOpsServiceEMCondition -Activity "Access package resource roles" -logPrefix $logPrefix -Condition { try { $check.Packages = [object[]]@(Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/v1.0/identityGovernance/entitlementManagement/accessPackages?`$filter=catalog/id eq '$($ServiceCatalogId)'&`$expand=resourceRoleScopes(`$expand=role,scope),catalog" -OutputType PSObject -DisableCache) } catch { Write-Verbose "$logPrefix Consistency check lookup failed (transient?) — retrying: $($_.Exception.Message)" return $false } $checkAssignments = @($check.Packages.ResourceRoleScopes | ForEach-Object { "$($_.Role.OriginId)_$($_.Scope.OriginId)" } | Sort-Object -Unique) # Packages of the catalog may hold further resource roles (e.g. added manually or by another deployment) @($expectedAssignments | Where-Object { $_ -notin $checkAssignments }).Count -eq 0 } if (-not $confirmed) { throw "Access Package role assignment consistency with Entra not achieved" } return [psobject[]]$check.Packages } } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCRN41b/bg+wO6y # saZd80ZJ6ifF3SzKLxNAgTMpfLYYfKCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEIIbNEWGLiEuOf4syLRV9ukbz35LUgVeX6ZS4hDH+ # 2J4QMA0GCSqGSIb3DQEBAQUABIIBgDsMv/4smD86heCToJP0GJJHx2scgG1FdT0O # e/d3dMoBJwcPo0/QbFtEyLoCeTU92nI5SvYV/69gkWZREsRJZGqzsolVUkZzI9Px # wctrwgeT3OnqnvtVF21pV0PCH6yUAKce4a14NimOoyWCosGrKtXxLkhqnEeoJYzQ # lrXi6pW7ANJy86KNRLH62hjljq23bsc0dHHSeCKVS+MFz9jeaXTW5tNwA5kAZa27 # nr5tP05P+LtZ2U5jq25VXba6MAEi2E1g5KvdpZfrMZ6VVpMN9lmZD4iDYNBr4MsI # 7C1goND1UQS6imkbXTMN5r06BrYEcCYCSyBLje9zrKNrijn4Et6D6pi2VnWQecsm # Gd6Pkf3kYlB3IP6Cp3q3lmcD1769A+DcjXnCgj23SIJ/WaKnORdB8kIE5PEihOdH # d87h0S9467LN3ggBG4GPE/vOnGCsjXdPZOGVBOkyGMOsH4MZ3JpzsdQOARBGXv48 # kFbLBxJdKJ2rMIxNDvkkUK0w9XCYB6GCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTQwMzRaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCSbl7rAANHL0MehVJgq8kwl39kLSdKbt69 # tn63zf0o3FwqQYZaqEEQR+4txZbErikwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAb0r+Q6rTzAtbhmJ5gmKPhZzaqmhZxpkJQGlCgHwgb1Sf/IC+ # 24/ZS/N1YghjomEkeHEW2IKqB614okhOcV6XfRjW5/Heza1HDUJV0RSjn1pvHs0I # YZRoomr+8oMJPPMbS8y4RgVBG0sjaLsQ27RaWFioXg3WuK4eY8UMu2zLgR0rg9kv # SqRhNCV89k5sVhln4dRYvz2CuiDj5/XL7bGQxoTkVTIQ2jOgR0s2A9uN8oP3pTTs # pOO9wPyZQc0H7Mm7cKp4I49xBLMaVLZNTYsetSF1WPYcJjfTGMuXs9mmkMEmLwWu # l3NMK0g9y76pHlsGjW0hKaENG1lKHCGiSvpttaM2/PPxajCEen/W1TtYHudopAcw # CkAg+q4JPQTWOn7C75rCSeDeCGbhKz4FxN1MsLSSGoXzXSIQJeDBe8tR1mOjbUg1 # scVIJ0H+DF3fo8lPFEnlP0Jrtc+EixycPxo7HjMX33ILORlGdkm259IiY9UrGk+w # p4y8U9kwmXg7+viJAtYWyDDYofx6qTq5opHwLpOo1BsHSPyTzUpj26x6O1Um7P6M # yN8zm2YNGJbwdyVnQwORitr6MdhcAbMO0EsMSNRH5RMvTTWyEybnPu1svoGrD/jm # n5mz3ohGe5pz46dUoW92P+5n8qj33LoxJ5c7+q9enmt+lqMZOursNHmy5Uk= # SIG # End signature block |