Skip to content

Commit

Permalink
#992: added support for custom access validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Badgerati committed Aug 7, 2023
1 parent b8165e1 commit b576a64
Show file tree
Hide file tree
Showing 4 changed files with 230 additions and 54 deletions.
86 changes: 86 additions & 0 deletions examples/web-auth-basic-access.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path)
Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop

# or just:
# Import-Module Pode

<#
This example shows how to use sessionless authentication, which will mostly be for
REST APIs. The example used here is Basic authentication.
Calling the '[POST] http://localhost:8085/users' endpoint, with an Authorization
header of 'Basic bW9ydHk6cGlja2xl' will display the uesrs. Anything else and
you'll get a 401 status code back.
Success:
Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' }
Failure:
Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cmljaw==' }
#>

# create a server, and start listening on port 8085
Start-PodeServer -Threads 2 {

# listen on localhost:8085
Add-PodeEndpoint -Address * -Port 8085 -Protocol Http

# setup RBAC
$rbac = New-PodeAuthAccess -Type Role

# setup basic auth (base64> username:password in header)
New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Access $rbac -Sessionless -ScriptBlock {
param($username, $password)

# here you'd check a real user storage, this is just for example
if ($username -eq 'morty' -and $password -eq 'pickle') {
return @{
User = @{
ID ='M0R7Y302'
Name = 'Morty'
Type = 'Human'
Roles = @('Developer')
}
}
}

return @{ Message = 'Invalid details supplied' }
}

# POST request to get list of users - there's no Roles, so any auth'd user can access
Add-PodeRoute -Method Post -Path '/users-all' -Authentication 'Validate' -ScriptBlock {
Write-PodeJsonResponse -Value @{
Users = @(
@{
Name = 'Deep Thought'
Age = 42
}
)
}
}

# POST request to get list of users - only Developer roles can access
Add-PodeRoute -Method Post -Path '/users-dev' -Authentication 'Validate' -Role Developer -ScriptBlock {
Write-PodeJsonResponse -Value @{
Users = @(
@{
Name = 'Leeroy Jenkins'
Age = 1337
}
)
}
}

# POST request to get list of users - only Admin roles can access
Add-PodeRoute -Method Post -Path '/users-admin' -Authentication 'Validate' -Role Admin -ScriptBlock {
Write-PodeJsonResponse -Value @{
Users = @(
@{
Name = 'Arthur Dent'
Age = 30
}
)
}
}

}
48 changes: 32 additions & 16 deletions src/Private/Authentication.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1963,14 +1963,21 @@ function Test-PodeAuthAccess
)

# get route access values - if none then skip
$routeAccess = $WebEvent.Route.Access.($Access.Property)
$routeAccess = $WebEvent.Route.Access[$Access.Type]
if ($Access.IsCustom) {
$routeAccess = $routeAccess[$Access.Name]
}

if (($null -eq $routeAccess) -or ($routeAccess.Length -eq 0)) {
return $true
}

# if there's no scriptblock, try the Property fallback
# if there's no scriptblock, try the Path fallback
if ($null -eq $Access.Scriptblock) {
$userAccess = $WebEvent.Auth.User.($Access.Property)
$userAccess = $WebEvent.Auth.User
foreach ($atom in $Access.Path.Split('.')) {
$userAccess = $userAccess.($atom)
}
}

# otherwise, invoke scriptblock
Expand All @@ -1980,23 +1987,32 @@ function Test-PodeAuthAccess
$userAccess = Invoke-PodeScriptBlock -ScriptBlock $Access.Scriptblock.Script -Arguments $_args -Return -Splat
}

# one or all match?
if ($Access.Match -ieq 'one') {
foreach ($item in $userAccess) {
if ($item -iin $routeAccess) {
return $true
}
}

return $false
# check for custom validator, or use default match logic
if ($null -ne $Access.Validator) {
$_args = @(,$userAccess) + @(,$routeAccess) + @($Access.Arguments)
$_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $Access.Validator.UsingVariables)
return (Invoke-PodeScriptBlock -ScriptBlock $Access.Validator.Script -Arguments $_args -Return -Splat)
}

# one or all match?
else {
foreach ($item in $routeAccess) {
if ($item -inotin $userAccess) {
return $false
if ($Access.Match -ieq 'one') {
foreach ($item in $userAccess) {
if ($item -iin $routeAccess) {
return $true
}
}

return $false
}
else {
foreach ($item in $routeAccess) {
if ($item -inotin $userAccess) {
return $false
}
}

return $true
return $true
}
}
}
89 changes: 83 additions & 6 deletions src/Public/Authentication.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,8 @@ function Add-PodeAuth
# ensure the Access contains a Property as a minimum
if (!(Test-PodeIsEmpty $Access)) {
foreach ($acc in $Access) {
if ([string]::IsNullOrEmpty($acc.Property)) {
throw "The supplied '$($acc.Type)' Access for the '$($Name)' authentication validator requires a valid Property for lookup (or a Scriptblock if custom)"
if ([string]::IsNullOrEmpty($acc.Path)) {
throw "The supplied '$($acc.Type)' Access for the '$($Name)' authentication validator requires a valid Path for lookup (or a Scriptblock if custom)"
}
}
}
Expand Down Expand Up @@ -1961,26 +1961,49 @@ function New-PodeAuthAccess
{
[CmdletBinding()]
param(
[Parameter()]
[string]
$Name,

[Parameter(Mandatory=$true)]
[ValidateSet('Role', 'Group', 'Scope', 'Attribute')]
[ValidateSet('Role', 'Group', 'Scope', 'Custom')]
[string]
$Type,

[Parameter()]
[scriptblock]
$ScriptBlock,
$ScriptBlock = $null,

[Parameter()]
[object[]]
$ArgumentList,

[Parameter()]
[scriptblock]
$Validator = $null,

[Parameter()]
[string]
$Path,

[Parameter()]
[ValidateSet('All', 'One')]
[string]
$Match = 'One'
)

# parse using variables
# for custom a validator and name are mandatory
if ($Type -ieq 'custom') {
if ([string]::IsNullOrEmpty($Name)) {
throw "A Name is required for creating Custom Access objects"
}

if ($null -eq $Validator) {
throw "A Validator scriptblock is required for creating Custom Access objects"
}
}

# parse using variables in scriptblock
$scriptObj = $null
if (!(Test-PodeIsEmpty $ScriptBlock)) {
$ScriptBlock, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState
Expand All @@ -1990,12 +2013,66 @@ function New-PodeAuthAccess
}
}

# parse using variables in validator
$validObj = $null
if (!(Test-PodeIsEmpty $Validator)) {
$Validator, $usingScriptVars = Convert-PodeScopedVariables -ScriptBlock $Validator -PSSession $PSCmdlet.SessionState
$validObj = @{
Script = $Validator
UsingVariables = $usingScriptVars
}
}

# default path
if ([string]::IsNullOrEmpty($Path)) {
$Path = "$($Type)s"
}

# return access object
return @{
Name = $Name
Type = $Type
IsCustom = ($Type -ieq 'custom')
ScriptBlock = $scriptObj
Validator = $validObj
Arguments = $ArgumentList
Property = "$($Type)s"
Path = $Path
Match = $Match
}
}

function Add-PodeAuthCustomAccess
{
[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.Access.Custom.ContainsKey($Name)) {
throw "Route '[$($r.Method)] $($r.Path)' already contains Custom Access with name '$($Name)'"
}

$r.Access.Custom[$Name] = $Value
}
}
}
Loading

0 comments on commit b576a64

Please sign in to comment.