EnterpriseRole.ps1

function Get-EnterpriseRole {
    <#
        .SYNOPSIS
        Get a list of enterprise roles

        .PARAMETER Role
        Role Name or ID
    #>

    [CmdletBinding()]

    [Enterprise]$enterprise = getEnterprise
    return $enterprise.roleData.Roles
}

function Get-KeeperEnterpriseRole {
    <#
        .SYNOPSIS
        Get a list of enterprise roles

        .PARAMETER Name
        Role Name or ID (exact match). Returns the single matching role.

        .PARAMETER Filter
        Search filter applied across all role properties (case-insensitive regex match).

        .PARAMETER Format
        Output format: table (default) or json.

        .PARAMETER Output
        File path to export results when Format is 'json'. Ignored for 'table' format.

        .EXAMPLE
        Get-KeeperEnterpriseRole
        Lists all enterprise roles in table format.

        .EXAMPLE
        Get-KeeperEnterpriseRole -Name "AdminRole"
        Returns the enterprise role named "AdminRole".

        .EXAMPLE
        Get-KeeperEnterpriseRole -Filter "manager"
        Returns all enterprise roles whose properties match "manager".

        .EXAMPLE
        Get-KeeperEnterpriseRole -Format json -Output "roles.json"
        Exports all enterprise roles to a JSON file.
    #>

    [CmdletBinding()]
    Param (
        [Parameter()][string] $Name,
        [Parameter()][string] $Filter,
        [Parameter()][ValidateSet('table', 'json')][string] $Format = 'table',
        [Parameter()][string] $Output
    )

    if ($Name) { $Name = $Name.Trim() }
    if ($Filter) { $Filter = $Filter.Trim() }

    $roles = Get-EnterpriseRole
    if (-not $roles) {
        Write-Warning "No enterprise roles found."
        return @()
    }

    if ($Name) {
        $roles = $roles | Where-Object { ($_.DisplayName -eq $Name) -or ($_.Id.ToString() -eq $Name) }
    }

    if ($Filter) {
        $filterLower = $Filter.ToLower()
        $roles = $roles | Where-Object {
            $text = ($_.PSObject.Properties.Value | ForEach-Object { "$_" }) -join ' '
            $text -match [regex]::Escape($filterLower)
        }
    }

    $result = @($roles)
    if ($result.Count -eq 0 -and ($Name -or $Filter)) {
        Write-Host "No matching enterprise roles found." -ForegroundColor Yellow
        return @()
    }

    if ($Format -eq 'json') {
        $json = $result | ConvertTo-Json -Depth 5
        if ($Output) {
            Set-Content -Path $Output -Value $json -Encoding utf8
            Write-Host "Results exported to: $Output" -ForegroundColor Green
        } else {
            return $json
        }
    } else {
        return $result
    }
}
New-Alias -Name ker -Value Get-KeeperEnterpriseRole

function Get-KeeperEnterpriseRoleUsers {
    <#
        .SYNOPSIS
        Get a list of enterprise users for a role

        .PARAMETER Role
        Role Name or ID
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $enterpriseData = $enterprise.enterpriseData
    $roleData = $enterprise.roleData
    $roleId = $null

    if ($Role -is [String]) {
        $ids = @(Get-KeeperEnterpriseRole | Where-Object { $_.Id -eq $Role -or $_.DisplayName -ieq $Role } | Select-Object -Property Id)
        if ($ids.Length -gt 1) {
            Write-Error -Message "Role name `"$Role`" is not unique. Use Role ID" -ErrorAction Stop
        }
        if ($ids.Length -eq 1 -and $null -ne $ids[0].Id) {
            $roleId = $ids[0].Id
        }
    }
    elseif ($Role -is [long]) {
        $ids = @(Get-KeeperEnterpriseRole | Where-Object { $_.Id -ceq $Role } | Select-Object -Property Id -First 1)
        if ($ids.Length -eq 1 -and $null -ne $ids[0].Id) {
            $roleId = $ids[0].Id
        }
    }
    elseif ($null -ne $Role.Id) {
        $roleId = $Role.Id
    }
    if ($roleId) {
        $erole = $null
        if ($roleData.TryGetRole($roleId, [ref]$erole)) {
            foreach ($userId in $roleData.GetUsersForRole($erole.Id)) {
                $user = $null
                if ($enterpriseData.TryGetUserById($userId, [ref]$user)) {
                    Write-Output $user
                }
            }
        }
        else {
            Write-Error -Message "Role `"$roleId`" not found" -ErrorAction Stop
        }
    }
    else {
        Write-Error -Message "Role `"$Role`" not found" -ErrorAction Stop
    }
}
New-Alias -Name keru -Value Get-KeeperEnterpriseRoleUsers
Register-ArgumentCompleter -CommandName Get-KeeperEnterpriseRoleUsers -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter

function Get-KeeperEnterpriseRoleTeams {
    <#
        .SYNOPSIS
        Get a list of enterprise teams for a role

        .PARAMETER Role
        Role Name or ID
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $enterpriseData = $enterprise.enterpriseData
    $roleData = $enterprise.roleData
    $roleId = $null

    if ($Role -is [String]) {
        $ids = @(Get-KeeperEnterpriseRole | Where-Object { $_.Id -eq $Role -or $_.DisplayName -ieq $Role } | Select-Object -Property Id)
        if ($ids.Length -gt 1) {
            Write-Error -Message "Role name `"$Role`" is not unique. Use Role ID" -ErrorAction Stop
        }
        if ($ids.Length -eq 1 -and $null -ne $ids[0].Id) {
            $roleId = $ids[0].Id
        }
    }
    elseif ($Role -is [long]) {
        $ids = @(Get-KeeperEnterpriseRole | Where-Object { $_.Id -ceq $Role } | Select-Object -Property Id -First 1)
        if ($ids.Length -eq 1 -and $null -ne $ids[0].Id) {
            $roleId = $ids[0].Id
        }
    }
    elseif ($null -ne $Role.Id) {
        $roleId = $Role.Id
    }
    if ($roleId) {
        $erole = $null
        if ($roleData.TryGetRole($roleId, [ref]$erole)) {
            foreach ($teamUid in $roleData.GetTeamsForRole($erole.Id)) {
                $team = $null
                if ($enterpriseData.TryGetTeam($teamUid, [ref]$team)) {
                    Write-Output $team
                }
            }
        }
        else {
            Write-Error -Message "Role `"$roleId`" not found" -ErrorAction Stop
        }
    }
    else {
        Write-Error -Message "Role `"$Role`" not found" -ErrorAction Stop
    }
}
New-Alias -Name kert -Value Get-KeeperEnterpriseRoleTeams
Register-ArgumentCompleter -CommandName Get-KeeperEnterpriseRoleTeams -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter

