Public/Access.ps1
<#
.SYNOPSIS Create a new type of Access scheme. .DESCRIPTION Create a new type of Access scheme, which retrieves the destination/resource's authorisation values which a user needs for access. .PARAMETER Type The inbuilt Type of Access this method is for: Role, Group, Scope, User. .PARAMETER Custom If supplied, the access Scheme will be flagged as using Custom logic. .PARAMETER ScriptBlock An optional ScriptBlock for retrieving authorisation values for the authenticated user, useful if the values reside in an external data store. This, or Path, is mandatory if using a Custom scheme. .PARAMETER ArgumentList An optional array of arguments to supply to the ScriptBlock. .PARAMETER Path An optional property Path within the $WebEvent.Auth.User object to extract authorisation values. The default Path is based on the Access Type, either Roles; Groups; Scopes; or Username. This, or ScriptBlock, is mandatory if using a Custom scheme. .EXAMPLE $role_access = New-PodeAccessScheme -Type Role .EXAMPLE $group_access = New-PodeAccessScheme -Type Group -Path 'Metadata.Groups' .EXAMPLE $scope_access = New-PodeAccessScheme -Type Scope -Scriptblock { param($user) return @(Get-ExampleAccess -Username $user.Username) } .EXAMPLE $custom_access = New-PodeAccessScheme -Custom -Path 'CustomProp' #> function New-PodeAccessScheme { [CmdletBinding(DefaultParameterSetName = 'Type_Path')] [OutputType([hashtable])] param( [Parameter(Mandatory = $true, ParameterSetName = 'Type_Scriptblock')] [Parameter(Mandatory = $true, ParameterSetName = 'Type_Path')] [ValidateSet('Role', 'Group', 'Scope', 'User')] [string] $Type, [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Scriptblock')] [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Path')] [switch] $Custom, [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Scriptblock')] [Parameter(ParameterSetName = 'Type_Scriptblock')] [scriptblock] $ScriptBlock, [Parameter(ParameterSetName = 'Custom_Scriptblock')] [Parameter(ParameterSetName = 'Type_Scriptblock')] [object[]] $ArgumentList, [Parameter(Mandatory = $true, ParameterSetName = 'Custom_Path')] [Parameter(ParameterSetName = 'Type_Path')] [string] $Path ) # for custom access a validator is mandatory if ($Custom) { if ([string]::IsNullOrWhiteSpace($Path) -and (Test-PodeIsEmpty $ScriptBlock)) { # A Path or ScriptBlock is required for sourcing the Custom access values throw ($PodeLocale.customAccessPathOrScriptBlockRequiredExceptionMessage) } } # parse using variables in scriptblock $scriptObj = $null if (!(Test-PodeIsEmpty $ScriptBlock)) { $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState $scriptObj = @{ Script = $ScriptBlock UsingVariables = $usingScriptVars } } # default path if (!$Custom -and (Test-PodeIsEmpty $ScriptBlock) -and [string]::IsNullOrWhiteSpace($Path)) { if ($Type -ieq 'user') { $Path = 'Username' } else { $Path = "$($Type)s" } } # return scheme return @{ Type = $Type IsCustom = $Custom.IsPresent ScriptBlock = $scriptObj Arguments = $ArgumentList Path = $Path } } <# .SYNOPSIS Add an authorisation Access method. .DESCRIPTION Add an authorisation Access method for use with Authentication methods, which will authorise access to Routes. Or they can be used independant of Authentication/Routes for custom scenarios. .PARAMETER Name A unique Name for the Access method. .PARAMETER Description A short description used by OpenAPI. .PARAMETER Scheme The access Scheme to use for retrieving credentials (From New-PodeAccessScheme). .PARAMETER ScriptBlock An optional Scriptblock, which can be used to invoke custom validation logic to verify authorisation. .PARAMETER ArgumentList An optional array of arguments to supply to the ScriptBlock. .PARAMETER Match An optional inbuilt Match method to use when verifying access to a Route, this only applies when no custom Validator scriptblock is supplied. (Default: One) "One" will allow access if the User has at least one of the Route's access values. "All" will allow access only if the User has all the values. "None" will allow access only if the User has none of the values. .EXAMPLE New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'Example' .EXAMPLE New-PodeAccessScheme -Type Group -Path 'Metadata.Groups' | Add-PodeAccess -Name 'Example' -Match All .EXAMPLE New-PodeAccessScheme -Type Scope -Scriptblock { param($user) return @(Get-ExampleAccess -Username $user.Username) } | Add-PodeAccess -Name 'Example' .EXAMPLE New-PodeAccessScheme -Custom -Path 'CustomProp' | Add-PodeAccess -Name 'Example' -ScriptBlock { param($userAccess, $customAccess) return $userAccess.Country -ieq $customAccess.Country } #> function Add-PodeAccess { [CmdletBinding(DefaultParameterSetName = 'Match')] param( [Parameter(Mandatory = $true)] [string] $Name, [string] $Description, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [hashtable] $Scheme, [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')] [scriptblock] $ScriptBlock, [Parameter(ParameterSetName = 'ScriptBlock')] [object[]] $ArgumentList, [Parameter(ParameterSetName = 'Match')] [ValidateSet('All', 'One', 'None')] [string] $Match = 'One' ) begin { $pipelineItemCount = 0 } process { $pipelineItemCount++ } end { if ($pipelineItemCount -gt 1) { throw ($PodeLocale.fnDoesNotAcceptArrayAsPipelineInputExceptionMessage -f $($MyInvocation.MyCommand.Name)) } # check name unique if (Test-PodeAccessExists -Name $Name) { # Access method already defined: $($Name) throw ($PodeLocale.accessMethodAlreadyDefinedExceptionMessage -f $Name) } # parse using variables in validator scriptblock $scriptObj = $null if (!(Test-PodeIsEmpty $ScriptBlock)) { $ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState $scriptObj = @{ Script = $ScriptBlock UsingVariables = $usingScriptVars } } # add access object $PodeContext.Server.Authorisations.Methods[$Name] = @{ Name = $Name Description = $Description Scheme = $Scheme ScriptBlock = $scriptObj Arguments = $ArgumentList Match = $Match.ToLowerInvariant() Cache = @{} Merged = $false Parent = $null } } } <# .SYNOPSIS Let's you merge multiple Access methods together, into a "single" Access method. .DESCRIPTION Let's you merge multiple Access methods together, into a "single" Access method. You can specify if only One or All of the methods need to pass to allow access, and you can also merge other merged Access methods for more advanced scenarios. .PARAMETER Name A unique Name for the Access method. .PARAMETER Access Mutliple Access method Names to be merged. .PARAMETER Valid How many of the Access methods are required to be valid, One or All. (Default: One) .EXAMPLE Merge-PodeAccess -Name MergedAccess -Access RbacAccess, GbacAccess -Valid All #> function Merge-PodeAccess { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string[]] $Access, [Parameter()] [ValidateSet('One', 'All')] [string] $Valid = 'One' ) # ensure the name doesn't already exist if (Test-PodeAccessExists -Name $Name) { throw ($PodeLocale.accessMethodAlreadyDefinedExceptionMessage -f $Name) #"Access method already defined: $($Name)" } # ensure all the access methods exist foreach ($accName in $Access) { if (!(Test-PodeAccessExists -Name $accName)) { throw ($PodeLocale.accessMethodNotExistForMergingExceptionMessage -f $accName) #"Access method does not exist for merging: $($accName)" } } # set parent access foreach ($accName in $Access) { $PodeContext.Server.Authorisations.Methods[$accName].Parent = $Name } # add auth method to server $PodeContext.Server.Authorisations.Methods[$Name] = @{ Name = $Name Access = @($Access) PassOne = ($Valid -ieq 'one') Cache = @{} Merged = $true Parent = $null } } <# .SYNOPSIS Assigns Custom Access value(s) to a Route. .DESCRIPTION Assigns Custom Access value(s) to a Route. .PARAMETER Route The Route to assign the Custom Access value(s). .PARAMETER Name The Name of the Access method the Custom Access value(s) are for. .PARAMETER Value The Custom Access Value(s) .EXAMPLE Add-PodeRoute -Method Get -Path '/users' -ScriptBlock {} -PassThru | Add-PodeAccessCustom -Name 'Example' -Value @{ Country = 'UK' } #> function Add-PodeAccessCustom { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [hashtable[]] $Route, [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [object[]] $Value ) begin { $routes = @() } process { $routes += $Route } end { foreach ($r in $routes) { if ($r.AccessMeta.Custom.ContainsKey($Name)) { throw ($PodeLocale.routeAlreadyContainsCustomAccessExceptionMessage -f $r.Method, $r.Path, $Name) #"Route '[$($r.Method)] $($r.Path)' already contains Custom Access with name '$($Name)'" } $r.AccessMeta.Custom[$Name] = $Value } } } <# .SYNOPSIS Get one or more Access methods. .DESCRIPTION Get one or more Access methods. .PARAMETER Name The Name of the Access method. If no name supplied, all methods will be returned. .EXAMPLE $methods = Get-PodeAccess .EXAMPLE $methods = Get-PodeAccess -Name 'Example' .EXAMPLE $methods = Get-PodeAccess -Name 'Example1', 'Example2' #> function Get-PodeAccess { [CmdletBinding()] [OutputType([object[]])] param( [Parameter()] [string[]] $Name ) # return all if no Name if ([string]::IsNullOrEmpty($Name) -or ($Name.Length -eq 0)) { return $PodeContext.Server.Authorisations.Methods.Values } # return filtered return @(foreach ($n in $Name) { $PodeContext.Server.Authorisations.Methods[$n] }) } <# .SYNOPSIS Test if an Access method exists. .DESCRIPTION Test if an Access method exists. .PARAMETER Name The Name of the Access method. .EXAMPLE if (Test-PodeAccessExists -Name 'Example') { } #> function Test-PodeAccessExists { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name ) return $PodeContext.Server.Authorisations.Methods.ContainsKey($Name) } <# .SYNOPSIS Test access values for a Source/Destination against an Access method. .DESCRIPTION Test access values for a Source/Destination against an Access method. .PARAMETER Name The Name of the Access method to use to verify the access. .PARAMETER Source An array of Source access values to pass to the Access method for verification against the Destination access values. (ie: User) .PARAMETER Destination An array of Destination access values to pass to the Access method for verification. (ie: Route) .PARAMETER ArgumentList An optional array of arguments to supply to the Access Scheme's ScriptBlock for retrieving access values. .EXAMPLE if (Test-PodeAccess -Name 'Example' -Source 'Developer' -Destination 'Admin') { } #> function Test-PodeAccess { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter()] [object[]] $Source = $null, [Parameter()] [object[]] $Destination = $null, [Parameter()] [object[]] $ArgumentList = $null ) # get the access method $access = $PodeContext.Server.Authorisations.Methods[$Name] # authorised if no destination values if (($null -eq $Destination) -or ($Destination.Length -eq 0)) { return $true } # if we have no source values, invoke the scriptblock if (($null -eq $Source) -or ($Source.Length -eq 0)) { if ($null -ne $access.Scheme.ScriptBlock) { $_args = $ArgumentList + @($access.Scheme.Arguments) $Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat } } # check for custom validator, or use default match logic if ($null -ne $access.ScriptBlock) { $_args = @(, $Source) + @(, $Destination) + @($access.Arguments) return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.ScriptBlock.Script -Arguments $_args -UsingVariables $access.ScriptBlock.UsingVariables -Return -Splat) } # not authorised if no source values if (($access.Match -ne 'none') -and (($null -eq $Source) -or ($Source.Length -eq 0))) { return $false } # one or all match? else { switch ($access.Match) { 'one' { foreach ($item in $Source) { if ($item -iin $Destination) { return $true } } } 'all' { foreach ($item in $Destination) { if ($item -inotin $Source) { return $false } } return $true } 'none' { foreach ($item in $Source) { if ($item -iin $Destination) { return $false } } return $true } } } # default is not authorised return $false } <# .SYNOPSIS Test the currently authenticated User's access against the supplied values. .DESCRIPTION Test the currently authenticated User's access against the supplied values. This will be the user in a WebEvent object. .PARAMETER Name The Name of the Access method to use to verify the access. .PARAMETER Value An array of access values to pass to the Access method for verification against the User. .EXAMPLE if (Test-PodeAccessUser -Name 'Example' -Value 'Developer', 'QA') { } #> function Test-PodeAccessUser { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [object[]] $Value ) # get the access method $access = $PodeContext.Server.Authorisations.Methods[$Name] # get the user $user = $WebEvent.Auth.User # if there's no scriptblock, try the Path fallback if ($null -eq $access.Scheme.Scriptblock) { $userAccess = $user foreach ($atom in $access.Scheme.Path.Split('.')) { $userAccess = $userAccess.($atom) } } # otherwise, invoke scriptblock else { $_args = @($user) + @($access.Scheme.Arguments) $userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat } # is the user authorised? return (Test-PodeAccess -Name $Name -Source $userAccess -Destination $Value) } <# .SYNOPSIS Test the currently authenticated User's access against the access values supplied for the current Route. .DESCRIPTION Test the currently authenticated User's access against the access values supplied for the current Route. .PARAMETER Name The Name of the Access method to use to verify the access. .EXAMPLE if (Test-PodeAccessRoute -Name 'Example') { } #> function Test-PodeAccessRoute { param( [Parameter(Mandatory = $true)] [string] $Name ) # get the access method $access = $PodeContext.Server.Authorisations.Methods[$Name] # get route access values if ($access.Scheme.IsCustom) { $routeAccess = $WebEvent.Route.AccessMeta.Custom[$access.Name] } else { $routeAccess = $WebEvent.Route.AccessMeta[$access.Scheme.Type] } # if no values then skip if (($null -eq $routeAccess) -or ($routeAccess.Length -eq 0)) { return $true } # tests values against user return (Test-PodeAccessUser -Name $Name -Value $routeAccess) } <# .SYNOPSIS Remove a specific Access method. .DESCRIPTION Remove a specific Access method. .PARAMETER Name The Name of the Access method. .EXAMPLE Remove-PodeAccess -Name 'RBAC' #> function Remove-PodeAccess { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] $Name ) process { $null = $PodeContext.Server.Authorisations.Methods.Remove($Name) } } <# .SYNOPSIS Clear all defined Access methods. .DESCRIPTION Clear all defined Access methods. .EXAMPLE Clear-PodeAccess #> function Clear-PodeAccess { [CmdletBinding()] param() $PodeContext.Server.Authorisations.Methods.Clear() } <# .SYNOPSIS Adds an access method as global middleware. .DESCRIPTION Adds an access method as global middleware. .PARAMETER Name The Name of the Middleware. .PARAMETER Access The Name of the Access method to use. .PARAMETER Route A Route path for which Routes this Middleware should only be invoked against. .EXAMPLE Add-PodeAccessMiddleware -Name 'GlobalAccess' -Access AccessName .EXAMPLE Add-PodeAccessMiddleware -Name 'GlobalAccess' -Access AccessName -Route '/api/*' #> function Add-PodeAccessMiddleware { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $Access, [Parameter()] [string] $Route ) if (!(Test-PodeAccessExists -Name $Access)) { throw ($PodeLocale.accessMethodNotExistExceptionMessage -f $Access) #"Access method does not exist: $($Access)" } Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList @{ Name = $Access } | Add-PodeMiddleware -Name $Name -Route $Route } <# .SYNOPSIS Automatically loads access ps1 files .DESCRIPTION Automatically loads access ps1 files from either an /access folder, or a custom folder. Saves space dot-sourcing them all one-by-one. .PARAMETER Path Optional Path to a folder containing ps1 files, can be relative or literal. .EXAMPLE Use-PodeAccess .EXAMPLE Use-PodeAccess -Path './my-access' #> function Use-PodeAccess { [CmdletBinding()] param( [Parameter()] [string] $Path ) Use-PodeFolder -Path $Path -DefaultPath 'access' } |