PAM/Config.ps1
|
#requires -Version 5.1 function script:toStringList { Param ($Value) if ($null -eq $Value) { return $null } $list = New-Object 'System.Collections.Generic.List[string]' foreach ($item in @($Value)) { if ($null -ne $item -and -not [string]::IsNullOrWhiteSpace([string]$item)) { [void]$list.Add([string]$item) } } if ($list.Count -eq 0) { return $null } return $list } function script:preparePamConfigValuesFromBoundParameters { Param ( [hashtable] $BoundParameters, [string[]] $ExcludeKeys = @() ) $values = @{} $skip = @( 'Verbose', 'Debug', 'ErrorAction', 'WarningAction', 'InformationAction', 'ErrorVariable', 'WarningVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'WhatIf', 'Confirm' ) + @($ExcludeKeys) foreach ($key in $BoundParameters.Keys) { if ($key -in $skip) { continue } $values[$key] = $BoundParameters[$key] } if ($BoundParameters.ContainsKey('PortMapping')) { $values['PortMapping'] = (toStringList $BoundParameters['PortMapping']) } if ($BoundParameters.ContainsKey('RegionNames')) { $values['RegionNames'] = (toStringList $BoundParameters['RegionNames']) } if ($BoundParameters.ContainsKey('ResourceGroups')) { $values['ResourceGroups'] = (toStringList $BoundParameters['ResourceGroups']) } if ($BoundParameters.ContainsKey('GcpRegionNames')) { $values['GcpRegionNames'] = (toStringList $BoundParameters['GcpRegionNames']) } if ($BoundParameters.ContainsKey('RemoveResourceRecords')) { $values['RemoveResourceRecords'] = (toStringList $BoundParameters['RemoveResourceRecords']) } if ($BoundParameters.ContainsKey('ForceDomainAdmin') -and $BoundParameters['ForceDomainAdmin']) { $values['ForceDomainAdmin'] = $true } return $values } function script:setPamTypedFieldValue { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [Parameter(Mandatory = $true)] [string] $FieldType, [string] $FieldLabel, [AllowEmptyString()] [object] $Value ) $rtf = New-Object KeeperSecurity.Vault.RecordTypeField($FieldType, $FieldLabel) [KeeperSecurity.Vault.ITypedField]$field = $null if (-not [KeeperSecurity.Vault.VaultDataExtensions]::FindTypedField($Record, $rtf, [ref]$field)) { if ($null -eq $Value -or [string]::IsNullOrEmpty([string]$Value)) { return } $field = [KeeperSecurity.Vault.VaultDataExtensions]::CreateTypedField($FieldType, $FieldLabel) if ($field) { $Record.Fields.Add($field) } } if (-not $field) { return } if ($null -eq $Value -or [string]::IsNullOrEmpty([string]$Value)) { while ($field.Count -gt 0) { $field.DeleteValueAt(0) } return } $field.ObjectValue = $Value } function script:setPamCheckboxField { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [Parameter(Mandatory = $true)] [string] $Label, [string] $TriBool ) if ([string]::IsNullOrWhiteSpace($TriBool)) { return } $normalized = $TriBool.Trim().ToLowerInvariant() if ($normalized -notin @('true', 'false')) { return } setPamTypedFieldValue -Record $Record -FieldType 'checkbox' -FieldLabel $Label -Value ($normalized -eq 'true') } function script:getPamHostnameField { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record ) $rtf = New-Object KeeperSecurity.Vault.RecordTypeField('pamHostname', $null) [KeeperSecurity.Vault.ITypedField]$field = $null if ([KeeperSecurity.Vault.VaultDataExtensions]::FindTypedField($Record, $rtf, [ref]$field) -and $field.Count -gt 0 -and $field.GetValueAt(0) -is [KeeperSecurity.Vault.FieldTypeHost]) { return [KeeperSecurity.Vault.FieldTypeHost]$field.GetValueAt(0) } return $null } function script:setPamHostnameField { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [string] $HostName, [string] $Port ) $hostObj = New-Object KeeperSecurity.Vault.FieldTypeHost $hostObj.HostName = if ($null -eq $HostName) { '' } else { $HostName } $hostObj.Port = if ($null -eq $Port) { '' } else { $Port } setPamTypedFieldValue -Record $Record -FieldType 'pamHostname' -FieldLabel $null -Value $hostObj } function script:applyPamConfigSchedule { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [string] $DefaultSchedule, [bool] $IsEdit ) if ($IsEdit -and [string]::IsNullOrWhiteSpace($DefaultSchedule)) { return } try { $schedule = New-Object KeeperSecurity.Vault.FieldSchedule $cron = if ($null -eq $DefaultSchedule) { '' } else { $DefaultSchedule.Trim() } if ([string]::IsNullOrEmpty($cron) -or [string]::Equals($cron, 'On-Demand', [StringComparison]::OrdinalIgnoreCase) -or [string]::Equals($cron, 'ON_DEMAND', [StringComparison]::OrdinalIgnoreCase)) { $schedule.Type = 'ON_DEMAND' } else { # Rotation CRON: 6 fields, e.g. "0 0 3 * * ?" $validation = [KeeperSecurity.Plugins.PAM.RotationUtils]::ValidateCronExpression($cron, $true) if (-not $validation.Item1) { throw "Invalid CRON `"$cron`" Error: $($validation.Item2)" } $schedule.Type = 'CRON' $schedule.Cron = $cron $schedule.TimeZone = 'Etc/UTC' } setPamTypedFieldValue -Record $Record -FieldType 'schedule' -FieldLabel 'defaultRotationSchedule' -Value $schedule } catch { if ($_.Exception.Message -match 'Invalid CRON') { throw } Write-Warning "Could not set default rotation schedule: $($_.Exception.Message). Ensure PAM record types are synced (Sync-Keeper)." } } function script:applyPamConfigEnvironmentFields { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [Parameter(Mandatory = $true)] [hashtable] $Values, [bool] $IsEdit ) $supplied = { param([string]$key) if ($IsEdit) { return $Values.ContainsKey($key) } return $Values.ContainsKey($key) -and -not [string]::IsNullOrWhiteSpace([string]$Values[$key]) } switch ($Record.TypeName) { 'pamNetworkConfiguration' { if (& $supplied 'NetworkId') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'networkId' -Value ([string]$Values['NetworkId']) } if (& $supplied 'NetworkCidr') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'networkCIDR' -Value ([string]$Values['NetworkCidr']) } } 'pamAwsConfiguration' { if (& $supplied 'AwsId') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'awsId' -Value ([string]$Values['AwsId']) } if (& $supplied 'AccessKeyId') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'accessKeyId' -Value ([string]$Values['AccessKeyId']) } if (& $supplied 'AccessSecretKey') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'accessSecretKey' -Value ([string]$Values['AccessSecretKey']) } if ($Values.ContainsKey('RegionNames') -and $Values['RegionNames']) { setPamTypedFieldValue -Record $Record -FieldType 'multiline' -FieldLabel 'regionNames' -Value (($Values['RegionNames'] -join "`n")) } } 'pamAzureConfiguration' { if (& $supplied 'AzureId') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'azureId' -Value ([string]$Values['AzureId']) } if (& $supplied 'ClientId') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'clientId' -Value ([string]$Values['ClientId']) } if (& $supplied 'ClientSecret') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'clientSecret' -Value ([string]$Values['ClientSecret']) } if (& $supplied 'SubscriptionId') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'subscriptionId' -Value ([string]$Values['SubscriptionId']) } if (& $supplied 'TenantId') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'tenantId' -Value ([string]$Values['TenantId']) } if ($Values.ContainsKey('ResourceGroups') -and $Values['ResourceGroups']) { setPamTypedFieldValue -Record $Record -FieldType 'multiline' -FieldLabel 'resourceGroups' -Value (($Values['ResourceGroups'] -join "`n")) } } 'pamGcpConfiguration' { if (& $supplied 'GcpId') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'pamGcpId' -Value ([string]$Values['GcpId']) } if (& $supplied 'ServiceAccountKey') { $sak = readPamMaybeFileContent -Value ([string]$Values['ServiceAccountKey']) try { setPamTypedFieldValue -Record $Record -FieldType 'json' -FieldLabel 'pamServiceAccountKey' -Value $sak } catch { Write-Warning "Could not set GCP service-account key field: $($_.Exception.Message). Ensure PAM record types are synced (Sync-Keeper)." } } if (& $supplied 'GoogleAdminEmail') { setPamTypedFieldValue -Record $Record -FieldType 'email' -FieldLabel 'pamGoogleAdminEmail' -Value ([string]$Values['GoogleAdminEmail']) } if ($Values.ContainsKey('GcpRegionNames') -and $Values['GcpRegionNames']) { setPamTypedFieldValue -Record $Record -FieldType 'multiline' -FieldLabel 'pamGcpRegionName' -Value (($Values['GcpRegionNames'] -join "`n")) } } 'pamDomainConfiguration' { if (& $supplied 'DomainId') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'pamDomainId' -Value ([string]$Values['DomainId']) } if ($IsEdit) { if ($Values.ContainsKey('DomainHostname') -or $Values.ContainsKey('DomainPort')) { $existing = getPamHostnameField -Record $Record $hostName = if ($Values.ContainsKey('DomainHostname')) { [string]$Values['DomainHostname'] } else { if ($existing) { $existing.HostName } else { '' } } $port = if ($Values.ContainsKey('DomainPort')) { [string]$Values['DomainPort'] } else { if ($existing) { $existing.Port } else { '' } } setPamHostnameField -Record $Record -HostName $hostName -Port $port } } else { $hostName = if ($Values.ContainsKey('DomainHostname')) { [string]$Values['DomainHostname'] } else { '' } $port = if ($Values.ContainsKey('DomainPort')) { [string]$Values['DomainPort'] } else { '' } if (-not [string]::IsNullOrWhiteSpace($hostName) -or -not [string]::IsNullOrWhiteSpace($port)) { setPamHostnameField -Record $Record -HostName $hostName -Port $port } } if ($Values.ContainsKey('DomainUseSsl')) { setPamCheckboxField -Record $Record -Label 'useSSL' -TriBool ([string]$Values['DomainUseSsl']) } if ($Values.ContainsKey('DomainScanDcCidr')) { setPamCheckboxField -Record $Record -Label 'scanDCCIDR' -TriBool ([string]$Values['DomainScanDcCidr']) } if (& $supplied 'DomainNetworkCidr') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'networkCIDR' -Value ([string]$Values['DomainNetworkCidr']) } if (& $supplied 'DomainUserMatch') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'userMatch' -Value ([string]$Values['DomainUserMatch']) } if ($Values.ContainsKey('DomainAdministrativeCredential') -and -not [string]::IsNullOrWhiteSpace([string]$Values['DomainAdministrativeCredential'])) { $dac = ([string]$Values['DomainAdministrativeCredential']).Trim() $force = [bool]$Values['ForceDomainAdmin'] if ($force) { if ($dac -notmatch '^[A-Za-z0-9\-_]{22}$') { Write-Host ("Warning: Invalid Domain Admin User UID: `"{0}`" (skipped)" -f (maskPamUid $dac)) } else { (New-Object KeeperSecurity.Plugins.PAM.PamConfigurationFacade($Record)).AdminCredentialRef = $dac } } else { $admin = $null try { [KeeperSecurity.Vault.TypedRecord]$byUid = $null if ($dac -match '^[A-Za-z0-9\-_]{22}$' -and [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::TryGetUserRecord($Vault, $dac, [ref]$byUid)) { $admin = $byUid } else { $admin = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord( $Vault, $dac, @('pamUser')) } if ($admin) { (New-Object KeeperSecurity.Plugins.PAM.PamConfigurationFacade($Record)).AdminCredentialRef = $admin.Uid } else { Write-Host ("Warning: Domain Admin User UID: `"{0}`" not found (skipped)." -f (maskPamUid $dac)) } } catch { Write-Host ("Warning: Domain Admin User UID: `"{0}`" not found (skipped)." -f (maskPamUid $dac)) Write-Debug "Domain admin resolve failed: $($_.Exception.Message)" } } } } 'pamOciConfiguration' { if (& $supplied 'OciId') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'pamOciId' -Value ([string]$Values['OciId']) } if (& $supplied 'OciAdminId') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'adminOcid' -Value ([string]$Values['OciAdminId']) } if (& $supplied 'OciAdminPublicKey') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'adminPublicKey' -Value (readPamMaybeFileContent -Value ([string]$Values['OciAdminPublicKey'])) } if (& $supplied 'OciAdminPrivateKey') { setPamTypedFieldValue -Record $Record -FieldType 'secret' -FieldLabel 'adminPrivateKey' -Value (readPamMaybeFileContent -Value ([string]$Values['OciAdminPrivateKey'])) } if (& $supplied 'OciTenancy') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'tenancyOci' -Value ([string]$Values['OciTenancy']) } if (& $supplied 'OciRegion') { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'regionOci' -Value ([string]$Values['OciRegion']) } } } if ($Values.ContainsKey('PortMapping') -and $Values['PortMapping']) { setPamTypedFieldValue -Record $Record -FieldType 'multiline' -FieldLabel 'portMapping' -Value (($Values['PortMapping'] -join "`n")) } if ($Values.ContainsKey('IdentityProvider') -and -not [string]::IsNullOrWhiteSpace([string]$Values['IdentityProvider'])) { setPamTypedFieldValue -Record $Record -FieldType 'text' -FieldLabel 'identityProviderUid' -Value ([string]$Values['IdentityProvider']) } } function script:applyPamConfigResources { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [KeeperSecurity.Plugins.PAM.IPamPlugin] $Plugin, [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [hashtable] $Values, [bool] $IsEdit ) $facade = New-Object KeeperSecurity.Plugins.PAM.PamConfigurationFacade($Record) if ($Values.ContainsKey('Gateway') -and -not [string]::IsNullOrWhiteSpace([string]$Values['Gateway'])) { $gateway = resolvePamGatewayController -Plugin $Plugin -Identifier ([string]$Values['Gateway']) -Vault $Vault if ($gateway -is [KeeperSecurity.Plugins.PAM.PamController]) { $facade.ControllerUid = if ($gateway.ControllerUid) { $gateway.ControllerUid } else { $gateway.Uid } } elseif (-not $IsEdit) { Write-Host "Warning: Gateway `"$($Values['Gateway'])`" not found." } } if ($Values.ContainsKey('SharedFolder') -and -not [string]::IsNullOrWhiteSpace([string]$Values['SharedFolder'])) { $folderUid = resolvePamConfigurationFolderUid -Vault $Vault -Identifier ([string]$Values['SharedFolder']) if (-not [string]::IsNullOrEmpty($folderUid)) { $resolved = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolvePamResourcesFolderUid($Vault, $folderUid) $facade.FolderUid = if ($resolved) { $resolved } else { $folderUid } } } elseif ($IsEdit -and [string]::IsNullOrEmpty($facade.FolderUid)) { throw 'Shared Folder not found' } if ($Values.ContainsKey('RemoveResourceRecords') -and $Values['RemoveResourceRecords']) { $toRemove = New-Object 'System.Collections.Generic.List[string]' foreach ($removeRef in @($Values['RemoveResourceRecords'])) { if ([string]::IsNullOrWhiteSpace([string]$removeRef)) { continue } $trimmed = ([string]$removeRef).Trim() try { $resolved = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord( $Vault, $trimmed, [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Rotation) if ($resolved) { [void]$toRemove.Add($resolved.Uid) } else { Write-Warning "Failed to find PAM record: $removeRef" } } catch { Write-Warning "Failed to find PAM record: $removeRef" Write-Debug "PAM resource resolve failed for remove-ref: $($_.Exception.Message)" } } if ($toRemove.Count -gt 0) { $facade.RemoveResourceRefs($toRemove) } } return $facade } function script:configurePamTunnelingIfNeeded { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Authentication.IAuthentication] $Auth, [Parameter(Mandatory = $true)] [string] $ConfigUid, [hashtable] $Values ) $keys = @( 'Connections', 'Tunneling', 'Rotation', 'ConnectionsRecording', 'TypescriptRecording', 'RemoteBrowserIsolation', 'AiThreatDetection', 'AiTerminateSessionOnDetection' ) $hasAny = $false foreach ($key in $keys) { if ($Values.ContainsKey($key) -and $null -ne $Values[$key]) { $hasAny = $true break } } if (-not $hasAny) { return } $get = { param($k) if ($Values.ContainsKey($k)) { return [string]$Values[$k] } return $null } [KeeperSecurity.Plugins.PAM.ConfigUtils]::ConfigureTunnelingAsync( $Auth, $ConfigUid, [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'Connections')), [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'Tunneling')), [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'Rotation')), [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'ConnectionsRecording')), [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'TypescriptRecording')), [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'RemoteBrowserIsolation')), [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'AiThreatDetection')), [KeeperSecurity.Plugins.PAM.ConfigUtils]::ParseTriState((& $get 'AiTerminateSessionOnDetection')) ).GetAwaiter().GetResult() | Out-Null } function script:movePamConfigToFolder { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Record, [Parameter(Mandatory = $true)] [string] $DestinationFolderUid ) $Vault.CacheKeeperRecord($Record) $sourceFolderUid = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecordSourceFolderUid($Vault, $Record.Uid) if ($null -eq $sourceFolderUid) { throw 'Cannot move PAM configuration: record is not initialized.' } if ([string]::Equals($sourceFolderUid, $DestinationFolderUid, [StringComparison]::Ordinal)) { return } $path = New-Object KeeperSecurity.Vault.RecordPath $path.RecordUid = $Record.Uid $path.FolderUid = $sourceFolderUid $Vault.MoveRecordToFolder($path, $DestinationFolderUid).GetAwaiter().GetResult() | Out-Null } function script:getPamConfigurationTypeSet { # Unary comma: keep HashSet as one object. try { $fromSdk = [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration if ($null -ne $fromSdk -and $fromSdk.Count -gt 0) { return , $fromSdk } } catch { Write-Debug "PAM configuration type set from SDK unavailable: $($_.Exception.Message)" } $fallback = [System.Collections.Generic.HashSet[string]]::new( [string[]]@( 'pamAwsConfiguration' 'pamAzureConfiguration' 'pamGcpConfiguration' 'pamDomainConfiguration' 'pamNetworkConfiguration' 'pamOciConfiguration' 'pamGitHubConfiguration' ), [StringComparer]::Ordinal) return , $fallback } function script:isPamConfigurationTypeName { Param ( [string] $TypeName, $AllowedTypes ) if ([string]::IsNullOrEmpty($TypeName) -or $null -eq $AllowedTypes) { return $false } try { return [bool]$AllowedTypes.Contains($TypeName) } catch { foreach ($allowed in @($AllowedTypes)) { if ($allowed -is [string] -and [string]::Equals($TypeName, $allowed, [StringComparison]::Ordinal)) { return $true } } return $false } } function script:resolvePamConfigSharedFolder { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [string] $ConfigUid ) [KeeperSecurity.Vault.KeeperRecord]$keeperRec = $null if ($Vault.TryGetKeeperRecord($ConfigUid, [ref]$keeperRec) -and ($keeperRec -is [KeeperSecurity.Vault.TypedRecord])) { try { $bySdk = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::GetConfigurationSharedFolder( $Vault, [KeeperSecurity.Vault.TypedRecord]$keeperRec) if ($null -ne $bySdk) { return $bySdk } } catch { Write-Debug "GetConfigurationSharedFolder failed: $($_.Exception.Message)" } } # Fallback when folder-tree lookup is empty. try { return [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::FindSharedFolderForRecord($Vault, $ConfigUid, $null) } catch { Write-Debug "FindSharedFolderForRecord failed: $($_.Exception.Message)" return $null } } function script:ensurePamRecordTypesSynced { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault ) try { [KeeperSecurity.Vault.SyncDownRestExtension]::EnsurePamRecordTypesAsync($Vault).GetAwaiter().GetResult() | Out-Null } catch { Write-Warning "Could not ensure PAM record types are synced: $($_.Exception.Message). Continuing; field schema may be incomplete." } } function script:resolvePamConfigurationRecord { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [string] $Identifier ) $trimmed = $Identifier.Trim() if ([string]::IsNullOrWhiteSpace($trimmed)) { return $null } try { $resolved = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolveRecord( $Vault, $trimmed, [KeeperSecurity.Plugins.PAM.PamRecordTypes]::Configuration) if ($null -ne $resolved) { return $resolved } } catch { if ($_.Exception.Message -match 'not unique') { throw "Configuration `"$trimmed`" is not unique. Use configuration UID." } Write-Debug "ResolveRecord for PAM configuration failed: $($_.Exception.Message)" } # Fallback if ResolveRecord returns null. UID list only — avoid List[TypedRecord]. $allowedTypes = getPamConfigurationTypeSet $titleMatchUids = New-Object 'System.Collections.Generic.List[string]' foreach ($record in $Vault.KeeperRecords) { if ($null -eq $record -or [string]::IsNullOrEmpty($record.Uid)) { continue } $tn = '' try { $tn = [string]$record.TypeName } catch { Write-Debug "Record TypeName read failed: $($_.Exception.Message)" } if (-not $tn) { $tn = [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordType($record) } if (-not (isPamConfigurationTypeName -TypeName $tn -AllowedTypes $allowedTypes)) { continue } $recUid = [string]$record.Uid if ([string]::Equals($recUid, $trimmed, [StringComparison]::Ordinal)) { [KeeperSecurity.Vault.KeeperRecord]$byUid = $null if ($Vault.TryGetKeeperRecord($recUid, [ref]$byUid)) { return ($byUid -as [KeeperSecurity.Vault.TypedRecord]) } return ($record -as [KeeperSecurity.Vault.TypedRecord]) } if ([string]::Equals([string]$record.Title, $trimmed, [StringComparison]::OrdinalIgnoreCase)) { [void]$titleMatchUids.Add($recUid) } } if ($titleMatchUids.Count -eq 1) { $matchUid = $null foreach ($u in $titleMatchUids) { $matchUid = [string]$u; break } if ($matchUid) { [KeeperSecurity.Vault.KeeperRecord]$byTitle = $null if ($Vault.TryGetKeeperRecord($matchUid, [ref]$byTitle)) { return ($byTitle -as [KeeperSecurity.Vault.TypedRecord]) } } } if ($titleMatchUids.Count -gt 1) { throw "Configuration `"$trimmed`" is not unique. Use configuration UID." } return $null } function script:maskPamUid { Param ([string] $Uid) if ([string]::IsNullOrWhiteSpace($Uid)) { return '****' } $trimmed = $Uid.Trim() if ($trimmed.Length -lt 8) { return '****' } return ($trimmed.Substring(0, 4) + '...' + $trimmed.Substring($trimmed.Length - 4)) } function script:testPamSecretFieldType { Param ( [string] $FieldName, [string] $FieldLabel ) $name = if ($FieldName) { $FieldName.ToLowerInvariant() } else { '' } $label = if ($FieldLabel) { $FieldLabel.ToLowerInvariant() } else { '' } $secretTypes = @('secret', 'password', 'hidden', 'privatekey', 'keypair', 'passkey') foreach ($t in $secretTypes) { if ($name -eq $t) { return $true } } # GCP service-account JSON and similar secrets if ($name -eq 'json') { return $true } if ($label -match 'secret|password|private|apikey|accesskey|clientsecret|serviceaccount') { return $true } return $false } function script:readPamMaybeFileContent { Param ( [string] $Value, [int] $MaxBytes = 2097152 ) if ([string]::IsNullOrWhiteSpace($Value)) { return $Value } $path = $Value.Trim() $exists = $false try { $exists = Test-Path -LiteralPath $path -PathType Leaf -ErrorAction Stop } catch { return $Value } if (-not $exists) { return $Value } try { $item = Get-Item -LiteralPath $path -ErrorAction Stop if ($item.Length -gt $MaxBytes) { $maxMb = [math]::Round($MaxBytes / 1MB, 2) throw "File `"$path`" exceeds the ${maxMb} MB limit." } return (Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction Stop) } catch { throw "Could not read file `"$path`": $($_.Exception.Message)" } } function script:getPamFieldDisplayName { Param ($Field) $type = if ($Field.FieldName) { [string]$Field.FieldName } else { '' } $label = if ($Field.FieldLabel) { [string]$Field.FieldLabel } else { '' } if ($type -eq 'schedule') { return 'Default Schedule' } if ($type -and $label) { return "($type).$label" } if ($type) { return "($type)" } return $label } function script:extractPamConfigDisplayFields { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.TypedRecord] $Config ) $fields = [ordered]@{} $all = @($Config.Fields) + @($Config.Custom) foreach ($field in $all) { if ($null -eq $field) { continue } $fname = [string]$field.FieldName if ($fname -eq 'pamResources' -or $fname -eq 'fileRef') { continue } if ($fname -eq 'schedule' -and [string]$field.FieldLabel -ne 'defaultRotationSchedule') { continue } $vals = New-Object 'System.Collections.Generic.List[string]' $flabel = [string]$field.FieldLabel if (testPamSecretFieldType -FieldName $fname -FieldLabel $flabel) { [void]$vals.Add('***') } elseif ($fname -eq 'schedule') { for ($i = 0; $i -lt $field.Count; $i++) { $sched = $field.GetValueAt($i) if ($null -eq $sched) { continue } $stype = '' try { $stype = [string]$sched.Type } catch { Write-Debug "Schedule type read failed: $($_.Exception.Message)" } if ($stype -eq 'CRON') { $cron = '' try { $cron = [string]$sched.Cron } catch { Write-Debug "Schedule cron read failed: $($_.Exception.Message)" } if (-not [string]::IsNullOrWhiteSpace($cron)) { [void]$vals.Add($cron.Trim()) } } } } else { foreach ($v in @([KeeperSecurity.Utils.RecordTypesUtils]::GetTypedFieldValues($field))) { if ($null -ne $v -and -not [string]::IsNullOrWhiteSpace([string]$v)) { [void]$vals.Add([string]$v) } } } if ($vals.Count -eq 0) { continue } $fields[(getPamFieldDisplayName $field)] = ($vals -join ', ') } return $fields } function script:buildPamConfigListRow { Param ( [Parameter(Mandatory = $true)] [KeeperSecurity.Vault.VaultOnline] $Vault, [Parameter(Mandatory = $true)] [string] $ConfigUid, [string] $FallbackTitle = '', [string] $FallbackTypeName = '', [bool] $VerboseOutput, [bool] $AsCommanderJson = $false, [bool] $IsDetail = $false ) if ([string]::IsNullOrEmpty($ConfigUid)) { return $null } [KeeperSecurity.Vault.KeeperRecord]$loaded = $null [void]$Vault.TryGetKeeperRecord($ConfigUid, [ref]$loaded) $title = $FallbackTitle $typeName = $FallbackTypeName $gatewayUid = '' $resourceUidList = New-Object 'System.Collections.Generic.List[string]' $fields = $null $adminCred = '' if ($null -ne $loaded) { if (-not $title) { $title = [string]$loaded.Title } try { if (-not $typeName) { $typeName = [string]$loaded.TypeName } } catch { Write-Debug "Config TypeName read failed: $($_.Exception.Message)" } if (-not $typeName) { $typeName = [KeeperSecurity.Utils.RecordTypesUtils]::KeeperRecordType($loaded) } $typed = $loaded -as [KeeperSecurity.Vault.TypedRecord] if ($null -ne $typed) { try { $facade = New-Object KeeperSecurity.Plugins.PAM.PamConfigurationFacade($typed) $gatewayUid = [string]$facade.ControllerUid foreach ($ref in @($facade.ResourceRef)) { $refUid = [string]$ref if (-not [string]::IsNullOrWhiteSpace($refUid)) { [void]$resourceUidList.Add($refUid) } } $adminCred = [string]$facade.AdminCredentialRef if ($VerboseOutput -or $IsDetail) { $fields = extractPamConfigDisplayFields -Config $typed } } catch { Write-Debug "PAM configuration facade/fields read failed: $($_.Exception.Message)" } } } $sharedFolder = resolvePamConfigSharedFolder -Vault $Vault -ConfigUid $ConfigUid $sharedFolderText = if ($sharedFolder) { "$($sharedFolder.Name) ($($sharedFolder.Uid))" } else { '' } $resourceJoined = ($resourceUidList -join ', ') if ($AsCommanderJson) { $sfObj = $null if ($sharedFolder) { $sfObj = [ordered]@{ name = [string]$sharedFolder.Name uid = [string]$sharedFolder.Uid } } if ($IsDetail) { $row = [ordered]@{ uid = $ConfigUid name = $title config_type = $typeName shared_folder = $sfObj gateway_uid = $gatewayUid resource_record_uids = @($resourceUidList.ToArray()) fields = if ($fields) { $fields } else { [ordered]@{} } } if ($typeName -eq 'pamDomainConfiguration' -and -not [string]::IsNullOrWhiteSpace($adminCred)) { $row['domain_administrative_credential'] = (maskPamUid $adminCred) } return [PSCustomObject]$row } $row = [ordered]@{ uid = $ConfigUid config_name = $title config_type = $typeName shared_folder = $sfObj gateway_uid = $gatewayUid resource_record_uids = @($resourceUidList.ToArray()) } if ($VerboseOutput -and $fields) { $row['fields'] = $fields } return [PSCustomObject]$row } $row = [ordered]@{ Uid = $ConfigUid ConfigName = $title ConfigType = $typeName SharedFolder = $sharedFolderText GatewayUid = $gatewayUid ResourceRecordUids = $resourceJoined } if ($fields -and $fields.Count -gt 0) { if ($IsDetail) { $row['Fields'] = $fields } elseif ($VerboseOutput) { $parts = foreach ($k in $fields.Keys) { "$k`: $($fields[$k])" } $row['Fields'] = ($parts -join '; ') } } return [PSCustomObject]$row } function Get-KeeperPamConfig { <# .SYNOPSIS List PAM configurations. .DESCRIPTION Lists PAM configurations. Equivalent to Commander: pam-config list See: https://docs.keeper.io/keeperpam/commander-cli/command-reference/keeperpam-commands#sub-command-config .EXAMPLE Get-KeeperPamConfig .EXAMPLE Get-KeeperPamConfig -Config '<CONFIG_UID>' -VerboseOutput -Format json #> [CmdletBinding()] [Alias('pam-config-list', 'pam-cfg-list')] Param ( [Parameter(Position = 0)] [Alias('c')] [string] $Config, [Parameter()] [Alias('v')] [switch] $VerboseOutput, [Parameter()] [ValidateSet('table', 'json')] [string] $Format = 'table' ) $plugin = ensurePamPlugin -SyncIfNeeded $false if (-not $plugin) { Write-Error -Message 'PAM plugin is not available. Enterprise admin access is required.' -ErrorAction Stop } $vault = getPamVault if (-not [string]::IsNullOrWhiteSpace($Config)) { $configuration = $null try { $configuration = resolvePamConfigurationRecord -Vault $vault -Identifier $Config } catch { if ($_.Exception.Message -match 'not unique') { if ($Format -eq 'json') { @{ error = "Configuration $Config is not unique. Use configuration UID." } | ConvertTo-Json -Depth 5 } else { Write-Output $_.Exception.Message } return } throw } if (-not $configuration) { if ($Format -eq 'json') { @{ error = "Configuration $Config not found" } | ConvertTo-Json -Depth 5 } else { Write-Output "Configuration `"$Config`" not found." } return } $detail = buildPamConfigListRow -Vault $vault -ConfigUid $configuration.Uid ` -FallbackTitle ([string]$configuration.Title) -FallbackTypeName ([string]$configuration.TypeName) ` -VerboseOutput $true -AsCommanderJson:($Format -eq 'json') -IsDetail $true if ($Format -eq 'json') { $detail | ConvertTo-Json -Depth 8 } else { $detailRows = New-Object 'System.Collections.Generic.List[object]' [void]$detailRows.Add([PSCustomObject]@{ Label = 'UID'; Value = [string]$detail.Uid }) [void]$detailRows.Add([PSCustomObject]@{ Label = 'Name'; Value = [string]$detail.ConfigName }) [void]$detailRows.Add([PSCustomObject]@{ Label = 'Config Type'; Value = [string]$detail.ConfigType }) [void]$detailRows.Add([PSCustomObject]@{ Label = 'Shared Folder'; Value = [string]$detail.SharedFolder }) [void]$detailRows.Add([PSCustomObject]@{ Label = 'Gateway UID'; Value = [string]$detail.GatewayUid }) [void]$detailRows.Add([PSCustomObject]@{ Label = 'Resource Record UIDs'; Value = [string]$detail.ResourceRecordUids }) if ($null -ne $detail.Fields) { foreach ($key in @($detail.Fields.Keys)) { [void]$detailRows.Add([PSCustomObject]@{ Label = [string]$key Value = [string]$detail.Fields[$key] }) } } $labelWidth = 0 foreach ($r in $detailRows) { $len = ([string]$r.Label).Length if ($len -gt $labelWidth) { $labelWidth = $len } } $fmt = "{0,$labelWidth} {1}" foreach ($r in $detailRows) { Write-Output ($fmt -f [string]$r.Label, [string]$r.Value) } } return } # String props for Sort-Object / Format-Table. $prepared = New-Object 'System.Collections.Generic.List[object]' $configMap = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::GetConfigurationRecords($vault) if ($null -ne $configMap) { foreach ($entry in $configMap.GetEnumerator()) { $typed = $entry.Value if ($null -eq $typed -or [string]::IsNullOrEmpty($typed.Uid)) { continue } [void]$prepared.Add([PSCustomObject]@{ Uid = [string]$typed.Uid Title = [string]$typed.Title TypeName = [string]$typed.TypeName }) } } $sorted = @($prepared | Sort-Object -Property @{ Expression = { $_.Title }; Ascending = $true }) $rows = New-Object 'System.Collections.Generic.List[object]' $nsfLines = New-Object 'System.Collections.Generic.List[string]' foreach ($item in $sorted) { $uid = [string]$item.Uid if ([string]::IsNullOrEmpty($uid)) { continue } [KeeperSecurity.Vault.KeeperRecord]$keeperRec = $null $typed = $null if ($vault.TryGetKeeperRecord($uid, [ref]$keeperRec) -and ($keeperRec -is [KeeperSecurity.Vault.TypedRecord])) { $typed = [KeeperSecurity.Vault.TypedRecord]$keeperRec } $inSharedFolder = $false if ($null -ne $typed) { $inSharedFolder = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::IsConfigurationInSharedFolder($vault, $typed) } if (-not $inSharedFolder) { try { $byPermission = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::FindSharedFolderForRecord($vault, $uid, $null) $inSharedFolder = ($null -ne $byPermission) } catch { $inSharedFolder = $false Write-Debug "Shared-folder permission fallback failed: $($_.Exception.Message)" } } if (-not $inSharedFolder) { [void]$nsfLines.Add(("Warning: Following configuration is not in the shared folder: UID: {0}, Title: {1}" -f $uid, [string]$item.Title)) continue } $row = buildPamConfigListRow -Vault $vault -ConfigUid $uid ` -FallbackTitle ([string]$item.Title) -FallbackTypeName ([string]$item.TypeName) ` -VerboseOutput:$VerboseOutput.IsPresent -AsCommanderJson:($Format -eq 'json') if ($null -ne $row) { [void]$rows.Add($row) } } if ($Format -eq 'json') { # Host NSF warnings so JSON on the success stream stays parseable. foreach ($line in $nsfLines) { Write-Host $line } @{ configurations = @($rows.ToArray()) } | ConvertTo-Json -Depth 8 return } foreach ($line in $nsfLines) { Write-Output $line } if ($rows.Count -eq 0) { Write-Output 'No PAM configurations found.' return } $rows.ToArray() | Format-Table -AutoSize } function New-KeeperPamConfig { <# .SYNOPSIS Create a new PAM configuration. .DESCRIPTION Creates a PAM configuration. .EXAMPLE New-KeeperPamConfig -Environment local -Title 'Network Config' -SharedFolder '<SF_UID>' -Gateway '<GATEWAY>' -NetworkId 'net1' -NetworkCidr '10.0.0.0/24' #> [CmdletBinding(SupportsShouldProcess = $true)] [Alias('pam-config-new', 'pam-cfg-new')] Param ( [Parameter(Mandatory = $true)] [Alias('env')] [ValidateSet('local', 'aws', 'azure', 'gcp', 'domain', 'oci', 'network', 'github')] [string] $Environment, [Parameter(Mandatory = $true)] [Alias('t')] [string] $Title, [Parameter(Mandatory = $true)] [Alias('sf', 'shared-folder')] [string] $SharedFolder, [Parameter()] [Alias('g')] [string] $Gateway, [Parameter()] [Alias('sc', 'schedule')] [string] $DefaultSchedule, [Parameter()] [Alias('pm', 'port-mapping')] [string[]] $PortMapping, [Parameter()] [Alias('idp', 'identity-provider')] [string] $IdentityProvider, [Parameter()] [Alias('c')] [ValidateSet('on', 'off', 'default')] [string] $Connections, [Parameter()] [Alias('u')] [ValidateSet('on', 'off', 'default')] [string] $Tunneling, [Parameter()] [Alias('r')] [ValidateSet('on', 'off', 'default')] [string] $Rotation, [Parameter()] [Alias('rbi', 'remote-browser-isolation')] [ValidateSet('on', 'off', 'default')] [string] $RemoteBrowserIsolation, [Parameter()] [Alias('cr', 'connections-recording')] [ValidateSet('on', 'off', 'default')] [string] $ConnectionsRecording, [Parameter()] [Alias('tr', 'typescript-recording')] [ValidateSet('on', 'off', 'default')] [string] $TypescriptRecording, [Parameter()] [Alias('ai-threat-detection')] [ValidateSet('on', 'off', 'default')] [string] $AiThreatDetection, [Parameter()] [Alias('ai-terminate-session-on-detection')] [ValidateSet('on', 'off', 'default')] [string] $AiTerminateSessionOnDetection, [Parameter()][Alias('network-id')][string] $NetworkId, [Parameter()][Alias('network-cidr')][string] $NetworkCidr, [Parameter()][Alias('aws-id')][string] $AwsId, [Parameter()][Alias('access-key-id')][string] $AccessKeyId, [Parameter()][Alias('access-secret-key')][string] $AccessSecretKey, [Parameter()][Alias('region-name')][string[]] $RegionNames, [Parameter()][Alias('azure-id')][string] $AzureId, [Parameter()][Alias('client-id')][string] $ClientId, [Parameter()][Alias('client-secret')][string] $ClientSecret, [Parameter()][Alias('subscription-id', 'subscription_id')][string] $SubscriptionId, [Parameter()][Alias('tenant-id')][string] $TenantId, [Parameter()][Alias('resource-group')][string[]] $ResourceGroups, [Parameter()][Alias('gcp-id')][string] $GcpId, [Parameter()][Alias('service-account-key')][string] $ServiceAccountKey, [Parameter()][Alias('google-admin-email')][string] $GoogleAdminEmail, [Parameter()][Alias('gcp-region')][string[]] $GcpRegionNames, [Parameter()][Alias('domain-id')][string] $DomainId, [Parameter()][Alias('domain-hostname')][string] $DomainHostname, [Parameter()][Alias('domain-port')][string] $DomainPort, [Parameter()][Alias('domain-use-ssl')][ValidateSet('true', 'false')][string] $DomainUseSsl, [Parameter()][Alias('domain-scan-dc-cidr')][ValidateSet('true', 'false')][string] $DomainScanDcCidr, [Parameter()][Alias('domain-network-cidr')][string] $DomainNetworkCidr, [Parameter()][Alias('domain-admin')][string] $DomainAdministrativeCredential, [Parameter()][Alias('domain-user-match')][string] $DomainUserMatch, [Parameter()][Alias('force-domain-admin')][switch] $ForceDomainAdmin, [Parameter()][Alias('oci-id')][string] $OciId, [Parameter()][Alias('oci-admin-id')][string] $OciAdminId, [Parameter()][Alias('oci-admin-public-key')][string] $OciAdminPublicKey, [Parameter()][Alias('oci-admin-private-key')][string] $OciAdminPrivateKey, [Parameter()][Alias('oci-tenancy')][string] $OciTenancy, [Parameter()][Alias('oci-region')][string] $OciRegion ) $plugin = ensurePamPlugin if (-not $plugin) { Write-Error -Message 'PAM plugin is not available. Enterprise admin access is required.' -ErrorAction Stop } $vault = getPamVault $auth = getPamEnterpriseAuth if (writePamComingSoonMessage -Environment $Environment) { return } if (-not $PSCmdlet.ShouldProcess($Title, 'Create PAM configuration')) { return } [string]$recordType = $null if (-not [KeeperSecurity.Plugins.PAM.PamConfigTypes]::TryResolveRecordType($Environment, [ref]$recordType)) { throw "Environment parameter is required. Supported options: $([KeeperSecurity.Plugins.PAM.PamConfigTypes]::GetSupportedConfigTypes())" } ensurePamRecordTypesSynced -Vault $vault $values = preparePamConfigValuesFromBoundParameters -BoundParameters $PSBoundParameters $record = [KeeperSecurity.Plugins.PAM.ConfigUtils]::CreateConfigurationRecord($vault, $recordType, $Title) $facade = applyPamConfigResources -Vault $vault -Plugin $plugin -Record $record -Values $values -IsEdit:$false if (-not ($facade -is [KeeperSecurity.Plugins.PAM.PamConfigurationFacade])) { throw 'Failed to initialize PAM configuration resources.' } applyPamConfigEnvironmentFields -Vault $vault -Record $record -Values $values -IsEdit:$false applyPamConfigSchedule -Record $record -DefaultSchedule $DefaultSchedule -IsEdit:$false $moveDestinationUid = resolvePamConfigurationFolderUid -Vault $vault -Identifier $SharedFolder $sharedFolderUid = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolvePamResourcesFolderUid($vault, $facade.FolderUid) if (-not $sharedFolderUid) { $sharedFolderUid = [KeeperSecurity.Plugins.PAM.PamVaultHelpers]::ResolvePamResourcesFolderUid($vault, $moveDestinationUid) } if ($sharedFolderUid) { $facade.FolderUid = $sharedFolderUid } if (-not $sharedFolderUid -or -not $moveDestinationUid) { throw "Could not resolve shared folder `"$SharedFolder`". Provide a shared folder UID, name, or path." } [KeeperSecurity.Utils.RecordTypesUtils]::AdjustTypedRecord($vault, $record) [KeeperSecurity.Plugins.PAM.ConfigUtils]::AddConfigurationRecordAsync($vault, $record).GetAwaiter().GetResult() | Out-Null try { [KeeperSecurity.Plugins.PAM.ConfigUtils]::EnsureConfigurationNetworkGraphAsync($auth, $record.Uid).GetAwaiter().GetResult() | Out-Null } catch { Write-Warning "Could not register PAM configuration network graph: $($_.Exception.Message)" } configurePamTunnelingIfNeeded -Auth $auth -ConfigUid $record.Uid -Values $values movePamConfigToFolder -Vault $vault -Record $record -DestinationFolderUid $moveDestinationUid if (-not [string]::IsNullOrEmpty($facade.ControllerUid)) { [KeeperSecurity.Plugins.PAM.ConfigUtils]::SetConfigurationGatewayAsync( $auth, $record.Uid, $facade.ControllerUid).GetAwaiter().GetResult() | Out-Null } $vault.SyncDown().GetAwaiter().GetResult() | Out-Null [void](syncPamPlugin -Plugin $plugin -Reload $true -ThrowOnError $false) Write-Output $record.Uid } function Set-KeeperPamConfig { <# .SYNOPSIS Edit an existing PAM configuration. .DESCRIPTION Updates a PAM configuration. .EXAMPLE Set-KeeperPamConfig -Uid '<CONFIG_UID>' -Title 'Updated Title' #> [CmdletBinding(SupportsShouldProcess = $true)] [Alias('pam-config-edit', 'pam-cfg-edit')] Param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Uid, [Parameter()] [Alias('env')] [ValidateSet('local', 'aws', 'azure', 'gcp', 'domain', 'oci', 'network', 'github')] [string] $Environment, [Parameter()] [Alias('t')] [string] $Title, [Parameter()] [Alias('sf', 'shared-folder')] [string] $SharedFolder, [Parameter()] [Alias('g')] [string] $Gateway, [Parameter()] [Alias('sc', 'schedule')] [string] $DefaultSchedule, [Parameter()] [Alias('pm', 'port-mapping')] [string[]] $PortMapping, [Parameter()] [Alias('idp', 'identity-provider')] [string] $IdentityProvider, [Parameter()] [Alias('rrr', 'remove-resource-record')] [string[]] $RemoveResourceRecords, [Parameter()] [Alias('c')] [ValidateSet('on', 'off', 'default')] [string] $Connections, [Parameter()] [Alias('u')] [ValidateSet('on', 'off', 'default')] [string] $Tunneling, [Parameter()] [Alias('r')] [ValidateSet('on', 'off', 'default')] [string] $Rotation, [Parameter()] [Alias('rbi', 'remote-browser-isolation')] [ValidateSet('on', 'off', 'default')] [string] $RemoteBrowserIsolation, [Parameter()] [Alias('cr', 'connections-recording')] [ValidateSet('on', 'off', 'default')] [string] $ConnectionsRecording, [Parameter()] [Alias('tr', 'typescript-recording')] [ValidateSet('on', 'off', 'default')] [string] $TypescriptRecording, [Parameter()] [Alias('ai-threat-detection')] [ValidateSet('on', 'off', 'default')] [string] $AiThreatDetection, [Parameter()] [Alias('ai-terminate-session-on-detection')] [ValidateSet('on', 'off', 'default')] [string] $AiTerminateSessionOnDetection, [Parameter()][Alias('network-id')][string] $NetworkId, [Parameter()][Alias('network-cidr')][string] $NetworkCidr, [Parameter()][Alias('aws-id')][string] $AwsId, [Parameter()][Alias('access-key-id')][string] $AccessKeyId, [Parameter()][Alias('access-secret-key')][string] $AccessSecretKey, [Parameter()][Alias('region-name')][string[]] $RegionNames, [Parameter()][Alias('azure-id')][string] $AzureId, [Parameter()][Alias('client-id')][string] $ClientId, [Parameter()][Alias('client-secret')][string] $ClientSecret, [Parameter()][Alias('subscription-id', 'subscription_id')][string] $SubscriptionId, [Parameter()][Alias('tenant-id')][string] $TenantId, [Parameter()][Alias('resource-group')][string[]] $ResourceGroups, [Parameter()][Alias('gcp-id')][string] $GcpId, [Parameter()][Alias('service-account-key')][string] $ServiceAccountKey, [Parameter()][Alias('google-admin-email')][string] $GoogleAdminEmail, [Parameter()][Alias('gcp-region')][string[]] $GcpRegionNames, [Parameter()][Alias('domain-id')][string] $DomainId, [Parameter()][Alias('domain-hostname')][string] $DomainHostname, [Parameter()][Alias('domain-port')][string] $DomainPort, [Parameter()][Alias('domain-use-ssl')][ValidateSet('true', 'false')][string] $DomainUseSsl, [Parameter()][Alias('domain-scan-dc-cidr')][ValidateSet('true', 'false')][string] $DomainScanDcCidr, [Parameter()][Alias('domain-network-cidr')][string] $DomainNetworkCidr, [Parameter()][Alias('domain-admin')][string] $DomainAdministrativeCredential, [Parameter()][Alias('domain-user-match')][string] $DomainUserMatch, [Parameter()][Alias('force-domain-admin')][switch] $ForceDomainAdmin, [Parameter()][Alias('oci-id')][string] $OciId, [Parameter()][Alias('oci-admin-id')][string] $OciAdminId, [Parameter()][Alias('oci-admin-public-key')][string] $OciAdminPublicKey, [Parameter()][Alias('oci-admin-private-key')][string] $OciAdminPrivateKey, [Parameter()][Alias('oci-tenancy')][string] $OciTenancy, [Parameter()][Alias('oci-region')][string] $OciRegion ) $plugin = ensurePamPlugin -SyncIfNeeded $false if (-not $plugin) { Write-Error -Message 'PAM plugin is not available. Enterprise admin access is required.' -ErrorAction Stop } $vault = getPamVault $auth = getPamEnterpriseAuth if ($PSBoundParameters.ContainsKey('Environment') -and (writePamComingSoonMessage -Environment $Environment)) { return } $configuration = resolvePamConfigurationRecord -Vault $vault -Identifier $Uid if (-not $configuration) { throw "PAM configuration `"$Uid`" not found" } if (-not $PSCmdlet.ShouldProcess($configuration.Title, 'Update PAM configuration')) { return } ensurePamRecordTypesSynced -Vault $vault if ($PSBoundParameters.ContainsKey('Environment')) { [string]$newType = $null if ([KeeperSecurity.Plugins.PAM.PamConfigTypes]::TryResolveRecordType($Environment, [ref]$newType) -and -not [string]::Equals($newType, $configuration.TypeName, [StringComparison]::Ordinal)) { $configuration.TypeName = $newType Write-Warning "Environment type changed to `"$Environment`". Review fields after edit." } } [KeeperSecurity.Utils.RecordTypesUtils]::AdjustTypedRecord($vault, $configuration) if ($PSBoundParameters.ContainsKey('Title') -and -not [string]::IsNullOrWhiteSpace($Title)) { $configuration.Title = $Title.Trim() } $values = preparePamConfigValuesFromBoundParameters -BoundParameters $PSBoundParameters -ExcludeKeys @('Uid') $facadeBefore = New-Object KeeperSecurity.Plugins.PAM.PamConfigurationFacade($configuration) $origGatewayUid = $facadeBefore.ControllerUid $origSharedFolderUid = $facadeBefore.FolderUid $facade = applyPamConfigResources -Vault $vault -Plugin $plugin -Record $configuration -Values $values -IsEdit:$true if (-not ($facade -is [KeeperSecurity.Plugins.PAM.PamConfigurationFacade])) { throw 'Failed to update PAM configuration resources.' } applyPamConfigEnvironmentFields -Vault $vault -Record $configuration -Values $values -IsEdit:$true applyPamConfigSchedule -Record $configuration -DefaultSchedule $DefaultSchedule -IsEdit:$true [KeeperSecurity.Utils.RecordTypesUtils]::AdjustTypedRecord($vault, $configuration) $vault.UpdateRecord($configuration).GetAwaiter().GetResult() | Out-Null $facade = New-Object KeeperSecurity.Plugins.PAM.PamConfigurationFacade($configuration) if (-not [string]::Equals($facade.ControllerUid, $origGatewayUid, [StringComparison]::Ordinal) -and -not [string]::IsNullOrEmpty($facade.ControllerUid)) { [KeeperSecurity.Plugins.PAM.ConfigUtils]::SetConfigurationGatewayAsync( $auth, $configuration.Uid, $facade.ControllerUid).GetAwaiter().GetResult() | Out-Null } if (-not [string]::Equals($facade.FolderUid, $origSharedFolderUid, [StringComparison]::Ordinal) -and -not [string]::IsNullOrEmpty($facade.FolderUid)) { $moveFolderUid = resolvePamConfigurationFolderUid -Vault $vault -Identifier $facade.FolderUid if (-not $moveFolderUid) { $moveFolderUid = $facade.FolderUid } movePamConfigToFolder -Vault $vault -Record $configuration -DestinationFolderUid $moveFolderUid } configurePamTunnelingIfNeeded -Auth $auth -ConfigUid $configuration.Uid -Values $values $vault.ScheduleSyncDown([TimeSpan]::FromMilliseconds(100)).GetAwaiter().GetResult() | Out-Null Write-Output "PAM configuration `"$($configuration.Title)`" updated." } function Remove-KeeperPamConfig { <# .SYNOPSIS Remove a PAM configuration. .DESCRIPTION Removes a PAM configuration. .EXAMPLE Remove-KeeperPamConfig -Uid '<CONFIG_UID>' #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] [Alias('pam-config-remove', 'pam-cfg-remove', 'pam-config-rm')] Param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Uid ) # Vault-only remove (no enterprise/PAM plugin required). $vault = getPamVault $config = resolvePamConfigurationRecord -Vault $vault -Identifier $Uid if (-not $config) { Write-Error -Message "Configuration `"$Uid`" not found" -ErrorAction Stop } $title = [string]$config.Title $configUid = [string]$config.Uid if (-not $PSCmdlet.ShouldProcess($title, 'Remove PAM configuration')) { return } [KeeperSecurity.Plugins.PAM.ConfigUtils]::RemovePamConfigurationAsync($vault, $configUid).GetAwaiter().GetResult() | Out-Null $vault.ScheduleSyncDown([TimeSpan]::FromMilliseconds(100)).GetAwaiter().GetResult() | Out-Null Write-Output "PAM configuration `"$title`" removed." } # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCWvsmLizUgHFEa # wBUpB9trTA0m/W8WYu0HTk6ED9ckR6CCITswggWNMIIEdaADAgECAhAOmxiO+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 # BDEiBCAtbHgADCRsJSKlc0qi2SiaZZf3jIGUBiw6ZS7cD3DHXjANBgkqhkiG9w0B # AQEFAASCAYB8suFXjkTXWf6MWBsoxEeUpd3U01KW2cxx0oQ+nwm6uwuhj/rD7I3W # HVbWY76eSLKq6EbNP0pTxsGPtfLMvtVUCkwG7TwWuRDqBhrkgY99+phdhIMqhtk0 # iUSPpwcEMUiVZF9g+EIJ0Zxs3WzDu5OcAi80/kCDdJuU1/zXUBicLIXw+9iTkgbO # yfZGrVbINJhpvTS/mm/qarUWU3IhTB8dQUoEFaknCqPtty1Aje0redIn37KhfVM9 # uPKMzZGQuP/b8dpV+omHPf9Fxz2bNtgEU6PCXsPFufKsuN7myapDpSs2Ziv9O9rm # vLoM0i2+mZHFhQH8f011rWbAoA2wDCWmnE52yCPYKFP3YSS0SsTKrzuihHCydLYj # 41m2oZA1tV8HhGtBlpQNp6qBBp7A2j1zvQcnpvlrZtClU8r0QYCQe5zok1rodO2U # g8CZEziNHi1ClEicoEjdKMBinqDjhnHx6KwU9+OpN16UnJcknqyMsEOigtNl08dS # ENjtET4XPdahggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTIxWjAv # BgkqhkiG9w0BCQQxIgQg97snFHHFKnC8HK1kKiNv1sdx1/Bg+QZFQhUtwfhgWgcw # DQYJKoZIhvcNAQEBBQAEggIAqtw8A2GLzzMbizLaiL9WpH6Qdm2fhxRfzqD0vynM # k7vKdZ7OuKumrq5GYHAr49yrEDcUtqMFjFd637Y/ebUf5w0AvM9yDX5pPg5UsfL9 # 3NZXK7JJHL5fLCmPmKgX9ZIG6ur36JaJDBpDmtl6lfs0cHIa1zvPDXPFoY9xjgjC # +7B84oFK3qUMCZCk5maVu28uETcOS+9IJM8szE6o2FCCd2U0NbTHIg9omRLcGyHZ # sdidPZPyd1Qbs12tlGUw5+jsgMWwRXybOqjMhvdp++Pg9pvY4JdRCas0A29U3diL # 7yo40x/RhyacTGLfxELoESWBbFdhP4GqygnT3OkXzYd3TG4dzG8DI9HmQeMO1I9+ # pbTHYu2/M85CJtWZCxkQEfykj7lH9vQLm0AkVyttZFrLqgbZEz4YC4vBqcQvmKZX # XssftHkRig78CwiBwGLblVFl7M7vhXMdfPx3uXQoAkTcWDTqcw6dkNyMhRkYQIES # HibKVQFZmcciOj7td3nSlqCQx2XOOJHsXjj+C3A2CtJF3gBF41hy/6PcJlL12uXr # HjRx8cQfGBPyYNgC1yKKU61qHw/wgkujaqubnJW67YU7hHhsCG0QR78NVyVQ+Wop # Lq0n3SqESetBAhNDFiV4o5Fs3lxLaaH+Ra+ELJ89vTRo0ASAl4+ztPbyJnPkHATy # sbg= # SIG # End signature block |