function Get-KeeperEnterpriseAdminRole {
    <#
        .SYNOPSIS
        Get a list of Administrator Permissions

        .PARAMETER Pattern
        Role search pattern
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $false)]$Pattern
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData
    $roles = $null

    if ($Pattern -is [String]) {
        $roles = Get-EnterpriseRole | Where-Object { $_.Id -eq $Pattern -or $_.DisplayName -match $Pattern } 
    }
    elseif ($Pattern -is [long]) {
        $roles = Get-EnterpriseRole | Where-Object { $_.Id -eq $Pattern } 
    }
    elseif ($null -ne $Pattern.Id) {
        $roles = $Pattern
    }
    else {
        $roles = Get-EnterpriseRole
    }
    if ($null -ne $roles -and $roles.Length -gt 0 ) {
        $roles = $roles | Sort-Object -Property DisplayName
        foreach ($role in $roles) {
            if ($null -ne $role.Id) {
                foreach ($rp in $roleData.GetRolePermissions($role.Id)) {
                    $rp
                }
            }
        }
    }        
    else {
        Write-Error -Message "Role `"$Role`" not found" -ErrorAction Stop
    }
}
New-Alias -Name kerap -Value Get-KeeperEnterpriseAdminRole

function Set-KeeperEnterpriseRole {
    <#
        .SYNOPSIS
        Updates Enterprise Role properties

        .DESCRIPTION
        Updates properties of an existing Enterprise Role, such as setting it as default for new users.

        .PARAMETER Role
        Role Name or ID, or EnterpriseRole object

        .PARAMETER NewUserInherit
        Set role as default for new users in the node. If specified, this will update the NewUserInherit property.

        .PARAMETER VisibleBelow
        Set role visibility to subnodes. If specified, this will update the VisibleBelow property.

        .PARAMETER NewDisplayName
        New role display name. If specified, this will update the role name.

        .EXAMPLE
        Set-KeeperEnterpriseRole -Role "MyRole" -NewUserInherit $true
        Sets the role "MyRole" as the default role for new users

        .EXAMPLE
        Set-KeeperEnterpriseRole -Role 123456789 -NewUserInherit $false
        Removes the role with ID 123456789 as the default role for new users

        .EXAMPLE
        Get-EnterpriseRole | Where-Object { $_.DisplayName -eq "MyRole" } | Set-KeeperEnterpriseRole -NewUserInherit $true
        Sets the role using pipeline input
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]$Role,
        [Parameter()][bool]$NewUserInherit,
        [Parameter()][bool]$VisibleBelow,
        [Parameter()][string]$NewDisplayName
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData
    $roleObject = resolveRole $roleData $Role

    if (-not $roleObject) {
        return
    }

    $updateParams = @{}
    if ($PSBoundParameters.ContainsKey('NewUserInherit')) {
        $updateParams['newUserInherit'] = $NewUserInherit
    }
    if ($PSBoundParameters.ContainsKey('VisibleBelow')) {
        $updateParams['visibleBelow'] = $VisibleBelow
    }
    if ($PSBoundParameters.ContainsKey('NewDisplayName')) {
        $updateParams['displayName'] = $NewDisplayName
    }

    if ($updateParams.Count -eq 0) {
        Write-Warning "No properties specified to update. Use -NewUserInherit, -VisibleBelow, or -NewDisplayName parameters."
        return
    }

    $roleName = $roleObject.DisplayName
    if ($PSCmdlet.ShouldProcess($roleName, "Update Enterprise Role")) {
        try {
            $updatedRole = $roleData.UpdateRole(
                $roleObject,
                $updateParams['newUserInherit'],
                $updateParams['visibleBelow'],
                $updateParams['displayName']
            ).GetAwaiter().GetResult()
            
            if ($updatedRole) {
                Write-Output "Role `"$($updatedRole.DisplayName)`" updated successfully"
                $updatedRole
            }
        }
        catch {
            Write-Error "Failed to update role `"$roleName`": $($_.Exception.Message)" -ErrorAction Stop
        }
    }
}
Register-ArgumentCompleter -CommandName Set-KeeperEnterpriseRole -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name kers -Value Set-KeeperEnterpriseRole

function Grant-KeeperEnterpriseRoleToUser {
    <#
        .SYNOPSIS
        Adds a user to an Enterprise Role

        .DESCRIPTION
        Assigns an existing enterprise user to an enterprise role.

        .PARAMETER Role
        Role Name, ID, or EnterpriseRole object

        .PARAMETER User
        User email, ID, or EnterpriseUser object

        .EXAMPLE
        Grant-KeeperEnterpriseRoleToUser -Role "MyRole" -User "user@example.com"
        Adds the user to the role

        .EXAMPLE
        Get-EnterpriseRole | Where-Object { $_.DisplayName -eq "MyRole" } | Grant-KeeperEnterpriseRoleToUser -User "user@example.com"
        Adds the user using pipeline input for the role
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)]$User
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData
    $enterpriseData = $enterprise.enterpriseData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $userObject = resolveUser $enterpriseData $User
    if (-not $userObject) {
        return
    }

    $roleName = $roleObject.DisplayName
    $userEmail = $userObject.Email
    if ($PSCmdlet.ShouldProcess("User `"$userEmail`" to Role `"$roleName`"", "Add")) {
        try {
            $roleData.AddUserToRole($roleObject, $userObject).GetAwaiter().GetResult() | Out-Null
            Write-Output "User `"$userEmail`" added to role `"$roleName`""
        }
        catch {
            Write-Error "Failed to add user `"$userEmail`" to role `"$roleName`": $($_.Exception.Message)" -ErrorAction Stop
        }
    }
}
Register-ArgumentCompleter -CommandName Grant-KeeperEnterpriseRoleToUser -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
Register-ArgumentCompleter -CommandName Grant-KeeperEnterpriseRoleToUser -ParameterName User -ScriptBlock $Keeper_ActiveUserCompleter
New-Alias -Name kerua -Value Grant-KeeperEnterpriseRoleToUser

function Revoke-KeeperEnterpriseRoleFromUser {
    <#
        .SYNOPSIS
        Removes a user from an Enterprise Role

        .DESCRIPTION
        Removes an enterprise user from an enterprise role.

        .PARAMETER Role
        Role Name, ID, or EnterpriseRole object

        .PARAMETER User
        User email, ID, or EnterpriseUser object

        .EXAMPLE
        Revoke-KeeperEnterpriseRoleFromUser -Role "MyRole" -User "user@example.com"
        Removes the user from the role

        .EXAMPLE
        Get-EnterpriseRole | Where-Object { $_.DisplayName -eq "MyRole" } | Revoke-KeeperEnterpriseRoleFromUser -User "user@example.com"
        Removes the user using pipeline input for the role
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)]$User
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData
    $enterpriseData = $enterprise.enterpriseData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $userObject = resolveUser $enterpriseData $User
    if (-not $userObject) {
        return
    }

    $roleName = $roleObject.DisplayName
    $userEmail = $userObject.Email
    if ($PSCmdlet.ShouldProcess("User `"$userEmail`" from Role `"$roleName`"", "Remove")) {
        try {
            $roleData.RemoveUserFromRole($roleObject, $userObject).GetAwaiter().GetResult() | Out-Null
            Write-Output "User `"$userEmail`" removed from role `"$roleName`""
        }
        catch {
            Write-Error "Failed to remove user `"$userEmail`" from role `"$roleName`": $($_.Exception.Message)" -ErrorAction Stop
        }
    }
}
Register-ArgumentCompleter -CommandName Revoke-KeeperEnterpriseRoleFromUser -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
Register-ArgumentCompleter -CommandName Revoke-KeeperEnterpriseRoleFromUser -ParameterName User -ScriptBlock $Keeper_ActiveUserCompleter
New-Alias -Name kerur -Value Revoke-KeeperEnterpriseRoleFromUser

function Grant-KeeperEnterpriseRoleToTeam {
    <#
        .SYNOPSIS
        Adds a team to an Enterprise Role

        .DESCRIPTION
        Assigns an existing enterprise team to an enterprise role.

        .PARAMETER Role
        Role Name, ID, or EnterpriseRole object

        .PARAMETER Team
        Team UID, Name, or EnterpriseTeam object

        .EXAMPLE
        Grant-KeeperEnterpriseRoleToTeam -Role "MyRole" -Team "Engineering"
        Adds the team to the role

        .EXAMPLE
        Grant-KeeperEnterpriseRoleToTeam -Role "MyRole" -Team "1P7A8XZ9K3J9H"
        Adds the team using Team UID

        .EXAMPLE
        Get-EnterpriseRole | Where-Object { $_.DisplayName -eq "MyRole" } | Grant-KeeperEnterpriseRoleToTeam -Team "Engineering"
        Adds the team using pipeline input for the role
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)]$Team
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData
    $enterpriseData = $enterprise.enterpriseData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $teamObject = resolveTeam $enterpriseData $Team
    if (-not $teamObject) {
        return
    }

    $roleName = $roleObject.DisplayName
    $teamName = $teamObject.Name
    if ($PSCmdlet.ShouldProcess("Team `"$teamName`" to Role `"$roleName`"", "Add")) {
        try {
            $roleData.AddTeamToRole($roleObject, $teamObject).GetAwaiter().GetResult() | Out-Null
            Write-Output "Team `"$teamName`" added to role `"$roleName`""
        }
        catch {
            Write-Error "Failed to add team `"$teamName`" to role `"$roleName`": $($_.Exception.Message)" -ErrorAction Stop
        }
    }
}
Register-ArgumentCompleter -CommandName Grant-KeeperEnterpriseRoleToTeam -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
Register-ArgumentCompleter -CommandName Grant-KeeperEnterpriseRoleToTeam -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
New-Alias -Name kerta -Value Grant-KeeperEnterpriseRoleToTeam

