Public/ServiceEM/New-EntraOpsSubscriptionLandingZone.ps1

<#
.SYNOPSIS
    Creates an EAM authorization structure for an Azure resource group or subscription.

.DESCRIPTION
    Provisions EAM groups, an Entitlement Management catalog, access packages,
    PIM policies and Azure role assignments for one scope. -DeploymentScope selects it:

    ResourceGroup (default) — scope "Rg-<Prefix>": catalog Catalog-Rg-<Prefix>,
      resource group RG-<Prefix> with all Azure role assignments on the resource group.

    Subscription — scope "Sub-<Prefix>": catalog Catalog-Sub-<Prefix>, no resource
      group; the same Azure role assignments are made on the subscription (-SubscriptionId).

    To share governance groups between landing zones, pass their IDs as
    -ControlPlaneDelegationGroupId / -ManagementPlaneDelegationGroupId / -AdministratorGroupId
    or use -GovernanceModel Centralized.

    Groups (PerService): SG-<Scope>-CatalogPlane-Members,
    SG-<Scope>-WorkloadPlane-Users, SG-<Scope>-WorkloadPlane-Admins,
    SG-<Scope>-ControlPlane-Admins and SG-<Scope>-ManagementPlane-Admins, and with -CreateM365Group the
    Microsoft 365 group <Scope> Members.

    Delegation and governance model behaviour:
    - When GovernanceModel = "PerService" (default), per-service groups are created
      for ControlPlane-Admins and ManagementPlane-Admins.
    - When GovernanceModel = "Centralized", ControlPlane-Admins,
      ManagementPlane-Admins and CatalogPlane-Members are resolved to tenant-wide
      shared groups via Resolve-EntraOpsServiceEMDelegationGroup.
    - Delegation group IDs are read from EntraOpsConfig.ServiceEM when not
      passed as parameters.

.PARAMETER DeploymentScope
    "ResourceGroup" (default) or "Subscription".

.PARAMETER ServiceMembers
    UPN(s) of users to add as initial WorkloadPlane-Users.
    Defaults to the signed-in identity.

.PARAMETER WorkloadPlaneAdmin
    UPN of the workload plane admin, assigned to the admin access package
    (ManagementPlane-Admins or WorkloadPlane-Admins) and to CatalogPlane-Members. Defaults to the
    signed-in identity only when -GroupOwnership is Eligible or Permanent.

.PARAMETER GroupOwnership
    Opt-in ownership of the WorkloadPlane groups (WorkloadPlane-Admins, WorkloadPlane-Users) for
    the workload plane admin: "None" (default), "Eligible" (PIM for Groups eligible owner) or "Permanent"
    (owner set when the groups are created). Owners can add members directly, bypassing access package
    approvals and access reviews, so a warning is shown. ControlPlane, ManagementPlane, CatalogPlane and
    tenant-wide delegation groups never get an owner.

.PARAMETER ControlPlaneAdmins
    UPN(s) of the initial members of the per-service ControlPlane-Admins group (PerService model only;
    in the Centralized model the tenant-wide group is managed outside ServiceEM). Added permanently, or as
    PIM for Groups eligible members with -EnablePimForGroups.
    Without this parameter an Entra administrator adds the members in the portal.

.PARAMETER CatalogPlaneMembers
    UPN(s) assigned to the CatalogPlane-Members access package, in addition to the
    WorkloadPlaneAdmin. CatalogPlane-Members (the administrator group) request WorkloadPlane-Admins and
    ManagementPlane-Admins; non-members can't request CatalogPlane-Members. Ignored with AdministratorGroupId.

.PARAMETER AddWorkloadPlaneAdminToUsers
    Also assigns the workload plane admin to the WorkloadPlane-Users access package, in addition to the
    admin access package. Not set by default, so admin accounts don't get data-plane user access.
    Defaults to EntraOpsConfig.ServiceEM.AddWorkloadPlaneAdminToUsers; an explicitly passed value wins.

.PARAMETER EnablePimForGroups
    Recommended. Manages ControlPlane-Admins and ManagementPlane-Admins with PIM for Groups:
    PIM activation policy and eligible instead of active membership through their access packages. Both
    groups hold permanent catalog roles that can't be PIM-protected otherwise. Requires Microsoft Entra ID
    Governance or Microsoft Entra Suite licenses (a warning is shown). Not set by default.

