EnterpriseUser.ps1

# Enterprise User Management Functions

function Add-KeeperEnterpriseUser {
    <#
    .SYNOPSIS
    Invites Enterprise Users

    .PARAMETER Node
    Node Name or ID

    .PARAMETER Email
    Email address to invite

    .PARAMETER Emails
    Extra email addresses to invite
    #>

    [CmdletBinding()]
    Param (
        [Parameter()][string] $FullName,        
        [Parameter()][string] $Node,        
        [Parameter(Position = 0, Mandatory = $true)] $Email,
        [Parameter(ValueFromRemainingArguments = $true)] $Emails
    )

    [Enterprise]$enterprise = getEnterprise
    [Int64] $nodeId = 0
    if ($Node) {
        $n = resolveSingleNode $Node
        if ($n) {
            $nodeId = $n.Id
        }
    } else {
        $nodeId = $enterprise.enterpriseData.RootNode.Id
    }

    $inviteOptions = New-Object KeeperSecurity.Enterprise.InviteUserOptions
    if ($nodeId -gt 0) {
        $inviteOptions.NodeId = $nodeId
    }
    if ($FullName) {
        $inviteOptions.FullName = $FullName
    }

    $user = $enterprise.enterpriseData.InviteUser($Email, $inviteOptions).GetAwaiter().GetResult()
    if ($user) {
        Write-Output "User `"$Email`" is invited"
    }

    $inviteOptions.FullName = $null
    foreach ($e in $Emails) {
        $user = $enterprise.enterpriseData.InviteUser($e, $inviteOptions).GetAwaiter().GetResult()
        if ($user) {
            Write-Output "User `"$e`" is invited"
        }
    }
}
New-Alias -Name invite-user -Value Add-KeeperEnterpriseUser

function Lock-KeeperEnterpriseUser {
    <#
        .Synopsis
        Locks Enterprise User

        .Parameter User
        User email, enterprise Id, or instance.
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $userObject = resolveUser $enterprise.enterpriseData $User
    $saved = $enterprise.enterpriseData.SetUserLocked($userObject, $true).GetAwaiter().GetResult()
    if ($saved) {
        Write-Output "User `"$($saved.Email)`" was locked"
    }
}
Register-ArgumentCompleter -CommandName Lock-KeeperEnterpriseUser -ParameterName User -ScriptBlock $Keeper_ActiveUserCompleter
New-Alias -Name lock-user -Value Lock-KeeperEnterpriseUser

function Unlock-KeeperEnterpriseUser {
    <#
        .Synopsis
        Unlocks Enterprise User

        .Parameter User
        User email, enterprise Id, or instance.
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $userObject = resolveUser $enterprise.enterpriseData $User
    $saved = $enterprise.enterpriseData.SetUserLocked($userObject, $false).GetAwaiter().GetResult()
    if ($saved) {
        Write-Output "User `"$($saved.Email)`" was unlocked"
    }
}
Register-ArgumentCompleter -CommandName Unlock-KeeperEnterpriseUser -ParameterName User -ScriptBlock $Keeper_LockedUserCompleter
New-Alias -Name unlock-user -Value Unlock-KeeperEnterpriseUser

