NabuNetManager.psm1
# script variables... $CurrentContext = @{ Host = $null Token = $null # access token for this current connection... TokenExpires = $null Name = $null } # helper classes first... class NabuArticleBase { NabuArticleBase($in) { $this.Title = $in.title $this.Article = $in.article $this.Created = [DateTime]::Parse($in.created) $this.ReferenceDate = $in.referenceDate } [string]$Title [string]$Article [DateTime]$Created [Nullable[DateTime]]$ReferenceDate } class NabuHost { NabuHost($name) { $this.Name = $name } NabuHost($name, $serialized) { $this.Name = $name $this.RemoteBaseUri = $serialized.RemoteBaseUri $this.Registered = $serialized.Registered $this.LastContacted = $serialized.LastContacted $this.Token = $serialized.Token $this.RemoteName = $serialized.RemoteName $this.RemoteTagline = $serialized.RemoteTagline } [string] $Name [string] $RemoteBaseUri [datetime] $Registered [datetime] $LastContacted [string] $Token [string] $RemoteName [string] $RemoteTagline } # public visible server context info class NabuHostInfo { NabuHostInfo($in, $name = $null) { $this.Name = $name ?? $in.Name $this.Uri = $in.RemoteBaseUri $this.LastContacted = $in.LastContacted $this.Registered = $in.Registered $this.HasToken = [bool]$in.Token $this.RemoteName = $in.RemoteName $this.RemoteTagline = $in.RemoteTagline } [string] $Name [string] $Uri [string] $RemoteTagline [string] $RemoteName [DateTime] $LastContacted [DateTime] $Registered [bool] $HasToken } class NabuTemplateInfo { NabuTemplateInfo($in, $name = $null) { $this.Id = $name ?? $in.Id $this.Subject = $in.Subject $this.Body = $in.Body } [ValidatePattern("^[a-zA-Z0-9]+$", ErrorMessage = "Only letters and digits allowed!")] [string] $Id [ValidatePattern("^[^\n\r]+$", ErrorMessage = "May not contain newlines!")] [string] $Subject [string] $Body } # helper functions next... function Save-RegisteredHost { [CmdletBinding()] param ( [Parameter()] [string]$Name, [Parameter()] [NabuHost]$Data ) $path = Join-Path -Path "~" -ChildPath ".nabunet" if (!(Test-Path -Path $path)) { New-Item -ItemType Directory -Path $path | Out-Null # $data = Import-PowerShellDataFile -Path $path # New-Object NabuHost -ArgumentList $data } $path = Join-Path -Path $path -ChildPath "$Name.xml" $Data | Export-Clixml -Path $path } function Load-RegisteredHost { [CmdletBinding()] param ( [Parameter()] [string] $Name ) $path = Join-Path -Path "~" -ChildPath ".nabunet" $path = Join-Path -Path $path -ChildPath "$Name.xml" if (Test-Path -Path $path) { Import-Clixml -Path $path | ForEach-Object { New-Object NabuHost -ArgumentList $Name, $_ } # | Where-Object { $_ -is [NabuHost] } # $data = Import-PowerShellDataFile -Path $path # New-Object NabuHost -ArgumentList $data } } function Get-AllRegisteredHost { [CmdletBinding()] param ( ) $path = Join-Path -Path "~" -ChildPath ".nabunet" if (Test-Path -Path $path) { Get-ChildItem -Path (Join-Path -Path $path -ChildPath "*.xml") } } function Call-Napi { [CmdletBinding(PositionalBinding = $false)] param ( [Parameter()] [NabuHost]$Connection = $null, [Parameter(Mandatory = $true)] [string]$Path, [Parameter()] [string]$Version = "v1", [Parameter()] [string]$Method = "GET", [Parameter()] [Object]$Body = $null, [Parameter()] [hashtable]$Query = $null ) $Token = $null if (!$Connection) { $Connection = $script:CurrentContext.Host $Token = $script:CurrentContext.Token $Expires = $script:CurrentContext.TokenExpires if ($null -eq $Token -or ($Expires -lt [DateTime]::UtcNow)) { # token is missing or expired... if ($null -ne $Connection.Token) { Write-Verbose "Access token needed, requesting..." $tmp = Call-Napi -Connection $Connection -Version $Version -Path "login" -Body @{ token = $Connection.Token } -Method "POST" if ($tmp.token) { $script:CurrentContext.Token = $tmp.token $Token = $tmp.token $script:CurrentContext.TokenExpires = [datetime]::Parse($tmp.validUntil).AddSeconds(-10) # safety margin... $Expires = $script:CurrentContext.TokenExpires $tmp = $null } else { $script:CurrentContext.Token = $null $script:CurrentContext.TokenExpires = $null throw "Login call didn't yield a valid token..." } } else { Write-Verbose "Access token needed, but no token provided, can only call anonymously!" } } else { Write-Verbose "Access token present and valid." } } if (!$Connection) { throw "Session is not connected! Use Connect-NabuNetHost first!" } $Uri = "$($Connection.RemoteBaseUri)/$Version/$Path" if ($Query) { $sep = "?" foreach ($qname in $Query.Keys) { $Uri += "$sep$qname=$([Uri]::EscapeDataString($Query[$qname]))" $sep = "&" } } Write-Verbose "Call: URI=$Uri" $hdr = @{} if ($Token) { $hdr["Authorization"] = "Bearer $Token" Write-Verbose "Authentication set" } if ($Body) { $Body = $Body | ConvertTo-Json $tmp = Invoke-RestMethod -Method $Method -Uri $Uri -UserAgent "NabuNetPS/1" -Headers $hdr -Body $Body -ContentType "application/json" -StatusCodeVariable code -SkipHttpErrorCheck } else { $tmp = Invoke-RestMethod -Method $Method -Uri $Uri -UserAgent "NabuNetPS/1" -Headers $hdr -StatusCodeVariable code -SkipHttpErrorCheck } switch ($code) { 200 { $tmp } # normal result. 204 {} # expected sometimes: no content. 302 { throw "Access denied!" } # 302 redirect to login page; should look into that and make the server return a better error, but it's OK for now. 404 { Write-Error "Item not found" } default { throw "Unkown/unexpected status code returned from remote API: $code" } } } # actual module functions start here... function Get-Account { [CmdletBinding()] param ( ) Call-Napi -Path "accounts"# | ForEach-Object { @{ UserName = $_ } } } function Get-ServerAnnouncement { [CmdletBinding()] param ( [switch]$Raw ) <# .SYNOPSIS Retreives the current server announcement message (maintenance notice) .DESCRIPTION The Nabu Server has a single, central special article for server maintenance info. If set, it will be shown prominently on the landing page and some other key pages as a headline with a link to the full article. .OUTPUTS NabuArticleBase .PARAMETER Raw If provided, the returned article info from the server will be the raw (markdown) text, if not a plain text rendering is provided. #> Call-Napi -Path "announcement" -Query @{ raw = $Raw.IsPresent } | ForEach-Object { New-Object NabuArticleBase -ArgumentList $_ } } function Clear-ServerAnnouncement { <# .SYNOPSIS Clears (removes) a server announcement message (maintenance notice) .DESCRIPTION The Nabu Server has a single, central special article for server maintenance info. If set, it will be shown prominently on the landing page and some other key pages as a headline with a link to the full article. .OUTPUTS nothing #> [CmdletBinding(PositionalBinding = $false, ConfirmImpact = "Medium", SupportsShouldProcess = $true)] param () if ($PSCmdlet.ShouldProcess($CurrentContext.Host.Name, "Remove announcement message on server")) { Write-Verbose "Removing server announcement message." Call-Napi -Path "announcement" -Method "DELETE" | Out-Null } } function Set-ServerAnnouncement { <# .SYNOPSIS Sets (updates or creates) a server announcement message (maintenance notice) .DESCRIPTION The Nabu Server has a single, central special article for server maintenance info. If set, it will be shown prominently on the landing page and some other key pages as a headline with a link to the full article. .OUTPUTS NabuArticleBase .PARAMETER Title The title for the article. .PARAMETER Article The body (full text, markdown!) for the article. .PARAMETER ReferenceDate If provided, indicates a date (and time) for the operation in question. The server will include that info on the details page, complete with absolute and relative server time info. .PARAMETER Raw If provided, the returned article info from the server will be the raw (markdown) text, if not a plain text rendering is provided. #> [CmdletBinding(PositionalBinding = $false, ConfirmImpact = "Medium", SupportsShouldProcess = $true)] param ( [Parameter(Position = 0, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string]$Title, [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string]$Article, [Parameter(ValueFromPipelineByPropertyName = $true)] [Nullable[datetime]]$ReferenceDate = $null, [switch]$Raw ) process { if ($PSCmdlet.ShouldProcess($CurrentContext.Host.Name, "Update announcement message on server, $Title")) { Write-Verbose "Setting server announcement message to '$Title', $($Article.Length) characters article..." Call-Napi -Path "announcement" -Method "POST" -Query @{ raw = $Raw.IsPresent } -Body @{ title = $Title; article = $Article; referenceDate = $ReferenceDate } | ForEach-Object { New-Object NabuArticleBase -ArgumentList $_ } } } } function Register-Host { <# .SYNOPSIS Creates a new server registration for the current user. .DESCRIPTION The server configuration is stored in the user home folder, subfolder ".nabunet" as a configuration file with the server name as a root name, psd1 as an extension. If a token is provided, it will be used for authentication, if not, the token needs to be provided by calling the Update-NabuNetHost cmdlet. .PARAMETER Name The local name to use for the server. Defaults to the host name if not specified. .PARAMETER Host The host name of the remote server. .PARAMETER Token The security token (API Token) as for the user. .PARAMETER Port The TCP port number - defaults to 443 for HTTPS. .PARAMETER Path The path of the NAPI interface, defaults to the "/napi" folder. #> [CmdletBinding(PositionalBinding = $false)] param ( [Parameter(Mandatory = $true, Position = 0)] [string]$Host, [Parameter(Position = 1)] [string]$Name = $null, [string]$Token = $null, [ValidateRange(1, 65535)] [int]$Port = 443, [ValidatePattern("^(/[a-zA-Z0-9\-\.\_]+)+$", ErrorMessage = "Not a valid path for NAPI. Must start with a / and only contain letters, numbers, dashes and dots.")] [string]$Path = "/napi" ) if ($null -eq $Name) { $Name = $Host } $h = Load-RegisteredHost -Name $Name if ($null -ne $h) { throw "The name $Name is already registered! Remove it first or use Update-NabuNetHost" } $h = New-Object NabuHost -ArgumentList $Name $h.RemoteBaseUri = "https://$($Host):$Port$Path" $h.Registered = [DateTime]::UtcNow $h.LastContacted = [DateTime]::MinValue $h.Token = $Token $h.RemoteName = $null $h.RemoteTagline = $null #-ArgumentList @{Host = $Host; Port = $port; Path = $Path; Registered = [DateTime]::UtcNow; LastContacted = [DateTime]::MinValue; RemoteName = $null; RemoteTagline = $null } Save-RegisteredHost -Name $Name -Data $h } function Connect-Host { <# .SYNOPSIS Connect the current powershell sessino to a server. .DESCRIPTION You can either save a connection with a name (see Register-NabuNetHost) and use that name for easy connectivity, or specify all required parameters here for a dynamic, not saved connection. .EXAMPLE Connect-NabuNetHost -Name testserver Connects to the previously registered server (and token!) called "testserver". .LINK Connect-NabuNetHost .PARAMETER Name The name of a registered server. .PARAMETER Host The host name of the remote server. .PARAMETER Token The security token (API Token) as for the user. .PARAMETER Port The TCP port number - defaults to 443 for HTTPS. .PARAMETER Path The path of the NAPI interface, defaults to the "/napi" folder. #> [CmdletBinding(PositionalBinding = $false)] param ( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Temporary")] [string]$Host, [Parameter(Position = 1, ParameterSetName = "Temporary")] [string]$Token = $null, [Parameter(ParameterSetName = "Temporary")] [ValidateRange(1, 65535)] [int]$Port = 443, [Parameter(ParameterSetName = "Temporary")] [ValidatePattern("^(/[a-zA-Z0-9\-\.\_]+)+$", ErrorMessage = "Not a valid path for NAPI. Must start with a / and only contain letters, numbers, dashes and dots.")] [string]$Path = "/napi", [Parameter(ParameterSetName = "Named", Mandatory = $true)] [string]$Name ) if ($PSCmdlet.ParameterSetName -eq "Named") { # call by registered name... $hi = Load-RegisteredHost -Name $Name } else { # call by temp parameters... $hi = New-Object NabuHost -ArgumentList "<none>" $hi.RemoteBaseUri = "https://$($Host):$Port$Path" $hi.Registered = [DateTime]::UtcNow $hi.LastContacted = [DateTime]::MinValue $hi.Token = $Token $hi.RemoteName = $null $hi.RemoteTagline = $null } Write-Verbose "Connecting to $($hi.RemoteBaseUri)..." $result = Call-Napi -Connection $hi -Path "info" if ($result) { $hi.LastContacted = [DateTime]::UtcNow $hi.RemoteName = $result.name $hi.RemoteTagline = $result.tagLine $script:CurrentContext.Host = $hi $script:CurrentContext.Token = $null $script:CurrentContext.TokenExpires = $null if ($PSCmdlet.ParameterSetName -eq "Named") { # update saved info with most recent data... Save-RegisteredHost -Name $Name -Data $hi } New-Object NabuHostInfo -ArgumentList $CurrentContext.Host, $CurrentContext.Name } else { throw "Remote system didn't return a valid info object!" } } function Get-Host { <# .SYNOPSIS Lists the current connected Nabu server or a list of all registered servers for the current user. .PARAMETER List If specified, the registered servers will be listed. If not, the currently connected one will be shown. .OUTPUTS NabuHostInfo #> [CmdletBinding(PositionalBinding = $false)] param ( [switch]$List ) if ($List.IsPresent) { #Get-AllRegisteredHost | ForEach-Object { Import-PowerShellDataFile -Path $_.FullName } Get-AllRegisteredHost | ForEach-Object { New-Object NabuHostInfo -ArgumentList (Import-Clixml -Path $_.FullName), $_.Name.Replace(".xml", "") } #| Where-Object { $_ -is [NabuHost] } } else { if ($CurrentContext.Host) { New-Object NabuHostInfo -ArgumentList $CurrentContext.Host, $CurrentContext.Name } } } function Get-MailTemplate { <# .SYNOPSIS Retreives an e-mail template from the NabuNet server. .DESCRIPTION Asks the server for the subject/body of the e-mail template. Needs site admin privileges. .PARAMETER Id The ID (key) of the mail template. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidatePattern("^[a-zA-Z0-9-]+$", ErrorMessage = "Only digits and letters allowed!")] [string]$Id ) Call-Napi -Path "template/$id" | ForEach-Object { New-Object NabuTemplateInfo -ArgumentList $_, $id } } function Set-MailTemplate { <# .SYNOPSIS Updated an e-mail template on the NabuNet server. .DESCRIPTION Sends the new subject/body of the e-mail template to the server. Needs site admin privileges. .PARAMETER Id The ID (key) of the mail template. .PARAMETER Subject The subject line for the e-mail. Can include handlebars.net placeholders according to the template definition. .PARAMETER Body The body of the e-mail. Can include handlebars.net placeholders and should be HTML formatted. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] param ( [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [ValidatePattern("^[a-zA-Z0-9-]+$", ErrorMessage = "Only digits and letters allowed!")] [string]$Id, [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [ValidateLength(1, 1024)] [ValidatePattern("^[^\n\r]+$", ErrorMessage = "No newlines allowed!")] [string]$Subject, [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] [string]$Body ) process { if ($PSCmdlet.ShouldProcess("$Id", "Updating mail template, new subject=$Subject, body=$($body.Length) characters.")) { Call-Napi -Path "template/$id" -Method "POST" -Body @{ Subject = $Subject; Body = $Body } } } } # approveaccount/{userName} function Approve-Account { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")] param ( [Parameter()] [ValidateLength(1, 32)] [ValidatePattern("^[a-zA-Z0-9-]+$", ErrorMessage = "Only digits and letters allowed!")] [string] $Name ) if ( $PSCmdlet.ShouldProcess($Name, "Confirm user account")) { Call-Napi -Path "approveaccount/$Name" -Method "PUT" } } Export-ModuleMember -Function Get-Host, Get-Account, Get-ServerAnnouncement, ` Clear-ServerAnnouncement, Set-ServerAnnouncement, Connect-Host, Register-Host, ` Get-MailTemplate, Set-MailTemplate, Approve-Account |