PAM/PamCommon.ps1
|
#requires -Version 5.1 # Shared PAM helpers $script:PamRotationScriptVerbs = New-Object 'System.Collections.Generic.HashSet[string]' [void]$script:PamRotationScriptVerbs.Add('list') [void]$script:PamRotationScriptVerbs.Add('l') [void]$script:PamRotationScriptVerbs.Add('add') [void]$script:PamRotationScriptVerbs.Add('new') [void]$script:PamRotationScriptVerbs.Add('n') [void]$script:PamRotationScriptVerbs.Add('a') [void]$script:PamRotationScriptVerbs.Add('edit') [void]$script:PamRotationScriptVerbs.Add('e') [void]$script:PamRotationScriptVerbs.Add('delete') [void]$script:PamRotationScriptVerbs.Add('d') function script:testPamRotationScriptVerb { Param ([string] $Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $false } $trimmed = $Value.Trim() foreach ($verb in $script:PamRotationScriptVerbs) { if ([string]::Equals($verb, $trimmed, [System.StringComparison]::OrdinalIgnoreCase)) { return $true } } return $false } function script:encodePamByteString { Param ( [Google.Protobuf.ByteString] $ByteString ) if ($null -eq $ByteString -or $ByteString.IsEmpty) { return '' } return [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($ByteString.ToByteArray()) } function script:resolvePamRotationRecord { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [string] $Identifier, [Parameter(Mandatory = $true)] [System.Collections.Generic.HashSet[string]] $AllowedTypes ) try { return [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $Identifier.Trim(), $AllowedTypes) } catch [System.InvalidOperationException] { Write-Output $_.Exception.Message throw } } function script:testPamRotationScriptOwnerException { Param ( [Parameter(Mandatory = $true)] [System.Exception] $Exception ) $ex = $Exception while ($null -ne $ex) { if ($ex -is [KeeperSecurity.Authentication.KeeperApiException]) { $code = [string]$ex.Code if ($code -eq 'only_owner_can_modify_scripts' -or $code -eq 'RS_ONLY_OWNER_CAN_MODIFY_SCRIPTS') { return $true } } $ex = $ex.InnerException } return $false } function script:updatePamRotationScriptRecord { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [Parameter(Mandatory = $true)] [ValidateSet('add', 'edit', 'remove')] [string] $Action ) try { $Vault.UpdateRecord($Record).GetAwaiter().GetResult() | Out-Null return $true } catch { if (testPamRotationScriptOwnerException -Exception $_.Exception) { switch ($Action) { 'add' { Write-Output 'Only the record owner can attach post-rotation scripts.' } 'edit' { Write-Output 'Only the record owner can edit post-rotation scripts.' } 'remove' { Write-Output 'Only the record owner can remove post-rotation scripts.' } } return $false } throw } } function script:getPamRotationVault { $vault = getVault if (-not $vault) { Write-Error -Message 'Vault is not available.' -ErrorAction Stop } return $vault } function script:confirmPamYesNo { Param ( [Parameter(Mandatory = $true)] [string] $Prompt, [bool] $DefaultYes = $true ) $suffix = if ($DefaultYes) { ' [Y/n]: ' } else { ' [y/N]: ' } $answer = Read-Host ($Prompt.TrimEnd() + $suffix) if ([string]::IsNullOrWhiteSpace($answer)) { return $DefaultYes } return ($answer.Trim().StartsWith('y', [System.StringComparison]::OrdinalIgnoreCase)) } function script:toPamUidBytes { Param ([string] $Uid) return [KeeperSecurity.Utils.CryptoUtils]::Base64UrlDecode($Uid) } function script:enumeratePamTypedFields { Param ([KeeperSecurity.Vault.TypedRecord] $Record) if ($null -eq $Record) { return @() } $fields = New-Object System.Collections.Generic.List[object] if ($null -ne $Record.Fields) { foreach ($f in $Record.Fields) { [void]$fields.Add($f) } } if ($null -ne $Record.Custom) { foreach ($f in $Record.Custom) { [void]$fields.Add($f) } } return , $fields.ToArray() } function script:getPamDefaultScheduleFromConfig { Param ([KeeperSecurity.Vault.TypedRecord] $Config) if ($null -eq $Config) { return $null } $scheduleField = $null foreach ($field in (enumeratePamTypedFields -Record $Config)) { if ($field.FieldName -ne 'schedule') { continue } if ($field.FieldLabel -eq 'defaultRotationSchedule') { $scheduleField = $field break } if ($null -eq $scheduleField) { $scheduleField = $field } } if ($null -eq $scheduleField -or $null -eq $scheduleField.Values -or $scheduleField.Values.Count -eq 0) { return $null } $value = $scheduleField.Values[0] if ($null -eq $value) { return $null } if ([string]::IsNullOrWhiteSpace($value.Type) -or [string]::Equals($value.Type, 'On-Demand', [System.StringComparison]::OrdinalIgnoreCase)) { return New-Object 'System.Collections.Generic.List[object]' } $dict = New-Object 'System.Collections.Generic.Dictionary[string,object]' $dict['type'] = $value.Type if (-not [string]::IsNullOrEmpty($value.Time)) { $dict['time'] = $value.Time $dict['utcTime'] = $value.Time } if (-not [string]::IsNullOrEmpty($value.Weekday)) { $dict['weekday'] = $value.Weekday } if (-not [string]::IsNullOrEmpty($value.Month)) { $dict['month'] = $value.Month } if (-not [string]::IsNullOrEmpty($value.MonthDay)) { $dict['monthDay'] = $value.MonthDay } if (-not [string]::IsNullOrEmpty($value.IntervalCount)) { $dict['intervalCount'] = $value.IntervalCount } if (-not [string]::IsNullOrEmpty($value.Cron)) { $dict['cron'] = $value.Cron } if (-not [string]::IsNullOrEmpty($value.TimeZone)) { $dict['tz'] = $value.TimeZone } $list = New-Object 'System.Collections.Generic.List[object]' [void]$list.Add($dict) return , $list } function script:toPamScheduleObjectList { Param ($ScheduleData) $list = New-Object 'System.Collections.Generic.List[object]' if ($null -eq $ScheduleData) { return , $list } if ($ScheduleData -is [System.Collections.Generic.List[object]]) { return , $ScheduleData } if ($ScheduleData -is [System.Collections.IDictionary]) { [void]$list.Add($ScheduleData) return , $list } if (($ScheduleData -is [System.Collections.IEnumerable]) -and -not ($ScheduleData -is [string])) { foreach ($item in $ScheduleData) { if ($null -ne $item) { [void]$list.Add($item) } } return , $list } [void]$list.Add($ScheduleData) return , $list } function script:getPamDefaultResourceUidFromConfig { Param ([KeeperSecurity.Vault.TypedRecord] $Config) if ($null -eq $Config -or $null -eq $Config.Fields) { return $null } foreach ($field in $Config.Fields) { if ($field.FieldName -ne 'pamResources') { continue } if ($null -eq $field.Values -or $field.Values.Count -eq 0) { return $null } $refs = $field.Values[0].ResourceRef if ($null -eq $refs -or $refs.Length -ne 1) { return $null } return [string]$refs[0] } return $null } function script:testPamNoopRecord { Param ([KeeperSecurity.Vault.TypedRecord] $Record) if ($null -eq $Record -or $null -eq $Record.Fields) { return $false } foreach ($field in $Record.Fields) { if ($field.FieldName -ne 'NOOP') { continue } if ($null -eq $field.Values -or $field.Values.Count -eq 0) { return $false } $value = [string]$field.Values[0] return (-not [string]::IsNullOrEmpty($value) -and [string]::Equals($value.Trim(), 'TRUE', [System.StringComparison]::OrdinalIgnoreCase)) } return $false } function script:formatPamPasswordComplexityInfoDisplay { Param ([KeeperSecurity.Utils.PasswordGenerationOptions] $Rules) if ($null -eq $Rules) { return '' } $chars = $Rules.SpecialCharacters if ([string]::IsNullOrEmpty($chars)) { $chars = '!@#$%^&*()_+=-[];,.<>?' } return ("Length: {0}; Lowercase: {1}; Uppercase: {2}; Digits: {3}; Symbols: {4}; Special Characters: {5}" -f ` $Rules.Length, $Rules.Lower, $Rules.Upper, $Rules.Digit, $Rules.Special, $chars) } function script:testPamUsesDefaultRotationSchedule { Param ( [KeeperSecurity.Vault.VaultOnline] $Vault, [string] $RecordUid, [string] $ConfigUid ) if ($null -eq $Vault -or [string]::IsNullOrWhiteSpace($RecordUid) -or [string]::IsNullOrWhiteSpace($ConfigUid)) { return $false } $config = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord( $Vault, $ConfigUid.Trim(), [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration) if ($null -eq $config) { return $false } $defaultSchedule = toPamScheduleObjectList (getPamDefaultScheduleFromConfig -Config $config) if ($defaultSchedule.Count -eq 0) { return $false } $cached = $Vault.GetRecordRotation($RecordUid.Trim()) if ($null -eq $cached -or [string]::IsNullOrWhiteSpace($cached.Schedule)) { return $false } try { $recordSchedule = toPamScheduleObjectList ( [KeeperSecurity.Plugins.PAM.RotationUtils]::ParseScheduleJsonString($cached.Schedule)) } catch { return $false } if ($recordSchedule.Count -eq 0) { return $false } return [string]::Equals( [KeeperSecurity.Plugins.PAM.RotationUtils]::SerializeScheduleData($recordSchedule), [KeeperSecurity.Plugins.PAM.RotationUtils]::SerializeScheduleData($defaultSchedule), [System.StringComparison]::Ordinal) } function script:resolvePamRotationTargetRecords { Param ( [KeeperSecurity.Vault.VaultOnline] $Vault, [string] $Record, [string] $Folder ) $recordUids = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal) $rotationTypes = [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Rotation if (-not [string]::IsNullOrWhiteSpace($Record)) { $resolved = $null try { $resolved = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $Record.Trim(), $rotationTypes) } catch [System.InvalidOperationException] { Write-Error -Message $_.Exception.Message -ErrorAction Stop } if ($null -ne $resolved) { [void]$recordUids.Add($resolved.Uid) } else { Write-Output ("Record `"{0}`" not found." -f $Record.Trim()) } } if (-not [string]::IsNullOrWhiteSpace($Folder)) { $folderName = $Folder.Trim() $folderNode = $null if (-not $Vault.TryGetFolder($folderName, [ref]$folderNode)) { Write-Output ("Folder `"{0}`" not found. Skipping." -f $folderName) } else { $folderUids = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal) $stack = New-Object System.Collections.Generic.Stack[object] $stack.Push($folderNode) while ($stack.Count -gt 0) { $node = $stack.Pop() if ($null -eq $node) { continue } if (-not [string]::IsNullOrEmpty($node.FolderUid)) { [void]$folderUids.Add($node.FolderUid) } if ($null -ne $node.Subfolders) { foreach ($subUid in $node.Subfolders) { $child = $null if ($Vault.TryGetFolder($subUid, [ref]$child) -and $null -ne $child) { $stack.Push($child) } } } } foreach ($folderUid in $folderUids) { $folder = $null if (-not $Vault.TryGetFolder($folderUid, [ref]$folder) -or $null -eq $folder) { continue } if ($null -eq $folder.Records) { continue } foreach ($uid in $folder.Records) { if ($recordUids.Contains($uid)) { continue } $typed = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $uid, $rotationTypes) if ($null -ne $typed) { [void]$recordUids.Add($uid) } } } } } $records = New-Object System.Collections.Generic.List[KeeperSecurity.Vault.TypedRecord] foreach ($uid in $recordUids) { $rec = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, $uid, $rotationTypes) if ($null -ne $rec) { [void]$records.Add($rec) } } return , $records } function script:invokeKeeperPamRotationEdit { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Authentication.IAuthentication] $Auth, [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [hashtable] $Options ) $Vault.SyncDown().GetAwaiter().GetResult() | Out-Null $complexityRules = $null if ($null -ne $Options.Complexity -and -not [string]::IsNullOrWhiteSpace([string]$Options.Complexity)) { $complexityRules = [KeeperSecurity.Plugins.PAM.RotationUtils]::ParsePasswordComplexityRules([string]$Options.Complexity) } elseif (-not [string]::IsNullOrWhiteSpace([string]$Options.ComplexityJson)) { try { $bytes = [System.Text.Encoding]::UTF8.GetBytes([string]$Options.ComplexityJson) $complexityRules = [KeeperSecurity.Plugins.PAM.RotationUtils]::ParsePasswordComplexityJson($bytes) } catch [System.ArgumentException] { Write-Error -Message $_.Exception.Message -ErrorAction Stop } } try { $scheduleData = [KeeperSecurity.Plugins.PAM.RotationUtils]::ParseScheduleOptions( [string]$Options.ScheduleJson, [string]$Options.ScheduleCron, [bool]$Options.OnDemand, [bool]$Options.ScheduleConfig) if ($null -ne $scheduleData) { $scheduleData = toPamScheduleObjectList $scheduleData } } catch { Write-Error -Message $_.Exception.Message -ErrorAction Stop } $pamConfigs = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::GetConfigurationRecords($Vault) $configRecord = $null if (-not [string]::IsNullOrWhiteSpace([string]$Options.Config)) { $configRecord = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord( $Vault, [string]$Options.Config, [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration) if ($null -eq $configRecord) { Write-Error -Message ("Record uid {0} is not a PAM Configuration record." -f $Options.Config) -ErrorAction Stop } } if (-not [string]::IsNullOrWhiteSpace([string]$Options.RotationProfile)) { $allowed = @('general', 'iam_user', 'scripts_only', 'saas') $profileCheck = ([string]$Options.RotationProfile).Trim().ToLowerInvariant() if ($allowed -notcontains $profileCheck) { Write-Error -Message 'Invalid rotation profile. Allowed values: general, iam_user, scripts_only, saas' -ErrorAction Stop } } $records = resolvePamRotationTargetRecords -Vault $Vault -Record ([string]$Options.Record) -Folder ([string]$Options.Folder) if ($records.Count -eq 0) { $types = [string]::Join(', ', [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Rotation) Write-Error -Message ("No PAM record is found. Valid PAM record types: {0}" -f $types) -ErrorAction Stop } Write-Output ("Selected {0} PAM record(s) for rotation" -f $records.Count) $skipped = New-Object System.Collections.Generic.List[object] $valid = New-Object System.Collections.Generic.List[object] $requests = New-Object System.Collections.Generic.List[Router.RouterRecordRotationRequest] $configuredResources = New-Object System.Collections.Generic.List[hashtable] $resourceTypes = [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Resource foreach ($record in $records) { $typeName = [string]$record.TypeName if ($resourceTypes.Contains($typeName)) { try { invokePamConfigureResourceRecord ` -Auth $Auth -Vault $Vault -Record $record -Options $Options ` -PamConfigs $pamConfigs -ConfigRecord $configRecord ` -Skipped $skipped -ConfiguredResources $configuredResources } catch [System.InvalidOperationException] { if (-not [string]::IsNullOrWhiteSpace([string]$Options.Folder)) { [void]$skipped.Add(@($record.Uid, $record.Title, 'Error', $_.Exception.Message)) } else { Write-Error -Message $_.Exception.Message -ErrorAction Stop } } continue } if ($typeName -ne 'pamUser') { continue } try { $editContext = resolvePamRecordEditContext -Options $Options -Record $record $resourceUidForDag = $null $configUidForDag = if ($null -ne $configRecord) { $configRecord.Uid } else { $null } if ($editContext.Profile -ne 'iam_user' -and -not $editContext.Noop -and -not [bool]$Options.ScheduleOnly) { $resourceUidForDag = resolvePamResourceUidForDag ` -Vault $Vault -Record $record -Options $Options ` -ConfigRecord $configRecord -PamConfigs $pamConfigs if ([string]::IsNullOrEmpty($configUidForDag)) { $cached = $Vault.GetRecordRotation($record.Uid) $configUidForDag = $cached.ConfigurationUid } } if (-not [string]::IsNullOrEmpty($resourceUidForDag) -and -not [string]::IsNullOrEmpty($configUidForDag)) { [KeeperSecurity.Plugins.PAM.PamRotationGraphEdit]::ConfigureUserAsync( $Auth, $Vault, $record, $resourceUidForDag, $configUidForDag, [bool]$editContext.Noop, [bool]$Options.ScheduleOnly ).GetAwaiter().GetResult() | Out-Null # NSF only: sync so rotation revision is fresh after graph link. if ([KeeperSecurity.Plugins.PAM.PamVaultHelpers]::IsKeeperNSFRecord($Vault, $record.Uid)) { $Vault.SyncDown().GetAwaiter().GetResult() } } $request = $null if (tryBuildPamUserRotationRequest ` -Vault $Vault -Record $record -Options $Options ` -ConfigRecord $configRecord -PamConfigs $pamConfigs ` -ScheduleData $scheduleData -ComplexityRules $complexityRules ` -EditContext $editContext -Skipped $skipped -Valid $valid ` -Request ([ref]$request)) { [void]$requests.Add($request) } } catch [System.InvalidOperationException] { if (-not [string]::IsNullOrWhiteSpace([string]$Options.Folder)) { [void]$skipped.Add(@($record.Uid, $record.Title, 'Error', $_.Exception.Message)) } else { Write-Error -Message $_.Exception.Message -ErrorAction Stop } } } if ($skipped.Count -gt 0) { Write-Output '' Write-Output 'The following record(s) were skipped:' Write-Output ("{0,-22} {1,-28} {2,-28} {3}" -f 'Record UID', 'Record Title', 'Problem', 'Description') foreach ($row in $skipped) { Write-Output ("{0,-22} {1,-28} {2,-28} {3}" -f $row[0], $row[1], $row[2], $row[3]) } } foreach ($summary in $configuredResources) { Write-Output '' Write-Output ("Resource `"{0}`" ({1}) configured for PAM rotation." -f $summary.RecordTitle, $summary.RecordUid) Write-Output (" PAM Configuration: {0}" -f $summary.ConfigUid) if (-not [string]::IsNullOrEmpty($summary.AdminUserUid)) { Write-Output (" Admin user linked: {0}" -f $summary.AdminUserUid) } if ($summary.RotationEnabled -eq $true) { Write-Output ' Rotation: Enabled' } elseif ($summary.RotationEnabled -eq $false) { Write-Output ' Rotation: Disabled' } } if ($requests.Count -eq 0) { return } if ($skipped.Count -gt 0 -and -not [bool]$Options.Force) { if (-not (confirmPamYesNo -Prompt 'Do you want to cancel password rotation?' -DefaultYes $true)) { return } } if ($valid.Count -gt 0) { Write-Output '' Write-Output 'The following record(s) will be updated:' Write-Output ("{0,-22} {1,-24} {2,-8} {3,-22} {4,-22} {5,-12} {6}" -f ` 'Record UID', 'Record Title', 'Enabled', 'Configuration UID', 'Resource UID', 'Schedule', 'Complexity') foreach ($row in $valid) { $enabled = if ($row[2]) { 'X' } else { '-' } Write-Output ("{0,-22} {1,-24} {2,-8} {3,-22} {4,-22} {5,-12} {6}" -f ` $row[0], $row[1], $enabled, $row[3], $row[4], $row[5], $row[6]) } } if (-not [bool]$Options.Force) { if (-not (confirmPamYesNo -Prompt 'Do you want to update password rotation?' -DefaultYes $true)) { return } } $unsupportedRevision = $false $failures = New-Object System.Collections.Generic.List[string] foreach ($request in $requests) { $recordUid = [KeeperSecurity.Utils.CryptoUtils]::Base64UrlEncode($request.RecordUid.ToByteArray()) try { [KeeperSecurity.Plugins.PAM.RouterUtils]::SetRecordRotationAsync($Auth, $request).GetAwaiter().GetResult() | Out-Null } catch { $raw = [string]$_.Exception.Message if ([KeeperSecurity.Plugins.PAM.PamVaultHelpers]::IsUnsupportedRotationRevisionError($raw) ` -and [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::IsKeeperNSFRecord($Vault, $recordUid)) { $unsupportedRevision = $true } else { [void]$failures.Add(("Record `"{0}`": Set rotation error: {1}" -f $recordUid, $raw)) } } } $Vault.SyncDown().GetAwaiter().GetResult() | Out-Null if ($unsupportedRevision) { Write-Warning 'Rotation was not updated because this feature is not supported in production yet. Coming soon.' } if ($failures.Count -gt 0) { Write-Error -Message ([string]::Join([Environment]::NewLine, $failures)) -ErrorAction Stop } } function script:resolvePamRecordEditContext { Param ( [hashtable] $Options, [KeeperSecurity.Vault.TypedRecord] $Record ) $profile = $null if (-not [string]::IsNullOrWhiteSpace([string]$Options.RotationProfile)) { $profile = ([string]$Options.RotationProfile).Trim().ToLowerInvariant() } elseif (-not [string]::IsNullOrWhiteSpace([string]$Options.IamAadConfig)) { $profile = 'iam_user' } $noop = ($profile -eq 'scripts_only') -or (testPamNoopRecord -Record $Record) if ($profile -eq 'saas') { if ([string]::IsNullOrWhiteSpace([string]$Options.SaasConfigUid)) { Write-Error -Message 'SaaS rotation profile requires --saas-config-uid to be specified.' -ErrorAction Stop } $noop = $true } return @{ Profile = $profile; Noop = $noop } } function script:resolvePamResourceUidForDag { Param ( [KeeperSecurity.Vault.VaultOnline] $Vault, [KeeperSecurity.Vault.TypedRecord] $Record, [hashtable] $Options, [KeeperSecurity.Vault.TypedRecord] $ConfigRecord, [System.Collections.Generic.Dictionary[string, KeeperSecurity.Vault.TypedRecord]] $PamConfigs ) if (-not [string]::IsNullOrWhiteSpace([string]$Options.Resource)) { $resource = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, [string]$Options.Resource, $null) if ($null -eq $resource) { Write-Error -Message ("Resource '{0}' not found" -f $Options.Resource) -ErrorAction Stop } return $resource.Uid } $cached = $Vault.GetRecordRotation($Record.Uid) if ($null -ne $cached -and -not [string]::IsNullOrEmpty($cached.ResourceUid)) { $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $cached.ConfigurationUid } if (-not [string]::Equals($cached.ResourceUid, $configUid, [System.StringComparison]::Ordinal)) { return $cached.ResourceUid } } $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $cached.ConfigurationUid } if (-not [string]::IsNullOrEmpty($configUid) -and $PamConfigs.ContainsKey($configUid)) { return (getPamDefaultResourceUidFromConfig -Config $PamConfigs[$configUid]) } return $null } function script:invokePamConfigureResourceRecord { Param ( [KeeperSecurity.Authentication.IAuthentication] $Auth, [KeeperSecurity.Vault.VaultOnline] $Vault, [KeeperSecurity.Vault.TypedRecord] $Record, [hashtable] $Options, [System.Collections.Generic.Dictionary[string, KeeperSecurity.Vault.TypedRecord]] $PamConfigs, [KeeperSecurity.Vault.TypedRecord] $ConfigRecord, [System.Collections.Generic.List[object]] $Skipped, [System.Collections.Generic.List[hashtable]] $ConfiguredResources ) $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $null } if ([string]::IsNullOrEmpty($configUid) -and -not [string]::IsNullOrWhiteSpace([string]$Options.Config)) { $resolved = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord( $Vault, [string]$Options.Config, [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration) $configUid = $resolved.Uid } if ([string]::IsNullOrEmpty($configUid)) { [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'No PAM Configuration', 'Specify a configuration UID parameter [--config]')) return } if (-not $PamConfigs.ContainsKey($configUid)) { [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'PAM Configuration is invalid', 'Specify a configuration UID parameter [--config]')) return } [KeeperSecurity.Plugins.PAM.PamRotationGraphEdit]::ConfigureResourceAsync( $Auth, $Vault, $Record, $configUid, [string]$Options.AdminUser, [bool]$Options.Enable, [bool]$Options.Disable ).GetAwaiter().GetResult() | Out-Null $adminUserUid = $null if (-not [string]::IsNullOrWhiteSpace([string]$Options.AdminUser)) { $adminUser = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord( $Vault, ([string]$Options.AdminUser).Trim(), @('pamUser')) $adminUserUid = $adminUser.Uid } $rotationEnabled = [KeeperSecurity.Plugins.PAM.RotationUtils]::ResolveRotationEnabled( [bool]$Options.Enable, [bool]$Options.Disable) [void]$ConfiguredResources.Add(@{ RecordUid = $Record.Uid RecordTitle = $Record.Title ConfigUid = $configUid AdminUserUid = $adminUserUid RotationEnabled = $rotationEnabled }) } function script:tryBuildPamUserRotationRequest { Param ( [KeeperSecurity.Vault.VaultOnline] $Vault, [KeeperSecurity.Vault.TypedRecord] $Record, [hashtable] $Options, [KeeperSecurity.Vault.TypedRecord] $ConfigRecord, [System.Collections.Generic.Dictionary[string, KeeperSecurity.Vault.TypedRecord]] $PamConfigs, $ScheduleData, $ComplexityRules, [hashtable] $EditContext, [System.Collections.Generic.List[object]] $Skipped, [System.Collections.Generic.List[object]] $Valid, [ref] $Request ) $Request.Value = $null $cached = $Vault.GetRecordRotation($Record.Uid) $profile = $EditContext.Profile $noop = [bool]$EditContext.Noop if ([bool]$Options.ScheduleOnly) { if (-not [string]::IsNullOrWhiteSpace([string]$Options.Folder) -and ($null -eq $cached -or $cached.Disabled)) { [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'Rotation not enabled', 'Skipped')) return $false } if ($null -eq $cached) { [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'No rotation info', 'Skipped')) return $false } } $configUid = if ($null -ne $ConfigRecord) { $ConfigRecord.Uid } else { $null } if ([string]::IsNullOrEmpty($configUid) -and -not [string]::IsNullOrWhiteSpace([string]$Options.IamAadConfig)) { $configUid = ([string]$Options.IamAadConfig).Trim() } if ([string]::IsNullOrEmpty($configUid) -and $profile -eq 'iam_user') { if (-not [string]::IsNullOrWhiteSpace([string]$Options.Config)) { $configUid = ([string]$Options.Config).Trim() } else { $configUid = $cached.ConfigurationUid } } if ([string]::IsNullOrEmpty($configUid) -and $null -ne $cached -and -not [string]::IsNullOrEmpty($cached.ConfigurationUid)) { $configUid = $cached.ConfigurationUid } if ([string]::IsNullOrEmpty($configUid)) { [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'No current PAM Configuration', 'Specify a configuration UID parameter [--config]')) return $false } if (-not $PamConfigs.ContainsKey($configUid)) { [void]$Skipped.Add(@($Record.Uid, $Record.Title, 'PAM Configuration is invalid', 'Specify a configuration UID parameter [--config]')) return $false } $pamConfig = $PamConfigs[$configUid] $recordSchedule = $null if ($null -ne $ScheduleData) { $recordSchedule = toPamScheduleObjectList $ScheduleData } elseif ($null -ne $cached -and -not [bool]$Options.ScheduleConfig -and -not [string]::IsNullOrEmpty($cached.Schedule)) { try { $recordSchedule = toPamScheduleObjectList ( [KeeperSecurity.Plugins.PAM.RotationUtils]::ParseScheduleJsonString($cached.Schedule)) } catch { $recordSchedule = New-Object 'System.Collections.Generic.List[object]' } } elseif ([bool]$Options.ScheduleConfig) { # Missing/empty defaultRotationSchedule => On-Demand (Commander/Python parity). $fromConfig = getPamDefaultScheduleFromConfig -Config $pamConfig $recordSchedule = if ($null -eq $fromConfig) { New-Object 'System.Collections.Generic.List[object]' } else { toPamScheduleObjectList $fromConfig } } $pwdComplexity = [byte[]]@() if ($null -ne $ComplexityRules) { if ([KeeperSecurity.Plugins.PAM.RotationUtils]::IsClearedPasswordComplexity($ComplexityRules)) { $pwdComplexity = [byte[]]@() } else { $pwdComplexity = [KeeperSecurity.Plugins.PAM.RotationUtils]::EncryptPasswordComplexity($ComplexityRules, $Record.RecordKey) } } elseif ($null -ne $cached -and $null -ne $cached.PasswordComplexity) { $pwdComplexity = $cached.PasswordComplexity } $disabled = if ($null -ne $cached) { [bool]$cached.Disabled } else { $false } if ([bool]$Options.Enable) { $disabled = $false } elseif ([bool]$Options.Disable) { $disabled = $true } $resourceUid = $null if ($profile -eq 'iam_user' -or -not [string]::IsNullOrWhiteSpace([string]$Options.IamAadConfig)) { $resourceUid = $null $noop = $false } elseif ($profile -eq 'saas') { $resourceUid = $null $noop = $true } elseif ($noop) { $resourceUid = $null } elseif (-not [string]::IsNullOrWhiteSpace([string]$Options.Resource)) { $resource = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord($Vault, [string]$Options.Resource, $null) if ($null -eq $resource) { Write-Error -Message ("Resource '{0}' not found" -f $Options.Resource) -ErrorAction Stop } $resourceUid = $resource.Uid } elseif ($profile -eq 'general' -and [string]::IsNullOrWhiteSpace([string]$Options.Resource)) { Write-Error -Message 'General rotation profile requires --resource to be specified.' -ErrorAction Stop } elseif ($null -ne $cached -and -not [string]::IsNullOrEmpty($cached.ResourceUid) -and -not [string]::Equals($cached.ResourceUid, $configUid, [System.StringComparison]::Ordinal)) { $resourceUid = $cached.ResourceUid } elseif (-not [bool]$Options.ScheduleOnly) { $resourceUid = getPamDefaultResourceUidFromConfig -Config $pamConfig if ([string]::IsNullOrEmpty($resourceUid) -and -not $noop) { Write-Error -Message ( ("Record `"{0}`" is not associated with any resource. Please use Set-KeeperPamRotation -Record RECORD -Resource RESOURCE to associate it." -f $Record.Uid) ) -ErrorAction Stop } } if (-not [string]::IsNullOrEmpty($resourceUid) -and [string]::Equals($resourceUid, $configUid, [System.StringComparison]::Ordinal)) { $resourceUid = $null } $recordScheduleList = toPamScheduleObjectList $recordSchedule $scheduleType = [KeeperSecurity.Plugins.PAM.RotationUtils]::GetScheduleType($recordScheduleList) $complexityDisplay = '' $decoded = $null if (-not [KeeperSecurity.Plugins.PAM.RotationUtils]::TryDecryptPasswordComplexity($pwdComplexity, $Record.RecordKey, [ref]$decoded)) { $complexityDisplay = '[decrypt failed]' } else { $complexityDisplay = [KeeperSecurity.Plugins.PAM.RotationUtils]::FormatPasswordComplexityDisplay($decoded) } [void]$Valid.Add(@( $Record.Uid, $Record.Title, (-not $disabled), $configUid, $(if ($null -eq $resourceUid) { '' } else { $resourceUid }), $scheduleType, $complexityDisplay )) $rq = New-Object Router.RouterRecordRotationRequest $rq.RecordUid = [Google.Protobuf.ByteString]::CopyFrom((toPamUidBytes -Uid $Record.Uid)) $rq.ConfigurationUid = [Google.Protobuf.ByteString]::CopyFrom((toPamUidBytes -Uid $configUid)) $rq.Schedule = [KeeperSecurity.Plugins.PAM.RotationUtils]::BuildSchedulePayload( [string]$Options.ScheduleJson, [bool]$Options.OnDemand, $recordScheduleList) $rq.PwdComplexity = [Google.Protobuf.ByteString]::CopyFrom($pwdComplexity) $rq.Disabled = $disabled $rq.Noop = $noop $rq.Revision = [long]$Vault.ResolveRecordRotationRevisionAsync($Record.Uid).GetAwaiter().GetResult() if (-not $noop -and -not [string]::IsNullOrEmpty($resourceUid)) { $rq.ResourceUid = [Google.Protobuf.ByteString]::CopyFrom((toPamUidBytes -Uid $resourceUid)) } if ($profile -eq 'saas' -and -not [string]::IsNullOrWhiteSpace([string]$Options.SaasConfigUid)) { $rq.SaasConfiguration = [Google.Protobuf.ByteString]::CopyFrom( (toPamUidBytes -Uid (([string]$Options.SaasConfigUid).Trim()))) } $Request.Value = $rq return $true } # --- PAM config helpers --- function script:getPamEnterpriseAuth { $enterprise = getEnterprise if (-not $enterprise -or -not $enterprise.loader -or -not $enterprise.loader.Auth) { Write-Error -Message 'Enterprise authentication is not available.' -ErrorAction Stop } return $enterprise.loader.Auth } function script:getPamVault { $vault = getVault if (-not $vault) { Write-Error -Message 'Vault is not available.' -ErrorAction Stop } return $vault } function script:resolvePamGatewayController { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Plugins.PAM.IPamPlugin] $Plugin, [Parameter(Mandatory = $true)] [string] $Identifier, [KeeperSecurity.Vault.VaultOnline] $Vault = $null ) $trimmed = $Identifier.Trim() if ([string]::IsNullOrWhiteSpace($trimmed)) { return $null } # Help catch sheet mistakes where SharedFolder UID is passed as -Gateway. if ($null -ne $Vault) { [KeeperSecurity.Vault.SharedFolder]$sf = $null if ($Vault.TryGetSharedFolder($trimmed, [ref]$sf) -and $null -ne $sf) { Write-Host ("Gateway `"$trimmed`" matches shared folder `"$($sf.Name)`". " + 'Use a gateway/controller UID or name, not the shared-folder UID.') return $null } } $controllers = getPamControllerList -Plugin $Plugin if ($controllers -is [object[]] -and $controllers.Length -eq 1 -and $controllers[0] -is [System.Collections.Generic.List[KeeperSecurity.Plugins.PAM.PamController]]) { $controllers = $controllers[0] } if ($null -eq $controllers) { $controllers = New-Object 'System.Collections.Generic.List[KeeperSecurity.Plugins.PAM.PamController]' } $controllerCount = 0 try { $controllerCount = [int]$controllers.Count } catch { $controllerCount = 0 } if ($controllerCount -eq 0) { # Controllers empty after failed sync — retry once before giving up. [void](syncPamPlugin -Plugin $Plugin -Reload $true -ThrowOnError $false) $controllers = getPamControllerList -Plugin $Plugin if ($controllers -is [object[]] -and $controllers.Length -eq 1 -and $controllers[0] -is [System.Collections.Generic.List[KeeperSecurity.Plugins.PAM.PamController]]) { $controllers = $controllers[0] } } $controller = [KeeperSecurity.Plugins.PAM.GatewayUtils]::FindGateway($controllers, $trimmed) if ($controller -is [KeeperSecurity.Plugins.PAM.PamController]) { return $controller } $nameMatches = 0 $nameMatch = $null foreach ($item in $controllers) { if ($item -is [KeeperSecurity.Plugins.PAM.PamController] -and [string]::Equals($item.ControllerName, $trimmed, [System.StringComparison]::OrdinalIgnoreCase)) { $nameMatches++ $nameMatch = $item } } if ($nameMatches -gt 1) { throw (New-Object KeeperSecurity.Plugins.PAM.PamGatewayAmbiguousException($trimmed)) } if ($nameMatches -eq 1) { return $nameMatch } return $null } function script:resolvePamConfigurationFolderUid { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [string] $Identifier ) if ([string]::IsNullOrWhiteSpace($Identifier)) { return $null } $trimmed = $Identifier.Trim() return [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolvePamConfigurationFolderUid($Vault, $trimmed) } function script:writePamComingSoonMessage { Param ( [string] $Environment ) if ([string]::IsNullOrWhiteSpace($Environment)) { return $false } $displayName = $null try { if ([KeeperSecurity.Plugins.PAM.PamConfigTypes]::IsComingSoonEnvironment($Environment.Trim(), [ref]$displayName)) { if ([string]::IsNullOrWhiteSpace($displayName)) { $displayName = $Environment.Trim() } Write-Host "Environment $displayName is not supported yet. It will be supported in a future release." return $true } } catch { Write-Debug "IsComingSoonEnvironment check failed: $($_.Exception.Message)" } return $false } # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB7HluF4M2HdC3U # OEL9n/chfFelEizTPgqki0o55/3+hqCCITswggWNMIIEdaADAgECAhAOmxiO+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 # twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg # MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit # eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS # 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM # swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC # Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3 # /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j # q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5 # OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo # 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU # tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm # KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP # TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq # hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK # r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda # qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+ # lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a # brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS # y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK # iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb # KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q # xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm # zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn # HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w # gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1 # c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo # dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi # 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg # xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF # cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ # m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS # GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1 # ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9 # MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7 # Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG # RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6 # X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd # BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx # XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF # BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln # aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy # bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL # BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj # aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0 # hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0 # F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT # mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf # ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE # wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh # OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX # gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO # LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG # WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg # AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 # IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex # MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx # FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy # NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI # hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3 # zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch # TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj # FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo # yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP # KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS # uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w # JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW # doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg # rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K # 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf # gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy # Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL # TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG # AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j # b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy # dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j # cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB # CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ # D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/ # ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu # +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o # bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h # ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn # M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol # /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY # xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc # CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB # ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx # oAMCAQICEAHdzU+FVN9jCMv0HhHagNUwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNjA2MDUwMDAwMDBaFw0yNzA2MDQyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCb4DRTV0sNQsa1 # 0YRh+bliabmLOVYr6S0+BSVvRJAN3SHP6x52i1Dkpki5xVDIH06ZnnsToVrgvTv+ # QxGwsn9SAPHEZ/PIJRFxbMR4ShDaptYyL4f0u4k/3HwRzIleWE4mTUonYH8BdgLw # /F53B7wa7VTDHtxXltYTibEOwJxYCOi4Zr2FYQhjw14/CHcqS3FSMs6YYU2T56+g # w819hQM3K0YlwTNOFoIm1v7/ZZZiJGH8uGDsvy1makh1Xyyo/wN8EbQ1nbslmePT # roPm9w7WqiP/yiq+CZHiuTk9JK5bEgkWG3ns+v25cI251WidJx3SU7IZnX0OTd6/ # ZdKhprD5Gcfy5GBbJdcYw2WycQRW0PT5BEt55xRE0heufkpDaTUN6RdOuJdXbkl0 # hV91IZIuhueEMCk3h5mDTlU5gImxqj0R/TbAxjSSGTKCeuYFkQIRqytSabdrZZ48 # kW5hOIZMVDY1f4kpPJa8UeEvDZXT3vrtj36aSJrwez2uh4FMNlkCAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT1 # SmCYU/7Yrz1fX66Ur5nSzlSYOzA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQBcavcUHNFEg872HDRq2+hRlnvaghCXv7X/6h9HSzjAQP3rt95BZty3ASqi # 2MYyGQLGdDl4DToe/WhajtEOBOYa83agW6tBvrfcKRrDrwJOMPTbwNYvn+GuiL4T # CKzXaytWiJJbrc5odc7Ecat2ZvJylpPmNainr4Q0LzzH23Gea/Mm/hIJTN4IGgrH # hrXiTIIW/ZUzrY6g8b3RZB4BA497n43wNdSqP+C3ntFw6NiGB4Z25SW4YntIxYPv # Kf37OVhF0xqxLC1sK/XxgK0EGQ6iaj8Ncpr2C5vSNZqfW2MndxOA1W67pgDpg83k # UWG+/YJeGhqOTF82/0kIzQXeI/lIqbnL/IJAJqSm/ROSpsGUKVbzk03cpTD55ZQX # WjM0fLirypBqY05T8gnh1L0fSwxr/SwJZ8OddivgyK1YOMn02nnsEG5kxBt9cMX4 # JCYABhypmAVDRvyYifEVdoFWv2gAXXW+PPRvlNa6E4aMCZrVcoKHiyeMAXOi1IC9 # mHvC2+foTSMFueq3AdnYfeKnZnAiKXKRhXcdHbQYcR2A7AIzIcqahPYr4FNEgb/E # /y/kypAkf0rMHlYl1kNqLs2Nv1UnMEHYT5YmDVLO63+1Trcw4zTZ70zuqIqeID/d # nbOlgtyG6DSRCL7f0E7kP18f4RoX5i1PkfeO4VJHsAuCeNG1qjGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQAd3NT4VU32MIy/QeEdqA1TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCCr3uxjZBPWG9whRrDlFb7ZRVD/H67Jh3AVLVkYN86MlTANBgkqhkiG9w0B # AQEFAASCAYA/YVon+iWNfGKDqDKKWLA71jqotmbbtrmWTjvBw24NEiUHEvMrT685 # CmPhsBjQaVhS7YOxIyUB4zDYxg1jZJYk5UKB2WH2U+LtLobNTk79ja6RqKAEeYcY # BNZJjp9JtOp45PUJQirbAH1ep/h/sx0KZOUq0tA7jftydLr+c/8vNG5YlKDF+nuT # JWvwaB4PyRbbI5K+AWeA73eqOxICG1WG5n66clYAM0bPaH/Heq5SH+vsX4b112sQ # 7DY9+OBCtfMg3fnYDZs+pW9rwFR2kRuXuepkUhyit3QEEX2qI6jby7+1sq6lKJUi # Up3M6oP+tVuzY6bnwcRK4Dmi8C/lpZV8QfcuGZRJHCs2ON6s0bNE7Thb0pGEHT+i # HGNdwmBeE/EhWda+T0CfnB/sMGhqVuKpQ++4NVAIjsf3nbiEeug7dQE1BXxi4V8s # De+9nG5hTHUwFnmcCkZtlrMJGF+YErwGceJoFS2ySrcdKqq9yK9v3FkGHerp0wN+ # aJQBq/xf6WmhggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTIyWjAv # BgkqhkiG9w0BCQQxIgQgWr9Qb/ZeEpm2N8QzjqnW/pRJZ1xiRFWH+KLYDjnjyQQw # DQYJKoZIhvcNAQEBBQAEggIAwbDvsOG4OhVnR3haVUy0GtchfwGUjSbmlpGqiZE+ # YnkHlyCJ3jqwt/3rpYxoiCG/Cn6+P97oQ01zgMLl6xQfsTIw8fE77Qc/dZOTC90X # 6wtiEAan6/4hehf/ZBluHpF8UgcAtdVyTJYJyjVXqbUeRwEf4dtRzkbkmXHybAnR # pe2gAtt8zP7k+Oud+X/5gPwqZt3HXsoTn91sKozmwWBRRV6RRbOQUVGdcmk3xqFq # jfaMLsNPd99fr7Ku1yKSc78+nKHYLaDD9m6uCi9SRsk00coWETqstTjFlbkTYuT8 # YhtjiF0ine2Bm4CGvybTZ/nKW9Ce+ePcUoGF/d1rWSsGD+Vjs7/elkEoO3DrmZzt # tWY+OHBvLMcvU5XVKBrhI2hago9eMsOaGj+zHvSO1PeZKuwR77tfmL4xlvYO7T46 # fnkK7+2lXaCSQXyUHDbYQvE1WKu1mnqkj58nYxmsJHFU0x+WR7GFNp1Rl8VpXomE # zkDvOMmjgqZim3JlwWCUgiy5gXxwZ/IEVR6qNGKUv4KCOBiepDL7tjyIfnBAO+C2 # t2hBphIysaFANCqahifw7Fr4Q6JHk9pN4oBkfRS2UAWnCcnA1gZ7YDcUdRqbUaud # UJyM/mekkNXGu4MoHzYMD/UMdwvubHJpx0EtqqTQOIgfflXR/AoXPWqRqSpBlOp6 # HDg= # SIG # End signature block |