function Move-KeeperEnterpriseUser {
    <#
        .Synopsis
        Transfers an enterprise user's vault (records, shared folders, teams) to another user

        .Parameter FromUser
        Email or enterprise user ID of the source user whose vault will be transferred

        .Parameter TargetUser
        Email or enterprise user ID of the destination user who will receive the vault

        .Parameter Force
        Skip confirmation prompt in non-interactive sessions

        .Description
        Transfers all records, shared folders, and team memberships from one enterprise user to another.
        The source user's vault contents are merged into the target user's vault. Use -Force to skip
        confirmation in automated scripts.

        .Example
        Move-KeeperEnterpriseUser -FromUser "departing@company.com" -TargetUser "replacement@company.com"
        Transfers the departing user's vault to the replacement user (prompts for confirmation)

        .Example
        transfer-user "departing@company.com" "replacement@company.com" -Force
        Transfers vault without confirmation prompt
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$FromUser,
        [Parameter(Position = 1, Mandatory = $true)]$TargetUser,
        [Switch] $Force
    )

    [Enterprise]$enterprise = getEnterprise

    $fromUserObject = resolveUser $enterprise.enterpriseData $FromUser
    if (-not $fromUserObject) {
        return
    }
    $targetUserObject = resolveUser $enterprise.enterpriseData $TargetUser
    if (-not $targetUserObject) {
        return
    }
    if (-not $Force.IsPresent) {
        if (Test-InteractiveSession) {
            Write-Output "This action cannot be undone.`n"
            $answer = Read-Host -Prompt "Do you want to proceed with transferring $($fromUserObject.Email) account (Yes/No)? > "
        }
        else {
            Write-Output('Non-interactive session. Use -Force parameter')
            $answer = 'no'
        }
        if ($answer -ne 'yes' -and $answer -ne 'y') {
            return
        }
    }
    $transferResult = $enterprise.enterpriseData.TransferUserAccount($enterprise.roleData, $fromUserObject, $targetUserObject).GetAwaiter().GetResult()
    if ($transferResult) {
        Write-Information "Successfully Transferred:"
        Write-Information " Records: $($transferResult.RecordsTransfered)"
        Write-Information " Shared Folders: $($transferResult.SharedFoldersTransfered)"
        Write-Information " Team: $($transferResult.TeamsTransfered)"
        if ($transferResult.RecordsCorrupted -gt 0 -or $transferResult.SharedFoldersCorrupted -gt 0 -or $transferResult.TeamsCorrupted -gt 0) {
            Write-Information "Failed to Transfer:"
            if ($transferResult.RecordsCorrupted -gt 0) {
                Write-Information " Records: $($transferResult.RecordsCorrupted)"
            }
            if ($transferResult.SharedFoldersCorrupted -gt 0) {
                Write-Information " Shared Folders: $($transferResult.SharedFoldersCorrupted)"
            }
            if ($transferResult.TeamsCorrupted -gt 0) {
                Write-Information " Team: $($transferResult.TeamsCorrupted)"
            }
        }
    }
}
Register-ArgumentCompleter -CommandName Move-KeeperEnterpriseUser -ParameterName FromUser -ScriptBlock $Keeper_LockedUserCompleter
Register-ArgumentCompleter -CommandName Move-KeeperEnterpriseUser -ParameterName TargetUser -ScriptBlock $Keeper_ActiveUserCompleter
New-Alias -Name transfer-user -Value Move-KeeperEnterpriseUser

