Public/ActiveDirectory/New-OnPremUserFromTemplate.ps1
|
function New-OnPremUserFromTemplate { <# .SYNOPSIS Create a new on-prem AD user by copying attributes and group memberships from a template user. .DESCRIPTION Creates a new Active Directory user account using a template user as the source of standard attributes and group memberships. High-level workflow: - Resolve and validate the template user (ByIdentity or BySearch) - Derive naming (SAM/UPN) from parameters or Resolve-Naming + config - Enforce idempotency (do not create if target UPN already exists) - Create the user (enabled + ChangePasswordAtLogon) - Copy selected attributes from template - Configure primary proxyAddress (SMTP) - Copy group memberships with allow/deny controls Supports -WhatIf/-Confirm via ShouldProcess. .PARAMETER TemplateIdentity Template user identity (sAMAccountName, DN, SID, GUID). Used with ByIdentity only. .PARAMETER TemplateSearch Hashtable of LDAP attribute=value filters to locate the template. BySearch only. Criteria are ANDed; first match is used. .PARAMETER GivenName New user's first name. Used for naming derivation when SAM/UPN prefix not supplied. .PARAMETER Surname New user's last name. Used for naming derivation when SAM/UPN prefix not supplied. .PARAMETER DisplayName New user's display name (e.g. "First Last"). Used for address book friendly name. .PARAMETER TargetOU Destination OU DN. If omitted, defaults to template user's OU. .PARAMETER SamAccountName New user's sAMAccountName. If omitted, derived by Resolve-Naming. .PARAMETER UpnPrefix UPN prefix (left of @). If omitted, derived by Resolve-Naming. UPN suffix comes from config. .PARAMETER CopyAttributes Attributes to copy from template. If omitted, uses config list (if present), else defaults. See NOTES for default list and mapping behavior. .PARAMETER ExcludedGroups Explicit deny-list for groups when copying memberships (applies after allow rules). .PARAMETER AllowedSecurityGroups Allow-list for Security groups to copy. Distribution groups are copied by default. .PARAMETER InitialPasswordLength Length of generated initial password. Default: 16. .PARAMETER Credential Credential used for all AD operations. .PARAMETER Server Domain controller to target for AD operations (optional). .PARAMETER ShowSummary Writes a human-readable summary to host while still returning the result object. .INPUTS None. .OUTPUTS PSCustomObject. Properties typically include: UserPrincipalName, SamAccountName, DisplayName, TargetOU, CopiedAttributes, GroupsAdded, InitialPassword. .EXAMPLE $cred = Get-Credential New-OnPremUserFromTemplate -TemplateIdentity 'john.smith' ` -GivenName 'Jane' -Surname 'Doe' -DisplayName 'Jane Doe' ` -Credential $cred .EXAMPLE $cred = Get-Credential New-OnPremUserFromTemplate -TemplateSearch @{ department='Engineering'; company='Acme' } ` -GivenName 'Bob' -Surname 'Johnson' -DisplayName 'Bob Johnson' ` -SamAccountName 'bobj' -UpnPrefix 'bob.johnson' ` -TargetOU 'OU=Engineering,OU=Users,DC=company,DC=com' ` -CopyAttributes @('description','department','company') ` -Server 'DC01.company.com' -Credential $cred .EXAMPLE $cred = Get-Credential New-OnPremUserFromTemplate -TemplateIdentity 'template.user' ` -GivenName 'Alice' -Surname 'Williams' -DisplayName 'Alice Williams' ` -ExcludedGroups @('Domain Users','Sensitive Group') ` -InitialPasswordLength 24 -Credential $cred -WhatIf .NOTES CONFIG - Requires TechToolbox runtime/config via Initialize-TechToolboxRuntime. - Expected keys (typical): - settings.tenant.upnSuffix - settings.naming.copyAttributes (optional) NAMING - If -SamAccountName or -UpnPrefix is omitted, Resolve-Naming is used. - Resolve-Naming must return .Sam and .UpnPrefix. IDEMPOTENCY - If a user with the target UPN exists, the function returns without creating a duplicate. COPYATTRIBUTES DEFAULTS + MAPPING - Default attributes (if config not provided): description, department, company, office, manager - 'office' maps to physicalDeliveryOfficeName. - Unknown names are treated as LDAP attributes and applied via -Replace. - 'manager' is copied only when template manager is a valid DN. GROUP COPY RULES - Distribution groups: copied by default - Security groups: copied only if group name appears in -AllowedSecurityGroups - -ExcludedGroups always wins - Group-add failures log warnings but do not stop provisioning PROXYADDRESSES - Sets primary proxyAddress as: SMTP:<UpnPrefix>@<upnSuffix> - Additional aliases are not added by this function. SECURITY - InitialPassword is returned in plaintext in output. Deliver securely. - Credential must have rights to create users, set password, modify attributes, and add group memberships. .LINK https://dan-damit.github.io/TechToolbox-Docs/user-provisioning.md .LINK about_UserProvisioning Initialize-TechToolboxRuntime Resolve-Naming Get-ADUser New-ADUser Set-ADUser Add-ADGroupMember #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByIdentity')] param( [Parameter(Mandatory, ParameterSetName = 'ByIdentity')] [string]$TemplateIdentity, [Parameter(Mandatory, ParameterSetName = 'BySearch')] [hashtable]$TemplateSearch, [Parameter(Mandatory)] [string]$GivenName, [Parameter(Mandatory)] [string]$Surname, [Parameter(Mandatory)] [string]$DisplayName, [string]$TargetOU, [string]$SamAccountName, [string]$UpnPrefix, [string[]]$CopyAttributes = @( 'description', 'department', 'company', 'office', 'manager' ), [string[]]$ExcludedGroups = @( 'Domain Admins', 'Enterprise Admins', 'Schema Admins', 'Administrators', 'Protected Users', 'Server Operators', 'Account Operators', 'Backup Operators', 'Print Operators', 'Group Policy Creator Owners', 'Key Admins', 'Enterprise Key Admins', 'DnsAdmins', 'DnsUpdateProxy', 'Cert Publishers', 'Read-only Domain Controllers', 'Enterprise Read-only Domain Controllers', 'Allowed RODC Password Replication Group', 'Denied RODC Password Replication Group', 'Cloneable Domain Controllers', 'Replicator' ), # Explicit per-run allow list (default = copy NO security groups) [string[]]$AllowedSecurityGroups = @(), [int]$InitialPasswordLength = 12, [Parameter(Mandatory)] [pscredential]$Credential, [string]$Server, [switch]$ShowSummary ) begin { $ErrorActionPreference = 'Stop' Initialize-TechToolboxRuntime Get-ActiveDirectoryModule $cfg = $script:cfg $Tenant = $cfg.settings.tenant $Naming = $cfg.settings.naming # Apply config-driven copy list when caller does not explicitly pass -CopyAttributes. if (-not $PSBoundParameters.ContainsKey('CopyAttributes') -and $Naming.copyAttributes) { $CopyAttributes = @($Naming.copyAttributes | Where-Object { $_ -and $_.ToString().Trim() }) } # Friendly attribute aliases -> LDAP names. $configToLdap = @{ 'description' = 'description' 'department' = 'department' 'company' = 'company' 'office' = 'physicalDeliveryOfficeName' 'manager' = 'manager' } # LDAP names -> Set-ADUser friendly parameter names. $LdapToParam = @{ 'description' = 'Description' 'department' = 'Department' 'company' = 'Company' 'physicalDeliveryOfficeName' = 'Office' 'manager' = 'Manager' } # Properties to request from template during lookup. $CopyLdapAttrs = foreach ($attr in $CopyAttributes) { if (-not $attr) { continue } $key = $attr.ToString().ToLowerInvariant() if ($configToLdap.ContainsKey($key)) { $configToLdap[$key] } else { $attr.ToString() } } # Build AD splat EARLY $adBase = @{ Credential = $Credential } if ($Server) { $adBase['Server'] = $Server } if (-not $AllowedSecurityGroups -or $AllowedSecurityGroups.Count -eq 0) { Write-Log -Level Info -Message "No AllowedSecurityGroups provided; security group memberships will NOT be copied (distribution groups will still copy)." } $allowedSecDns = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) $excludedDns = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) foreach ($n in ($AllowedSecurityGroups | Where-Object { $_ -and $_.Trim() })) { $g = Get-ADGroup @adBase -Identity $n.Trim() -ErrorAction SilentlyContinue if ($g) { [void]$allowedSecDns.Add($g.DistinguishedName) } else { Write-Log -Level Warn -Message "AllowedSecurityGroup not found: $n" } } foreach ($n in ($ExcludedGroups | Where-Object { $_ -and $_.Trim() })) { $g = Get-ADGroup @adBase -Identity $n.Trim() -ErrorAction SilentlyContinue if ($g) { [void]$excludedDns.Add($g.DistinguishedName) } else { Write-Log -Level Warn -Message "ExcludedGroup not found: $n" } } # Always include these for later logic $templateProps = @($CopyLdapAttrs + 'memberOf' + 'adminCount') | Select-Object -Unique switch ($PSCmdlet.ParameterSetName) { 'ByIdentity' { $templateUser = Get-ADUser @adBase -Identity $TemplateIdentity -Properties $templateProps } 'BySearch' { $clauses = foreach ($k in $TemplateSearch.Keys) { $v = $TemplateSearch[$k] if ($null -eq $v) { continue } $v = ($v.ToString() -replace "'", "''") "($k -eq '$v')" } if (-not $clauses) { throw "TemplateSearch is empty or contains only null values; provide at least one key/value." } $filter = ($clauses -join ' -and ') $templateUser = Get-ADUser @adBase -Filter $filter -Properties $templateProps | Select-Object -First 1 if (-not $templateUser) { throw "Template user not found using exact-match filter: $filter" } } } if ($templateUser.adminCount -eq 1) { throw "Template user '$($templateUser.SamAccountName)' has adminCount=1 (protected/admin). Choose a non-privileged template." } Set-Variable -Name templateUser -Value $templateUser -Scope 1 Set-Variable -Name adBase -Value $adBase -Scope 1 Set-Variable -Name allowedSecDns -Value $allowedSecDns -Scope 1 Set-Variable -Name excludedDns -Value $excludedDns -Scope 1 } process { # Breadcrumb #1: entering function Write-Log -Level Info -Message ("Entering New-OnPremUserFromTemplate (ParamSet={0})" -f $PSCmdlet.ParameterSetName) # 1) Derive naming via config (unless caller overrides) if (-not $UpnPrefix -or -not $SamAccountName) { $nm = Resolve-Naming -Naming $Naming -GivenName $GivenName -Surname $Surname if (-not $UpnPrefix) { $UpnPrefix = $nm.UpnPrefix } if (-not $SamAccountName) { $SamAccountName = $nm.Sam } } $newUpn = "$UpnPrefix@$($Tenant.upnSuffix)" # 2) Resolve target OU (default to template's OU) if (-not $TargetOU) { $TargetOU = ($templateUser.DistinguishedName -replace '^CN=.*?,') } Write-Log -Level Info -Message ("Provisioning: DisplayName='{0}', Sam='{1}', UPN='{2}', OU='{3}'" -f $DisplayName, $SamAccountName, $newUpn, $TargetOU) # 3) Idempotency check $exists = Get-ADUser @adBase -LDAPFilter "(userPrincipalName=$newUpn)" -ErrorAction SilentlyContinue if ($exists) { Write-Log -Level Warn -Message "User UPN '$newUpn' already exists. Aborting." return } # 4) Create new user $initialPassword = Get-NewPassword -length $InitialPasswordLength -nonAlpha 3 $securePass = ConvertTo-SecureString $initialPassword -AsPlainText -Force $newParams = @{ Name = $DisplayName DisplayName = $DisplayName GivenName = $GivenName Surname = $Surname SamAccountName = $SamAccountName UserPrincipalName = $newUpn Enabled = $false # set $false if prefer disabled on creation Path = $TargetOU ChangePasswordAtLogon = $true AccountPassword = $securePass } if ($PSCmdlet.ShouldProcess($newUpn, "Create AD user")) { New-ADUser @adBase @newParams Write-Log -Level Ok -Message ("Created AD user: {0}" -f $newUpn) } # 5) Copy selected attributes from template (uses mappings from begin{}) $friendlyProps = @{} $otherAttrs = @{} foreach ($attr in $CopyAttributes) { if (-not $attr) { continue } $key = $attr.ToString() $ldapName = $configToLdap[$key.ToLowerInvariant()] if (-not $ldapName) { $ldapName = $key } # treat unknown as raw LDAP (e.g., extensionAttribute1) $val = $templateUser.$ldapName if ($null -eq $val) { continue } if ($val -is [string] -and [string]::IsNullOrWhiteSpace($val)) { continue } if ($ldapName -eq 'manager') { # Manager must be DN; set via friendly param if it looks like a DN if ($val -is [string] -and $val -match '^CN=.+,DC=.+') { $friendlyProps['Manager'] = $val } else { Write-Verbose "Skipping manager; value is not a DN: $val" } continue } if ($LdapToParam.ContainsKey($ldapName)) { $friendlyProps[$LdapToParam[$ldapName]] = $val } else { $otherAttrs[$ldapName] = $val } } # Avoid double-setting Office via friendly and LDAP at once if ($friendlyProps.ContainsKey('Office') -and $otherAttrs.ContainsKey('physicalDeliveryOfficeName')) { $null = $otherAttrs.Remove('physicalDeliveryOfficeName') } if ($PSCmdlet.ShouldProcess($newUpn, "Apply copied attributes")) { if ($friendlyProps.Count -gt 0) { Set-ADUser @adBase -Identity $SamAccountName @friendlyProps } if ($otherAttrs.Count -gt 0) { Set-ADUser @adBase -Identity $SamAccountName -Replace $otherAttrs } Write-Log -Level Ok -Message "Copied attributes applied from template." } # 6) proxyAddresses — single primary at creation (idempotent) $primaryProxy = "SMTP:$UpnPrefix@$($Tenant.upnSuffix)" $proxiesToSet = @($primaryProxy) if ($PSCmdlet.ShouldProcess($newUpn, "Set primary proxyAddress")) { Set-ADUser @adBase -Identity $SamAccountName -Replace @{ proxyAddresses = $proxiesToSet } Write-Log -Level Ok -Message "Primary proxyAddress applied." } # 7) Copy group memberships (Distribution by default; Security via allow-list) $tmplGroupDNs = @($templateUser.memberOf) if (-not $tmplGroupDNs) { $tmplGroupDNs = @() } $tmplGroups = foreach ($dn in $tmplGroupDNs) { Get-ADGroup @adBase -Identity $dn -Properties GroupCategory -ErrorAction SilentlyContinue } $toAddGroups = $tmplGroups | Where-Object { $_ -and ( $_.GroupCategory -eq 'Distribution' -or ($_.GroupCategory -eq 'Security' -and $allowedSecDns.Contains($_.DistinguishedName)) ) -and (-not $excludedDns.Contains($_.DistinguishedName)) } $toAdd = @( $toAddGroups | ForEach-Object { $_.Name } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique ) if ($PSCmdlet.ShouldProcess($newUpn, "Add group memberships")) { $added = 0 foreach ($grp in $toAddGroups) { try { Add-ADGroupMember @adBase -Identity $grp.DistinguishedName -Members $SamAccountName -ErrorAction Stop $added++ Write-Log -Level Info -Message ("Added to: {0}" -f $grp.Name) } catch { Write-Log -Level Warn -Message ("Group add failed '{0}': {1}" -f $grp.Name, $_.Exception.Message) } } Write-Log -Level Ok -Message ("Group additions complete: {0} added" -f $added) } $skippedSecurity = $tmplGroups | Where-Object { $_ -and $_.GroupCategory -eq 'Security' -and (-not $allowedSecDns.Contains($_.DistinguishedName)) } if ($skippedSecurity) { $skippedSecurityNames = @( $skippedSecurity | ForEach-Object { $_.Name } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique ) if ($skippedSecurityNames.Count -gt 0) { Write-Log -Level Info -Message ("Skipped security groups (not allow-listed): {0}" -f ($skippedSecurityNames -join ', ')) } } # 8) Output summary (force visible + return) $result = [pscustomobject]@{ UserPrincipalName = $newUpn SamAccountName = $SamAccountName DisplayName = $DisplayName Enabled = $newParams.Enabled TargetOU = $TargetOU CopiedAttributes = $CopyAttributes GroupsAdded = $toAdd InitialPassword = $initialPassword # caller is responsible for secure handling } if ($ShowSummary) { $result | Format-List | Out-Host } Write-Output $result } end { } } # SIG # Begin signature block # MIIfAgYJKoZIhvcNAQcCoIIe8zCCHu8CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB3PQendu18Ad4p # Ab+lXozmcDsX/ELNDaOrK4+EBozoG6CCGEowggUMMIIC9KADAgECAhAR+U4xG7FH # qkyqS9NIt7l5MA0GCSqGSIb3DQEBCwUAMB4xHDAaBgNVBAMME1ZBRFRFSyBDb2Rl # IFNpZ25pbmcwHhcNMjUxMjE5MTk1NDIxWhcNMjYxMjE5MjAwNDIxWjAeMRwwGgYD # VQQDDBNWQURURUsgQ29kZSBTaWduaW5nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A # MIICCgKCAgEA3pzzZIUEY92GDldMWuzvbLeivHOuMupgpwbezoG5v90KeuN03S5d # nM/eom/PcIz08+fGZF04ueuCS6b48q1qFnylwg/C/TkcVRo0WFcKoFGT8yGxdfXi # caHtapZfbSRh73r7qR7w0CioVveNBVgfMsTgE0WKcuwxemvIe/ptmkfzwAiw/IAC # Ib0E0BjiX4PySbwWy/QKy/qMXYY19xpRItVTKNBtXzADUtzPzUcFqJU83vM2gZFs # Or0MhPvM7xEVkOWZFBAWAubbMCJ3rmwyVv9keVDJChhCeLSz2XR11VGDOEA2OO90 # Y30WfY9aOI2sCfQcKMeJ9ypkHl0xORdhUwZ3Wz48d3yJDXGkduPm2vl05RvnA4T6 # 29HVZTmMdvP2475/8nLxCte9IB7TobAOGl6P1NuwplAMKM8qyZh62Br23vcx1fXZ # TJlKCxBFx1nTa6VlIJk+UbM4ZPm954peB/fIqEacm8LkZ0cPwmLE5ckW7hfK4Trs # o+RaudU1sKeA+FvpOWgsPccVRWcEYyGkwbyTB3xrIBXA+YckbANZ0XL7fv7x29hn # gXbZipGu3DnTISiFB43V4MhNDKZYfbWdxze0SwLe8KzIaKnwlwRgvXDMwXgk99Mi # EbYa3DvA/5ZWikLW9PxBFD7Vdr8ZiG/tRC9I2Y6fnb+PVoZKc/2xsW0CAwEAAaNG # MEQwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQW # BBRfYLVE8caSc990rnrIHUjoB7X/KjANBgkqhkiG9w0BAQsFAAOCAgEAiGB2Wmk3 # QBtd1LcynmxHzmu+X4Y5DIpMMNC2ahsqZtPUVcGqmb5IFbVuAdQphL6PSrDjaAR8 # 1S8uTfUnMa119LmIb7di7TlH2F5K3530h5x8JMj5EErl0xmZyJtSg7BTiBA/UrMz # 6WCf8wWIG2/4NbV6aAyFwIojfAcKoO8ng44Dal/oLGzLO3FDE5AWhcda/FbqVjSJ # 1zMfiW8odd4LgbmoyEI024KkwOkkPyJQ2Ugn6HMqlFLazAmBBpyS7wxdaAGrl18n # 6bS7QuAwCd9hitdMMitG8YyWL6tKeRSbuTP5E+ASbu0Ga8/fxRO5ZSQhO6/5ro1j # PGe1/Kr49Uyuf9VSCZdNIZAyjjeVAoxmV0IfxQLKz6VOG0kGDYkFGskvllIpQbQg # WLuPLJxoskJsoJllk7MjZJwrpr08+3FQnLkRuisjDOc3l4VxFUsUe4fnJhMUONXT # Sk7vdspgxirNbLmXU4yYWdsizz3nMUR0zebUW29A+HYme16hzrMPOeyoQjy4I5XX # 3wXAFdworfPEr/ozDFrdXKgbLwZopymKbBwv6wtT7+1zVhJXr+jGVQ1TWr6R+8ea # tIOFnY7HqGaxe5XB7HzOwJKdj+bpHAfXft1vUoiKr16VajLigcYCG8MdwC3sngO3 # JDyv2V+YMfsYBmItMGBwvizlQ6557NbK95EwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqG # SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYg # MjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphB # cr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6p # vF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHe # HYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEd # gkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjU # jsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bR # VFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeS # LsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIV # NSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL # 6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2Zd # SoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFU # eEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEw # DQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/ # T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQ # E7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9r # EVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y # 1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gx # dEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3t # y9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcy # tL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEB # YTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud # /v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiS # uEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZP # ubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsF # ADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNV # BAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hB # MjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzEL # MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE # aWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUg # MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMr # V7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8 # dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7M # rxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZ # ZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFO # nHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+n # igNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeIt # K/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1 # zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk # 8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsW # eupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAk # prxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0G # A1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQG # fHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYB # BQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEy # NTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hB # MjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcB # MA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWL # pQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgj # g8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3Q # YIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5 # bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUG # tMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNE # suEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6U # Arb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG # 0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWV # FjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5 # t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjs # arfNZzGCBg4wggYKAgEBMDIwHjEcMBoGA1UEAwwTVkFEVEVLIENvZGUgU2lnbmlu # ZwIQEflOMRuxR6pMqkvTSLe5eTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3 # AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG # AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCCPrblSGfL/ # k9a/nW4orrIcd3Mlevxa4lB2SV5FCpAstTANBgkqhkiG9w0BAQEFAASCAgAkQ+y1 # Cz33qoWv9oJFxHDh19bAqxFwHJUIDex2qfsFvk9EcHPaAYtOHa/G3rnm2wTRCQS/ # DlZs+9Y+R/ZSY7+jt65uOqbnyqRLje7/PBUY59g3U9Zwt0iKJp+9O6XZItiimuOA # ThA2T6INvwUtJ5BdFBJ06wwD/EiKqHUY/q+bcDri5/o1pWg9L/it04g4SxRTTGf2 # ZwnhNGiyrVk7NNqgfUu0FHS+WtzD+WXAJQbzTbTQI8GhtCa1unbtxbGUD99hwxbB # ZTmbqosnvhcZuou3sKx45Kd4h5ci5wWnvnaWKwqR7WmBRT5I4LUNNAEdMx44QS3C # ma/k22WM/DhU5d1TLrJfz3rlebPhUq1SJFGbOEzkaMaB5QV+VbPdzg915HvYqwBJ # aeeV4Xj7YcAFvcYA8gLtc+iDxcX7qHji/j+7fR2VwyIs1piBBEDZwN/9uikSs9K8 # avFSFeLZaSjU/2HQTBem52SYh/oZAHPfWCAx9wEdlidPGIlQaQsjb1HpFdCytGZ8 # 76lIej9ShOlfhTHiEylhij9yr6qdlvibC/OJNt8MhyZZw1yMfX2qLxBMr98DwMjA # whlQPDW6BOQoJAPQfzd3rvmrELj538v2fKnkYNqDWQAnA5UhcgT1BXOX9jgnXDNz # Tx3cEn4dK6KPf0zITpoQPxLzFAdqjlr5o+Eoy6GCAyYwggMiBgkqhkiG9w0BCQYx # ggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcg # UlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeVdGgwDQYJYIZI # AWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJ # BTEPFw0yNjA1MDExNzE1NDlaMC8GCSqGSIb3DQEJBDEiBCDbeDB2UN3L9tXIdnHK # dllR6GRPAiZJ5QiV8w2KpnFVRTANBgkqhkiG9w0BAQEFAASCAgCd2bsRIoyquM1S # 1VNZv4vQwNT9ZBU6MPnq6UoRLRQEUTlm3aCRw/WqIMheyZUsLTLd0oEns0zO0KGd # 12rq5/zXKz+/wyhNeQsqt9splR+zTGplZQXoBDqKlk1e6Q43dlxdNaKeMxoMsnU7 # 379inp1bGCozkbtmYY1aXzgn0Moag3/x0v9FlwhHh3GyjtGCNe6ucu7DePOAp1Cw # uW9SSJ3ZBJi5A09loGcSasOFBEk4r3FbwgqnTGxQHxe8NxPmutZgJI9ZYJKD0SP5 # grbrfj74gL/qc8jAefrR5bMVT9PRJY49tgLd1hDJL9Jnslh+mcnlm8fiN52Wtjkw # p2gJ/mELoLoEetVT6dkNc2IY/puiHb/kKgujxpeK3E0cHFPAxnUrdprvueIDMYW/ # GMUia83sNzXHdu+lz6F8YbarYUS/vCtiofITTAKW0xSheM10PeGvBGhM48zDm1Yw # RgvedQT0btLmnH9tb1NU1AWT5z+S7rigTXPwNdvVMJwWxsFRH7BSapN3q3+tdrrl # SsJeIC4oYxlSDqPNJvIyQ/OQWF9KNuGEapJX4wCjHCYnN/MTWhSetzNwbKemRrb3 # nkOI9tcJ966lap2Ked7aziWzW4Ex/udpmArz/IlwXhH3Hrp0WZRFAocO1Rd5csjM # HyXqQ8okOaqo4/FyMimFYyQM6y4u3w== # SIG # End signature block |