function Revoke-KeeperEnterpriseRoleFromTeam {
    <#
        .SYNOPSIS
        Removes a team from an Enterprise Role

        .DESCRIPTION
        Removes an enterprise team from an enterprise role.

        .PARAMETER Role
        Role Name, ID, or EnterpriseRole object

        .PARAMETER Team
        Team UID, Name, or EnterpriseTeam object

        .EXAMPLE
        Revoke-KeeperEnterpriseRoleFromTeam -Role "MyRole" -Team "Engineering"
        Removes the team from the role

        .EXAMPLE
        Revoke-KeeperEnterpriseRoleFromTeam -Role "MyRole" -Team "1P7A8XZ9K3J9H"
        Removes the team using Team UID

        .EXAMPLE
        Get-EnterpriseRole | Where-Object { $_.DisplayName -eq "MyRole" } | Revoke-KeeperEnterpriseRoleFromTeam -Team "Engineering"
        Removes the team using pipeline input for the role
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)]$Team
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData
    $enterpriseData = $enterprise.enterpriseData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $teamObject = resolveTeam $enterpriseData $Team
    if (-not $teamObject) {
        return
    }

    $roleName = $roleObject.DisplayName
    $teamName = $teamObject.Name
    if ($PSCmdlet.ShouldProcess("Team `"$teamName`" from Role `"$roleName`"", "Remove")) {
        try {
            $roleData.RemoveTeamFromRole($roleObject, $teamObject).GetAwaiter().GetResult() | Out-Null
            Write-Output "Team `"$teamName`" removed from role `"$roleName`""
        }
        catch {
            Write-Error "Failed to remove team `"$teamName`" from role `"$roleName`": $($_.Exception.Message)" -ErrorAction Stop
        }
    }
}
Register-ArgumentCompleter -CommandName Revoke-KeeperEnterpriseRoleFromTeam -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
Register-ArgumentCompleter -CommandName Revoke-KeeperEnterpriseRoleFromTeam -ParameterName Team -ScriptBlock $Keeper_TeamNameCompleter
New-Alias -Name kertr -Value Revoke-KeeperEnterpriseRoleFromTeam