.PARAMETER EnableWorkloadPlanePimForGroups
    Manages WorkloadPlane-Admins with PIM for Groups in the same way, e.g. for multi-activation
    scenarios. Requires Microsoft Entra ID Governance or Microsoft Entra Suite licenses (a warning is shown).
    Not set by default.

.PARAMETER CreateM365Group
    Creates the Microsoft 365 group "<Scope>-<Prefix> Members". It is meant for the
    collaboration of the service team: a group mailbox and calendar for email and ChatOps notifications
    and, when SharePoint Online or Microsoft Teams is used, a SharePoint site or team as knowledge base.
    The group is added to every access package, so all users assigned to an access package
    become members. It gets no PIM for Groups eligibilities or other access. Not created by default.
    Defaults to EntraOpsConfig.ServiceEM.CreateM365Group; an explicitly passed value wins.

.PARAMETER SkipAzureResourceGroup
    When set, no Azure resource group and no Azure role assignments are created.

.PARAMETER AzureRegion
    Azure region for the resource group (e.g. "westeurope"). Required unless
    -SkipAzureResourceGroup is set or -DeploymentScope is "Subscription".

.PARAMETER SubscriptionId
    Subscription in which the resource group (or, for -DeploymentScope Subscription, the subscription
    role assignments) is created. Required unless -SkipAzureResourceGroup is set. Validated before any
    object is created; the previous Azure context is restored afterwards.

.PARAMETER DeploymentPrefix
    Prefix used in all group DisplayNames and catalog names. Defaults to
    "Default". Use the subscription or workload name
    (e.g. "Sub-Management", "Sub-Connectivity").

.PARAMETER SkipControlPlaneDelegation
    Skips creation of the per-service ControlPlane-Admins group and its Catalog
    Owner / Azure UAA PIM eligible assignments. Applied
    automatically when ControlPlaneDelegationGroupId is provided or when
    GovernanceModel is Centralized.

.PARAMETER SkipCatalogOwnerAssignment
    Do not assign the Catalog Owner role to ControlPlane-Admins. Without this switch,
    ControlPlane-Admins get a permanent (not PIM-protected) Catalog Owner assignment and can modify the
    catalog, access packages and policies. Prefer an eligible Identity Governance Administrator
    assignment via PIM instead.

.PARAMETER SkipManagementPlaneDelegation
    Skips creation of the per-service ManagementPlane-Admins group and its
    entitlement/Azure delegation. Applied automatically when
    ManagementPlaneDelegationGroupId is provided or when GovernanceModel is
    Centralized.

.PARAMETER GovernanceModel
    Governance model for the landing zone deployment. Valid values: "Centralized", "PerService".
    
    Centralized: Uses tenant-wide delegation groups (ControlPlane-Admins and
    ManagementPlane-Admins) that are shared across all landing zones. Requires
    pre-existing groups or permissions to create them.
    
    PerService: Creates per-service admin groups for each landing zone. No
    pre-existing groups required. This is the default for simple deployments.
    
    Defaults to "PerService" unless overridden in EntraOpsConfig.json or via
    this parameter.

    If the tenant-wide delegation groups of the Centralized model can't be resolved, the cmdlet stops
    with an error before any object is created, unless -AllowPerServiceFallback is set.

.PARAMETER AllowPerServiceFallback
    Opt-in for the Centralized governance model: when the tenant-wide ControlPlane or ManagementPlane
    delegation group can't be resolved or created, fall back to the PerService model and create
    per-service ControlPlane-Admins and ManagementPlane-Admins groups (with their privileged catalog and
    Azure roles) instead of stopping with an error. Not set by default.

.PARAMETER ControlPlaneDelegationGroupId
    Object ID of an existing Entra group to use as ControlPlane-Admins instead of
    creating a per-service group, e.g. the ControlPlane-Admins of a governance landing zone shared by
    several workloads. Receives the Catalog Owner role and a PIM eligible User Access Administrator assignment.
    Falls back to EntraOpsConfig.ServiceEM.ControlPlaneDelegationGroupId.

.PARAMETER ManagementPlaneDelegationGroupId
    Object ID of an existing Entra group to use as ManagementPlane-Admins instead of
    creating a per-service group. Receives the AP Assignment Manager catalog role, approver role
    in access package policies, and a PIM eligible Contributor role.
    Falls back to EntraOpsConfig.ServiceEM.ManagementPlaneDelegationGroupId.

