SecretsManager.ps1

#requires -Version 5.1

$Keeper_KSMAppCompleter = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    $result = @()
    [KeeperSecurity.Vault.VaultOnline]$private:vault = getVault
    if (-not $vault) {
        return $null
    }

    $toComplete = $wordToComplete
    if ($toComplete.Length -ge 1) {
        if ($toComplete[0] -eq '''') {
            $toComplete = $toComplete.Substring(1, $toComplete.Length - 1)
            $toComplete = $toComplete -replace '''''', ''''
        }
        if ($toComplete[0] -eq '"') {
            $toComplete = $toComplete.Substring(1, $toComplete.Length - 1)
            $toComplete = $toComplete -replace '""', '"'
            $toComplete = $toComplete -replace '`"', '"'
        }
    }

    $toComplete += '*'
    foreach ($app in $vault.KeeperApplications) {
        if ($app.Title -like $toComplete) {
            $name = $app.Title
            if ($name -match ' ') {
                $name = $name -replace '''', ''''''
                $name = '''' + $name + ''''
            }
            $result += $name
        }
    }

    if ($result.Count -gt 0) {
        return $result
    }
    else {
        return $null
    }
}

function Get-KeeperSecretManagerApp {
    <#
        .Synopsis
        Get Keeper Secret Manager Applications

        .Parameter Uid
        Record UID

        .Parameter Filter
        Return matching applications only

        .Parameter Detail
        Application details
    #>

    [CmdletBinding()]
    Param (
        [string] $Uid,
        [string] $Filter,
        [Switch] $Detail
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    if ($Uid) {
        [KeeperSecurity.Vault.ApplicationRecord] $application = $null
        if ($vault.TryGetKeeperRecord($Uid, [ref]$application)) {
            if (-not $application.Type -eq 'app') {
                throw "No application found with UID '$Uid'."
            }
            if ($Detail.IsPresent) {
                return $vault.GetSecretManagerApplication($application.Uid, $false).GetAwaiter().GetResult()
            }
            else {
                return $application
            }
        }
        else {
            throw "No application found with UID '$Uid'."
        }
    }
    else {
        $applications = $vault.KeeperRecords | Where-Object { $_.Type -eq 'app' }
        $results = @()

        foreach ($application in $applications) {
            if ($Filter) {
                $match = @($application.Uid, $application.Title) | Select-String $Filter | Select-Object -First 1
                if (-not $match) {
                    continue
                }
            }
            if ($Detail.IsPresent) {
                $results += $vault.GetSecretManagerApplication($application.Uid, $false).GetAwaiter().GetResult()
            }
            else {
                $results += $application
            }
        }
        return $results
    }
}
New-Alias -Name ksm -Value Get-KeeperSecretManagerApp

function Add-KeeperSecretManagerApp {
    <#
        .Synopsis
        Creates Keeper Secret Manager Application

        .Parameter Name
        Secret Manager Application
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)][string]$AppName
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    $vault.CreateSecretManagerApplication($AppName).GetAwaiter().GetResult()
}
New-Alias -Name ksm-create -Value Add-KeeperSecretManagerApp

function Remove-KeeperSecretManagerApp {
    <#
        .SYNOPSIS
        Deletes a Keeper Secret Manager Application

        .DESCRIPTION
        This cmdlet deletes a Keeper Secrets Manager application by UID.

        .PARAMETER Uid
        The UID of the application to delete.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]
        [string] $Uid
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault

    if ($PSCmdlet.ShouldProcess("Secrets Manager App UID: $Uid", "Delete")) {
        $vault.DeleteSecretManagerApplication($Uid).GetAwaiter().GetResult()
        Write-Host "Secrets Manager Application with UID '$Uid' has been deleted." -ForegroundColor Green
    }
}
New-Alias -Name ksm-delete -Value Remove-KeeperSecretManagerApp

function Grant-KeeperSecretManagerFolderAccess {
    <#
        .Synopsis
        Adds shared folder to KSM Application

        .Parameter App
       KSM Application UID or Title

        .Parameter Secret
       Shared Folder UID or Name

        .Parameter CanEdit
        Enable write access to shared secrets

    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)][string]$App,
        [Parameter(Mandatory = $true)][string]$Secret,
        [Parameter()][switch]$CanEdit
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    $apps = Get-KeeperSecretManagerApp -Filter $App
    if (-not $apps) {
        Write-Error -Message "Cannot find Secret Manager Application: $App" -ErrorAction Stop
    }
    [KeeperSecurity.Vault.ApplicationRecord]$application = $apps[0]

    [string]$uid = $null
    $sfs = Get-KeeperSharedFolder -Filter $Secret
    if ($sfs) {
        $uid = $sfs[0].Uid
    }
    else {
        $recs = Get-KeeperRecord -Filter $Secret
        if ($recs) {
            $uid = $recs[0].Uid
        }
    }
    if (-not $uid) {
        Write-Error -Message "Cannot find Shared Folder: $Secret" -ErrorAction Stop
    }
    $vault.ShareToSecretManagerApplication($application.Uid, $uid, $CanEdit.IsPresent).GetAwaiter().GetResult()
}
Register-ArgumentCompleter -CommandName Grant-KeeperSecretManagerFolderAccess -ParameterName Secret -ScriptBlock $Keeper_SharedFolderCompleter
Register-ArgumentCompleter -CommandName Grant-KeeperSecretManagerFolderAccess -ParameterName App -ScriptBlock $Keeper_KSMAppCompleter
New-Alias -Name ksm-share -Value Grant-KeeperSecretManagerFolderAccess

function Revoke-KeeperSecretManagerFolderAccess {
    <#
        .Synopsis
        Removes Shared Folder from KSM Application

        .Parameter App
        Secret Manager Application

        .Parameter Secret
       Shared Folder UID or Name
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)][string]$App,
        [Parameter(Mandatory = $true)][string]$Secret
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    $apps = Get-KeeperSecretManagerApp -Filter $App
    if (-not $apps) {
        Write-Error -Message "Cannot find Secret Manager Application: $App" -ErrorAction Stop
    }
    [KeeperSecurity.Vault.ApplicationRecord]$application = $apps[0]

    [string]$uid = $null
    $sfs = Get-KeeperSharedFolder -Filter $Secret
    if ($sfs) {
        $uid = $sfs[0].Uid
    }
    else {
        $recs = Get-KeeperRecord -Filter $Secret
        if ($recs) {
            $uid = $recs[0].Uid
        }
    }
    if (-not $uid) {
        Write-Error -Message "Cannot find Shared Folder: $Secret" -ErrorAction Stop
    }
    $vault.UnshareFromSecretManagerApplication($application.Uid, $uid).GetAwaiter().GetResult()
}
Register-ArgumentCompleter -CommandName Revoke-KeeperSecretManagerFolderAccess -ParameterName Secret -ScriptBlock $Keeper_SharedFolderCompleter
Register-ArgumentCompleter -CommandName Revoke-KeeperSecretManagerFolderAccess -ParameterName App -ScriptBlock $Keeper_KSMAppCompleter
New-Alias -Name ksm-unshare -Value Revoke-KeeperSecretManagerFolderAccess

function Add-KeeperSecretManagerClient {
    <#
        .Synopsis
        Adds client/device to KSM Application

        .Parameter App
        KSM Application UID or Title

        .Parameter Name
        Client or Device Name

        .Parameter UnlockIP
        Enable write access to shared secrets
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)][string]$App,
        [Parameter()][string]$Name,
        [Parameter()][switch]$UnlockIP,
        [Parameter()][switch]$B64
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    $apps = Get-KeeperSecretManagerApp -Filter $App
    if (-not $apps) {
        Write-Error -Message "Cannot find Secret Manager Application: $App" -ErrorAction Stop
    }
    [KeeperSecurity.Vault.ApplicationRecord]$application = $apps[0]

    $rs = $vault.AddSecretManagerClient($application.Uid, $UnlockIP.IsPresent, $null, $null, $name).GetAwaiter().GetResult()
    if ($rs) {
        if ($B64.IsPresent) {
            $configuration = $vault.GetConfiguration($rs.Item2).GetAwaiter().GetResult()
            if ($configuration) {
                $configData = [KeeperSecurity.Utils.JsonUtils]::DumpJson($configuration, $true)
                [System.Convert]::ToBase64String($configData)
        
            }
        }
        else {
            $rs.Item2
        }
    
    }
}
Register-ArgumentCompleter -CommandName Add-KeeperSecretManagerClient -ParameterName App -ScriptBlock $Keeper_KSMAppCompleter
New-Alias -Name ksm-addclient -Value Add-KeeperSecretManagerClient

function Remove-KeeperSecretManagerClient {
    <#
        .Synopsis
        Removes client/device from KSM Application

        .Parameter App
        KSM Application UID or Title

        .Parameter Name
        Client Id or Device Name

    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    Param (
        [Parameter(Mandatory = $true)][string]$App,
        [Parameter(Mandatory = $true)][string]$Name
    )

    [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    $apps = Get-KeeperSecretManagerApp -Filter $App -Detail
    if (-not $apps) {
        Write-Error -Message "Cannot find Secret Manager Application: $App" -ErrorAction Stop
    }
    [KeeperSecurity.Vault.ApplicationRecord]$application = $apps[0]

    $device = $application.Devices | Where-Object { $_.Name -ceq $Name -or $_.ShortDeviceId -ceq $Name }
    if (-not $device) {
        Write-Error -Message "Cannot find Device: $Name" -ErrorAction Stop
    }

    if ($PSCmdlet.ShouldProcess($application.Title, "Removing KSM Device '$($device.Name)'")) {
        $vault.DeleteSecretManagerClient($application.Uid, $device.DeviceId).GetAwaiter().GetResult() | Out-Null
        Write-Information -MessageData "Device $($device.Name) has been deleted from KSM application `"$($application.Title)`"."
    }
}

Register-ArgumentCompleter -CommandName Remove-KeeperSecretManagerClient -ParameterName App -ScriptBlock $Keeper_KSMAppCompleter
New-Alias -Name ksm-rmclient -Value Remove-KeeperSecretManagerClient

function Test-KeeperUserNeedsUpdate {
    <#
    .SYNOPSIS
        Determines whether any of the user's record or folder shares need to be updated.

    .DESCRIPTION
        Iterates through a list of share UIDs and checks if any need their permissions updated
        for the given user.

    .PARAMETER Vault
        The Keeper VaultOnline context.

    .PARAMETER User
        The username/email of the user.

    .PARAMETER IsAdmin
        True if full permissions (edit/share/manage) are expected.

    .PARAMETER ShareUids
        A list of UIDs representing shared records or folders.

    .PARAMETER ApplicationUid
        The UID of the Secrets Manager Application record.

    .OUTPUTS
        Boolean — True if any share requires an update for the user.
    #>


    param (
        [Parameter(Mandatory)]
        $vault,

        [Parameter(Mandatory)]
        [string]$User,

        [Parameter(Mandatory)]
        [bool]$IsAdmin,

        [Parameter(Mandatory)]
        [System.Collections.Generic.List[string]]$ShareUids,

        [Parameter(Mandatory)]
        [string]$ApplicationUid
    )

    foreach ($uid in $ShareUids) {
        $needsUpdate = Test-KeeperShareNeedsUpdate -Vault $vault -User $User -ShareUid $uid -Elevated $IsAdmin -ApplicationUid $ApplicationUid
        if ($needsUpdate) {
            return $true
        }
    }

    return $false
}

function Test-KeeperShareNeedsUpdate {
    <#
    .SYNOPSIS
        Determines whether a share (record or folder) needs its permissions updated for a user.

    .DESCRIPTION
        Compares current permissions of a user on a shared record or shared folder against expected `elevated` (admin) status.

    .PARAMETER Vault
        The VaultOnline object.

    .PARAMETER User
        Username/email of the user being evaluated.

    .PARAMETER ShareUid
        UID of the shared record or shared folder.

    .PARAMETER Elevated
        $true for admin-level (edit/share or manage), $false otherwise.

    .PARAMETER ApplicationUid
        UID of the Secrets Manager application record.

    .OUTPUTS
        [bool] — $true if an update is required, $false if current permissions match expected.
    #>


    param (
        [Parameter(Mandatory)] $Vault,
        [Parameter(Mandatory)] [string] $User,
        [Parameter(Mandatory)] [string] $ShareUid,
        [Parameter(Mandatory)] [bool] $Elevated,
        [Parameter(Mandatory)] [string] $ApplicationUid
    )

    $appInfo = $vault.GetSecretManagerApplication($ApplicationUid, $true).GetAwaiter().GetResult()

    if (-not $appInfo) {
        return $false
    }

    $isRecordShare = $false
    foreach ($share in $appInfo.Shares) {
        $shareId = $share.SecretUid
        $recordTypeEnum = [KeeperSecurity.Vault.SecretManagerSecretType]::Record
        if ($shareId -eq $ShareUid -and $share.SecretType -eq $recordTypeEnum) {
            $isRecordShare = $true
            break
        }
    }

    if ($isRecordShare) {
        $recordUids = [System.Collections.Generic.List[string]]::new()
        $recordUids.Add($ShareUid)

        $recordShares = $Vault.GetSharesForRecords($recordUids).GetAwaiter().GetResult()
        $shareInfo = $recordShares | Where-Object { $_.RecordUid -eq $ShareUid }

        if (-not $shareInfo) {
            return $false
        }

        $userPerms = $shareInfo.UserPermissions | Where-Object { $_.Username -eq $User }
        if (-not $userPerms) {
            return $true
        }

        return ($userPerms.CanEdit -ne $Elevated -or $userPerms.CanShare -ne $Elevated)
    }
    else {
        $sharedFolder = $null
        if (-not $Vault.TryGetSharedFolder($ShareUid, [ref]$sharedFolder)) {
            return $false
        }

        $folderPerms = $sharedFolder.UsersPermissions | Where-Object { $_.Uid -eq $User }
        if (-not $folderPerms) {
            return $true
        }

        return ($folderPerms.ManageUsers -ne $Elevated -or $folderPerms.ManageRecords -ne $Elevated)
    }
}


function Invoke-KeeperSharedFolderShareAction {
    <#
    .SYNOPSIS
        Adds or removes users from shared folders based on permission needs.

    .DESCRIPTION
        For each shared folder and user, checks if permissions are out of sync. If so:
        - Removes user from the folder if action is "remove"
        - Adds/updates user with appropriate permissions if action is "add"

    .PARAMETER Vault
        The VaultOnline context.

    .PARAMETER ApplicationUid
        UID of the Secrets Manager Application.

    .PARAMETER SharedFolders
        A list of shared folder UIDs.

    .PARAMETER Group
        The group name ("admins" or other) to determine permission level.

    .PARAMETER Users
        List of user emails or UIDs.

    .PARAMETER ShareFolderAction
        Either "remove" or "add" to define how permissions should be applied.

    .EXAMPLE
        Invoke-KeeperSharedFolderShareAction -Vault $vault -ApplicationUid "abc123" -SharedFolders @("UID1") -Group "admins" -Users @("user@example.com") -ShareFolderAction "add"
    #>


    param (
        [Parameter(Mandatory)]
        $vault,

        [Parameter(Mandatory)]
        [string]$ApplicationUid,

        [Parameter(Mandatory)]
        [string[]]$SharedFolders,

        [Parameter(Mandatory)]
        [string]$Group,

        [Parameter(Mandatory)]
        [string[]]$Users,

        [Parameter(Mandatory)]
        [ValidateSet("grant", "remove")]
        [string]$ShareFolderAction
    )

    foreach ($folder in $SharedFolders) {
        foreach ($user in $Users) {
            $isAdmin = ($Group -eq "admins")
            $needsUpdate = Test-KeeperShareNeedsUpdate -Vault $vault -User $user -ShareUid $folder -Elevated $isAdmin -ApplicationUid $ApplicationUid

            if ($needsUpdate) {
                if ($ShareFolderAction -eq "remove") {
                    Write-Debug "Removing user '$user' from shared folder '$folder'..."
                    $vault.RemoveUserFromSharedFolder($folder, $user, [KeeperSecurity.Vault.UserType]::User).GetAwaiter().GetResult() | Out-Null
                }
                else {
                    Write-Debug "Adding user '$user' to shared folder '$folder' with permissions (admin: $isAdmin)..."
                    $options = New-Object KeeperSecurity.Vault.SharedFolderUserOptions
                    $options.ManageUsers = $isAdmin
                    $options.ManageRecords = $isAdmin
                    $vault.PutUserToSharedFolder($folder, $user, [KeeperSecurity.Vault.UserType]::User, $options).GetAwaiter().GetResult() | Out-Null
                }
            }
        }
    }
}

function Invoke-KeeperHandleRecordShares {
    <#
    .SYNOPSIS
        Shares or revokes Keeper records for specified users based on group access level.

    .DESCRIPTION
        Iterates over records and users. If the user's permissions are out of sync:
        - Revokes the share if 'revoke' action is specified.
        - Otherwise, shares the record with edit/share permissions if the group is 'admins'.

    .PARAMETER Vault
        VaultOnline session object.

    .PARAMETER ApplicationUid
        UID of the application record (used to resolve appInfo for permission checks).

    .PARAMETER RecordShares
        List of records with RecordUid properties (e.g., RecordSharePermissions objects).

    .PARAMETER Group
        Group string ("admins" or anything else), used to determine full or limited permission.

    .PARAMETER Users
        List of user emails to apply actions to.

    .PARAMETER ShareRecordAction
        "revoke" to remove access, anything else to add/update the share.

    .EXAMPLE
        Invoke-KeeperHandleRecordShares -Vault $vault -ApplicationUid "abc123" -RecordShares $records -Group "admins" -Users @("user@example.com") -ShareRecordAction "add"
    #>


    param (
        [Parameter(Mandatory)]
        $vault,

        [Parameter(Mandatory)]
        [string]$ApplicationUid,

        [Parameter(Mandatory)]
        [System.Collections.IEnumerable]$RecordShares,

        [Parameter(Mandatory)]
        [string]$Group,

        [Parameter(Mandatory)]
        [string[]]$Users,

        [Parameter(Mandatory)]
        [ValidateSet("revoke", "share")]
        [string]$ShareRecordAction
    )

    foreach ($record in $RecordShares) {
        $recordUid = $record.RecordUid
        foreach ($user in $Users) {
            $isAdmin = ($Group -eq "admins")

            $needsUpdate = Test-KeeperShareNeedsUpdate -Vault $vault -User $user -ShareUid $recordUid -Elevated $isAdmin -ApplicationUid $ApplicationUid

            if ($needsUpdate) {
                if ($ShareRecordAction -eq "revoke") {
                    Write-Debug "Revoking user '$user' from record '$recordUid'..."
                    $vault.RevokeShareFromUser($recordUid, $user).GetAwaiter().GetResult() | Out-Null
                }
                else {
                    Write-Debug "Sharing record '$recordUid' with user '$user' (Edit: $isAdmin, Share: $isAdmin)..."
                    $options = New-Object KeeperSecurity.Vault.SharedFolderRecordOptions
                    $options.CanEdit = $isAdmin
                    $options.CanShare = $isAdmin
                    $vault.ShareRecordWithUser($recordUid, $user, $options).GetAwaiter().GetResult() | Out-Null
                }
            }
        }
    }
}

function Update-KeeperShareUserPermissions {
    <#
    .SYNOPSIS
        Updates app-related record and shared folder permissions for a given user.

    .DESCRIPTION
        Based on existing share state, grants/revokes appropriate permissions across shared folders and records.
        Uses app record metadata to determine what to share and with whom.

    .PARAMETER Vault
        The VaultOnline object.

    .PARAMETER ApplicationUid
        The UID of the Secrets Manager Application record.

    .PARAMETER UserUid
        The username or email of the user whose permissions need update.

    .PARAMETER Removed
        Optional string flag to indicate user should be unshared. If null, grants are applied instead.

    .EXAMPLE
        Update-KeeperShareUserPermissions -Vault $vault -ApplicationUid "abc123" -UserUid "bob@example.com"

        Update-KeeperShareUserPermissions -Vault $vault -ApplicationUid "abc123" -UserUid "bob@example.com" -Removed "true"
    #>


    param (
        [Parameter(Mandatory)]
        $vault,

        [Parameter(Mandatory)]
        [string]$ApplicationUid,

        [Parameter(Mandatory)]
        [string]$UserUid,

        [string]$Removed
    )
    $ApplicationUidsForShare = [System.Collections.Generic.List[string]]::new()
    $ApplicationUidsForShare.Add($ApplicationUid)

    $appShares = $vault.GetSharesForRecords($ApplicationUidsForShare).GetAwaiter().GetResult()
    $userPermissions = ($appShares | Where-Object { $_.RecordUid -eq $ApplicationUid }).UserPermissions

    $appInfo = $vault.GetSecretManagerApplication($ApplicationUid, $true).GetAwaiter().GetResult()

    $recordTypeEnum = [KeeperSecurity.Vault.SecretManagerSecretType]::Record
    $folderTypeEnum = [KeeperSecurity.Vault.SecretManagerSecretType]::Folder

    $shareUids = $appInfo.Shares | ForEach-Object { $_.SecretUid }
    $sharesRecords = $appInfo.Shares | Where-Object { $_.SecretType -eq $recordTypeEnum } | ForEach-Object { $_.SecretUid }
    $sharedFolders = $appInfo.Shares | Where-Object { $_.SecretType -eq $folderTypeEnum } | ForEach-Object { $_.SecretUid }

    $RecordUidsForShare = [System.Collections.Generic.List[string]]::new()
    if($null -ne $sharesRecords){
        $sharesRecords | ForEach-Object { $RecordUidsForShare.Add($_) }
    }
    $recordShares = $vault.GetSharesForRecords($RecordUidsForShare).GetAwaiter().GetResult()

    $admins = $userPermissions | Where-Object { $_.CanEdit -and $_.Username -ne $vault.Auth.Username } | Select-Object -ExpandProperty Username
    $viewers = $userPermissions | Where-Object { -not $_.CanEdit } | Select-Object -ExpandProperty Username
    $removedUsers = if ($Removed) { @($UserUid) } else { @() }

    $appUsersMap = @{
        "admins"  = $admins
        "viewers" = $viewers
        "removed" = $removedUsers
    }

    foreach ($group in $appUsersMap.Keys) {
        $usersList = $appUsersMap[$group]
        if ($usersList.Count -eq 0) { continue }

        $userResults = @()
        foreach ($user in $usersList) {
            $needsUpdate = Test-KeeperUserNeedsUpdate -Vault $vault -User $user -IsAdmin:($group -eq "admins") -ShareUids $shareUids -ApplicationUid $ApplicationUid
            if ($needsUpdate) {
                $userResults += $user
            }
        }

        $shareFolderAction = if ($Removed) { "remove" } else { "grant" }
        $shareRecordAction = if ($Removed) { "revoke" } else { "share" }

        Invoke-KeeperSharedFolderShareAction -Vault $vault -ApplicationUid $ApplicationUid -SharedFolders $sharedFolders -Group $group -Users $userResults -ShareFolderAction $shareFolderAction
        Invoke-KeeperHandleRecordShares -Vault $vault -ApplicationUid $ApplicationUid -RecordShares $recordShares -Group $group -Users $userResults -ShareRecordAction $shareRecordAction
    }
}

function Test-KeeperUserIsSharable {
    <#
    .SYNOPSIS
        Verifies if a given user UID is eligible for record sharing.

    .DESCRIPTION
        Calls `GetUsersForShare()` and checks if the specified user is in `GroupUsers` or `SharesWith`.
        Throws if no shareable users are available at all.

    .PARAMETER vault
        The VaultOnline object.

    .PARAMETER UserUid
        The UID or email of the user to validate for sharing.

    .OUTPUTS
        Boolean — $true if the user can be shared with, otherwise $false.

    .EXAMPLE
        Test-KeeperUserIsSharable -Vault $vault -UserUid "alice@example.com"
    #>


    param (
        [Parameter(Mandatory)]
        $vault,

        [Parameter(Mandatory)]
        [string]$UserUid
    )

    $users = $vault.GetUsersForShare().GetAwaiter().GetResult()

    if (-not $users -or ($users.SharesFrom.Count -eq 0 -and $users.GroupUsers.Count -eq 0 -and $users.SharesWith.Count -eq 0)) {
        Write-Error "No users found for sharing."
        throw "No users found for sharing. [ShareSecretsManagerApplicationWithUser: userUid=$UserUid]"
    }

    if ($users.GroupUsers -contains $UserUid -or $users.SharesWith -contains $UserUid) {
        return $true
    }
    else {
        Write-Host "User '$UserUid' is not found in the list of users eligible for sharing."
        return $false
    }
}

function Invoke-KeeperHandleAppSharePermissions {
    <#
    .SYNOPSIS
        Shares or revokes the Keeper Secrets Manager application record with a user.

    .DESCRIPTION
        If sharing, checks whether the user is eligible for sharing. If not, sends an invite.
        If unsharing, revokes the user's access to the application record.

    .PARAMETER Vault
        The VaultOnline object.

    .PARAMETER AppInfo
        The AppInfo object representing the application and its shares.

    .PARAMETER UserUid
        The UID (email) of the user.

    .PARAMETER IsAdmin
        If true, grants CanEdit and CanShare permissions.

    .PARAMETER Unshare
        If true, revokes the share instead of granting it.

    .EXAMPLE
        Invoke-KeeperHandleAppSharePermissions -Vault $vault -AppInfo $appInfo -UserUid "alice@example.com" -IsAdmin $true -Unshare:$false
    #>


    param (
        [Parameter(Mandatory)]
        $Vault,

        [Parameter(Mandatory)]
        $AppInfo,

        [Parameter(Mandatory)]
        [string]$UserUid,

        [Parameter(Mandatory)]
        [bool]$IsAdmin,

        [Parameter(Mandatory)]
        [bool]$Unshare
    )

    $recordUid = $AppInfo.Uid

    if (-not $Unshare) {
        $isShareable = Test-KeeperUserIsSharable -Vault $Vault -UserUid $UserUid

        if (-not $isShareable) {
            $Vault.SendShareInvitationRequest($UserUid).GetAwaiter().GetResult() | Out-Null
            Write-Host "Share invitation request has been sent to user '$UserUid'. Please wait for the user to accept before sharing the application."
            return
        }

        $recordPermissions = New-Object KeeperSecurity.Vault.SharedFolderRecordOptions
        $recordPermissions.CanEdit = $IsAdmin
        $recordPermissions.CanShare = $IsAdmin

        Write-Debug "Sharing application record '$recordUid' with user '$UserUid' (Edit: $IsAdmin, Share: $IsAdmin)..."
        $Vault.ShareRecordWithUser($recordUid, $UserUid, $recordPermissions).GetAwaiter().GetResult() | Out-Null
    }
    else {
        Write-Debug "Revoking user '$UserUid' from application record '$recordUid'..."
        $Vault.RevokeShareFromUser($recordUid, $UserUid).GetAwaiter().GetResult() | Out-Null
    }
}


function Invoke-KeeperAppShareWithUser {
    <#
    .SYNOPSIS
        Shares or unshares a Secrets Manager Application with a user.

    .DESCRIPTION
        Handles full flow of granting/revoking access to an application record, its shared folders,
        and records by calling permission update helpers and syncing the vault state.

    .PARAMETER ApplicationId
        UID of the Application record.

    .PARAMETER UserUid
        Username or email of the target user.

    .PARAMETER Unshare
        Switch to unshare instead of share.

    .PARAMETER IsAdmin
        Whether to give full edit/share/manage access when sharing.

    .EXAMPLE
        Invoke-KeeperAppShareWithUser -Vault $vault -ApplicationId "abc123" -UserUid "alice@example.com" -IsAdmin $true

    .EXAMPLE
        Invoke-KeeperAppShareWithUser -Vault $vault -ApplicationId "abc123" -UserUid "bob@example.com" -Unshare
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ApplicationId,

        [Parameter(Mandatory)]
        [string]$UserUid,

        [Parameter()]
        [switch]$Unshare,

        [Parameter()]
        [switch]$IsAdmin
    )

    [KeeperSecurity.Vault.VaultOnline]$private:vault = getVault
    $record = $null
    
    if (-not $vault) {
        return $null
    }

    if (-not $vault.TryGetKeeperRecord($ApplicationId, [ref]$record)) {
        throw "Application record not found for UID '$ApplicationId'"
    }

    if (-not ($record -is [KeeperSecurity.Vault.ApplicationRecord])) {
        throw "Record with UID '$ApplicationId' is not an application record."
    }

    $application = [KeeperSecurity.Vault.ApplicationRecord]$record

    $appInfo = $vault.GetSecretManagerApplication($ApplicationId, $true).GetAwaiter().GetResult()

    if (-not $appInfo) {
        throw "AppInfo not found for application UID '$($application.Uid)'"
    }

    Invoke-KeeperHandleAppSharePermissions -Vault $vault -AppInfo $appInfo -UserUid $UserUid -IsAdmin $IsAdmin.IsPresent -Unshare $Unshare.IsPresent

    $vault.SyncDown().GetAwaiter().GetResult() | Out-Null

    $removedUser = if ($Unshare.IsPresent) { $UserUid } else { $null }
    Update-KeeperShareUserPermissions -Vault $vault -ApplicationUid $ApplicationId -UserUid $UserUid -Removed $removedUser

    # Do a Full Sync
    $vault.Storage.Clear()
    $vault.Storage.VaultSettings.Load()
    $vault.ScheduleSyncDown([TimeSpan]::FromMilliseconds(0)) | Out-Null
}

function Grant-KeeperAppAccess {
    <#
    .SYNOPSIS
        Grants a user access to a Secrets Manager Application.

    .DESCRIPTION
        Shares the application record, associated shared folders, and records
        with the specified user. Supports admin or viewer level access.

    .PARAMETER ApplicationId
        UID of the application record.

    .PARAMETER UserUid
        Email or UID of the user to grant access to.

    .PARAMETER IsAdmin
        If specified, grants edit/share/manage permissions (admin access).

    .EXAMPLE
        Grant-KeeperAppAccess -ApplicationId "abc123" -UserUid "alice@example.com" -IsAdmin
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ApplicationId,

        [Parameter(Mandatory)]
        [string]$UserUid,

        [Parameter()]
        [switch]$IsAdmin
    )

    try {
        Write-Host "Granting Secrets Manager application access to '$UserUid'..." -ForegroundColor Cyan

        Invoke-KeeperAppShareWithUser -ApplicationId $ApplicationId -UserUid $UserUid -IsAdmin:$IsAdmin

        Write-Host "Successfully granted access to application '$ApplicationId' for user '$UserUid'." -ForegroundColor Green
    }
    catch {
        Write-Host "Failed to grant access to '$UserUid'. Error: $_" -ForegroundColor Red
    }
}


function Revoke-KeeperAppAccess {
    <#
    .SYNOPSIS
        Revokes a user's access to a Secrets Manager Application.

    .DESCRIPTION
        Unshares the application record, shared folders, and any related records
        from the specified user and updates permissions accordingly.

    .PARAMETER ApplicationId
        UID of the application record.

    .PARAMETER UserUid
        Email or UID of the user to revoke access from.

    .EXAMPLE
        Revoke-KeeperAppAccess -ApplicationId "abc123" -UserUid "bob@example.com"
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ApplicationId,

        [Parameter(Mandatory)]
        [string]$UserUid
    )

    try {
        Write-Host "Revoking Secrets Manager application access from '$UserUid'..." -ForegroundColor Cyan

        Invoke-KeeperAppShareWithUser -ApplicationId $ApplicationId -UserUid $UserUid -Unshare

        Write-Host "Successfully revoked access to application '$ApplicationId' from user '$UserUid'." -ForegroundColor Green
    }
    catch {
        Write-Host "Failed to revoke access from '$UserUid'. Error: $_" -ForegroundColor Red
    }
}

# SIG # Begin signature block
# MIIngQYJKoZIhvcNAQcCoIIncjCCJ24CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAlYMV067liZ2W0
# +Dy4ui2SqD2AL0xDKQHMpq8CtMXFL6CCIQQwggWNMIIEdaADAgECAhAOmxiO+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
# twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG
# SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx
# CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy
# RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg
# Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH
# JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf
# UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w
# 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk
# tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb
# qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm
# cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6
# 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK
# QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo
# 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB
# Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche
# MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB
# /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU
# 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG
# CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j
# c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig
# NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v
# dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI
# hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd
# 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC
# qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl
# /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC
# RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT
# gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/
# a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37
# xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL
# NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0
# YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ
# RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG
# sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0
# ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN
# BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr
# PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM
# gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg
# nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC
# EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0
# p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa
# khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0
# XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I
# HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2
# FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH
# X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2
# 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD
# VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k
# TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD
# AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj
# ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t
# L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww
# HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB
# ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j
# fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI
# moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf
# JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx
# oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3
# LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx
# 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9
# Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I
# Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug
# 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5
# Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQ
# C65mvFq6f5WHxvnpBOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0
# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAw
# MDAwMFoXDTM1MTEyNTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp
# Z2lDZXJ0MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjE
# iDtqmeOlwf0KMCBDEr4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOc
# Re8+CEJp+3R2O8oo76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/
# GLoUb35SfWHh43rOH3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0Cha
# V76Nhnj37DEYTX9ReNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8U
# uKGn9966fR5X6kgXj3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHw
# SJ+QQRZ1fisD8UTVDSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4
# EfvFrpVNnes4c16Jidj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzI
# Xp4P0wXkgNs+CO/CacBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3Jyidx
# W48jwBqIJqImd93NRxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizch
# NULpUEoA6Vva7b1XCB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJ
# cv6dQ4aEKOX5AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/
# BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE
# AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w
# HQYDVR0OBBYEFJ9XLAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuG
# SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw
# OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG
# TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT
# QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB
# AD2tHh92mVvjOIQSR9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq
# 3igpwrPvBmZdrlWBb0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcH
# zBMutB6HzeledbDCzFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTV
# OoJ4eTq7gj9UFAL1UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4H
# v5swO+aAXxWUm3WpByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgt
# d7/fvWTlCs30VAGEsshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaid
# RJXrI+UzB6vAlk/8a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhd
# mm4bhYsVA6G2WgNFYagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dH
# PoWrUhftNpFC5H7QEY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDi
# CLg4D+TPVgKx2EgEdeoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7z
# cEO1xwcdcqJsyz/JceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHSTCCBTGgAwIBAgIQ
# BaOjGrg1T58olh09AgdhuDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEX
# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0
# ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTI0
# MTIzMTAwMDAwMFoXDTI1MTIzMDIzNTk1OVowgdExEzARBgsrBgEEAYI3PAIBAxMC
# VVMxGTAXBgsrBgEEAYI3PAIBAhMIRGVsYXdhcmUxHTAbBgNVBA8MFFByaXZhdGUg
# T3JnYW5pemF0aW9uMRAwDgYDVQQFEwczNDA3OTg1MQswCQYDVQQGEwJVUzERMA8G
# A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xHTAbBgNVBAoTFEtlZXBl
# ciBTZWN1cml0eSBJbmMuMR0wGwYDVQQDExRLZWVwZXIgU2VjdXJpdHkgSW5jLjCC
# AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAM7/rBevApUP+XJjlSxdyASA
# AnLFQ1r4NFXPo/S0RaTv1OCahApEeSN6oy+0OwbLNlwaQeooOanMcZhh64/+fF8S
# zCMHDc/Pv8aBsd1B2XIw/VT+Nawfj0NxAX1zpKPp/tPqavm6smRDMOAeOo7qLxzI
# u68bS2EnqvST1367tMpxhggrVl3GYKPhdCPeNDRskwheCSxI2czR8oe7mguo2nVa
# ZR5VEq4xYkMZwTuT7RN8ER4r5crOSbJFyabp79SgYP7NyKmDcYZ6XJ26AfZsEDZr
# e4VhzaqO0rl8i5HBmVmDKwU0PaIoAUdyeultIaS5oe0FjcTjGtrkBl+B7TCtvN1J
# RE9Tmy3spnqLyvlRhrVJdDKCGovQKKJk87BAjIoiNSmEXs0H0PbB1ZYOA6m4ce7/
# BOmUafliYWBqrWHmHixqi/ha5ZKxKlYxGlikD4p1WlMmDEBhg3RPodW1Z5eGq92Z
# exMGOWsfOQp3YhTDdMOA7tjWP2XzAaebGxCeOENEpQIDAQABo4ICAjCCAf4wHwYD
# VR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYEFOcovsKg6xAz
# zjzRmmWQRpa7p47MMD0GA1UdIAQ2MDQwMgYFZ4EMAQMwKTAnBggrBgEFBQcCARYb
# aHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB/wQEAwIHgDATBgNV
# HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3J0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggIBALIq
# AoEjkKZluMiOffwU+V+wiKkmDblKIZymyszEZot+niB6g7tRXrWkQo6gn8OG2qG6
# IO8L+o0VvwW0+V08p6gVqb0jeR9kCm7kDZk2RmzevhZDrRbZj0Q7Kb3pIeD9KEuc
# RfEF0UGqgp0q7jerFXPzKtQk5kJpP65sSRV7bghIMWtq5sdHn/iGUMj+8Fd9AExq
# 4kR+dyTw/6p1ZFiY7pIv4YAjjDrjkyUMSogt6ej9YGwTC8yVXJsjarLq2F+svwn8
# NlU+T03U/ZjXc/ZxDc5g3iqrl5Gm9QCaLhG2aLIrGRXN59Pcokp7JFNa6nkkWSSg
# h4w01tz+xRSyiqKWAXNs2lHTD2F9ceGlz9Uw/RvPhPcl6bILqJcR6RUkzZtrKHNK
# j85PBm/Kmurx0co5xRxXsXsF3tmp2r+Tt11veA9je+pyzuqE/kRQPn5hF8fIRuea
# h7JVMaaHBTMbRaDcVFioGmCGHUx270yhLapA0eYXpZJv0n62QIMoX9NPcW2EcwhL
# WGAV1IW+TIo/xcprAXBtXCO/mhscgInbMzesdg0uWsboiy4HfeTEzCe9ld54biUK
# TJQu4wqbzkN5SGewOKTd/+c4k5w6yzuUWsk3YZpjWqsgpTlA3zU591uvMFsq0FYd
# A3Py8YsVabLwTxz9d7kpBAHTPRYwDcsKNLGMPc+6MYIF0zCCBc8CAQEwfTBpMQsw
# CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERp
# Z2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIw
# MjEgQ0ExAhAFo6MauDVPnyiWHT0CB2G4MA0GCWCGSAFlAwQCAQUAoIGEMBgGCisG
# AQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEID9n
# aY2/CqblulAcECruw3mVbH7ZqYlKOy3HW70R2MLpMA0GCSqGSIb3DQEBAQUABIIB
# gLzMWmrj/ijDVKRhqKhlWaUR0AAFAUDLjKrr9e1DKWhx8GU6XrJRhCMFoCZECyrp
# 7bmDTI0iezbIVRwMGC4yIwHwe/Cm3o+wX8I8DbpSHMt+6ydSHXX6ofMD+k6oYRhw
# OiUkhkiVWRFycHeKn4DLc1VljoJO34ikOxedg+WJq8AbJNWE2ggJi2pDOK7pjrYf
# uvHQGvcxZx0REaNrorEhdmZooVr1QCMQwdHpv6+HA/mLrxOBe9X6C7L8jkmw7Eo3
# PXd+qXK34mUTGWP3VZ7H1WKfFFGhjuHoKmiLKr7wQYMbcErum759q55pMqJtPW24
# rHBO5QzaixgriWbba6AZ9XlSvdQ2+2Tj1jqTAT1oVZCxqNO6TwzM024RToysFsOc
# /+zl16bztFEMZM/PVFMkIwCzv9lmjxIJpex3wZWtUcpHo0A/2wnoG9hSfbS6ejRZ
# AgMbKt/2OfllfwbjAqNrWqQV8kcs9Mey0imnzGPSy6G97K+rmXJAKaNkBdJOf9Ej
# 8aGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+V
# h8b56QTjMwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yNTA2MzAxNzI2MTlaMC8GCSqGSIb3DQEJBDEi
# BCBroRcHt67Liqyd9gvKThK4TvE96OIQj/9e9nttakvH4TANBgkqhkiG9w0BAQEF
# AASCAgAqpTqKtNe7EiLwfJLw6beNmEevziebcTzla+cfIldvDrpLLhgcdtDe1vR+
# Lq0Ncf7G7PZWrKTG3LjIFjTZyLNXUv1qB5mDVQJmYRqg7JXjBt0eRBVCJqwoGxQK
# /M87QmEmydcXBPZ6nAOaDTRyYx3Cg4CLoBQqG+NMOU2PL5FLVOyPoCoF4ihYDJdY
# QHdGL2LFoc6rvHj8g8SeVn6QSLcmGN/mRYwF0aSp8h/48P3vuft8W4orjP89n/fs
# 5Isk5u1i9WYWj9PhKbm7CrZQE54mwnR6snz5e1TZIlzHu4+tiWTR6AqCwmhQpr6n
# cc0a1GOApvlXCgkizEKxPDOwaLG58cz/5IqWXOtAQ2JSlCsCnNpn02K6syUK61Gl
# XsQUCUq9mMfNJKrqhXq3GNfS6SDPtYoETkr3B+m0cLBkjci3RcOaYoYeFMzQyQyS
# 5qBMHSI6fnw49gqFAem/qdLFiF/tEg3lzTmOxxPTPeKiSrLrCUvRteHxGaYmBXRI
# Vsat1Rb0IIJJzxC3y/daKQuRGL7AXBLHehr4bNkuQAw8pH4X2x2BI/45kNm+p/eC
# Azay54Jv1c0Xk7Toa2hqOBQrLmfhd9zGVl9ItoERQh28q3D3EZZnddy1ZNfIINEU
# +Td+sL9TVCURmykNjtaBJPepyeu9relt0r60u0WAkfDIBWVuZA==
# SIG # End signature block