Public/ServiceEM/New-EntraOpsServiceEntraGroup.ps1
|
<# .SYNOPSIS Creates Entra security and Microsoft 365 groups for each service role. .DESCRIPTION Creates one Entra group per entry in the ServiceRoles object. Security groups are created for roles with an empty groupType or groupType = "Security". Microsoft 365 (Unified) groups are created for roles with groupType = "Unified". ControlPlane, ManagementPlane and CatalogPlane security groups are created as role-assignable (isAssignableToRole = $true) to protect their members and owners from less privileged administrators. WorkloadPlane-Admins are role-assignable only with -RoleAssignableWorkloadPlaneAdmins; all other security groups (e.g. WorkloadPlane-Users) and Unified groups are not role-assignable. Creating role-assignable groups requires the caller to have Privileged Role Administrator or Global Administrator. The setting only applies when a group is created; Entra ID can't change it afterwards. Group names follow the convention: <GroupPrefix><Delimiter><ServiceName><Delimiter><AccessLevel><Delimiter><RoleName> A GroupPrefix that ends with '-', '_' or '.' (e.g. "SG_") is followed directly by the ServiceName. Idempotent: an existing group with exactly the expected MailNickname is reused, but only when it can't have been created by a less privileged user: ControlPlane, ManagementPlane and CatalogPlane groups must be role-assignable security groups; all other groups must be role-assignable or already be a resource of the catalog Catalog-<ServiceName>. Otherwise, or when the MailNickname is ambiguous, the function stops before creating any group. Other groups with the <ServiceName>. MailNickname prefix aren't returned. Only called directly for custom implementations; New-EntraOpsServiceBootstrap is the standard entry point. .PARAMETER ServiceName Name of the service. Forms the central segment of the group MailNickname and DisplayName. .PARAMETER WorkloadPlaneAdmin Optional Graph API owner URL for the group owner, in the form: "https://graph.microsoft.com/v1.0/users/<ObjectId>" This can also be provided as just the ObjectId (GUID), and the function will automatically construct the proper OData bind URL. Only set as owner of WorkloadPlane groups; omit to create groups without owners. .PARAMETER GroupPrefix Prefix prepended to all group DisplayNames and MailNicknames. Defaults to "SG". .PARAMETER GroupNamingDelimiter Delimiter between name segments. Only "-" is supported: the other ServiceEM cmdlets find the groups by their "-" delimited display name suffix (e.g. *-ControlPlane-Admins). .PARAMETER RoleAssignableWorkloadPlaneAdmins Creates WorkloadPlane-Admins as role-assignable group. .PARAMETER ServiceRoles EntraOps service roles object. Each row produces one group. The accessLevel, name, and groupType columns control the group variant. .PARAMETER logPrefix Text prepended to verbose messages. Defaults to the function name. .PARAMETER CreatedGroupIds Used by New-EntraOpsServiceBootstrap to record the IDs returned by group creation, including when a later step fails. Existing groups are never recorded. This collection is only used for failure cleanup. .EXAMPLE New-EntraOpsServiceEntraGroup ` -ServiceName "MyService" ` -WorkloadPlaneAdmin "https://graph.microsoft.com/v1.0/users/00000000-0000-0000-0000-000000000001" ` -ServiceRoles $roles Creates all security and Microsoft 365 groups for "MyService". Returns all group objects. #> function New-EntraOpsServiceEntraGroup { [OutputType([psobject[]])] [cmdletbinding()] param( [Parameter(Mandatory)] [string]$ServiceName, [string]$WorkloadPlaneAdmin, [string]$GroupPrefix = "SG", [ValidateSet('-')] [string]$GroupNamingDelimiter = "-", [switch]$RoleAssignableWorkloadPlaneAdmins, [Parameter(Mandatory)] [psobject[]]$ServiceRoles, [Parameter(DontShow)] [AllowEmptyCollection()] [System.Collections.Generic.List[string]]$CreatedGroupIds, [string]$logPrefix = "[$($MyInvocation.MyCommand)]" ) begin { # ServiceName is part of mailNickname and of the $search query below if ($ServiceName -notmatch '^[A-Za-z0-9_.-]+$') { throw "ServiceName '$ServiceName' is invalid. Use letters, digits, '_', '.' or '-'." } $groupNamePrefix = if ($GroupPrefix -match '[-_.]$') { $GroupPrefix } else { "$GroupPrefix$GroupNamingDelimiter" } # Normalize WorkloadPlaneAdmin to the OData bind format if (-not [string]::IsNullOrWhiteSpace($WorkloadPlaneAdmin)) { # Check if WorkloadPlaneAdmin is already in OData URL format (users or servicePrincipals) if ($WorkloadPlaneAdmin -match '^https://graph\.microsoft\.com/v1\.0/(users|servicePrincipals)/') { $ownerUri = $WorkloadPlaneAdmin Write-Verbose "$logPrefix WorkloadPlaneAdmin provided as OData URL: $ownerUri" } else { # Assume it's just an ObjectId and construct the OData URL # Validate it looks like a GUID if ($WorkloadPlaneAdmin -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { $ownerUri = "https://graph.microsoft.com/v1.0/users/$WorkloadPlaneAdmin" Write-Verbose "$logPrefix WorkloadPlaneAdmin converted to OData URL: $ownerUri" } else { throw "WorkloadPlaneAdmin must be either a valid GUID (ObjectId) or a full OData URL (https://graph.microsoft.com/v1.0/users/<ObjectId> or https://graph.microsoft.com/v1.0/servicePrincipals/<ObjectId>). Received: $WorkloadPlaneAdmin" } } } else { Write-Verbose "$logPrefix No group owner requested; creating groups without owners" } try { #Groups $groups = @() Write-Verbose "$logPrefix Looking up Groups" $groups += Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/v1.0/groups?`$search=`"mailNickname:$ServiceName.`"" -ConsistencyLevel "eventual" -OutputType PSObject -DisableCache -ThrowOnFailure # $search also matches tokens inside the nickname (e.g. PIM.<ServiceName>.*) $groups = @($groups | Where-Object { $_.MailNickname -like "$ServiceName.*" }) } catch { # Without the lookup result every group would be created again (mailNickname isn't unique for security groups) throw "Failed to look up the existing groups of '$ServiceName'; no group was created: $($_.Exception.Message)" } $catalogDisplayName = "Catalog-$ServiceName" $provenance = @{ CatalogResourceIds = $null } $getCatalogResourceIds = { if ($null -eq $provenance.CatalogResourceIds) { $encodedCatalogDisplayName = ConvertTo-EntraOpsODataStringLiteral -Value $catalogDisplayName $catalogs = @(Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/v1.0/identityGovernance/entitlementManagement/catalogs?`$filter=displayName eq '$encodedCatalogDisplayName'&`$expand=resources" -OutputType PSObject -DisableCache -ThrowOnFailure | Where-Object { $_.DisplayName -eq $catalogDisplayName }) $provenance.CatalogResourceIds = if ($catalogs.Count -eq 1) { @($catalogs[0].Resources.OriginId | Where-Object { $_ }) } else { @() } } $provenance.CatalogResourceIds } $assertReusableGroup = { param($existingGroup, [bool]$RequireRoleAssignable) $details = $existingGroup if ($null -eq $existingGroup.IsAssignableToRole -or $null -eq $existingGroup.SecurityEnabled) { $details = Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/v1.0/groups/$($existingGroup.Id)?`$select=id,isAssignableToRole,securityEnabled" -OutputType PSObject -DisableCache -ThrowOnFailure } if ($details.IsAssignableToRole -eq $true -and $details.SecurityEnabled -eq $true) { return } $groupLabel = "'$($existingGroup.DisplayName)' ($($existingGroup.Id), mailNickname $($existingGroup.MailNickname))" if ($RequireRoleAssignable) { throw "Existing group $groupLabel isn't a role-assignable security group and isn't reused, because any user who can create groups could have created it. Verify and delete the group, or rename its mailNickname, then run the landing zone again." } if ($existingGroup.Id -notin @(& $getCatalogResourceIds)) { throw "Existing group $groupLabel isn't a resource of the catalog '$catalogDisplayName' and isn't reused, because any user who can create groups could have created it. Verify the group and delete it, or add it as resource to '$catalogDisplayName', then run the landing zone again." } } $expectedNicknames = [System.Collections.Generic.List[string]]::new() # Base parameters shared by all group types $groupParams = @{ description = "" securityEnabled = $true } # Unified (Microsoft 365) groups cannot be role-assignable $unifiedParams = $groupParams + @{ displayName = "" mailNickname = "" groupTypes = @("Unified") mailEnabled = $true isAssignableToRole = $false #"members@odata.bind" = $members } $secParams = $groupParams + @{ displayName = "" mailNickname = "" mailEnabled = $false isAssignableToRole = $true } } process { Write-Verbose "$logPrefix Beginning EntraGroup" Write-Verbose "$logPrefix Processing $(($ServiceRoles|Measure-Object).Count) Groups" foreach ($ServiceRole in $ServiceRoles) { $validationErrors = @() # Validate ServiceRole properties if ([string]::IsNullOrWhiteSpace($ServiceRole.Name)) { $validationErrors += "ServiceRole.Name is required" } # Construct names $unifiedParams.Description = "Team $(($ServiceRole.accessLevel +" "+ $ServiceRole.name).trim()) supporting $ServiceName" $unifiedParams.DisplayName = "$ServiceName $($ServiceRole.Name)" $unifiedParams.MailNickname = "$ServiceName.$($ServiceRole.Name)" $secParams.Description = "Team $(($ServiceRole.accessLevel +" "+ $ServiceRole.name).trim()) supporting $ServiceName" if ([string]::IsNullOrEmpty($ServiceRole.accessLevel)) { $secParams.DisplayName = "$groupNamePrefix$ServiceName$($GroupNamingDelimiter)$($ServiceRole.Name)" $secParams.MailNickname = "$ServiceName.$($ServiceRole.Name)" } else { $secParams.DisplayName = "$groupNamePrefix$ServiceName$($GroupNamingDelimiter)$($ServiceRole.accessLevel)$($GroupNamingDelimiter)$($ServiceRole.Name)" $secParams.MailNickname = "$ServiceName.$($ServiceRole.accessLevel).$($ServiceRole.Name)" } $secParams.isAssignableToRole = $ServiceRole.accessLevel -in @('ControlPlane', 'ManagementPlane', 'CatalogPlane') -or ($RoleAssignableWorkloadPlaneAdmins -and $ServiceRole.accessLevel -eq 'WorkloadPlane' -and $ServiceRole.Name -eq 'Admins') # Validate displayName length (max 256 chars) if ($secParams.DisplayName.Length -gt 256) { $validationErrors += "DisplayName '$($secParams.DisplayName)' exceeds maximum length of 256 characters (current: $($secParams.DisplayName.Length))" } if ($unifiedParams.DisplayName.Length -gt 256) { $validationErrors += "DisplayName '$($unifiedParams.DisplayName)' exceeds maximum length of 256 characters (current: $($unifiedParams.DisplayName.Length))" } # Validate mailNickname length (max 64 chars) and format $mailNicknamePattern = '^[a-zA-Z0-9_.-]+$' if ($secParams.MailNickname.Length -gt 64) { $validationErrors += "MailNickname '$($secParams.MailNickname)' exceeds maximum length of 64 characters (current: $($secParams.MailNickname.Length))" } if ($secParams.MailNickname -notmatch $mailNicknamePattern) { $validationErrors += "MailNickname '$($secParams.MailNickname)' contains invalid characters. Only alphanumeric, underscore, dot, and hyphen allowed." } if ($unifiedParams.MailNickname.Length -gt 64) { $validationErrors += "MailNickname '$($unifiedParams.MailNickname)' exceeds maximum length of 64 characters (current: $($unifiedParams.MailNickname.Length))" } if ($unifiedParams.MailNickname -notmatch $mailNicknamePattern) { $validationErrors += "MailNickname '$($unifiedParams.MailNickname)' contains invalid characters. Only alphanumeric, underscore, dot, and hyphen allowed." } # Throw if validation errors found if ($validationErrors.Count -gt 0) { throw "VALIDATION FAILED for ServiceRole '$($ServiceRole.Name)':`n - $($validationErrors -join "`n - ")" } Write-Verbose "$logPrefix Validated group parameters for '$($ServiceRole.Name)'" # Owners can manage membership, so only WorkloadPlane groups get one $secParams.Remove("owners@odata.bind") if ($ServiceRole.accessLevel -eq "WorkloadPlane" -and -not [string]::IsNullOrWhiteSpace($ownerUri)) { $secParams["owners@odata.bind"] = @($ownerUri) } $isSecurityGroup = [string]::IsNullOrEmpty($ServiceRole.groupType) -or $ServiceRole.groupType -eq "Security" $expectedNickname = if ($ServiceRole.groupType -eq "Unified") { $unifiedParams.MailNickname } elseif ($isSecurityGroup) { $secParams.MailNickname } else { $null } if ($expectedNickname) { $expectedNicknames.Add($expectedNickname) $existingGroups = @($groups | Where-Object { $_.MailNickname -eq $expectedNickname }) if ($existingGroups.Count -gt 1) { throw "Found $($existingGroups.Count) groups with mailNickname '$expectedNickname' ($($existingGroups.Id -join ', ')). Delete the groups that weren't created by ServiceEM, then run the landing zone again." } if ($existingGroups.Count -eq 1) { $requireRoleAssignable = $ServiceRole.groupType -ne "Unified" -and $ServiceRole.accessLevel -in @('ControlPlane', 'ManagementPlane', 'CatalogPlane') & $assertReusableGroup $existingGroups[0] $requireRoleAssignable Write-Verbose "$logPrefix Reusing existing group $($existingGroups[0].DisplayName) ($($existingGroups[0].Id))" } } try { if ($ServiceRole.groupType -eq "Unified" -and $groups.MailNickname -notcontains $unifiedParams.MailNickname) { Write-Verbose "$logPrefix $($unifiedParams|ConvertTo-Json -Compress)" $newGroup = Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/groups" -Body ($unifiedParams | ConvertTo-Json -Depth 10) -OutputType PSObject -ThrowOnFailure if ($null -ne $CreatedGroupIds -and $newGroup.Id) { $CreatedGroupIds.Add($newGroup.Id) } $groups += $newGroup } elseif ($isSecurityGroup -and $groups.MailNickname -notcontains $secParams.MailNickname) { Write-Verbose "$logPrefix $($secParams|ConvertTo-Json -Compress)" $newGroup = Invoke-EntraOpsMsGraphQuery -Method POST -Uri "/v1.0/groups" -Body ($secParams | ConvertTo-Json -Depth 10) -OutputType PSObject -ThrowOnFailure if ($null -ne $CreatedGroupIds -and $newGroup.Id) { $CreatedGroupIds.Add($newGroup.Id) } $groups += $newGroup } } catch { throw "Failed to create group for service role '$($ServiceRole.accessLevel) $($ServiceRole.Name)': $($_.Exception.Message)" } } } end { Write-Verbose "$logPrefix Verifying Groups are available" $refIds = @($groups | Where-Object { $_.MailNickname -in $expectedNicknames } | ForEach-Object { $_.id } | Where-Object { $_ }) $check = @{} $confirmed = Wait-EntraOpsServiceEMCondition -Activity "Groups" -logPrefix $logPrefix -Condition { $check.Groups = @() $check.Groups += Invoke-EntraOpsMsGraphQuery -Method GET -Uri "/v1.0/groups?`$search=`"mailNickname:$ServiceName.`"" -ConsistencyLevel "eventual" -OutputType PSObject -DisableCache $check.Groups = @($check.Groups | Where-Object { $_.MailNickname -in $expectedNicknames }) $chkIds = @($check.Groups.id | Where-Object { $_ }) $refIds.Count -gt 0 -and $chkIds.Count -ge $refIds.Count -and (Compare-Object $refIds $chkIds | Measure-Object).Count -eq 0 } if (-not $confirmed) { throw "Group object consistency with Entra not achieved" } return [psobject[]]$check.Groups } } # SIG # Begin signature block # MIIniAYJKoZIhvcNAQcCoIIneTCCJ3UCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCQ75SE0OlUP37P # zDI9kZs+DAsA+C43BJB+pQon4d8VsKCCIDwwggXJMIIEsaADAgECAhAbtY8lKt8j # 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 # ARUwLwYJKoZIhvcNAQkEMSIEIACu0Gb9fAvhvN0qIfuBIJLcu9hFDXFIJTER64IL # 2ywJMA0GCSqGSIb3DQEBAQUABIIBgKMp3IfHHcuLbMaOa9aU00byLbzH6AZ537ZJ # LzhuzNW0GheAKhNnNYjoRHjikI0iFQQSURCTjjhmKbrRI+7mVOzXZBtvwSlhxOtS # yoazl3vMvbD3g00lGSoCtYlkjU7ZF5SKb4CfkSejt26rTLlf6zvTGSAPHHUZzgfr # tfxFD+XEp1rAGocZxhP+PN4KW+wXUfu4EzxlNetOonvMnpLxrjfBwx/lKu2g57Jp # IgnuAklW4xs1OHOgU51g+J/jRKmiYEqYjdZriA23SVWQPNZtx3XdftwpFmHn70of # v4AWjxajqY3gHUnVBWg3H+Wc1hdn/puYs4VQbd21ZDybS0OnizlXj/oCOfp3wHOB # GForWKRTJL1k80j6uVfJYg12zt3rM6PygcDaq7rHW1Waqv1+A1nNl/8Q8gjO5HaL # TfRqP6tZpFny2BwSBxxZI67HAk7twG/y7xnx85FHEBDS4wlAx8lTCkWsa3avsI6S # KadQnqaPyzhtGFUQOmR85JsXQCmvb6GCBAIwggP+BgkqhkiG9w0BCQYxggPvMIID # 6wIBATBqMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0 # ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIQ # KPB3wRw2vf5fdDJHcCcuAzANBglghkgBZQMEAgIFAKCCAVYwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNjEwMjcwNTQxMTNaMDcG # CyqGSIb3DQEJEAIvMSgwJjAkMCIEIIW+kOEK0kONfMkotq9IsJqyCBd87PiwEmxY # 05EFJcQ8MD8GCSqGSIb3DQEJBDEyBDDhjdWVEra5AtwY4GCQ1vHqgUgA2lo0Aupx # Fvw5/XiTHbGHcqkZjjhCS5KoCHI8HucwgZ8GCyqGSIb3DQEJEAIMMYGPMIGMMIGJ # MIGGBBRXFGhBDKha80JO+RZKUTYQ9NONmDBuMFqkWDBWMQswCQYDVQQGEwJQTDEh # MB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0 # dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECECjwd8EcNr3+X3QyR3AnLgMwDQYJKoZI # hvcNAQEBBQAEggIAgT9oraPcbwvEbqcZM5idDog7mcSzAPP6joIxl1mGnxG9TDRF # OVzgcf/PJdTbPOT2RaXzn3P5u2lf0jbK8ZlttmtMerX73GVnaiyq6G8K/g1PGbx4 # zXkoOFaTOUcmgbbvk0V92Pe5XT97IxOQVtnHNnEHpTCdtIMF1nguRL21bLi9NPMh # VX8z5JCqb8xqVT7LhJJkcx3GJj+p3unSmI7LRDX3Rcu8XmaCVrv/z01ngLpr0Jx6 # gQCjTZDyBuKt/Z+Rd/rWHWNlJHxfICpoN0E08b2V2yGsvxid9/QQYIxUbV9GaISu # GfoL62uiDHfI8mSkuwjnBosLaAbiqEkqw3/ebys8yCM+cQgohFnhrjhFKmtZs/m2 # xeJPZ/EvPk1hldUcBvtdqSAVuP2n42fS/qxgnzGdio3+h0+w6UPcLeRw+mn7ukpu # 65bH9bt25ckHuTQ7KZ5RwHSnQIeK27BKs7OknJU2Ju3MC9ts+XF9ThbRCb1ioAWX # 0rThSW5nSNltN6KVnP/UPWr2JDrSkgrcXSncL1sVzAzXSVOoYc6f3UoUDsa1by8i # SwN3VYGsf40wcKlABQqkKP+CxT7C82qNMc+GaYc+/cfsiPFIxSW7L4iS1PepVbWk # YkoPtyDlXfqiJq8qLmeZqKBSFs1nuUMNN1EGdCfDR0P0I3TGD9evUbjl00A= # SIG # End signature block |