.PARAMETER AdministratorGroupId
    Object ID of an existing Entra group to use as CatalogPlane-Members instead of
    creating a per-service group. Controls who can request elevated access packages. Falls back to EntraOpsConfig.ServiceEM.AdministratorGroupId.
    Required with GovernanceModel "Centralized".

.PARAMETER ControlPlaneGroupName
    Display name of the tenant-wide ControlPlane delegation group to look up or
    create when GovernanceModel is Centralized. Defaults to
    "PRG-Tenant-ControlPlane-IdentityOps". Overridden by
    EntraOpsConfig.ServiceEM.ControlPlaneGroupName.

.PARAMETER ManagementPlaneGroupName
    Display name of the tenant-wide ManagementPlane delegation group to look up
    or create when GovernanceModel is Centralized. Defaults to
    "PRG-Tenant-ManagementPlane-PlatformOps". Overridden by
    EntraOpsConfig.ServiceEM.ManagementPlaneGroupName.

.PARAMETER GroupPrefix
    Prefix of the security group display names (e.g. "SG" for SG-Rg-<Prefix>-WorkloadPlane-Users).
    A prefix ending with '-', '_' or '.' is used as is (e.g. "SG_" for SG_Rg-<Prefix>-WorkloadPlane-Users).
    Defaults to EntraOpsConfig.ServiceEM.GroupPrefix, otherwise "SG"; an explicitly passed value wins.

.PARAMETER RoleAssignableWorkloadPlaneAdmins
    Creates WorkloadPlane-Admins as role-assignable group. ControlPlane, ManagementPlane and CatalogPlane groups
    are always role-assignable, WorkloadPlane-Users never. Only applies when a group is created. Defaults to
    EntraOpsConfig.ServiceEM.RoleAssignableWorkloadPlaneAdmins; an explicitly passed value wins.

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

    .EXAMPLE
    New-EntraOpsSubscriptionLandingZone -DeploymentPrefix "Management" `
        -AzureRegion "westeurope" -SubscriptionId "<subscription-id>" `
        -WorkloadPlaneAdmin "admin@contoso.com" `
        -ServiceMembers @("alice@contoso.com", "bob@contoso.com")

    Creates the Rg-Management scope with one catalog, the resource group RG-Management
    in West Europe and the Azure role assignments on it. Per-service admin groups are
    created (PerService governance model default).

.EXAMPLE
    New-EntraOpsSubscriptionLandingZone -DeploymentPrefix "Connectivity" -DeploymentScope Subscription `
        -SubscriptionId "<subscription-id>" -WorkloadPlaneAdmin "admin@contoso.com"

    Creates the Sub-Connectivity scope with one catalog and assigns the Azure roles on
    the subscription instead of a resource group.

.EXAMPLE
    $governance = New-EntraOpsServiceBootstrap -ServiceName "Sub-Connectivity" -SkipAzureResourceGroup `
        -EnablePimForGroups -ServiceRoles (@"
accessLevel,name,groupType
CatalogPlane,Members,
ControlPlane,Admins,
ManagementPlane,Admins,
"@ | ConvertFrom-Csv)
    $groupId = { param($name) ($governance.Groups | Where-Object DisplayName -like "*-$name").Id }

    New-EntraOpsSubscriptionLandingZone -DeploymentPrefix "Connectivity" `
        -AzureRegion "northeurope" -SubscriptionId "<subscription-id>" `
        -ControlPlaneDelegationGroupId (& $groupId 'ControlPlane-Admins') `
        -ManagementPlaneDelegationGroupId (& $groupId 'ManagementPlane-Admins') `
        -AdministratorGroupId (& $groupId 'CatalogPlane-Members')

    Separates governance and workload groups: a governance scope
    Sub-Connectivity without Azure resources holds ControlPlane-Admins, ManagementPlane-Admins and
    CatalogPlane-Members; the workload landing zone Rg-Connectivity uses them as delegated groups for
    its catalog roles, approvals, access reviews and Azure roles on RG-Connectivity. Repeat the second
    call for further workloads.

    .EXAMPLE
    New-EntraOpsSubscriptionLandingZone -DeploymentPrefix "Dev" `
        -SkipAzureResourceGroup

    Creates all Entra ID groups, the EM catalog and access packages for the Rg-Dev
    scope without Azure resources. Useful for development
    environments or Entra-only access structures.