function Remove-KeeperEnterpriseUser {
    <#
        .Synopsis
        Removes Enterprise User

        .Parameter User
        User email, enterprise Id, or instance.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$User,
        [Switch] $Force
    )

    [Enterprise]$enterprise = getEnterprise
    $userObject = resolveUser $enterprise.enterpriseData $User
    if (-not $Force.IsPresent) {
        Write-Output  "`nDeleting a user will also delete any records owned and shared by this user."
        "Before you delete this user, we strongly recommend you lock their account"
        "and transfer any important records to other user.`n"
        "This action cannot be undone."

        if ($PSCmdlet.ShouldProcess($userObject.Email, "Removing Enterprise User")) {
            $enterprise.enterpriseData.DeleteUser($userObject).GetAwaiter().GetResult() | Out-Null
            Write-Output "User $($userObject.Email) has been deleted"
        }
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseUser -ParameterName User -ScriptBlock $Keeper_EnterpriseUserCompleter
New-Alias -Name delete-user -Value Remove-KeeperEnterpriseUser

function Invoke-ResendKeeperEnterpriseInvite {
    <#
        .Synopsis
        Resends enterprise invitation email to a user

        .Parameter User
        User email address

        .Description
        Resends the enterprise invitation email to a user who has not yet accepted their invitation.
        The user must be in Inactive status (not yet accepted invitation).

        .Example
        Invoke-ResendKeeperEnterpriseInvite -User "user@example.com"
        Resends invitation email to user@example.com
    #>

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

    [Enterprise]$enterprise = getEnterprise
    
    [KeeperSecurity.Enterprise.EnterpriseUser]$userObject = $null
    
    if ($enterprise.enterpriseData.TryGetUserByEmail($User, [ref]$userObject)) {
        # here user is found by email
        Write-Debug "User `"$User`" found by email"
    }
    elseif ($User -match '^\d+$') {
        $userId = [long]$User
        if (-not $enterprise.enterpriseData.TryGetUserById($userId, [ref]$userObject)) {
            Write-Error "User `"$User`" not found" -ErrorAction Stop
            return
        }
        # here user is found by ID
        Write-Debug "User `"$User`" found by ID"
    }
    else {
        # here user is not found by email or ID
        Write-Error "User `"$User`" not found" -ErrorAction Stop
        return
    }

    if ($userObject.UserStatus -ne [KeeperSecurity.Enterprise.UserStatus]::Inactive) {
        Write-Error "User has already accepted invitation. Only inactive users can have invitations resent." -ErrorAction Stop
        return
    }

    try {
        $enterprise.enterpriseData.ResendEnterpriseInvite($userObject).GetAwaiter().GetResult() | Out-Null
        Write-Output "Invite for $User resent."
    }
    catch {
        Write-Error "Failed to resend invite: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Invoke-ResendKeeperEnterpriseInvite -ParameterName User -ScriptBlock $Keeper_EnterpriseUserCompleter

function Set-KeeperEnterpriseUserMasterPasswordExpire {
    <#
        .Synopsis
        Sets master password expiration for an enterprise user

        .Parameter User
        User email address

        .Description
        Sets the master password expiration for an active enterprise user, requiring them to change their password on next login.

        .Example
        Set-KeeperEnterpriseUserMasterPasswordExpire -User "user@example.com"
        Sets master password expiration for user@example.com
    #>

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

    [Enterprise]$enterprise = getEnterprise
    
    $userObject = resolveUser $enterprise.enterpriseData $User
    if (-not $userObject) {
        Write-Error "User `"$User`" not found" -ErrorAction Stop
        return
    }

    if ($userObject.UserStatus -ne [KeeperSecurity.Enterprise.UserStatus]::Active) {
        Write-Error "User $User is not active" -ErrorAction Stop
        return
    }

    try {
        $enterprise.enterpriseData.SetMasterPasswordExpire($userObject.Email).GetAwaiter().GetResult() | Out-Null
        Write-Output "Master password expiration set for $User"
    }
    catch {
        Write-Error "Failed to set master password expiration: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Set-KeeperEnterpriseUserMasterPasswordExpire -ParameterName User -ScriptBlock $Keeper_ActiveUserCompleter

class EnterpriseUserAliasState {
    [string]$NormalizedAlias
    [string]$ExistingSecondaryAlias
    [bool]$IsAlreadyPrimary
    [System.Collections.Generic.List[string]]$Aliases

    EnterpriseUserAliasState([string]$normalizedAlias) {
        $this.NormalizedAlias = $normalizedAlias
        $this.Aliases = [System.Collections.Generic.List[string]]::new()
        $this.IsAlreadyPrimary = $false
    }
}

function Get-MatchingEnterpriseUserAlias {
    param(
        [Parameter(Mandatory)][System.Collections.Generic.List[string]]$Aliases,
        [Parameter(Mandatory)][string]$Alias
    )
    $normalizedAlias = $Alias.Trim().ToLowerInvariant()
    foreach ($existingAlias in $Aliases) {
        if ($existingAlias.ToLowerInvariant() -eq $normalizedAlias) { return $existingAlias }
    }
    return $null
}

function Get-EnterpriseUserAliasState {
    param(
        [Parameter(Mandatory)][Enterprise]$Enterprise,
        [Parameter(Mandatory)][KeeperSecurity.Enterprise.EnterpriseUser]$User,
        [Parameter(Mandatory)][string]$AliasEmail
    )

    $state = [EnterpriseUserAliasState]::new($AliasEmail.Trim().ToLowerInvariant())
    foreach ($alias in $Enterprise.userAliasData.GetAliasesForUser($User.Id)) {
        [void]$state.Aliases.Add($alias)
    }
    $state.ExistingSecondaryAlias = Get-MatchingEnterpriseUserAlias -Aliases $state.Aliases -Alias $state.NormalizedAlias
    $state.IsAlreadyPrimary = ($User.Email.ToLowerInvariant() -eq $state.NormalizedAlias)
    return $state
}

function Update-EnterpriseUserAliasCache {
    param(
        [Parameter(Mandatory)][Enterprise]$Enterprise
    )
    $Enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null
}

function Set-KeeperEnterpriseUserPrimaryAlias {
    param(
        [Parameter(Mandatory)][Enterprise]$Enterprise,
        [Parameter(Mandatory)][long]$EnterpriseUserId,
        [Parameter(Mandatory)][string]$Alias
    )
    $rq = New-Object Authentication.EnterpriseUserAliasRequest
    $rq.EnterpriseUserId = $EnterpriseUserId
    $rq.Alias = $Alias
    $Enterprise.loader.Auth.ExecuteAuthRest("enterprise/enterprise_user_set_primary_alias", $rq).GetAwaiter().GetResult() | Out-Null
}

function Test-KeeperEnterpriseUserAliasConflict {
    param([string]$Status)
    return ($Status -and $Status.ToLowerInvariant().Contains('conflict'))
}

function Add-KeeperEnterpriseUserAlias {
    <#
        .SYNOPSIS
        Adds an alias email address to an enterprise user

        .DESCRIPTION
        Adds an alternate email alias for an enterprise user. If the alias already exists for the user,
        it is set as the primary alias.

        .PARAMETER User
        User email address, alias email address, or enterprise user ID

        .PARAMETER Alias
        Alias email address to add

        .EXAMPLE
        Add-KeeperEnterpriseUserAlias -User "user@example.com" -Alias "alias@example.com"
        Adds alias@example.com as an alias for user@example.com

        .EXAMPLE
        kuser-alias-add "user@example.com" "alias@example.com"
        Adds an alias using the kuser-alias-add alias
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)][string] $User,
        [Parameter(Position = 1, Mandatory = $true)][string] $Alias
    )

    if ([string]::IsNullOrWhiteSpace($Alias)) {
        Write-Error "Alias parameter is mandatory." -ErrorAction Stop
        return
    }

    $aliasEmail = $Alias.Trim().ToLowerInvariant()

    [Enterprise]$enterprise = getEnterprise
    try {
        Update-EnterpriseUserAliasCache -Enterprise $enterprise
    }
    catch {
        Write-Warning "Enterprise data reload failed before alias lookup: $($_.Exception.Message)"
    }

    $userObject = resolveUser $enterprise.enterpriseData $User $enterprise.userAliasData
    [EnterpriseUserAliasState]$aliasState = Get-EnterpriseUserAliasState -Enterprise $enterprise -User $userObject -AliasEmail $aliasEmail

    if ($aliasState.IsAlreadyPrimary) {
        Write-Output "Alias `"$($userObject.Email)`" already exists for this user."
        return
    }

    try {
        if ($aliasState.ExistingSecondaryAlias) {
            Set-KeeperEnterpriseUserPrimaryAlias -Enterprise $enterprise -EnterpriseUserId $userObject.Id -Alias $aliasState.ExistingSecondaryAlias
            Write-Output "Alias `"$($aliasState.ExistingSecondaryAlias)`" set as primary for user `"$($userObject.Email)`"."
        }
        else {
            $addRq = New-Object Authentication.EnterpriseUserAddAliasRequest
            $addRq.Primary = $true
            $addRq.EnterpriseUserId = $userObject.Id
            $addRq.Alias = $aliasState.NormalizedAlias

            $rq = New-Object Authentication.EnterpriseUserAddAliasRequestV2
            [void]$rq.EnterpriseUserAddAliasRequest.Add($addRq)

            $rs = $enterprise.loader.Auth.ExecuteAuthRest(
                "enterprise/enterprise_user_add_alias",
                $rq,
                [Authentication.EnterpriseUserAddAliasResponse],
                1
            ).GetAwaiter().GetResult()

            $statusList = @($rs.Status)
            if ($statusList.Count -eq 0) {
                Write-Error "Failed to add alias: no status returned from server." -ErrorAction Stop
            }

            $setPrimaryOnConflict = $false
            foreach ($st in $statusList) {
                if ($st.Status -ne 'success') {
                    if (Test-KeeperEnterpriseUserAliasConflict -Status $st.Status) {
                        $setPrimaryOnConflict = $true
                    }
                    else {
                        Write-Error "Failed to add alias for user $($st.EnterpriseUserId): $($st.Status)" -ErrorAction Stop
                    }
                }
            }

            if ($setPrimaryOnConflict) {
                $canonicalAlias = $aliasState.ExistingSecondaryAlias
                if (-not $canonicalAlias) {
                    $canonicalAlias = Get-MatchingEnterpriseUserAlias -Aliases $aliasState.Aliases -Alias $aliasState.NormalizedAlias
                }
                if (-not $canonicalAlias) { $canonicalAlias = $aliasState.NormalizedAlias }
                Set-KeeperEnterpriseUserPrimaryAlias -Enterprise $enterprise -EnterpriseUserId $userObject.Id -Alias $canonicalAlias
                Write-Output "Alias `"$canonicalAlias`" set as primary for user `"$($userObject.Email)`"."
            }
            else {
                Write-Output "Alias `"$($aliasState.NormalizedAlias)`" added for user `"$($userObject.Email)`"."
            }
        }
    }
    catch {
        if (Test-KeeperEnterpriseUserAliasConflict -Status $_.Exception.Message) {
            Set-KeeperEnterpriseUserPrimaryAlias -Enterprise $enterprise -EnterpriseUserId $userObject.Id -Alias $aliasState.NormalizedAlias
            Write-Output "Alias `"$($aliasState.NormalizedAlias)`" set as primary for user `"$($userObject.Email)`"."
        }
        else {
            Write-Error "Failed to add alias: $($_.Exception.Message)" -ErrorAction Stop
        }
    }

    try {
        Update-EnterpriseUserAliasCache -Enterprise $enterprise
    }
    catch {
        Write-Warning "Alias operation succeeded but enterprise data reload failed: $($_.Exception.Message)"
    }
}
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseUserAlias -ParameterName User -ScriptBlock $Keeper_EnterpriseUserCompleter
New-Alias -Name kuser-alias-add -Value Add-KeeperEnterpriseUserAlias

function Remove-KeeperEnterpriseUserAlias {
    <#
        .SYNOPSIS
        Removes an alias email address from an enterprise user

        .DESCRIPTION
        Removes an alternate email alias from an enterprise user.

        .PARAMETER User
        User email address, alias email address, or enterprise user ID

        .PARAMETER Alias
        Alias email address to remove. Cannot be the user's current primary email.

        .PARAMETER Force
        Skip confirmation prompt and remove the alias

        .EXAMPLE
        Remove-KeeperEnterpriseUserAlias -User "user@example.com" -Alias "alias@example.com"
        Removes alias@example.com from user@example.com

        .EXAMPLE
        Remove-KeeperEnterpriseUserAlias -User "user@example.com" -Alias "alias@example.com" -Force
        Removes the alias without a confirmation prompt

        .EXAMPLE
        kuser-alias-remove "user@example.com" "alias@example.com"
        Removes an alias using the kuser-alias-remove alias
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    Param (
        [Parameter(Position = 0, Mandatory = $true)][string] $User,
        [Parameter(Position = 1, Mandatory = $true)][string] $Alias,
        [Parameter()][switch] $Force
    )

    if ([string]::IsNullOrWhiteSpace($Alias)) {
        Write-Error "Alias parameter is mandatory." -ErrorAction Stop
        return
    }

    $aliasEmail = $Alias.Trim().ToLowerInvariant()

    [Enterprise]$enterprise = getEnterprise
    $userObject = resolveUser $enterprise.enterpriseData $User $enterprise.userAliasData

    if ($userObject.Email.ToLowerInvariant() -eq $aliasEmail) {
        Write-Error "Cannot remove the user's primary email address `"$($userObject.Email)`". Set another alias as primary first." -ErrorAction Stop
        return
    }

    if ($Force -or $PSCmdlet.ShouldProcess("$($userObject.Email) -> $aliasEmail", "Remove Enterprise User Alias")) {
        [EnterpriseUserAliasState]$aliasState = Get-EnterpriseUserAliasState -Enterprise $enterprise -User $userObject -AliasEmail $aliasEmail
        $canonicalAlias = $aliasState.ExistingSecondaryAlias
        if (-not $canonicalAlias) {
            $canonicalAlias = $aliasState.NormalizedAlias
        }

        try {
            $rq = New-Object Authentication.EnterpriseUserAliasRequest
            $rq.EnterpriseUserId = $userObject.Id
            $rq.Alias = $canonicalAlias
            $enterprise.loader.Auth.ExecuteAuthRest("enterprise/enterprise_user_delete_alias", $rq).GetAwaiter().GetResult() | Out-Null
            Write-Output "Alias `"$canonicalAlias`" removed from user `"$($userObject.Email)`"."
        }
        catch {
            Write-Error "Failed to remove alias: $($_.Exception.Message)" -ErrorAction Stop
        }

        try {
            Update-EnterpriseUserAliasCache -Enterprise $enterprise
        }
        catch {
            Write-Warning "Alias removal succeeded but enterprise data reload failed: $($_.Exception.Message)"
        }
    }
    else {
        Write-Output "Alias removal cancelled."
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseUserAlias -ParameterName User -ScriptBlock $Keeper_EnterpriseUserCompleter
New-Alias -Name kuser-alias-remove -Value Remove-KeeperEnterpriseUserAlias

function Get-KeeperEnterpriseUserTeam {
    <#
        .SYNOPSIS
        Gets the list of enterprise teams for a user.

        .DESCRIPTION
        Returns teams the specified enterprise user belongs to.

        .PARAMETER User
        User email, enterprise user ID, or EnterpriseUser object.

        .EXAMPLE
        Get-KeeperEnterpriseUserTeam -User "user@example.com"

        .EXAMPLE
        Get-KeeperEnterpriseUserTeam -User 123456789
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $enterpriseData = $enterprise.enterpriseData
    $userObject = resolveUser $enterpriseData $User
    if (-not $userObject) { return @() }

    $teamUids = @($enterpriseData.GetTeamsForUser($userObject.Id) | Select-Object -Unique)
    if ($teamUids.Count -eq 0) { return @() }

    $teams = foreach ($teamUid in $teamUids) {
        $team = $null
        if ($enterpriseData.TryGetTeam($teamUid, [ref]$team)) { $team }
    }
    return @($teams | Sort-Object { $_.Name })
}
Register-ArgumentCompleter -CommandName Get-KeeperEnterpriseUserTeam -ParameterName User -ScriptBlock $Keeper_EnterpriseUserCompleter
New-Alias -Name keut -Value Get-KeeperEnterpriseUserTeam

function Update-KeeperEnterpriseTeamUser {
    <#
        .Synopsis
        Updates a user's type in an enterprise team

        .Parameter Team
        Team name or UID

        .Parameter User
        User email address

        .Parameter UserType
        User type: 0, 1, or 2. 0 = User (normal member), 1 = Administrator (can manage team, sees shared folders), 2 = Administrator Only (can manage team but does not see shared folders)

        .Description
        Updates the user type for a user in a specific enterprise team. UserType must be 0, 1, or 2.

        .Example
        Update-KeeperEnterpriseTeamUser -Team "Engineering" -User "user@example.com" -UserType 1
        Makes user@example.com an Administrator in the Engineering team

        .Example
        Update-KeeperEnterpriseTeamUser -Team "Engineering" -User "user@example.com" -UserType 0
        Makes user@example.com a normal User in the Engineering team
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)][string] $Team,
        [Parameter(Position = 1, Mandatory = $true)][string] $User,
        [Parameter(Position = 2, Mandatory = $true)][int] $UserType
    )

    [Enterprise]$enterprise = getEnterprise
    
    if ([string]::IsNullOrWhiteSpace($Team)) {
        Write-Error "Team name parameter is mandatory." -ErrorAction Stop
        return
    }

    if ([string]::IsNullOrWhiteSpace($User)) {
        Write-Error "User email parameter is mandatory." -ErrorAction Stop
        return
    }

    if ($UserType -lt 0 -or $UserType -gt 2) {
        Write-Error "User type must be 0, 1, or 2" -ErrorAction Stop
        return
    }

    $teamObject = resolveTeam $enterprise.enterpriseData $Team
    if (-not $teamObject) {
        Write-Error "Team `"$Team`" not found" -ErrorAction Stop
        return
    }

    $userObject = resolveUser $enterprise.enterpriseData $User
    if (-not $userObject) {
        Write-Error "User `"$User`" not found" -ErrorAction Stop
        return
    }

    if ($userObject.UserStatus -ne [KeeperSecurity.Enterprise.UserStatus]::Active) {
        Write-Error "User $User is not active" -ErrorAction Stop
        return
    }

    try {
        $enterprise.enterpriseData.TeamEnterpriseUserUpdate($teamObject, $userObject, $UserType).GetAwaiter().GetResult() | Out-Null
        Write-Output "Team user $User updated"
    }
    catch {
        Write-Error "Failed to update team user: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Update-KeeperEnterpriseTeamUser -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
Register-ArgumentCompleter -CommandName Update-KeeperEnterpriseTeamUser -ParameterName User -ScriptBlock $Keeper_ActiveUserCompleter

function Update-KeeperEnterpriseUser {
    <#
        .Synopsis
        Updates enterprise user information

        .Parameter User
        User email address

        .Parameter Node
        Node name or ID (optional)

        .Parameter FullName
        User's full name (optional)

        .Parameter JobTitle
        User's job title (optional)

        .Parameter InviteeLocale
        User's locale for invitations (optional)

        .Description
        Updates enterprise user information including node assignment, full name, job title, and invitee locale.
        If node is not specified, the user's current parent node is used.

        .Example
        Update-KeeperEnterpriseUser -User "user@example.com" -FullName "John Doe" -JobTitle "Software Engineer"
        Updates user's full name and job title

        .Example
        Update-KeeperEnterpriseUser -User "user@example.com" -Node "Engineering" -InviteeLocale "en-US"
        Moves user to Engineering node and sets locale
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)][string] $User,
        [Parameter()][string] $Node,
        [Parameter()][string] $FullName,
        [Parameter()][string] $JobTitle,
        [Parameter()][string] $InviteeLocale
    )

    [Enterprise]$enterprise = getEnterprise
    
    if ([string]::IsNullOrWhiteSpace($User)) {
        Write-Error "User email parameter is mandatory." -ErrorAction Stop
        return
    }

    $userObject = resolveUser $enterprise.enterpriseData $User
    if (-not $userObject) {
        Write-Error "User `"$User`" not found" -ErrorAction Stop
        return
    }

    if ($userObject.UserStatus -ne [KeeperSecurity.Enterprise.UserStatus]::Active) {
        Write-Error "User $User is not active" -ErrorAction Stop
        return
    }

    $nodeId = $null
    
    if ($Node) {
        $nodeObject = resolveSingleNode $Node
        if ($nodeObject) {
            $nodeId = $nodeObject.Id
        } else {
            Write-Warning "Node `"$Node`" not found so we are taking user's parent node"
        }
    }

    try {
        $updatedUser = $enterprise.enterpriseData.EnterpriseUserUpdate($userObject, $nodeId, $FullName, $JobTitle, $InviteeLocale).GetAwaiter().GetResult()
        Write-Output "User $User updated"
        return $updatedUser
    }
    catch {
        Write-Error "Failed to update user: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Update-KeeperEnterpriseUser -ParameterName User -ScriptBlock $Keeper_ActiveUserCompleter


# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCArhlY4m9aXBuDQ
# tYC4f0HuSve96QHEgEzWAxsvWw/2lqCCITswggWNMIIEdaADAgECAhAOmxiO+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
# BDEiBCC0egcQQVGbmfEf3MrcFPY3HgbcDkFBP1Mk9zE6g1DZajANBgkqhkiG9w0B
# AQEFAASCAYAOcvib2J4TGxXGOwi/LcdI9UpHQIiFmmqfpdUoWSJ4Yvhce5tzswOR
# xeD1A064wltQmbuJAhlymdVoDf9K0vUQaTH/QSZVTprth+g+XU4LVz5OKNjfkfy6
# 25v1KqkOGwNvWSsPOX/i6ZQvL4UICsCGfIPM0Cl9dGXmIWXu3P6NAbmPBrxNZrP+
# 446V0w/gHXUIxGajnRKXa35elkC9ftHbVov9q0uLAggfy1ZZKtTVLsp2S6J6vGpG
# rNwgI/dEQnz98jUHnTbYmxhGaDvhdHrcEMEM9hl2dT0/aywAN8WxyVOPMCh1o4SN
# ZAx7E1lE19tjymPlYa9ofgLpIOt0AAcG1VItUV3nTMVPEZr+QZZtXoZ3eV4T/qWM
# fEYBKsTVKIypbBNPDcH2w1y20ijWueo55ejLjXNXPnOC43Czd4ZCY79faFZ936pb
# FLBwvy9K0WscZq7+v2GnkRAOJJqJyshO1CtmNrOCTHPUXvWY/JjkgjnGopV6Obcz
# niqCCBFx6yChggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwODE0MjExOTEzWjAv
# BgkqhkiG9w0BCQQxIgQgIvHPhP6np63VKni4ovpyhocEeNv4sI3UbmMmVVtMQAsw
# DQYJKoZIhvcNAQEBBQAEggIAQAip0AIRS58NdF/+YVWfbH4s6/n8Vg2tP5jTT3QY
# 81Wsm++qJEeW1m+gELyFHl9uEjbYr5GKafDJ7GWPQBSgfaONCtSy1gUnbIBhvvTn
# HbySqJXiEcKmfzAmJ0aNqGBxvHYPcN7HD2YL2Dac7hqExOiwtnKXvVVnoBre9Lrk
# Gs1geIwU8YeTw1gheySS8CAyt8ef+b17tUQhNIqLTFAvI31e5Hxr2DLBYGHhoomS
# 53DJYlvOm5i1PK8oQ7/AfGLIKnRU3yKgb85r5Tr7marv7SpnnXUGemcWb8rJpg1G
# TFw62K+hIVK1b2GhjmSew0dA+7n1ejVOuXyt9gtu75yytgXjWcScPDWOLeq2MElB
# MUO/J4Gz1yCmh0AS1Uy0TSfIQLfD98zRBB7EADfGKR1FCwwyAjFPlwL4p9bNn5Vb
# XjU/19R/0+6tTNYjaO6vADzi6mrhcA/Ns0C5GlvSvn14pytEYOdj2CEICVNzq2pN
# I84Zh+AepryhsJe+xSgTlhviSIMd9D2eKM+D2geXfvfobxr+EumS7i3jvfExkmdg
# 5PQveqkHhAU00+JYLyzSB/lerao0rtghVdDdzg5FyfO10MQJVioDMmVuhokptQxW
# Pm2JBB1VUZ/SL03E6xEomc3tjnzBHwD7npalJjLryMGfnA2nifJtCVkNlPP/5265
# ieg=
# SIG # End signature block