function New-KeeperEnterpriseRole {
    <#
        .SYNOPSIS
        Create new enterprise role in the Keeper Enterprise.

        .DESCRIPTION
        Creates new enterprise role with optional settings for parent node, new user inheritance, visibility, and enforcements.

        .PARAMETER Role
        Role Name of the new role.

        .PARAMETER ParentNode
        Parent node name or ID. If not specified, the role will be created in the root node.

        .PARAMETER NewUser
        Assign this role to new users. Valid values: 'ON', 'OFF'. Default is 'OFF'.

        .PARAMETER VisibleBelow
        Make role visible to all subnodes. Valid values: 'ON', 'OFF'. Default is 'OFF'.

        .PARAMETER Enforcement
        Sets role enforcement in KEY:VALUE format. Can be repeated multiple times.

        .PARAMETER Force
        Do not prompt for confirmation when a role with the same name already exists.

        .EXAMPLE
        New-KeeperEnterpriseRole -Role "Manager"
        Creates a role named "Manager" in the root node.

        .EXAMPLE
        New-KeeperEnterpriseRole -Role "Manager", "Employee" -ParentNode "Sales" -NewUser "ON"
        Creates two roles "Manager" and "Employee" in the "Sales" node, assigned to new users.

        .EXAMPLE
        New-KeeperEnterpriseRole -Role "Admin" -ParentNode 123456789 -VisibleBelow "ON" -Enforcement "logout_timer_desktop:3600"
        Creates an "Admin" role in node 123456789, visible to subnodes, with a logout timer enforcement.

        .EXAMPLE
        New-KeeperEnterpriseRole -Role "TestRole" -Force
        Creates a role even if one with the same name already exists, without prompting.
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $false)]
        [string[]]$Role,
        
        [Parameter()][string]$ParentNode,
        
        [Parameter()][ValidateSet('ON', 'OFF')][string]$NewUser = 'OFF',
        
        [Parameter()][ValidateSet('ON', 'OFF')][string]$VisibleBelow = 'OFF',
        
        [Parameter()][string[]]$Enforcement,
        
        [Parameter()][switch]$Force
    )

    [Enterprise]$enterprise = getEnterprise
    $enterpriseData = $enterprise.enterpriseData
    $roleData = $enterprise.roleData
    $auth = $enterprise.loader.Auth

    $nodeId = $null
    if (-not [string]::IsNullOrWhiteSpace($ParentNode)) {
        $ParentNode = $ParentNode.Trim()
        $parsedId = 0
        if ([long]::TryParse($ParentNode, [ref]$parsedId)) {
            $node = $null
            if ($enterpriseData.TryGetNode($parsedId, [ref]$node)) {
                $nodeId = $parsedId
            }
        }
        
        if (-not $nodeId) {
            $nodes = @($enterpriseData.Nodes | Where-Object { $_.DisplayName -ieq $ParentNode })
            if ($nodes.Count -eq 1) {
                $nodeId = $nodes[0].Id
            }
            elseif ($nodes.Count -eq 0) {
                Write-Error "Node `"$ParentNode`" not found" -ErrorAction Stop
                return
            }
            else {
                Write-Error "More than one node with name `"$ParentNode`" are found. Use Node ID." -ErrorAction Stop
                return
            }
        }
    }
    else {
        $nodeId = $enterpriseData.RootNode.Id
    }

    $newUserInherit = $NewUser -eq 'ON'
    $visibleBelowBool = $VisibleBelow -eq 'ON'

    $uniqueRoles = $Role | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
    if ($uniqueRoles.Count -ne ($Role | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count) {
        Write-Warning "Duplicate role names detected in input. Only unique names will be processed."
    }
    $Role = $uniqueRoles
    $allRoles = Get-EnterpriseRole

    if ($Enforcement -and $Enforcement.Count -gt 0) {
        foreach ($enf in $Enforcement) {
            if (-not [string]::IsNullOrWhiteSpace($enf) -and $enf -notmatch '^[^:]+:.+$') {
                Write-Warning "Enforcement `"$enf`" does not match KEY:VALUE format. It will be skipped."
            }
        }
    }

    $createdRoles = @()
    foreach ($roleName in $Role) {
        if ([string]::IsNullOrWhiteSpace($roleName)) {
            Write-Warning "Skipping empty role name"
            continue
        }

        $roleName = $roleName.Trim()
        if ($roleName.Length -gt 255) {
            Write-Error "Role name `"$roleName`" exceeds maximum length of 255 characters" -ErrorAction Continue
            continue
        }

        $existingRoles = $allRoles | Where-Object { $_.DisplayName -ieq $roleName }
        if ($existingRoles.Count -gt 0) {
            if (-not $Force) {
                $confirmation = Read-Host "Role with name `"$roleName`" already exists. Do you want to create a new one? (Yes/No)"
                if ($confirmation -notmatch '^[Yy]([Ee][Ss])?$') {
                    Write-Output "Skipping role `"$roleName`""
                    continue
                }
            }
            else {
                Write-Verbose "Role `"$roleName`" already exists, but Force is set. Creating anyway."
            }
        }

        if ($PSCmdlet.ShouldProcess($roleName, "Create Enterprise Role")) {
            try {
                $createdRole = $roleData.CreateRole($roleName, $nodeId, $newUserInherit).GetAwaiter().GetResult()
                
                if (-not $createdRole) {
                    Write-Error "Failed to create role `"$roleName`"" -ErrorAction Continue
                    continue
                }

                Write-Output "Role `"$roleName`" created successfully (ID: $($createdRole.Id))"

                if ($visibleBelowBool) {
                    try {
                        $updatedRole = $roleData.UpdateRole($createdRole, $null, $true, $null).GetAwaiter().GetResult()
                        if ($updatedRole) {
                            Write-Verbose "Role `"$roleName`" set to visible below subnodes"
                        }
                    }
                    catch {
                        Write-Warning "Failed to set VisibleBelow for role `"$roleName`": $($_.Exception.Message)"
                    }
                }

                if ($Enforcement -and $Enforcement.Count -gt 0) {
                    foreach ($enf in $Enforcement) {
                        if ([string]::IsNullOrWhiteSpace($enf)) {
                            continue
                        }

                        $parts = $enf -split ':', 2
                        if ($parts.Count -ne 2) {
                            Write-Warning "Invalid enforcement format `"$enf`". Expected KEY:VALUE format. Skipping."
                            continue
                        }

                        $enforcementKey = $parts[0].Trim()
                        $enforcementValue = $parts[1].Trim()

                        if ([string]::IsNullOrWhiteSpace($enforcementKey) -or [string]::IsNullOrWhiteSpace($enforcementValue)) {
                            Write-Warning "Invalid enforcement format `"$enf`". Key and Value cannot be empty. Skipping."
                            continue
                        }

                        try {
                            $enfCmd = New-Object KeeperSecurity.Commands.RoleEnforcementAddCommand
                            $enfCmd.RoleId = $createdRole.Id
                            $enfCmd.Enforcement = $enforcementKey
                            $enfCmd.Value = $enforcementValue

                            [KeeperSecurity.Authentication.AuthExtensions]::ExecuteAuthCommand($auth, $enfCmd).GetAwaiter().GetResult() | Out-Null
                            Write-Verbose "Added enforcement `"$enforcementKey`" = `"$enforcementValue`" to role `"$roleName`""
                        }
                        catch {
                            Write-Warning "Failed to add enforcement `"$enf`" to role `"$roleName`": $($_.Exception.Message)"
                        }
                    }
                }

                try {
                    $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null
                }
                catch {
                    Write-Warning "Failed to reload enterprise data after creating role `"$roleName`": $($_.Exception.Message)"
                }

                $finalRole = $null
                if ($roleData.TryGetRole($createdRole.Id, [ref]$finalRole)) {
                    $createdRoles += $finalRole
                }
                else {
                    Write-Warning "Role `"$roleName`" was created (ID: $($createdRole.Id)) but could not be retrieved after reload. The role may still exist."
                    $createdRoles += $createdRole
                }
            }
            catch {
                Write-Error "Failed to create role `"$roleName`": $($_.Exception.Message)" `
                    -ErrorAction Continue `
                    -ErrorId "RoleCreationFailed" `
                    -Category InvalidOperation
                continue
            }
        }
    }

    return $createdRoles
}
Register-ArgumentCompleter -CommandName New-KeeperEnterpriseRole -ParameterName ParentNode -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    $result = @()
    [Enterprise]$enterprise = $Script:Context.Enterprise
    if (-not $enterprise) {
        return $null
    }
    if ($wordToComplete) {
        $to_complete = $wordToComplete + '*'
    }
    else {
        $to_complete = '*'
    }
    foreach ($node in $enterprise.enterpriseData.Nodes) {
        if ($node.DisplayName -like $to_complete) {
            $nodeName = $node.DisplayName
            if ($nodeName -match '[\s'']') {
                $nodeName = $nodeName -replace '''', ''''''
                $nodeName = "'${nodeName}'"
            }
            $result += $nodeName
        }
    }
    if ($result.Count -gt 0) {
        return $result
    }
    else {
        return $null
    }
}
New-Alias -Name keradd -Value New-KeeperEnterpriseRole

function Remove-KeeperEnterpriseRole {
    <#
        .SYNOPSIS
        Delete an enterprise role

        .DESCRIPTION
        Removes an enterprise role from the Keeper Enterprise. This operation cannot be undone.

        .PARAMETER Role
        Role Name, ID, or EnterpriseRole object to delete

        .PARAMETER Force
        Do not prompt for confirmation before deleting the role

        .EXAMPLE
        Remove-KeeperEnterpriseRole -Role "MyRole"
        Deletes the role named "MyRole" after confirmation

        .EXAMPLE
        Remove-KeeperEnterpriseRole -Role "MyRole" -Force
        Deletes the role named "MyRole" without prompting for confirmation

        .EXAMPLE
        Get-EnterpriseRole | Where-Object { $_.DisplayName -eq "MyRole" } | Remove-KeeperEnterpriseRole
        Deletes a role using pipeline input
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='High')]
    Param (
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)]$Role,
        [Parameter()][switch]$Force
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $roleName = $roleObject.DisplayName
    $roleId = $roleObject.Id

    if (-not $Force -and -not $PSCmdlet.ShouldProcess($roleName, "Delete Enterprise Role")) {
        return
    }

    try {
        $roleData.DeleteRole($roleObject).GetAwaiter().GetResult() | Out-Null
        Write-Output "Role `"$roleName`" (ID: $roleId) deleted successfully"
    }
    catch {
        Write-Error "Failed to delete role `"$roleName`": $($_.Exception.Message)" `
            -ErrorAction Stop `
            -ErrorId "RoleDeletionFailed" `
            -Category InvalidOperation
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseRole -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name kerdel -Value Remove-KeeperEnterpriseRole

function Add-KeeperEnterpriseRoleManagedNode {
    <#
    .SYNOPSIS
    Adds a managed node to an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Node
    Node name or ID to add as a managed node

    .PARAMETER Cascade
    Cascade node management to subnodes

    .DESCRIPTION
    Adds a node as a managed node to an enterprise role. This allows the role to manage the specified node and optionally cascade management to subnodes.

    .EXAMPLE
    Add-KeeperEnterpriseRoleManagedNode -Role "AdminRole" -Node "Sales"
    Adds the Sales node as a managed node to the AdminRole

    .EXAMPLE
    Add-KeeperEnterpriseRoleManagedNode -Role 123456789 -Node 987654321 -Cascade
    Adds node 987654321 as a managed node to role 123456789 with cascade enabled
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)][string]$Node,
        [Parameter()][bool]$Cascade = $false
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $targetNode = resolveSingleNode $Node
    if (-not $targetNode) {
        Write-Error -Message "Node `"$Node`" not found" -ErrorAction Stop
    }

    try {
        $roleData.RoleManagedNodeAdd($roleObject, $targetNode, ($Cascade -eq $true)).GetAwaiter().GetResult() | Out-Null
        $nodeDisplayName = if ([string]::IsNullOrEmpty($targetNode.DisplayName)) { $targetNode.Id.ToString() } else { $targetNode.DisplayName }
        Write-Output "Managed node `"$nodeDisplayName`" added to role `"$($roleObject.DisplayName)`" successfully."
    }
    catch {
        Write-Error -Message "Failed to add managed node: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseRoleManagedNode -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Add-KeeperRoleManagedNode -Value Add-KeeperEnterpriseRoleManagedNode

function Update-KeeperEnterpriseRoleManagedNode {
    <#
    .SYNOPSIS
    Updates a managed node configuration for an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Node
    Node name or ID of the managed node to update

    .PARAMETER Cascade
    Cascade node management to subnodes

    .DESCRIPTION
    Updates the cascade setting for a managed node in an enterprise role.

    .EXAMPLE
    Update-KeeperEnterpriseRoleManagedNode -Role "AdminRole" -Node "Sales" -Cascade
    Updates the Sales managed node in AdminRole to enable cascade
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)][string]$Node,
        [Parameter()][bool]$Cascade = $false
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $targetNode = resolveSingleNode $Node
    if (-not $targetNode) {
        Write-Error -Message "Node `"$Node`" not found" -ErrorAction Stop
    }

    try {
        $roleData.RoleManagedNodeUpdate($roleObject, $targetNode, ($Cascade -eq $true)).GetAwaiter().GetResult() | Out-Null
        $nodeDisplayName = if ([string]::IsNullOrEmpty($targetNode.DisplayName)) { $targetNode.Id.ToString() } else { $targetNode.DisplayName }
        Write-Output "Managed node `"$nodeDisplayName`" updated for role `"$($roleObject.DisplayName)`" successfully."
    }
    catch {
        Write-Error -Message "Failed to update managed node: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Update-KeeperEnterpriseRoleManagedNode -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Update-KeeperRoleManagedNode -Value Update-KeeperEnterpriseRoleManagedNode

function Remove-KeeperEnterpriseRoleManagedNode {
    <#
    .SYNOPSIS
    Removes a managed node from an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Node
    Node name or ID of the managed node to remove

    .DESCRIPTION
    Removes a node from the managed nodes list of an enterprise role.

    .EXAMPLE
    Remove-KeeperEnterpriseRoleManagedNode -Role "AdminRole" -Node "Sales"
    Removes the Sales node from the managed nodes of AdminRole
    #>

    [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)][string]$Node
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $targetNode = resolveSingleNode $Node
    if (-not $targetNode) {
        Write-Error -Message "Node `"$Node`" not found" -ErrorAction Stop
    }

    $nodeDisplayName = if ([string]::IsNullOrEmpty($targetNode.DisplayName)) { $targetNode.Id.ToString() } else { $targetNode.DisplayName }

    if ($PSCmdlet.ShouldProcess("Managed node `"$nodeDisplayName`" from role `"$($roleObject.DisplayName)`"", "Remove")) {
        try {
            $roleData.RoleManagedNodeRemove($roleObject, $targetNode).GetAwaiter().GetResult() | Out-Null
            Write-Output "Managed node `"$nodeDisplayName`" deleted from role `"$($roleObject.DisplayName)`" successfully."
        }
        catch {
            Write-Error -Message "Failed to remove managed node: $($_.Exception.Message)" -ErrorAction Stop
        }
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseRoleManagedNode -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Remove-KeeperRoleManagedNode -Value Remove-KeeperEnterpriseRoleManagedNode

function Add-KeeperEnterpriseRolePrivilege {
    <#
    .SYNOPSIS
    Adds privileges to a managed node for an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Node
    Node name or ID of the managed node

    .PARAMETER Privilege
    One or more privilege names to add. Valid values: MANAGE_NODES, MANAGE_USER, MANAGE_LICENCES, MANAGE_ROLES, MANAGE_TEAMS, TRANSFER_ACCOUNT, RUN_REPORTS, VIEW_TREE, MANAGE_BRIDGE, MANAGE_COMPANIES, SHARING_ADMINISTRATOR, APPROVE_DEVICE, MANAGE_RECORD_TYPES, RUN_COMPLIANCE_REPORTS

    .DESCRIPTION
    Adds privileges to a managed node for an enterprise role. The node must already be a managed node for the role.

    .EXAMPLE
    Add-KeeperEnterpriseRolePrivilege -Role "AdminRole" -Node "Sales" -Privilege "MANAGE_USERS", "MANAGE_TEAMS"
    Adds MANAGE_USERS and MANAGE_TEAMS privileges to the Sales managed node for AdminRole

    .EXAMPLE
    Add-KeeperEnterpriseRolePrivilege -Role 123456789 -Node "Sales" -Privilege "RUN_REPORTS"
    Adds RUN_REPORTS privilege using role ID
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)][string]$Node,
        [Parameter(Position = 2, Mandatory = $true)][string[]]$Privilege
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $targetNode = resolveSingleNode $Node
    if (-not $targetNode) {
        Write-Error -Message "Node `"$Node`" not found" -ErrorAction Stop
    }

    $managedNodes = $roleData.GetManagedNodes() | Where-Object { $_.RoleId -eq $roleObject.Id -and $_.ManagedNodeId -eq $targetNode.Id }
    if ($managedNodes.Count -eq 0) {
        $nodeDisplayName = if ([string]::IsNullOrEmpty($targetNode.DisplayName)) { $targetNode.Id.ToString() } else { $targetNode.DisplayName }
        Write-Error -Message "Role `"$($roleObject.DisplayName)`" does not have node `"$nodeDisplayName`" as a managed node. Use Add-KeeperEnterpriseRoleManagedNode first." -ErrorAction Stop
    }

    $privilegeList = New-Object System.Collections.Generic.List[KeeperSecurity.Enterprise.RoleManagedNodePrivilege]
    $invalidPrivileges = @()

    foreach ($priv in $Privilege) {
        $privTrimmed = ($priv.Trim() -replace '\s+', '_').ToUpperInvariant()
        try {
            $parsedPriv = [Enum]::Parse([KeeperSecurity.Enterprise.RoleManagedNodePrivilege], $privTrimmed, $true)
            $privilegeList.Add($parsedPriv)
        }
        catch {
            $invalidPrivileges += $priv.Trim()
        }
    }

    if ($invalidPrivileges.Count -gt 0) {
        $validValues = [System.Enum]::GetNames([KeeperSecurity.Enterprise.RoleManagedNodePrivilege]) -join ", "
        Write-Error -Message "Invalid privileges: $($invalidPrivileges -join ', '). Valid values: $validValues" -ErrorAction Stop
    }

    if ($privilegeList.Count -eq 0) {
        Write-Error -Message "No valid privileges specified." -ErrorAction Stop
    }

    try {
        $responses = $roleData.RoleManagedNodePrivilegeAddBatch($roleObject, $targetNode, $privilegeList).GetAwaiter().GetResult()
        
        for ($i = 0; $i -lt $responses.Count; $i++) {
            $response = $responses[$i]
            $privilege = $privilegeList[$i]
            if ($response.IsSuccess) {
                Write-Output "Command: $($response.command), Privilege: $privilege, Result: $($response.result)"
            }
            else {
                Write-Output "Command: $($response.command), Privilege: $privilege, Result: $($response.result), Code: $($response.resultCode), Message: $($response.message)"
            }
        }
    }
    catch {
        Write-Error -Message "Failed to add privileges: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseRolePrivilege -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Add-KeeperRolePrivilege -Value Add-KeeperEnterpriseRolePrivilege

function Remove-KeeperEnterpriseRolePrivilege {
    <#
    .SYNOPSIS
    Removes privileges from a managed node for an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Node
    Node name or ID of the managed node

    .PARAMETER Privilege
    One or more privilege names to remove. Valid values: MANAGE_NODES, MANAGE_USER, MANAGE_LICENCES, MANAGE_ROLES, MANAGE_TEAMS, TRANSFER_ACCOUNT, RUN_REPORTS, VIEW_TREE, MANAGE_BRIDGE, MANAGE_COMPANIES, SHARING_ADMINISTRATOR, APPROVE_DEVICE, MANAGE_RECORD_TYPES, RUN_COMPLIANCE_REPORTS

    .DESCRIPTION
    Removes privileges from a managed node for an enterprise role.

    .EXAMPLE
    Remove-KeeperEnterpriseRolePrivilege -Role "AdminRole" -Node "Sales" -Privilege "MANAGE_USERS"
    Removes MANAGE_USERS privilege from the Sales managed node for AdminRole
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$Role,
        [Parameter(Position = 1, Mandatory = $true)][string]$Node,
        [Parameter(Position = 2, Mandatory = $true)][string[]]$Privilege
    )

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    $targetNode = resolveSingleNode $Node
    if (-not $targetNode) {
        Write-Error -Message "Node `"$Node`" not found" -ErrorAction Stop
    }

    $managedNodes = $roleData.GetManagedNodes() | Where-Object { $_.RoleId -eq $roleObject.Id -and $_.ManagedNodeId -eq $targetNode.Id }
    if ($managedNodes.Count -eq 0) {
        $nodeDisplayName = if ([string]::IsNullOrEmpty($targetNode.DisplayName)) { $targetNode.Id.ToString() } else { $targetNode.DisplayName }
        Write-Error -Message "Role `"$($roleObject.DisplayName)`" does not have node `"$nodeDisplayName`" as a managed node. Use Add-KeeperEnterpriseRoleManagedNode first." -ErrorAction Stop
    }

    $privilegeList = New-Object System.Collections.Generic.List[KeeperSecurity.Enterprise.RoleManagedNodePrivilege]
    $invalidPrivileges = @()

    foreach ($priv in $Privilege) {
        $privTrimmed = ($priv.Trim() -replace '\s+', '_').ToUpperInvariant()
        try {
            $parsedPriv = [Enum]::Parse([KeeperSecurity.Enterprise.RoleManagedNodePrivilege], $privTrimmed, $true)
            $privilegeList.Add($parsedPriv)
        }
        catch {
            $invalidPrivileges += $priv.Trim()
        }
    }

    if ($invalidPrivileges.Count -gt 0) {
        $validValues = [System.Enum]::GetNames([KeeperSecurity.Enterprise.RoleManagedNodePrivilege]) -join ", "
        Write-Error -Message "Invalid privileges: $($invalidPrivileges -join ', '). Valid values: $validValues" -ErrorAction Stop
    }

    if ($privilegeList.Count -eq 0) {
        Write-Error -Message "No valid privileges specified." -ErrorAction Stop
    }

    try {
        $responses = $roleData.RoleManagedNodePrivilegeRemoveBatch($roleObject, $targetNode, $privilegeList).GetAwaiter().GetResult()
        
        for ($i = 0; $i -lt $responses.Count; $i++) {
            $response = $responses[$i]
            $privilege = $privilegeList[$i]
            if ($response.IsSuccess) {
                Write-Output "Command: $($response.command), Privilege: $privilege, Result: $($response.result)"
            }
            else {
                Write-Output "Command: $($response.command), Privilege: $privilege, Result: $($response.result), Code: $($response.resultCode), Message: $($response.message)"
            }
        }
    }
    catch {
        Write-Error -Message "Failed to remove privileges: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseRolePrivilege -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Remove-KeeperRolePrivilege -Value Remove-KeeperEnterpriseRolePrivilege

function Add-KeeperEnterpriseRoleEnforcement {
    <#
    .SYNOPSIS
    Adds enforcements to an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Enforcement
    Enforcement(s) in KEY=value format. Can be semicolon or comma separated. Multiple enforcements can be provided as an array.

    .DESCRIPTION
    Adds enforcement policies to an enterprise role. Enforcements are specified in KEY=value format.
    Multiple enforcements can be provided separated by semicolons or commas, or as an array.

    .EXAMPLE
    Add-KeeperEnterpriseRoleEnforcement -Role "AdminRole" -Enforcement "TWO_FACTOR_DURATION_WEB=3600"
    Adds a two-factor authentication duration enforcement

    .EXAMPLE
    Add-KeeperEnterpriseRoleEnforcement -Role "AdminRole" -Enforcement "TWO_FACTOR_DURATION_WEB=3600;MASTER_PASSWORD_MINIMUM_LENGTH=12"
    Adds multiple enforcements separated by semicolons
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    if ($null -eq $Enforcement -or $Enforcement.Count -eq 0) {
        Write-Error -Message "Enforcement parameter is required. Format: KEY=value;KEY2=value2 (semicolon or comma separated)." -ErrorAction Stop
    }

    $enforcementDict = New-Object 'System.Collections.Generic.Dictionary[KeeperSecurity.Enterprise.RoleEnforcementPolicies,string]'
    $enforcementKeys = New-Object System.Collections.Generic.List[KeeperSecurity.Enterprise.RoleEnforcementPolicies]
    $invalidEnforcements = @()

    foreach ($item in $Enforcement) {
        # since we are using the same separator for both semicolon and comma
        $parts = $item -split '[;,]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        
        foreach ($part in $parts) {
            $trimmedPart = $part.Trim()
            if ([string]::IsNullOrWhiteSpace($trimmedPart)) { continue }

            $separatorIndex = $trimmedPart.IndexOf('=')
            if ($separatorIndex -lt 0) {
                $separatorIndex = $trimmedPart.IndexOf(':')
            }

            $key = $null
            $value = $null

            if ($separatorIndex -gt 0) {
                $key = $trimmedPart.Substring(0, $separatorIndex).Trim()
                $value = $trimmedPart.Substring($separatorIndex + 1).Trim()
            }
            else {
                $key = $trimmedPart
            }

            $parsedKey = ConvertTo-RoleEnforcementPolicy $key
            if ($null -ne $parsedKey) {
                $enforcementDict[$parsedKey] = $value
                $enforcementKeys.Add($parsedKey)
            }
            else {
                $invalidEnforcements += $key
            }
        }
    }

    if ($invalidEnforcements.Count -gt 0) {
        $validValues = [System.Enum]::GetNames([KeeperSecurity.Enterprise.RoleEnforcementPolicies]) -join ", "
        Write-Error -Message "Invalid enforcements: $($invalidEnforcements -join ', '). Valid values: $validValues" -ErrorAction Stop
    }

    if ($enforcementKeys.Count -eq 0) {
        Write-Error -Message "No valid enforcements specified." -ErrorAction Stop
    }

    try {
        $responses = $roleData.RoleEnforcementAddBatch($roleObject, $enforcementDict).GetAwaiter().GetResult()
        
        for ($i = 0; $i -lt $responses.Count; $i++) {
            $response = $responses[$i]
            $enforcementKey = $enforcementKeys[$i]
            if ($response.IsSuccess) {
                $value = if ($enforcementDict.ContainsKey($enforcementKey) -and $enforcementDict[$enforcementKey]) { "=$($enforcementDict[$enforcementKey])" } else { "" }
                Write-Output "Command: $($response.command), Enforcement: $enforcementKey$value, Result: $($response.result)"
            }
            else {
                Write-Output "Command: $($response.command), Enforcement: $enforcementKey, Result: $($response.result), Code: $($response.resultCode), Message: $($response.message)"
            }
        }
    }
    catch {
        Write-Error -Message "Failed to add enforcements: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Add-KeeperEnterpriseRoleEnforcement -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Add-KeeperRoleEnforcement -Value Add-KeeperEnterpriseRoleEnforcement

function Update-KeeperEnterpriseRoleEnforcement {
    <#
    .SYNOPSIS
    Updates enforcements for an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Enforcement
    Enforcement(s) in KEY=value format. Can be semicolon or comma separated. Multiple enforcements can be provided as an array.

    .DESCRIPTION
    Updates enforcement policies for an enterprise role. Enforcements are specified in KEY=value format.

    .EXAMPLE
    Update-KeeperEnterpriseRoleEnforcement -Role "AdminRole" -Enforcement "TWO_FACTOR_DURATION_WEB=7200"
    Updates the two-factor authentication duration enforcement

    .EXAMPLE
    Update-KeeperEnterpriseRoleEnforcement -Role "AdminRole" -Enforcement "TWO_FACTOR_DURATION_WEB=7200,MASTER_PASSWORD_MINIMUM_LENGTH=16"
    Updates multiple enforcements separated by commas
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    if ($null -eq $Enforcement -or $Enforcement.Count -eq 0) {
        Write-Error -Message "Enforcement parameter is required. Format: KEY=value;KEY2=value2 (semicolon or comma separated)." -ErrorAction Stop
    }

    $enforcementDict = New-Object 'System.Collections.Generic.Dictionary[KeeperSecurity.Enterprise.RoleEnforcementPolicies,string]'
    $enforcementKeys = New-Object System.Collections.Generic.List[KeeperSecurity.Enterprise.RoleEnforcementPolicies]
    $invalidEnforcements = @()

    foreach ($item in $Enforcement) {
        $parts = $item -split '[;,]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        
        foreach ($part in $parts) {
            $trimmedPart = $part.Trim()
            if ([string]::IsNullOrWhiteSpace($trimmedPart)) { continue }

            $separatorIndex = $trimmedPart.IndexOf('=')
            if ($separatorIndex -lt 0) {
                $separatorIndex = $trimmedPart.IndexOf(':')
            }

            $key = $null
            $value = $null

            if ($separatorIndex -gt 0) {
                $key = $trimmedPart.Substring(0, $separatorIndex).Trim()
                $value = $trimmedPart.Substring($separatorIndex + 1).Trim()
            }
            else {
                $key = $trimmedPart
            }

            $parsedKey = ConvertTo-RoleEnforcementPolicy $key
            if ($null -ne $parsedKey) {
                $enforcementDict[$parsedKey] = $value
                $enforcementKeys.Add($parsedKey)
            }
            else {
                $invalidEnforcements += $key
            }
        }
    }

    if ($invalidEnforcements.Count -gt 0) {
        $validValues = [System.Enum]::GetNames([KeeperSecurity.Enterprise.RoleEnforcementPolicies]) -join ", "
        Write-Error -Message "Invalid enforcements: $($invalidEnforcements -join ', '). Valid values: $validValues" -ErrorAction Stop
    }

    if ($enforcementKeys.Count -eq 0) {
        Write-Error -Message "No valid enforcements specified." -ErrorAction Stop
    }

    try {
        $responses = $roleData.RoleEnforcementUpdateBatch($roleObject, $enforcementDict).GetAwaiter().GetResult()
        
        for ($i = 0; $i -lt $responses.Count; $i++) {
            $response = $responses[$i]
            $enforcementKey = $enforcementKeys[$i]
            if ($response.IsSuccess) {
                $value = if ($enforcementDict.ContainsKey($enforcementKey) -and $enforcementDict[$enforcementKey]) { "=$($enforcementDict[$enforcementKey])" } else { "" }
                Write-Output "Command: $($response.command), Enforcement: $enforcementKey$value, Result: $($response.result)"
            }
            else {
                Write-Output "Command: $($response.command), Enforcement: $enforcementKey, Result: $($response.result), Code: $($response.resultCode), Message: $($response.message)"
            }
        }
    }
    catch {
        Write-Error -Message "Failed to update enforcements: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Update-KeeperEnterpriseRoleEnforcement -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Update-KeeperRoleEnforcement -Value Update-KeeperEnterpriseRoleEnforcement

function Remove-KeeperEnterpriseRoleEnforcement {
    <#
    .SYNOPSIS
    Removes enforcements from an Enterprise Role

    .PARAMETER Role
    Role Name or ID

    .PARAMETER Enforcement
    Enforcement key(s) to remove. Can be semicolon or comma separated. For remove operations, use KEY only (no value).

    .DESCRIPTION
    Removes enforcement policies from an enterprise role. Only the enforcement key is required (no value).

    .EXAMPLE
    Remove-KeeperEnterpriseRoleEnforcement -Role "AdminRole" -Enforcement "TWO_FACTOR_DURATION_WEB"
    Removes the TWO_FACTOR_DURATION_WEB enforcement

    .EXAMPLE
    Remove-KeeperEnterpriseRoleEnforcement -Role "AdminRole" -Enforcement "TWO_FACTOR_DURATION_WEB;MASTER_PASSWORD_MINIMUM_LENGTH"
    Removes multiple enforcements separated by semicolons

    .EXAMPLE
    Remove-KeeperEnterpriseRoleEnforcement -Role "AdminRole" -Enforcement "TWO_FACTOR_DURATION_WEB,MASTER_PASSWORD_MINIMUM_LENGTH"
    Removes multiple enforcements separated by commas
    #>

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

    [Enterprise]$enterprise = getEnterprise
    $roleData = $enterprise.roleData

    $roleObject = resolveRole $roleData $Role
    if (-not $roleObject) {
        return
    }

    if ($null -eq $Enforcement -or $Enforcement.Count -eq 0) {
        Write-Error -Message "Enforcement parameter is required. Format: KEY (for remove operations, use KEY only)." -ErrorAction Stop
    }

    $enforcementKeys = New-Object System.Collections.Generic.List[KeeperSecurity.Enterprise.RoleEnforcementPolicies]
    $invalidEnforcements = @()

    foreach ($item in $Enforcement) {
        $parts = $item -split '[;,]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        
        foreach ($part in $parts) {
            $trimmedPart = $part.Trim()
            if ([string]::IsNullOrWhiteSpace($trimmedPart)) { continue }

            $separatorIndex = $trimmedPart.IndexOf('=')
            if ($separatorIndex -lt 0) {
                $separatorIndex = $trimmedPart.IndexOf(':')
            }

            $key = if ($separatorIndex -gt 0) { $trimmedPart.Substring(0, $separatorIndex).Trim() } else { $trimmedPart }

            $parsedKey = ConvertTo-RoleEnforcementPolicy $key
            if ($null -ne $parsedKey) {
                $enforcementKeys.Add($parsedKey)
            }
            else {
                $invalidEnforcements += $key
            }
        }
    }

    if ($invalidEnforcements.Count -gt 0) {
        $validValues = [System.Enum]::GetNames([KeeperSecurity.Enterprise.RoleEnforcementPolicies]) -join ", "
        Write-Error -Message "Invalid enforcements: $($invalidEnforcements -join ', '). Valid values: $validValues" -ErrorAction Stop
    }

    if ($enforcementKeys.Count -eq 0) {
        Write-Error -Message "No valid enforcements specified." -ErrorAction Stop
    }

    try {
        $responses = $roleData.RoleEnforcementRemoveBatch($roleObject, $enforcementKeys).GetAwaiter().GetResult()
        
        for ($i = 0; $i -lt $responses.Count; $i++) {
            $response = $responses[$i]
            $enforcementKey = $enforcementKeys[$i]
            if ($response.IsSuccess) {
                Write-Output "Command: $($response.command), Enforcement: $enforcementKey, Result: $($response.result)"
            }
            else {
                Write-Output "Command: $($response.command), Enforcement: $enforcementKey, Result: $($response.result), Code: $($response.resultCode), Message: $($response.message)"
            }
        }
    }
    catch {
        Write-Error -Message "Failed to remove enforcements: $($_.Exception.Message)" -ErrorAction Stop
    }
}
Register-ArgumentCompleter -CommandName Remove-KeeperEnterpriseRoleEnforcement -ParameterName Role -ScriptBlock $Keeper_RoleNameCompleter
New-Alias -Name Remove-KeeperRoleEnforcement -Value Remove-KeeperEnterpriseRoleEnforcement

function Copy-KeeperEnterpriseRole {
    <#
        .SYNOPSIS
        Copies an enterprise role to another node with enforcements, users, and teams.

        .DESCRIPTION
        Creates a new role on the target node with the same NewUserInherit and VisibleBelow as the source role,
        copies all enforcements, and optionally copies users and teams from the source role to the new role.

        .PARAMETER SourceRole
        Role name, ID, or EnterpriseRole object to copy from.

        .PARAMETER TargetNode
        Target node name or ID where the new role will be created.

        .PARAMETER NewRoleName
        Display name for the new role.

        .PARAMETER CopyUsers
        Copy users from the source role to the new role. Default is $true.

        .PARAMETER CopyTeams
        Copy teams from the source role to the new role. Default is $true.

        .PARAMETER Force
        Reload enterprise data before running.

        .EXAMPLE
        Copy-KeeperEnterpriseRole -SourceRole "Test-App" -TargetNode "dev" -NewRoleName "second dev"
        Creates a new role "second dev" on node "dev" with enforcements, users, and teams from "Test-App".

        .EXAMPLE
        Copy-KeeperEnterpriseRole -SourceRole "AdminRole" -TargetNode 123456789 -NewRoleName "AdminRole-Copy" -CopyUsers $false
        Copies only enforcements and teams (no users) to the new role.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    Param (
        [Parameter(Position = 0, Mandatory = $true)]$SourceRole,
        [Parameter(Position = 1, Mandatory = $true)][string]$TargetNode,
        [Parameter(Position = 2, Mandatory = $true)][string]$NewRoleName,
        [Parameter()][bool]$CopyUsers = $true,
        [Parameter()][bool]$CopyTeams = $true,
        [Parameter()][switch]$Force
    )

    if ($Force) {
        Sync-KeeperEnterprise | Out-Null
    }

    [Enterprise]$enterprise = getEnterprise
    $enterpriseData = $enterprise.enterpriseData
    $roleData = $enterprise.roleData

    $sourceRoleObject = resolveRole $roleData $SourceRole
    if (-not $sourceRoleObject) {
        return
    }

    $targetNodeObject = resolveSingleNode $TargetNode
    if (-not $targetNodeObject) {
        return
    }

    $nodeId = $targetNodeObject.Id
    $newRoleNameTrimmed = $NewRoleName.Trim()
    $sourceName = $sourceRoleObject.DisplayName

    if ($PSCmdlet.ShouldProcess("Role `"$newRoleNameTrimmed`" on node `"$($targetNodeObject.DisplayName)`"", "Copy from `"$sourceName`"")) {
        try {
            $newRole = $roleData.CreateRole($newRoleNameTrimmed, $nodeId, $sourceRoleObject.NewUserInherit).GetAwaiter().GetResult()
            if (-not $newRole) {
                Write-Error "Failed to create role `"$newRoleNameTrimmed`"" -ErrorAction Stop
                return
            }

            if ($newRole.VisibleBelow -ne $sourceRoleObject.VisibleBelow) {
                try {
                    $updated = $roleData.UpdateRole($newRole, $null, $sourceRoleObject.VisibleBelow, $null).GetAwaiter().GetResult()
                    if ($updated) {
                        $newRole = $updated
                    }
                }
                catch {
                    Write-Warning "Failed to set VisibleBelow for role `"$newRoleNameTrimmed`": $($_.Exception.Message)"
                }
            }

            $sourceEnforcements = @($roleData.GetEnforcementsForRole($sourceRoleObject.Id))
            if ($sourceEnforcements.Count -gt 0) {
                $enforcementDict = New-Object 'System.Collections.Generic.Dictionary[KeeperSecurity.Enterprise.RoleEnforcementPolicies,string]'
                foreach ($re in $sourceEnforcements) {
                    $enforcementTypeStr = $re.EnforcementType
                    if ([string]::IsNullOrWhiteSpace($enforcementTypeStr)) { continue }
                    $normalized = $enforcementTypeStr -replace '_', ''
                    $parsed = $null
                    if ([System.Enum]::TryParse([KeeperSecurity.Enterprise.RoleEnforcementPolicies], $normalized, $true, [ref]$parsed)) {
                        $enforcementDict[$parsed] = if ($re.Value) { $re.Value } else { '' }
                    }
                }
                if ($enforcementDict.Count -gt 0) {
                    $roleData.RoleEnforcementAddBatch($newRole, $enforcementDict).GetAwaiter().GetResult() | Out-Null
                }
            }

            $usersCopied = 0
            if ($CopyUsers) {
                $sourceUserIds = @($roleData.GetUsersForRole($sourceRoleObject.Id))
                foreach ($userId in $sourceUserIds) {
                    $user = $null
                    if ($enterpriseData.TryGetUserById($userId, [ref]$user)) {
                        try {
                            $roleData.AddUserToRole($newRole, $user).GetAwaiter().GetResult() | Out-Null
                            $usersCopied++
                        }
                        catch {
                            Write-Warning "Could not add user `"$($user.Email)`" to new role: $($_.Exception.Message)"
                        }
                    }
                }
            }

            $teamsCopied = 0
            if ($CopyTeams) {
                $sourceTeamUids = @($roleData.GetTeamsForRole($sourceRoleObject.Id))
                foreach ($teamUid in $sourceTeamUids) {
                    $team = $null
                    if ($enterpriseData.TryGetTeam($teamUid, [ref]$team)) {
                        try {
                            $roleData.AddTeamToRole($newRole, $team).GetAwaiter().GetResult() | Out-Null
                            $teamsCopied++
                        }
                        catch {
                            Write-Warning "Could not add team `"$($team.Name)`" to new role: $($_.Exception.Message)"
                        }
                    }
                }
            }

            try {
                $enterprise.loader.Load().GetAwaiter().GetResult() | Out-Null
            }
            catch {
                Write-Warning "Failed to reload enterprise data: $($_.Exception.Message)"
            }

            $msg = "Role `"$newRoleNameTrimmed`" created with enforcements from `"$sourceName`""
            if ($usersCopied -gt 0 -or $teamsCopied -gt 0) {
                $msg += " ($usersCopied user(s), $teamsCopied team(s) copied)"
            }
            $msg += "."
            Write-Output $msg
        }
        catch {
            Write-Error "Copy role failed: $($_.Exception.Message)" -ErrorAction Stop
        }
    }
}
Register-ArgumentCompleter -CommandName Copy-KeeperEnterpriseRole -ParameterName SourceRole -ScriptBlock $Keeper_RoleNameCompleter
Register-ArgumentCompleter -CommandName Copy-KeeperEnterpriseRole -ParameterName TargetNode -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
    $result = @()
    [Enterprise]$enterprise = $Script:Context.Enterprise
    if (-not $enterprise) { return $null }
    $to_complete = if ($wordToComplete) { $wordToComplete + '*' } else { '*' }
    foreach ($node in $enterprise.enterpriseData.Nodes) {
        if ($node.DisplayName -like $to_complete) {
            $nodeName = $node.DisplayName
            if ($nodeName -match '[\s'']') { $nodeName = $nodeName -replace '''', ''''''; $nodeName = "'${nodeName}'" }
            $result += $nodeName
        }
    }
    if ($result.Count -gt 0) { return $result }
    return $null
}
New-Alias -Name kercopy -Value Copy-KeeperEnterpriseRole
# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBxkoHDt1qYbhJh
# bbfKPNscx5tq1GmBt6TwVtiZYbThe6CCITswggWNMIIEdaADAgECAhAOmxiO+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
# oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE
# BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy
# dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB
# MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC
# NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ
# cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC
# VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK
# ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5
# IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a
# vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ
# aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc
# vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC
# h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt
# 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk
# Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s
# FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm
# 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw
# ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG
# 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB
# BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC
# B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p
# bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI
# QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT
# QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA
# A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6
# z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2
# IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL
# iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu
# FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc
# H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA
# 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB
# +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b
# WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A
# g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH
# 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB
# MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD
# VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI
# QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB
# hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE
# AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ
# BDEiBCDk55YwMZ9Ct6Wnz7plsbs77v1n03d4byVOMJGj9KUr9DANBgkqhkiG9w0B
# AQEFAASCAYA6n6SVJqDwJKyegvRshEtaH0M7U6tqw2aEdFY4dRx9fMHy7u7OuhEC
# W+DRqvaVCt/AzCIpnJXw9LLoXb3Y+zbNBRJKfotKDe7eOP4+jW8iB1e7Mi3Mlmxl
# sIHjlwifC/5qOjTCoaw0p+uOT/tfrk5aDp7KDLP8dBR1DuXW639tgwguy/NQtTPo
# ojWX54Vye0QST4t2z/TsZIKfFlxgimXUizpi4WBbxhBkxuseH7L3YM/7Hy9tSd3I
# fU93rXCgt/AshfVXRK0qfbR/Llpm9Pb5icn3W6ePuqtSWp897KUSuQ5ytu6Z9OqS
# 8ws/xa+i4qOW2+stPOijtRRBYKUx2XfCWYZ3EDw/6vXSxhA5ayfCSDLA+avdEjHE
# m6Mfagj77fSnC36uxxMI9huBmkbVjrOM+NuEKCBXWnFW8TtbmxBHj2ZDudA6QDe+
# O75+97JQXMhr7E+fx00/ylGaZ49NsklvTnnuBd96xS0jPBwcvyytz/yeXtgo0QCt
# g0APAM6or/2hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNTI2MDQwNjQwWjAv
# BgkqhkiG9w0BCQQxIgQgwl42IqIozEyqgpV57/a/L4YeZNDItdqT8JCsV1ZEcA0w
# DQYJKoZIhvcNAQEBBQAEggIAAP+xxO0rWyG/u5rDtFCFzvBGAt1TErqnEU/gheu1
# V6NuEevOMJydvCx6Cx96ytqzOhKFYUX8zmHmtc7OSJvNPa3RcZ1cF7YZJJbCHpRZ
# eiolncuDIztl9mdlvXo9C6HVjbHef9eRV0CXd4H8UhVy1Q6M3Jci6pMHqMNYCIFX
# 4219bOkw/20ybSgr/2nH9WIuNMyxqiE23AEsRXrE9QMhuC+tcOtx9Y9Pg8Pdn2tV
# THOStOq7Qr3FXI/E2VaQKh5tIMFvoZljahA5rJrPdadHiIhJisMlddBshdQ6rYxB
# 2iFnvFJ2gbkYc+Ntn8/cqzkeHNq3iBvukjdjRJnaWBSnNoWweCy9u/RHkEFBuDIC
# V3rJkizFoePBQ2WC5re7u5ckbExCFqWo7NpW+mRmitM1R0HHWbQSjdKsrneTDY9D
# t1RORGewdqbgv0lzvZ3fVyA+F9KzpsMtqz7szv9yENMj9/aJqvTx//whe4ciyDQT
# nkVkwLJGdqX8uA29yF479hnHmKh4cSwsIiKRPOO/B0JH5PMV6bwjNqTNh+eQLs0m
# svsR47DDzdYLUZB4+S7B/nrZvfSzzhl/x0NisDwri2agQgsyYrxVncOJC+eeXFdr
# w3k0jloK2oqFpxZYUXMqQh6lY92h8XhOPdSWeStkywjajfjvc4OVugKNMKmt/ELc
# GKM=
# SIG # End signature block