diff --git a/examples/web-auth-basic-access.ps1 b/examples/web-auth-basic-access.ps1 new file mode 100644 index 000000000..96275d46c --- /dev/null +++ b/examples/web-auth-basic-access.ps1 @@ -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 + } + ) + } + } + +} \ No newline at end of file diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index f5e16bf70..a507b332f 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -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 @@ -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 + } } } \ No newline at end of file diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index f993faea5..637cc82ab 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -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)" } } } @@ -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 @@ -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 + } + } } \ No newline at end of file diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 44e6d383c..2564b6c06 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -139,10 +139,6 @@ function Add-PodeRoute [string[]] $Scope, - [Parameter()] - [string[]] - $Attribute, - [switch] $AllowAnon, @@ -194,20 +190,20 @@ function Add-PodeRoute $IfExists = $RouteGroup.IfExists } - if ($null -ne $RouteGroup.Role) { - $Role = $RouteGroup.Role + $Role + if ($null -ne $RouteGroup.Access.Role) { + $Role = $RouteGroup.Access.Role + $Role } - if ($null -ne $RouteGroup.Group) { - $Group = $RouteGroup.Group + $Group + if ($null -ne $RouteGroup.Access.Group) { + $Group = $RouteGroup.Access.Group + $Group } - if ($null -ne $RouteGroup.Scope) { - $Scope = $RouteGroup.Scope + $Scope + if ($null -ne $RouteGroup.Access.Scope) { + $Scope = $RouteGroup.Access.Scope + $Scope } - if ($null -ne $RouteGroup.Attribute) { - $Attribute = $RouteGroup.Attribute + $Attribute + if ($null -ne $RouteGroup.Access.Custom) { + $CustomAccess = $RouteGroup.Access.Custom } } @@ -272,6 +268,11 @@ function Add-PodeRoute $Middleware = (@(Get-PodeAuthMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) } + # custom access + if ($null -eq $CustomAccess) { + $CustomAccess = @{} + } + # workout a default content type for the route $ContentType = Find-PodeRouteContentType -Path $Path -ContentType $ContentType @@ -310,10 +311,10 @@ function Add-PodeRoute Middleware = $Middleware Authentication = $Authentication Access = @{ - Roles = $Role - Groups = $Group - Scopes = $Scope - Attributes = $Attribute + Role = $Role + Group = $Group + Scope = $Scope + Custom = $CustomAccess } Endpoint = @{ Protocol = $_endpoint.Protocol @@ -913,10 +914,6 @@ function Add-PodeRouteGroup [string[]] $Scope, - [Parameter()] - [string[]] - $Attribute, - [switch] $AllowAnon ) @@ -970,20 +967,20 @@ function Add-PodeRouteGroup $IfExists = $RouteGroup.IfExists } - if ($null -ne $RouteGroup.Role) { - $Role = $RouteGroup.Role + $Role + if ($null -ne $RouteGroup.Access.Role) { + $Role = $RouteGroup.Access.Role + $Role } - if ($null -ne $RouteGroup.Group) { - $Group = $RouteGroup.Group + $Group + if ($null -ne $RouteGroup.Access.Group) { + $Group = $RouteGroup.Access.Group + $Group } - if ($null -ne $RouteGroup.Scope) { - $Scope = $RouteGroup.Scope + $Scope + if ($null -ne $RouteGroup.Access.Scope) { + $Scope = $RouteGroup.Access.Scope + $Scope } - if ($null -ne $RouteGroup.Attribute) { - $Attribute = $RouteGroup.Attribute + $Attribute + if ($null -ne $RouteGroup.Access.Custom) { + $CustomAccess = $RouteGroup.Access.Custom } } @@ -998,10 +995,10 @@ function Add-PodeRouteGroup AllowAnon = $AllowAnon IfExists = $IfExists Access = @{ - Roles = $Role - Groups = $Group - Scopes = $Scope - Attributes = $Attribute + Role = $Role + Group = $Group + Scope = $Scope + Custom = $CustomAccess } }