#>

function New-EntraOpsSubscriptionLandingZone {
    [OutputType([System.String])]
    [cmdletbinding()]
    param(
        [string[]]$ServiceMembers,

        [string]$WorkloadPlaneAdmin,

        [switch]$AddWorkloadPlaneAdminToUsers,

        [ValidateSet("None", "Eligible", "Permanent")]
        [string]$GroupOwnership = "None",

        [string[]]$ControlPlaneAdmins,

        [string[]]$CatalogPlaneMembers,

        [switch]$EnablePimForGroups,

        [switch]$EnableWorkloadPlanePimForGroups,

        [switch]$CreateM365Group,

        [switch]$SkipAzureResourceGroup,

        [switch]$SkipControlPlaneDelegation,

        [switch]$SkipCatalogOwnerAssignment,

        [switch]$SkipManagementPlaneDelegation,

        [ValidateSet("Centralized", "PerService")]
        [string]$GovernanceModel,

        [switch]$AllowPerServiceFallback,

        [string]$AzureRegion,

        [ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')]
        [string]$SubscriptionId,

        [string]$DeploymentPrefix = "Default",

        [ValidateSet("ResourceGroup", "Subscription")]
        [string]$DeploymentScope = "ResourceGroup",

        [string]$ControlPlaneDelegationGroupId = "",

        [string]$ManagementPlaneDelegationGroupId = "",

        [string]$AdministratorGroupId = "",

        [string]$ControlPlaneGroupName = "PRG-Tenant-ControlPlane-IdentityOps",

        [string]$ManagementPlaneGroupName = "PRG-Tenant-ManagementPlane-PlatformOps",

        [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_.-]*$')]
        [string]$GroupPrefix = "SG",

        [switch]$RoleAssignableWorkloadPlaneAdmins,

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

    begin {
        $scopeName = if ($DeploymentScope -eq "Subscription") { "Sub" } else { "Rg" }
        $serviceRoles = @(
            [pscustomobject]@{accessLevel = ""; name = "Members"; groupType = "Unified"},
            [pscustomobject]@{accessLevel = "CatalogPlane"; name = "Members"; groupType = ""},
            [pscustomobject]@{accessLevel = "WorkloadPlane"; name = "Users"; groupType = ""},
            [pscustomobject]@{accessLevel = "WorkloadPlane"; name = "Admins"; groupType = ""},
            [pscustomobject]@{accessLevel = "ControlPlane"; name = "Admins"; groupType = ""}
        )
        Write-Verbose "$logPrefix Deployment scope: $DeploymentScope"

        # Load EntraOpsConfig.json if not already loaded
        if ($null -eq $Global:EntraOpsConfig) {
            $configPaths = @(
                "$PWD/EntraOpsConfig.json"
                "$PSScriptRoot/EntraOpsConfig.json"
                $env:ENTRAOPS_CONFIG
            ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
            
            $configLoaded = $false
            foreach ($configPath in $configPaths) {
                if (Test-Path $configPath) {
                    try {
                        $Global:EntraOpsConfig = Get-Content -Path $configPath -Raw | ConvertFrom-Json -AsHashtable
                        Write-Verbose "$logPrefix Loaded EntraOpsConfig.json from: $configPath"
                        $configLoaded = $true
                        break
                    } catch {
                        Write-Verbose "$logPrefix Failed to load config from $configPath : $_"
                    }
                }
            }
            
            if (-not $configLoaded) {
                Write-Verbose "$logPrefix No EntraOpsConfig.json found. Using parameter defaults."
                $Global:EntraOpsConfig = @{}
            }
        } else {
            Write-Verbose "$logPrefix Using existing `$Global:EntraOpsConfig"
        }

        # Landing zone defaults from EntraOpsConfig; explicit parameters take precedence
        $configRegion = [string](Get-EntraOpsServiceEMConfigValue -Path 'DefaultAzureRegion')
        if ([string]::IsNullOrWhiteSpace($AzureRegion) -and -not [string]::IsNullOrWhiteSpace($configRegion)) {
            $AzureRegion = $configRegion
            Write-Verbose "$logPrefix Using DefaultAzureRegion '$AzureRegion' from EntraOpsConfig"
        }
        foreach ($switchName in 'SkipCatalogOwnerAssignment', 'CreateM365Group', 'AddWorkloadPlaneAdminToUsers', 'RoleAssignableWorkloadPlaneAdmins') {
            if (-not $PSBoundParameters.ContainsKey($switchName) -and (Get-EntraOpsServiceEMConfigValue -Path $switchName) -eq $true) {
                Set-Variable -Name $switchName -Value ([switch]$true)
                Write-Verbose "$logPrefix Using $switchName from EntraOpsConfig"
            }
        }
        $configGroupPrefix = [string](Get-EntraOpsServiceEMConfigValue -Path 'GroupPrefix')
        if (-not $PSBoundParameters.ContainsKey('GroupPrefix') -and -not [string]::IsNullOrWhiteSpace($configGroupPrefix)) {
            $GroupPrefix = $configGroupPrefix
            Write-Verbose "$logPrefix Using GroupPrefix '$GroupPrefix' from EntraOpsConfig"
        }

        if (-not $SkipAzureResourceGroup -and $DeploymentScope -ne "Subscription" -and [string]::IsNullOrWhiteSpace($AzureRegion)) {
            throw "Parameter -AzureRegion (or ServiceEM.DefaultAzureRegion) is required unless -SkipAzureResourceGroup is specified or -DeploymentScope is Subscription."
        }
        if (-not $SkipAzureResourceGroup) {
            if ([string]::IsNullOrWhiteSpace($SubscriptionId)) {
                throw "Parameter -SubscriptionId is required unless -SkipAzureResourceGroup is specified."
            }
            $currentAzTenantId = (Get-AzContext).Tenant.Id
            if ([string]::IsNullOrWhiteSpace($currentAzTenantId)) {
                throw "No Azure context found. Sign in with Connect-EntraOps before creating an Azure Resource Group."
            }
            # Fail before any Entra object is created if the subscription isn't accessible in the current tenant
            Get-AzSubscription -SubscriptionId $SubscriptionId -TenantId $currentAzTenantId -ErrorAction Stop | Out-Null
        }

        # Read delegation Group IDs and group names from EntraOpsConfig when not supplied as parameters.
        # A non-empty config value also auto-activates the corresponding skip flag.
        foreach ($configName in 'ControlPlaneDelegationGroupId', 'ManagementPlaneDelegationGroupId', 'AdministratorGroupId') {
            $configValue = [string](Get-EntraOpsServiceEMConfigValue -Path $configName)
            if ([string]::IsNullOrWhiteSpace((Get-Variable -Name $configName -ValueOnly)) -and -not [string]::IsNullOrWhiteSpace($configValue)) {
                Write-Verbose "$logPrefix Reading $configName from EntraOpsConfig"
                Set-Variable -Name $configName -Value $configValue
            }
        }

        # Read GovernanceModel from parameter > config > default (PerService)
        $governanceModelValue = "PerService"
        $governanceModelSource = "default"
        $configGovernanceModel = [string](Get-EntraOpsServiceEMConfigValue -Path 'GovernanceModel')
        if ($PSBoundParameters.ContainsKey('GovernanceModel')) {
            $governanceModelValue = $GovernanceModel
            $governanceModelSource = "parameter"
        } elseif (-not [string]::IsNullOrWhiteSpace($configGovernanceModel)) {
            $governanceModelValue = $configGovernanceModel
            $governanceModelSource = "config"
        }

        Write-Verbose "$logPrefix Using governance model '$governanceModelValue' (source: $governanceModelSource)"
        # Centralized removes CatalogPlane-Members, the requestor scope, approver and reviewer fallback of the policies
        if ($governanceModelValue -eq "Centralized" -and [string]::IsNullOrWhiteSpace($AdministratorGroupId)) {
            throw "The Centralized governance model requires -AdministratorGroupId (or ServiceEM.AdministratorGroupId in EntraOpsConfig): no per-service CatalogPlane-Members group is created. Use -GovernanceModel PerService or configure the administrator group."
        }

        # Auto-resolve or create role-assignable delegation groups.
        # Searches by config ID, then by default group name, then creates if permissions allow.
        # Group names from config take precedence over the parameter defaults.
        foreach ($configName in 'ControlPlaneGroupName', 'ManagementPlaneGroupName') {
            $configValue = [string](Get-EntraOpsServiceEMConfigValue -Path $configName)
            if (-not [string]::IsNullOrWhiteSpace($configValue)) {
                Set-Variable -Name $configName -Value $configValue
            }
        }

        if ($governanceModelValue -eq "Centralized") {
            # Centralized model: Use tenant-wide delegation groups
            Write-Verbose "$logPrefix Centralized governance model - using tenant-wide delegation groups"

            # Falls back to PerService only with -AllowPerServiceFallback
            $centralizedFailed = $false
            $centralizedError = $null
            
            try {
                $ControlPlaneDelegationGroupId = Resolve-EntraOpsServiceEMDelegationGroup `
                    -Plane "ControlPlane" `
                    -GroupId $ControlPlaneDelegationGroupId `
                    -DefaultGroupName $ControlPlaneGroupName `
                    -ConfigKey "ControlPlaneDelegationGroupId" `
                    -logPrefix $logPrefix
                $SkipControlPlaneDelegation = $true
            } catch {
                $centralizedFailed = $true
                $centralizedError = $_
                Write-Warning "$logPrefix Failed to resolve ControlPlane delegation group: $_"
            }
            
            if (-not $centralizedFailed) {
                try {
                    $ManagementPlaneDelegationGroupId = Resolve-EntraOpsServiceEMDelegationGroup `
                        -Plane "ManagementPlane" `
                        -GroupId $ManagementPlaneDelegationGroupId `
                        -DefaultGroupName $ManagementPlaneGroupName `
                        -ConfigKey "ManagementPlaneDelegationGroupId" `
                        -logPrefix $logPrefix
                    $SkipManagementPlaneDelegation = $true
                } catch {
                    $centralizedFailed = $true
                    $centralizedError = $_
                    Write-Warning "$logPrefix Failed to resolve ManagementPlane delegation group: $_"
                }
            }
            
            if ($centralizedFailed -and -not $AllowPerServiceFallback) {
                throw "$logPrefix The Centralized governance model failed to resolve the tenant-wide delegation groups; nothing was created. Create the delegation groups and set their IDs in EntraOpsConfig.json (ServiceEM.ControlPlaneDelegationGroupId / ServiceEM.ManagementPlaneDelegationGroupId), use -GovernanceModel PerService, or pass -AllowPerServiceFallback to create per-service ControlPlane-Admins and ManagementPlane-Admins groups instead. Error: $centralizedError"
            }
            if ($centralizedFailed) {
                Write-Warning "$logPrefix =========================================="
                Write-Warning "$logPrefix CENTRALIZED GOVERNANCE MODEL FAILED"
                Write-Warning "$logPrefix =========================================="
                Write-Warning "$logPrefix Falling back to PerService governance model (-AllowPerServiceFallback)."
                Write-Warning "$logPrefix PerService will create per-service admin groups instead."
                Write-Warning "$logPrefix "
                Write-Warning "$logPrefix To use Centralized model, either:"
                Write-Warning "$logPrefix 1. Create delegation groups manually and add IDs to EntraOpsConfig.json"
                Write-Warning "$logPrefix 2. Grant RoleManagement.ReadWrite.Directory permission for auto-creation"
                Write-Warning "$logPrefix =========================================="
                
                # Reset delegation group IDs to trigger per-service creation
                $ControlPlaneDelegationGroupId = ""
                $ManagementPlaneDelegationGroupId = ""
                $SkipControlPlaneDelegation = $false
                $SkipManagementPlaneDelegation = $false
                $governanceModelValue = "PerService"
            } else {
                # Remove per-service ControlPlane, ManagementPlane, CatalogPlane groups from ServiceRoles
                Write-Verbose "$logPrefix Removing ControlPlane/ManagementPlane/CatalogPlane from per-service groups"
                $serviceRoles = @($serviceRoles | Where-Object {
                    -not (($_.accessLevel -eq "ControlPlane" -and $_.name -eq "Admins") -or
                          ($_.accessLevel -eq "ManagementPlane" -and $_.name -eq "Admins") -or
                          ($_.accessLevel -eq "CatalogPlane" -and $_.name -eq "Members"))
                })
            }
        } else {
            # PerService model: Keep per-service groups, but still resolve delegation if IDs provided
            Write-Verbose "$logPrefix PerService governance model - creating per-service admin groups"
            
            if (-not [string]::IsNullOrWhiteSpace($ControlPlaneDelegationGroupId)) {
                $ControlPlaneDelegationGroupId = Resolve-EntraOpsServiceEMDelegationGroup `
                    -Plane "ControlPlane" `
                    -GroupId $ControlPlaneDelegationGroupId `
                    -DefaultGroupName $ControlPlaneGroupName `
                    -ConfigKey "ControlPlaneDelegationGroupId" `
                    -logPrefix $logPrefix
                $SkipControlPlaneDelegation = $true
            }

            if (-not [string]::IsNullOrWhiteSpace($ManagementPlaneDelegationGroupId)) {
                $ManagementPlaneDelegationGroupId = Resolve-EntraOpsServiceEMDelegationGroup `
                    -Plane "ManagementPlane" `
                    -GroupId $ManagementPlaneDelegationGroupId `
                    -DefaultGroupName $ManagementPlaneGroupName `
                    -ConfigKey "ManagementPlaneDelegationGroupId" `
                    -logPrefix $logPrefix
                $SkipManagementPlaneDelegation = $true
            }
        }

        # Add ManagementPlane-Admins unless it is delegated.
        if (-not $SkipManagementPlaneDelegation) {
            $serviceRoles += [pscustomobject]@{accessLevel = "ManagementPlane"; name = "Admins"; groupType = ""}
        }

        if ($SkipControlPlaneDelegation) {
            Write-Verbose "$logPrefix Removing ControlPlane components from ServiceRoles"
            $serviceRoles = @($serviceRoles | Where-Object { $_.accessLevel -ne "ControlPlane" })
        }
        
        # Log final switch states for troubleshooting
        Write-Verbose "$logPrefix =========================================="
        Write-Verbose "$logPrefix FINAL DELEGATION SWITCH STATES:"
        Write-Verbose "$logPrefix SkipControlPlaneDelegation: $SkipControlPlaneDelegation"
        Write-Verbose "$logPrefix SkipManagementPlaneDelegation: $SkipManagementPlaneDelegation"
        Write-Verbose "$logPrefix ControlPlaneDelegationGroupId: $(if ($ControlPlaneDelegationGroupId) { 'SET' } else { 'NOT SET' })"
        Write-Verbose "$logPrefix ManagementPlaneDelegationGroupId: $(if ($ManagementPlaneDelegationGroupId) { 'SET' } else { 'NOT SET' })"
        Write-Verbose "$logPrefix GovernanceModel: $governanceModelValue"
        Write-Verbose "$logPrefix =========================================="
    }

    process {
        Write-Verbose "$logPrefix Processing LZ"

        $splatServiceBootstrap = @{
            ServiceName                      = "$scopeName-$DeploymentPrefix"
            GroupPrefix                      = $GroupPrefix
            AddWorkloadPlaneAdminToUsers     = $AddWorkloadPlaneAdminToUsers
            RoleAssignableWorkloadPlaneAdmins = $RoleAssignableWorkloadPlaneAdmins
            EnablePimForGroups               = $EnablePimForGroups
            EnableWorkloadPlanePimForGroups  = $EnableWorkloadPlanePimForGroups
            CreateM365Group                  = $CreateM365Group
            GroupOwnership                   = $GroupOwnership
            AzureRegion                      = $AzureRegion
            ServiceRoles                     = $serviceRoles
            SkipControlPlaneDelegation       = $SkipControlPlaneDelegation
            SkipCatalogOwnerAssignment       = $SkipCatalogOwnerAssignment
            SkipManagementPlaneDelegation    = $SkipManagementPlaneDelegation
            ControlPlaneDelegationGroupId    = $ControlPlaneDelegationGroupId
            ManagementPlaneDelegationGroupId = $ManagementPlaneDelegationGroupId
            AdministratorGroupId             = $AdministratorGroupId
            SkipAzureResourceGroup           = $SkipAzureResourceGroup
            AzureScope                       = $DeploymentScope
        }
        if (-not $SkipAzureResourceGroup) {
            $splatServiceBootstrap.SubscriptionId = $SubscriptionId
        }
        foreach ($optional in 'ServiceMembers', 'CatalogPlaneMembers') {
            if ($PSBoundParameters.ContainsKey($optional)) { $splatServiceBootstrap[$optional] = Get-Variable -Name $optional -ValueOnly }
        }
        if (-not [string]::IsNullOrWhiteSpace($WorkloadPlaneAdmin)) {
            $splatServiceBootstrap.WorkloadPlaneAdmin = $WorkloadPlaneAdmin
        }
        if ($PSBoundParameters.ContainsKey('ControlPlaneAdmins')) {
            if ($serviceRoles | Where-Object { $_.accessLevel -eq "ControlPlane" -and $_.name -eq "Admins" }) {
                $splatServiceBootstrap.ControlPlaneAdmins = $ControlPlaneAdmins
            } else {
                Write-Warning "$logPrefix No per-service ControlPlane-Admins group is created (governance model '$governanceModelValue' or delegated ControlPlane); -ControlPlaneAdmins is ignored"
            }
        }

        return New-EntraOpsServiceBootstrap @splatServiceBootstrap
    }
}

# SIG # Begin signature block
# MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBGcRMaTNGuDLKS
# LtoJxHKX5BYvEOmgAOk3vqOEAkaqU6CCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j
# 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
# ARUwLwYJKoZIhvcNAQkEMSIEIIuFrkA7sVchfyUEB2GZO2xDfVgEJiLt18CtN43e
# kPWXMA0GCSqGSIb3DQEBAQUABIIBgFCobUk18M6hnXvAdXnUBxQoKMnV8oKXtoW9
# dY5awXU0Z0AQhEeB8Z6qXUeLCqTDcNNJy8DtYQwmw7DfenqsLM/nCvKZx9YTd7d+
# nM9EVYcqgfj6+XANFSJUMnlA8k9NdoLM5DKvTgMyr44FJ6r8FlffR1pPmUHcrmvJ
# jH2KuhPVc17mixAxw3L+KFTVUT89ajfaIHxIWjan91T+NkitdLNMOudLxRBVayFj
# zNmXamvSRWE80EKtAtbD6ae944YMcAMDZ5UWyrUdTlMa93B5sdGWRZjpeg8FW6zO
# SbzR9klrM22daadezxG/GfnU2obyg5dL87OHW5qebB5aNvKfUNik9JmOUmgZj3J3
# P+0Bv4+UQMBO8oUNfKS1ReJgqXm1I+q/yrSwTLjbU8Xogo5NkKvSeqN0af06DQdT
# Gera2cN/bGLocED8YqRRPMldc8KYF5thDYSS9DWsknxyZwGtSaP6XUnlKrcaiexk
# 8Mp7lsCv3Z3HgHgOWkMBBPY/KR0k+KGCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID
# 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0
# ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ
# KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD
# MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjIxMTQwNDVaMDcG
# CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY
# 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDCKnXDS2542RSn7EhOTKPHAUr6OZ7hbOrp4
# JI2LZDM4LBzYL4AVEEDJz24jg9ZnV14wgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ
# MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh
# MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0
# dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI
# hvcNAQEBBQAEggIAKot2wnKLz9/bikYprm2oaHYNqwVLI5KgKP3RJjZ3EGtLvDzT
# gSYcONzLQ54M5fEgK5isTnnczUgQRs6QE+h9o6EFMIY+LrULq0iGLCGrCz47cRcE
# U6UXfyMxxreYWZ7qn2Mgs+hW8ipFs7mboeE8yqtbCBol3FT5bRmUfq/nnxbq4O1N
# A4aS9KjziyzAZO9kbTAdCG4XAgGg9cOCNgLsTtXPpx/7DZVXPJb6jYCDH2sNP3mL
# NxpZdnz+ORP/I7/MmKMpD7ONw+iNZqBRwCHx5w6qvwrlatG9CmLWDGaqDsnUWsKF
# da2A71dtqwff1Kkj3lTW8GloMTW6bCe3y5i/LEHeYtYBIEX9sdddlrBQmjGKD47p
# TKgLnJTcbWQ7q7dl8eoESzas9S2GGBrFfs5/GAPwkRIb8IFtC41ETkyPojs+sr6e
# 9573UHNB7XcUmXbMnSYagflSZEMoQrjvp3uPltqDkDkRySHkCpJ+oaxSnrLXdWxh
# JwH2Bjs1DS8GFzGhytcTSbzm9NhiNztKJvCOh9xns04itjREpaE1Z1jjlkUjQyW8
# 2D1jwmrNoRTukTp2updEmyzvegp+KR5yzWF7Y1tmfBd3FGqoL1pcEXmvKeWkjAP6
# DBqmYXK35xSYO02WHP2ewScmjf60aL76GHAsGTRNmDC6w7gE4/1MJrlcwYc=
# SIG # End signature block