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
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjcwNTQxMDZaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCSbl7rAANHL0MehVJgq8kwl39kLSdKbt69
# tn63zf0o3FwqQYZaqEEQR+4txZbErikwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAkwH9MvRFj29ABadSDq1F4lOxmQgWrNqhWS2k5ZAQ8kfluFAw
# qBJ0iRanJdjpbLxW6I8/PXs9mXj7pjDakKWI5qdHXweJoqsTjhJoao7RH+uZg8/Z
# 69PKRC6ov3vCClv/ehckM0agQMF48I5fbapgmqdjqGYfOXiidktir+i8wsLZCkuP
# 4SvyIheiQ5bCyBCCcyVJqyozs9tu5jdCGk5bx4VHTRfbBkIBFlTL8Odf7Gmo5bgr
# uDrRQ/qG6Hxh8cHH6sDHzSNMuxfJGDV7w8aTKhk+O2wOI0gIATZ0+O1sACSzmJvv
# 3kDGT+8deUQ+C0EQeU2T0B5goNRLHqw2IPEs+RmALf/mOnGTIKrZKcR2n/ae4Ny6
# aHPJ8Cogzl0hOoACWXKr51BptVFk+BfK0rs5CNAUZxdQAtB14GsihkW9eIj96dU7
# T9FdFi18dD1d5BfLmUhRyisUAPFqsTg1LLLJbDJY6tZWgf1H+4SnecdA1FwX3EMq
# ar1NlZGyY+SwRU+SYSPqRggR2gq+oCkHu5IWYEuFXVyNMDdOaVgYyv9hgdMwBCOx
# CXR8JueCKlT0wkOJ/jRGF8PY6RvDRo+IDuhs/TJPG8NWpA3LScVPw1lTbUX/QZ3z
# Ybc0aY8QtvJJ3qCRVj8AdkcergZRFuoapU2skKY5/ZL024lZ3nBu7XCvPDE=
# SIG # End signature block