PPS.psm1

# Override Write-Verbose in this module so calling function is added to the message
function script:Write-Verbose
{
    [CmdletBinding()]
    param
    (
       [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)] [String] $Message
    )

    begin
    {}

    process
    {
        try
        {
            $PSBoundParameters['Message'] = $((Get-PSCallStack)[1].Command) + ': ' + $PSBoundParameters['Message']
        }
        catch
        {}

        Microsoft.PowerShell.Utility\Write-Verbose @PSBoundParameters
    }

    end
    {}
}

function Connect-Pps
{
    <#
        .SYNOPSIS
            Connect to Pleasant Password Server

        .DESCRIPTION
            Connect to Pleasant Password Server

        .PARAMETER Uri
            URI of server to connect to, eg. https://password.company.tld:10001

        .PARAMETER Credential
            Credential object with username and password used to connect to Pleasant Password Server with

        .PARAMETER Username
            Username used to connect to Pleasant Password Server with

        .PARAMETER Password
            Password used to connect to Pleasant Password Server with

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            Connect-Pps -Uri https://password.company.tld:10001
    #>


    [CmdletBinding(DefaultParameterSetName='Default')]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [string]
        $Uri,

        [Parameter(ParameterSetName='Credential', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [pscredential]
        $Credential,

        [Parameter(ParameterSetName='UserPass', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [string]
        $Username,

        [Parameter(ParameterSetName='UserPass', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [string]
        $Password,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')

        # FIXXXME - should this be moved out - so it is run when module is imported?
        if (-not $script:SessionList)
        {
            Write-Verbose -Message 'Creating session list'
            $script:SessionList = @{}
        }
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            if ($Uri -notmatch ':') {$Uri = "https://${Uri}:10001"}
            $Uri = $Uri -replace '/$'

            if (-not ($Credential -or $Username -or $Password))
            {
                $PSBoundParameters['Credential'] = Get-Credential -Message 'PPS'
            }


            $oauth2Params = @{
                Uri          = "$Uri/OAuth2/Token"
                ReturnHeader = $true
            }
            $PSBoundParameters.GetEnumerator() | Where-Object -Property Key -In -Value 'Credential','Username','Password' | ForEach-Object -Process {
                $oauth2Params[$_.Key] = $_.Value
            }

            $script:SessionList[$Session] = [PSCustomObject] @{
                Uri     = $Uri
                Headers = Connect-OAuth2 @oauth2Params
            }
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}

function Export-PpsEntry
{
    <#
        .SYNOPSIS
            xxx

        .DESCRIPTION
            xxx

        .PARAMETER RootPath
            xxx

        .PARAMETER WithId
            xxx

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            xxx
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory=$true)]
        [string]
        $RootPath,

        [Parameter()]
        [switch]
        $WithId,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')

        $p = @{
            Session = $Session
        }

        function GetGrp ([guid] $GroupId, [string[]] $Path = @())
        {
            $group = Get-PpsGroup @p -Id $GroupId
            $group.Credentials  | ForEach-Object -Process {
                $e = [PSCustomObject] @{
                    Path     = $Path -join '/'
                    Name     = $_.Name
                    Username = $_.UserName
                    Password = Get-PpsEntry @p -Id $_.Id -PasswordOnly
                    Url      = $_.Url
                    Notes    = $_.Notes -replace "`r`n","`n"
                }
                if ($WithId) {$e | Add-Member -NotePropertyName Id -NotePropertyValue $_.Id}
                $e
            }
            $group.Children | ForEach-Object -Process {
                GetGrp -GroupId $_.Id -Path ($Path + $_.Name)
            }
        }
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $groupId = Get-PpsGroup @p -Path $RootPath -ReturnId
            GetGrp -GroupId $groupId
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
function Get-PpsEntry
{
    <#
        .SYNOPSIS
            Get credential entry from Pleasant Password Server

        .DESCRIPTION
            Get credential entry from Pleasant Password Server

        .PARAMETER Id
            ID of entry to get info about

        .PARAMETER Path
            xxx

        .PARAMETER Name
            xxx

        .PARAMETER AllowMultiple
            xxx

        .PARAMETER ReturnPSCredential
            xxx

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            Get-PpsEntry -Id 5cbfabe7-70ee-4041-a1e0-263c9170f650
    #>


    [CmdletBinding(DefaultParameterSetName='Id')]
    [OutputType([PSCustomObject], ParameterSetName='Id')]
    [OutputType([PSCustomObject], ParameterSetName='Path')]
    [OutputType([PSCredential], ParameterSetName='IdCred')]
    [OutputType([PSCredential], ParameterSetName='PathCred')]
    param
    (
        [Parameter(ParameterSetName='Id', Mandatory=$true, Position=0)]
        [Parameter(ParameterSetName='IdCred', Mandatory=$true, Position=0)]
        [Parameter(ParameterSetName='IdPw', Mandatory=$true, Position=0)]
        [guid]
        $Id,

        [Parameter(ParameterSetName='Path', Mandatory=$true)]
        [Parameter(ParameterSetName='PathCred', Mandatory=$true)]
        [string]
        $Path,

        [Parameter(ParameterSetName='Path')]
        [Parameter(ParameterSetName='PathCred')]
        [string]
        $Name,

        [Parameter(ParameterSetName='Path')]
        [Parameter(ParameterSetName='PathCred')]
        [switch]
        $AllowMultiple,

        [Parameter(ParameterSetName='IdCred', Mandatory=$true)]
        [Parameter(ParameterSetName='PathCred', Mandatory=$true)]
        [switch]
        $ReturnPSCredential,

        [Parameter(ParameterSetName='IdPw', Mandatory=$true)]
        [switch]
        $PasswordOnly,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $p = @{
                Session = $Session
            }

            if ($PasswordOnly -and $Id)
            {
                $entry = @([PSCustomObject] @{Id = $Id; Password = ''})
            }
            elseif ($Id)
            {
                $entry = @(Invoke-PpsApiRequest @p -Uri "credential/$Id")
            }
            elseif ($Path)
            {
                $group = Get-PpsGroup @p -Path $Path
                $entry = @($group.Credentials)
                if ($Name)
                {
                    $entry = @($entry | Where-Object -Property Name -Like -Value $Name)
                }
                if ($entry.Count -gt 1 -and -not $AllowMultiple)
                {
                    throw "More than one ($($entry.Count)) entries returned and -AllowMultiple is not set"
                }
            }
            else
            {
                throw 'Should never happen'
            }

            foreach ($e in $entry)
            {
                $e.Password = Invoke-PpsApiRequest @p -Uri "credential/$($e.Id)/password"
            }

            # Return
            if ($ReturnPSCredential)
            {
                foreach ($e in $entry)
                {
                    [pscredential]::new($e.Username, ($e.Password | ConvertTo-SecureString -AsPlainText -Force))
                }
            }
            elseif ($PasswordOnly)
            {
                foreach ($e in $entry)
                {
                    $e.Password
                }
            }
            else
            {
                $entry
            }
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
function Get-PpsGroup
{
    <#
        .SYNOPSIS
            Get credential group (folder) from Pleasant Password Server

        .DESCRIPTION
            Get credential group (folder) from Pleasant Password Server

        .PARAMETER All
            xxx

        .PARAMETER Id
            ID of group to get info about

        .PARAMETER Path
            xxx

        .PARAMETER ReturnId
            xxx

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            Get-PpsGroup -Id f6190167-a9a1-4386-a8cd-ae46008c9188

        .EXAMPLE
            Get-PpsGroup -All
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(ParameterSetName='All', Mandatory=$true)]
        [switch]
        $All,

        [Parameter(ParameterSetName='Id', Mandatory=$true, Position=0)]
        [guid]
        $Id,

        [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0)]
        [string]
        $Path,

        [Parameter(ParameterSetName='Id')]
        [Parameter(ParameterSetName='Path')]
        [switch]
        $ReturnId,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')

        # FIXXXME - should this be moved out - so it is run when module is imported?
        if (-not $script:GroupCache)
        {
            Write-Verbose -Message 'Creating group cache'
            $script:GroupCache = @{}
        }
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $p = @{
                Session = $Session
            }

            $uri = 'credentialgroup'

            if ($Path)
            {
                if ($script:GroupCache.ContainsKey($Path))
                {
                    $Id = $script:GroupCache[$Path]
                }
                elseif ($Path -ceq 'Root')
                {
                    $script:GroupCache[$Path] = $Id = Invoke-PpsApiRequest @p -Uri "$uri/root"
                }
                elseif ($Path -notmatch '/')
                {
                    throw "First part of path should always be Root, not $Path"
                }
                else
                {
                    $parentPath, $name = $Path -split '/(?=[^/]*$)'
                    $parent = Get-PpsGroup @p -Path $parentPath
                    try
                    {
                        $script:GroupCache[$Path] = $Id = $parent.Children | Where-Object -Property Name -CEQ -Value $Name | Select-Object -First 1 -ExpandProperty Id
                    }
                    catch
                    {
                        throw "Group path $Path not found"
                    }
                }
            }

            if ($Id)
            {
                $uri = "$uri/$Id"
            }

            if ($ReturnId)
            {
                # Return
                $Id
            }
            else
            {
                # Return
                Invoke-PpsApiRequest @p -Uri $uri
            }
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
function Import-PpsEntry
{
    <#
        .SYNOPSIS
            xxx

        .DESCRIPTION
            xxx

        .PARAMETER RootPath
            xxx

        .PARAMETER InputObject
            xxx

        .PARAMETER NoCheck
            xxx

        .PARAMETER CheckProperty
            xxx

        .PARAMETER DryRun
            xxx

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            xxx

        .EXAMPLE
            xxx
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory=$true)]
        [string]
        $RootPath,

        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [PSCustomObject]
        $InputObject,

        [Parameter()]
        [switch]
        $NoCheck,

        [Parameter()]
        [string[]]
        $CheckProperty = @('Name', 'Username'),

        [Parameter()]
        [switch]
        $DryRun,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')

        $p = @{
            Session = $Session
        }

        $allProperties = @('Name', 'Username', 'Password', 'Url', 'Notes')

        if (-not $NoCheck)
        {
            # Some versions of PowerShell doesn't seem to trigger ValidateScript on an empty array. That's why it's located here
            if (-not $CheckProperty.Count) {throw 'CheckProperty should not be empty'}
            $CheckProperty | ForEach-Object -Process {if ($_ -notin $allProperties) {throw "$_ is not allowed in CheckProperty, only $($allProperties -join ',') is allowed"}}
            $CheckProperty = @('Path') + $CheckProperty

            try
            {
                $ErrorActionPreference = 'Stop'
                $existing = @(Export-PpsEntry @p -RootPath $RootPath -WithId)
                if (-not ($existingHash = $existing | Group-Object -Property $CheckProperty -AsHashTable -AsString))
                {
                    $existingHash = @{}
                }
            }
            catch
            {
                $existing = @()
                $existingHash = @{}
            }
        }
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $fullPath = if ($InputObject.Path) {$RootPath + '/' + $InputObject.Path} else {$RootPath}

            $entryParams = @{}
            $allProperties | ForEach-Object -Process {
                $entryParams[$_] = $InputObject.$_
            }

            if ($NoCheck)
            {
                "Creating $($InputObject.Path), $($InputObject.Name)"
                if (-not $DryRun)
                {
                    $null = New-PpsEntry @p -Path $fullPath @entryParams
                }
            }
            else
            {
                $key = ($InputObject | Group-Object -Property $CheckProperty -AsHashTable -AsString).Keys | Select-Object -First 1
                if ($e = @($existingHash[$key] | Where-Object -FilterScript {-not $_._PROCESSED_}))
                {
                    if ($e.Count -gt 1) {Write-Warning -Message "Found $($e.Count) objects matching ""$key"", just selecting first match"}
                    $e = $e[0]
                    $e | Add-Member -NotePropertyName _PROCESSED_ -NotePropertyValue $true
                    if (Compare-Object -ReferenceObject $e -DifferenceObject $InputObject -Property $allProperties -CaseSensitive)
                    {
                        "Updating $key" | Write-Host
                        if (-not $DryRun)
                        {
                            $null = Get-PpsEntry @p -Id $e.Id | Set-PpsEntry @p @entryParams
                        }
                    }
                    else
                    {
                        "OK $key" | Write-Host
                    }
                }
                else
                {
                    "Creating $key" | Write-Host
                    if (-not $DryRun)
                    {
                        $null = New-PpsEntry @p -Path $fullPath @entryParams
                    }
                }
            }

        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        # Not processed
        #$existing | Where-Object -FilterScript {-not $_._PROCESSED_}

        Write-Verbose -Message 'End'
    }
}
function Invoke-PpsApiRequest
{
    <#
        .SYNOPSIS
            Invoke API request against Pleasant Password Server

        .DESCRIPTION
            Invoke API request against Pleasant Password Server

        .PARAMETER Uri
            xxx

        .PARAMETER Method
            xxx

        .PARAMETER Data
            xxx

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            xxx
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory=$true)]
        [string]
        $Uri,

        [Parameter()]
        [Microsoft.PowerShell.Commands.WebRequestMethod]
        $Method = 'Get',

        [Parameter()]
        [object]
        $Data,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')

        # FIXXXME - should this be moved out - so it is run when module is imported?
        if (-not $script:SessionList)
        {
            Write-Verbose -Message 'Creating session list'
            $script:SessionList = @{}
        }
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            if (-not ($s = $script:SessionList[$Session]))
            {
                throw "Session <$Session> not found"
            }

            $requestParams = @{
                Method          = $Method
                Uri             = "$($s.Uri)/api/v4/rest/$Uri"  # FIXXXME - v4 should'n be hardcoded here!
                Headers         = $s.Headers
                UseBasicParsing = $true
            }
            if ($Data)
            {
                $requestParams['Body']        = ConvertTo-Json -Compress -Depth 9 -InputObject $Data
                $requestParams['ContentType'] = 'application/json; charset=utf-8'
            }

            Write-Verbose -Message ($requestParams | ConvertTo-Json)

            # Return
            Invoke-RestMethod @requestParams
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
function New-PpsEntry
{
    <#
        .SYNOPSIS
            Create new credential entry in Pleasant Password Server

        .DESCRIPTION
            Create new credential entry in Pleasant Password Server

        .PARAMETER Entry
            Object with data to create in Pleasant Password Server

        .PARAMETER GroupId
            ID of credential group to create credential entry in

        .PARAMETER Name
            Name of credential in Pleasant Password Server

        .PARAMETER Username
            Username of credential in Pleasant Password Server

        .PARAMETER Password
            Password of credential in Pleasant Password Server

        .PARAMETER PSCredential
            xxx

        .PARAMETER Url
            Url of credential in Pleasant Password Server

        .PARAMETER Notes
            Notes of credential in Pleasant Password Server

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            New-PpsEntry -GroupId f6190167-a9a1-4386-a8cd-ae46008c9188 -Name name -Username uname -Password pw -Url http://abc -Notes "This i a note"

        .EXAMPLE
            @{GroupId='f6190167-a9a1-4386-a8cd-ae46008c9188'; Name='name'; Username='user'; Password='pw'} | New-PpsEntry
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(ParameterSetName='Entry', Mandatory=$true, ValueFromPipeline=$true)]
        [PSCustomObject]
        $Entry,

        [Parameter(ParameterSetName='Properties', Mandatory=$true)]
        [Parameter(ParameterSetName='PSCredential', Mandatory=$true)]
        [guid]
        $GroupId,

        [Parameter(ParameterSetName='PropertiesPath', Mandatory=$true)]
        [Parameter(ParameterSetName='PSCredentialPath', Mandatory=$true)]
        [string]
        $Path,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PSCredential')]
        [Parameter(ParameterSetName='PropertiesPath')]
        [Parameter(ParameterSetName='PSCredentialPath')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Name,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PropertiesPath')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Username,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PropertiesPath')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Password,

        [Parameter(ParameterSetName='PSCredential', Mandatory=$true)]
        [Parameter(ParameterSetName='PSCredentialPath', Mandatory=$true)]
        [PSCredential]
        $PSCredential,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PSCredential')]
        [Parameter(ParameterSetName='PropertiesPath')]
        [Parameter(ParameterSetName='PSCredentialPath')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Url,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PSCredential')]
        [Parameter(ParameterSetName='PropertiesPath')]
        [Parameter(ParameterSetName='PSCredentialPath')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Notes,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $p = @{
                Session = $Session
            }

            if (-not $Entry)
            {
                if ($PSCredential)
                {
                    $Username = $PSCredential.UserName
                    $Password = $PSCredential.GetNetworkCredential().Password
                }

                if ($Path)
                {
                    $GroupId = New-PpsGroup @p -Path $Path -ReturnId
                }

                $Entry = [PSCustomObject] @{
                    GroupId  = $GroupId
                    Name     = $Name
                    Username = $Username
                    Password = $Password
                    Url      = $Url
                    Notes    = $Notes
                }
            }

            $id = Invoke-PpsApiRequest @p -Uri "credential" -Data $Entry -Method Post

            # Return
            [PSCustomObject] @{
                Id  = $id
                Uri = "$($script:SessionList[$Session].Uri)/WebClient/Main?itemId=$id"
            }
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
function New-PpsGroup
{
    <#
        .SYNOPSIS
            Create new credential group (folder) from Pleasant Password Server

        .DESCRIPTION
            Create new credential group (folder) from Pleasant Password Server

        .PARAMETER Group
            Object contining group info

        .PARAMETER ParentId
            ID of parent group

        .PARAMETER Name
            Name of the new group (folder)

        .PARAMETER Path
            xxx

        .PARAMETER ReturnId
            xxx

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            New-PpsGroup -ParentId f6190167-a9a1-4386-a8cd-ae46008c9188 -Name "New Password Group"
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(ParameterSetName='Group', Mandatory=$true, ValueFromPipeline=$true)]
        [PSCustomObject]
        $Group,

        [Parameter(ParameterSetName='Properties', Mandatory=$true)]
        [guid]
        $ParentId,

        [Parameter(ParameterSetName='Properties', Mandatory=$true)]
        [string]
        $Name,

        [Parameter(ParameterSetName='Path', Mandatory=$true)]
        [string]
        $Path,

        [Parameter()]
        [switch]
        $ReturnId,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $p = @{
                Session = $Session
            }

            if ($Path)
            {
                try
                {
                    # Return
                    Get-PpsGroup @p -Path $Path -ReturnId:$ReturnId
                }
                catch
                {
                    if ($Path -notmatch '/')
                    {
                        throw "First part of path should always be Root, not $Path"
                    }

                    $parentPath, $n = $Path -split '/(?=[^/]*$)'
                    $parentId = New-PpsGroup @p -Path $parentPath -ReturnId

                    # Return
                    New-PpsGroup @p -ParentId $ParentId -Name $n -ReturnId:$ReturnId
                }
            }
            else
            {
                if (-not $Group)
                {
                    $Group = [PSCustomObject] @{
                        ParentId = $ParentId
                        Name     = $Name
                    }
                }

                $id = Invoke-PpsApiRequest @p -Uri "credentialgroup" -Data $Group -Method Post

                # Return
                Get-PpsGroup @p -Id $Id -ReturnId:$ReturnId
            }
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
function Remove-PpsEntry
{
    <#
        .SYNOPSIS
            Remove credential entry from Pleasant Password Server

        .DESCRIPTION
            Remove credential entry from Pleasant Password Server

        .PARAMETER Id
            ID of entry to get info about

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            Remove-PpsEntry -Id 5cbfabe7-70ee-4041-a1e0-263c9170f650
    #>


    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory=$true, Position=0, ValueFromPipelineByPropertyName=$true)]
        [guid]
        $Id,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $p = @{
                Session = $Session
            }

            Invoke-PpsApiRequest @p -Method Delete -Uri "credential/$Id"
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
function Set-PpsEntry
{
    <#
        .SYNOPSIS
            Update existing credential entry in Pleasant Password Server

        .DESCRIPTION
            Update existing credential entry in Pleasant Password Server

        .PARAMETER Entry
            Object with updated info

        .PARAMETER Name
            Name of credential in Pleasant Password Server

        .PARAMETER Username
            Username of credential in Pleasant Password Server

        .PARAMETER Password
            Password of credential in Pleasant Password Server

        .PARAMETER PSCredential
            xxx

        .PARAMETER Url
            Url of credential in Pleasant Password Server

        .PARAMETER Notes
            Notes of credential in Pleasant Password Server

        .PARAMETER Session
            Makes it possible to connect to multiple Pleasant Password Servers

        .EXAMPLE
            $e=Get-PpsEntry -Id c079a48c-a465-4605-9477-2b4baa743e6f; $e.Username='user'; $e|Set-PpsEntry
    #>


    [CmdletBinding(DefaultParameterSetName='Properties')]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [PSCustomObject]
        $Entry,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PSCredential')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Name,

        [Parameter(ParameterSetName='Properties')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Username,

        [Parameter(ParameterSetName='Properties')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Password,

        [Parameter(ParameterSetName='PSCredential', Mandatory=$true)]
        [PSCredential]
        $PSCredential,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PSCredential')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Url,

        [Parameter(ParameterSetName='Properties')]
        [Parameter(ParameterSetName='PSCredential')]
        [AllowNull()]
        [AllowEmptyString()]
        [string]
        $Notes,

        [Parameter()]
        [string]
        $Session = 'Default'
    )

    begin
    {
        Write-Verbose -Message "Begin (ErrorActionPreference: $ErrorActionPreference)"
        $origErrorActionPreference = $ErrorActionPreference
        $verbose = $PSBoundParameters.ContainsKey('Verbose') -or ($VerbosePreference -ne 'SilentlyContinue')
    }

    process
    {
        Write-Verbose -Message "Process begin (ErrorActionPreference: $ErrorActionPreference)"

        try
        {
            # Make sure that we don't continue on error, and that we catches the error
            $ErrorActionPreference = 'Stop'

            $p = @{
                Session = $Session
            }

            if ($PSCredential)
            {
                $PSBoundParameters['Username'] = $PSCredential.UserName
                $PSBoundParameters['Password'] = $PSCredential.GetNetworkCredential().Password
            }

            'Name', 'Username', 'Password', 'Url', 'Notes' | ForEach-Object -Process {
                if ($PSBoundParameters.ContainsKey($_))
                {
                    $Entry.$_ = $PSBoundParameters[$_]
                }
            }

            $id = $Entry.Id
            $null = Invoke-PpsApiRequest @p -Uri "credential/$($Entry.Id)" -Data $Entry -Method Put

            # Return
            [PSCustomObject] @{
                Id  = $id
                Uri = "$($script:SessionList[$Session].Uri)/WebClient/Main?itemId=$id"
            }
        }
        catch
        {
            Write-Verbose -Message "Encountered an error: $_"
            Write-Error -ErrorAction $origErrorActionPreference -Exception $_.Exception
        }
        finally
        {
            $ErrorActionPreference = $origErrorActionPreference
        }

        Write-Verbose -Message 'Process end'
    }

    end
    {
        Write-Verbose -Message 'End'
    }
}
Export-ModuleMember -Function New-PpsEntry
Export-ModuleMember -Function New-PpsGroup
Export-ModuleMember -Function Connect-Pps
Export-ModuleMember -Function Get-PpsGroup
Export-ModuleMember -Function Invoke-PpsApiRequest
Export-ModuleMember -Function Import-PpsEntry
Export-ModuleMember -Function Get-PpsEntry
Export-ModuleMember -Function Remove-PpsEntry
Export-ModuleMember -Function Set-PpsEntry
Export-ModuleMember -Function Export-PpsEntry