diff --git a/docs/Tutorials/Authentication/Inbuilt/Session.md b/docs/Tutorials/Authentication/Inbuilt/Session.md new file mode 100644 index 000000000..809a8a05e --- /dev/null +++ b/docs/Tutorials/Authentication/Inbuilt/Session.md @@ -0,0 +1,59 @@ +# Sessions + +Pode has support for Sessions when using Authentication, by default if you call a Route with authentication and you already have a session on the request then you're "authenticated". If there's no session, then the authentication logic is invoked, and if the details are invalid you're redirected to a login screen. + +If you have a need to use multiple authentication methods for login, and the user can chose the one they want, then on Routes there's no simple way of say which authentication is required. However, under the hood they all create a session object which can be used as a "shared" authentication method. + +This sessions authenticator can be used to pass authentication if a valid session in on the request, or to automatically redirect to a login page if there is no valid session. Useful for if you're using multiple authentication methods the user can choose from. + +## Usage + +To add sessions authentication you can use [`Add-PodeAuthSession`](../../../../Functions/Authentication/Add-PodeAuthSession). The following example will validate a user's credentials on login using Form authentication, but the home page uses session authentication to just verify there's a valid session: + +```powershell +Start-PodeServer { + # endpoint and view engine + Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + Set-PodeViewEngine -Type Pode + + # enable sessions + Enable-PodeSessionMiddleware -Duration 120 -Extend + + # setup form auth for login + New-PodeAuthScheme -Form | Add-PodeAuth -Name 'FormAuth' -FailureUrl '/login' -SuccessUrl '/' -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 = @{ Name = 'Morty' } } + } + + return @{ Message = 'Invalid details supplied' } + } + + # setup session auth for routes and logout + Add-PodeAuthSession -Name 'SessionAuth' -FailureUrl '/login' + + # home page: use session auth, and redirect to login if no valid session + Add-PodeRoute -Method Get -Path '/' -Authentication SessionAuth -ScriptBlock { + Write-PodeViewResponse -Path 'auth-home' + } + + # login page: use form auth here to actually verify the user's credentials + Add-PodeRoute -Method Get -Path '/login' -Authentication FormAuth -Login -ScriptBlock { + Write-PodeViewResponse -Path 'auth-login' -FlashMessages + } + + # login check: again, use form auth + Add-PodeRoute -Method Post -Path '/login' -Authentication FormAuth -Login + + # logout - use session auth here to purge the session + Add-PodeRoute -Method Post -Path '/logout' -Authentication SessionAuth -Logout +} +``` + +### User Object + +If a valid session is found on the request, then the user object set at `$WebEvent.Auth.User` will take the form of which ever authentication method using for login. + +The user object will simply be loaded from the session. diff --git a/docs/Tutorials/Authentication/Overview.md b/docs/Tutorials/Authentication/Overview.md index 1bdd7f62c..a38b3ba7f 100644 --- a/docs/Tutorials/Authentication/Overview.md +++ b/docs/Tutorials/Authentication/Overview.md @@ -164,7 +164,7 @@ The `Auth` object will also contain: | IsAuthenticated | States if the request is for an authenticated user, can be `$true`, `$false` or `$null` | | Store | States whether the authentication is for a session, and will be stored as a cookie | | IsAuthorised | If using [Authorisation](../../Authorisation/Overview), this value will be `$true` or `$false` depending on whether or not the authenticated user is authorised to access the Route. If not using Authorisation this value will just be `$true` | -| Access | If using [Authorisation](../../Authorisation/Overview), this property will contain the access values for the User per Access method. If not using Authorisation this value will just be an empty hashtable | +| Name | The name(s) of the Authentication methods which passed - useful if you're using merged Authentications and you want to know which one(s) passed | The following example get the user's name from the `Auth` object: diff --git a/docs/Tutorials/Authorisation/Overview.md b/docs/Tutorials/Authorisation/Overview.md index 61d80bbd4..fb59aa807 100644 --- a/docs/Tutorials/Authorisation/Overview.md +++ b/docs/Tutorials/Authorisation/Overview.md @@ -2,31 +2,27 @@ Authorisation can either be used in conjunction with [Authentication](../../Authentication/Overview) and [Routes](../../Routes/Overview), or on it's own for custom scenarios. -When used with Authentication, Pode can automatically authorise access to Routes based on Roles; Groups; Scopes; Users; or custom validation logic for you. When authorisation fails Pode will respond with an HTTP 403 status code. +When used with Authentication, Pode can automatically authorise access to Routes based on Roles; Groups; Scopes; Users; or custom validation logic for you, using the currently authenticated User's details. When authorisation fails Pode will respond with an HTTP 403 status code. With authentication, Pode will set the following properties on the `$WebEvent.Auth` object: | Name | Description | | ---- | ----------- | | IsAuthorised | This value will be `$true` or `$false` depending on whether or not the authenticated user is authorised to access the Route | -| Access | This property will contain the access values for the User per Access method | ## Create an Access Method -To validate authorisation in Pode you'll first need to create an Access method using [`Add-PodeAuthAccess`](../../../Functions/Authentication/Add-PodeAuthAccess). At its most simple you'll just need a Name, Type and possibly a Match type. +To validate authorisation in Pode you'll first need to create an Access scheme using [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme), and then an Access method using [`Add-PodeAccess`](../../../Functions/Authentication/Add-PodeAccess). At its most simple you'll just need a Name, Type and possibly a Match type. For example, you can create a simple Access method for any of the inbuilt types as follows: ```powershell -Add-PodeAuthAccess -Name 'RoleExample' -Type Role -Add-PodeAuthAccess -Name 'GroupExample' -Type Group -Add-PodeAuthAccess -Name 'ScopeExample' -Type Scope -Add-PodeAuthAccess -Name 'UserExample' -Type User +New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'RoleExample' +New-PodeAccessScheme -Type Group | Add-PodeAccess -Name 'GroupExample' +New-PodeAccessScheme -Type Scope | Add-PodeAccess -Name 'ScopeExample' +New-PodeAccessScheme -Type User | Add-PodeAccess -Name 'UserExample' ``` -!!! note - These Types mainly apply when using the Access method with Authentication/Routes. If you're going to be using the Access method in a more adhoc manner via [`Test-PodeAuthAccess`](../../../Functions/Authentication/Test-PodeAuthAccess) then the Type doesn't apply. - ### Match Type Pode supports 3 inbuilt "Match" types for validating access to resources: One, All and None. The default Match type is One; each of them are applied as follows: @@ -40,12 +36,12 @@ Pode supports 3 inbuilt "Match" types for validating access to resources: One, A For example, to setup an Access method where a User must be in every Group that a Route specifies: ```powershell -Add-PodeAuthAccess -Name 'GroupExample' -Type Group -Match All +New-PodeAccessScheme -Type Group | Add-PodeAccess -Name 'GroupExample' -Match All ``` ### User Access Lookup -When using Access methods with Authentication, Pode will lookup the User's "access values" from the `$WebEvent.Auth.User` object. The property within this object that Pode uses depends on the `-Type` supplied to [`Add-PodeAuthAccess`](../../../Functions/Authentication/Add-PodeAuthAccess): +When using Access methods with Authentication and Routes, Pode will lookup the User's "access values" from the `$WebEvent.Auth.User` object. The property within this object that Pode uses depends on the `-Type` supplied to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme): | Type | Property | | ---- | -------- | @@ -53,21 +49,21 @@ When using Access methods with Authentication, Pode will lookup the User's "acce | Group | Groups | | Scope | Scopes | | User | Username | -| Custom | Custom.[Name] | +| Custom | n/a - you must supply a `-Path` or `-ScriptBlock` to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme) | You can override this default lookup in one of two ways, by either supplying a custom property `-Path` or a `-ScriptBlock` for more a more advanced lookup (ie: external sources). !!! note - If you're using Access methods in a more adhoc manner via [`Test-PodeAuthAccess`](../../../Functions/Authentication/Test-PodeAuthAccess), the `-Path` property does nothing. However, if you don't supply a `-Source` to this function then the `-ScriptBlock` will be invoked. + If you're using Access methods in a more adhoc manner via [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess), the `-Path` property does nothing. However, if you don't supply a `-Source` to this function then the `-ScriptBlock` will be invoked. #### Lookup Path -The `-Path` property on [`Add-PodeAuthAccess`](../../../Functions/Authentication/Add-PodeAuthAccess) allows you to specify a custom property path within the `$WebEvent.Auth.User` object, which will be used to retrieve the access values for the User. +The `-Path` property on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme) allows you to specify a custom property path within the `$WebEvent.Auth.User` object, which will be used to retrieve the access values for the User. For example, if you have Roles for the User set in a `Roles` property within a `Metadata` property, then you'd use: ```powershell -Add-PodeAuthAccess -Name 'RoleExample' -Type Role -Path 'Metadata.Roles' +New-PodeAccessScheme -Type Role -Path 'Metadata.Roles' | Add-PodeAccess -Name 'RoleExample' <# $User = @{ @@ -83,44 +79,48 @@ And Pode will retrieve the appropriate data for you. #### Lookup ScriptBlock -If the source access values you require are not stored in the `$WebEvent.Auth.User` object but else where (ie: external source), then you can supply a `-ScriptBlock` on [`Add-PodeAuthAccess`](../../../Functions/Authentication/Add-PodeAuthAccess). When Pode attempts to retrieve access values for the User, or another Source, this scriptblock will be invoked. +If the source access values you require are not stored in the `$WebEvent.Auth.User` object but else where (ie: external source), then you can supply a `-ScriptBlock` on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme). When Pode attempts to retrieve access values for the User, or another Source, this scriptblock will be invoked. !!! note - When using this scriptblock with Authentication the currently authenticated User will be supplied as the first parameter, followed by the `-ArgumentList` values. When using the Access methods in a more adhoc manner via [`Test-PodeAuthAccess`](../../../Functions/Authentication/Test-PodeAuthAccess), just the `-ArgumentList` values are supplied. + When using this scriptblock with Authentication the currently authenticated User will be supplied as the first parameter, followed by the `-ArgumentList` values. When using the Access methods in a more adhoc manner via [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess), just the `-ArgumentList` values are supplied. For example, if the Role values you need to retrieve are stored in some SQL database: ```powershell -Add-PodeAuthAccess -Name 'RoleExample' -Type Role -ScriptBlock { +$scheme = New-PodeAccessScheme -Type Role -ScriptBlock { param($user) return Invoke-Sqlcmd -Query "SELECT Roles FROM UserRoles WHERE Username = '$($user.Username)'" -ServerInstance '(local)' } + +$scheme | Add-PodeAccess -Name 'RoleExample' ``` Or if you need to get the Groups from AD: ```powershell -Add-PodeAuthAccess -Name 'GroupExample' -Type Group -ScriptBlock { +$scheme = New-PodeAccessScheme -Type Group -ScriptBlock { param($user) return Get-ADPrincipalGroupMembership $user.Username | select name } + +$scheme | Add-PodeAccess -Name 'GroupExample' ``` ### Custom Validator -By default Pode will perform basic array contains checks, to see if the Source/Destination access values meet the `-Match` type required. +By default Pode will perform basic array contains checks, to see if the Source/Destination access values meet the `-Match` type required which was set on [`Add-PodeAccess`](../../../Functions/Access/Add-PodeAccess). For example, if the User has just the Role value `Developer`, and Route has `-Role` values of `Developer` and `QA` supplied, and the `-Match` type is left as `One`, then "if the User Role is contained within the Routes Roles" access is authorised. -However, if you require a more custom/advanced validation logic to be applied, you can supply a custom `-Validator` scriptblock to [`Add-PodeAuthAccess`](../../../Functions/Authentication/Add-PodeAuthAccess). The scriptblock will be supplied with the "Source" access values as the first parameter; the "Destination" access values as the second parameter; then followed by the `-ArgumentList` values. This scriptblock should return a boolean value: true if authorisation granted, or false otherwise. +However, if you require a more custom/advanced validation logic to be applied, you can supply a `-ScriptBlock` to [`Add-PodeAccess`](../../../Functions/Authentication/Add-PodeAccess). The scriptblock will be supplied with the "Source" access values as the first parameter; the "Destination" access values as the second parameter; and then followed by the `-ArgumentList` values. This scriptblock should return a boolean value: true if authorisation granted, or false otherwise. !!! note - Supplying a `-Validator` scriptblock will override the `-Match` type supplied, as this scriptblock will be used for validation instead of Pode's inbuilt Match logic. + Supplying a `-ScriptBlock` will override the `-Match` type supplied, as this scriptblock will be used for validation instead of Pode's inbuilt Match logic. For example, if you want to validate that the User's Scopes definitely contains a Route's first Scope value and then at least any 1 of the other Scope values: ```powershell -Add-PodeAuthAccess -Name 'ScopeExample' -Type Scope -ScriptBlock { +New-PodeAccessScheme -Type Scope | Add-PodeAccess -Name 'ScopeExample' -ScriptBlock { param($userScopes, $routeScopes) if ($routeScopes[0] -inotin $userScopes) { @@ -137,25 +137,25 @@ Add-PodeAuthAccess -Name 'ScopeExample' -Type Scope -ScriptBlock { } ``` -## Using with Authentication +## Using with Routes -The Access methods will mostly commonly be used in conjunction with [Authentication](../../Authentication/Overview) and [Routes](../../Routes/Overview). When used together, Pode will automatically validate Route authorisation for you as a part of the Authentication flow. If authorisation fails, an HTTP 403 status code will be returned. +The Access methods will most commonly be used in conjunction with [Authentication](../../Authentication/Overview) and [Routes](../../Routes/Overview). When used together, Pode will automatically validate Route Authorisation for after the Authentication flow. If authorisation fails, an HTTP 403 status code will be returned. -After creating an Access method as outlined above, you can supply the Access method Name to [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth) using the `-Access` property. +After creating an Access method as outlined above, you can supply the Access method Name to [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute), and other Route functions, using the `-Access` parameter. -On [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) and [`Add-PodeRouteGroup`](../../../Functions/Routes/Add-PodeRouteGroup) there are the following parameters: `-Role`, `-Group`, `-Scope`, and `-User`. You can supply one ore more string values to these parameters, depending on which Access method type you're using. +On [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) and [`Add-PodeRouteGroup`](../../../Functions/Routes/Add-PodeRouteGroup) there are also the following parameters: `-Role`, `-Group`, `-Scope`, and `-User`. You can supply one ore more string values to these parameters, depending on which Access method type you're using. -For example, to verify access to a Route to authorise only Developer role accounts: +For example, to verify access to a Route to authorise only Developer role users: ```powershell Start-PodeServer { Add-PodeEndpoint -Address * -Port 8080 -Protocol Http # create a simple role access method - Add-PodeAuthAccess -Name 'RoleExample' -Type Role + New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'RoleExample' # setup Basic authentication - New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'AuthExample' -Sessionless -Access 'RoleExample' -ScriptBlock { + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'AuthExample' -Sessionless -ScriptBlock { param($username, $password) # here you'd check a real user storage, this is just for example @@ -173,12 +173,12 @@ Start-PodeServer { } # create a route which only developers can access - Add-PodeRoute -Method Get -Path '/route1' -Role 'Developer' -Authentication 'AuthExample' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/route1' -Role 'Developer' -Authentication 'AuthExample' -Access 'RoleExample' -ScriptBlock { Write-PodeJsonResponse -Value @{ 'Value' = 'Hello!' } } # create a route which only admins can access - Add-PodeRoute -Method Get -Path '/route2' -Role 'Admin' -Authentication 'AuthExample' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/route2' -Role 'Admin' -Authentication 'AuthExample' -Access 'RoleExample' -ScriptBlock { Write-PodeJsonResponse -Value @{ 'Value' = 'Hi!' } } } @@ -198,9 +198,9 @@ Invoke-RestMethod -Uri http://localhost:8080/route2 -Method Get -Headers @{ Auth ## Merging -Similar to Authentication methods, you can also merge Access methods using [`Merge-PodeAuthAccess`](../../../Functions/Authentication/Merge-PodeAuthAccess). This allows you to have an access strategy where multiple authorisations are required to pass for a user to be fully authorised, or just one of several possible methods. +Similar to Authentication methods, you can also merge Access methods using [`Merge-PodeAccess`](../../../Functions/Authentication/Merge-PodeAccess). This allows you to have an access strategy where multiple authorisations are required to pass for a user to be fully authorised, or just one of several possible methods. -When you merge access methods together, it becomes a new access method which you can supply to `-Access` on [`Add-PodeAuth`](../../../Functions/Authentication/Add-PodeAuth). By default the merged access method expects just one to pass, but you can state that you require all to pass via the `-Valid` parameter on [`Merge-PodeAuthAccess`](../../../Functions/Authentication/Merge-PodeAuthAccess). +When you merge access methods together, it becomes a new access method which you can supply to `-Access` on [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute). By default the merged access method expects just one to pass, but you can state that you require all to pass via the `-Valid` parameter on [`Merge-PodeAccess`](../../../Functions/Authentication/Merge-PodeAccess). Using the same example above, we could add Group authorisation to this as well so the Developers have to be in a Software Group, and the Admins in a Operations Group: @@ -209,14 +209,14 @@ Start-PodeServer { Add-PodeEndpoint -Address * -Port 8080 -Protocol Http # create simple role and group access methods - Add-PodeAuthAccess -Name 'RoleExample' -Type Role - Add-PodeAuthAccess -Name 'GroupExample' -Type Group + New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'RoleExample' + New-PodeAccessScheme -Type Group | Add-PodeAccess -Name 'GroupExample' # setup a merged access - Merge-PodeAuthAccess -Name 'MergedExample' -Access 'RoleExample', 'GroupExample' -Valid All + Merge-PodeAccess -Name 'MergedExample' -Access 'RoleExample', 'GroupExample' -Valid All # setup Basic authentication - New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'AuthExample' -Sessionless -Access 'MergedExample' -ScriptBlock { + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'AuthExample' -Sessionless -ScriptBlock { param($username, $password) # here you'd check a real user storage, this is just for example @@ -235,12 +235,12 @@ Start-PodeServer { } # create a route which only developers can access - Add-PodeRoute -Method Get -Path '/route1' -Role 'Developer' -Group 'Software' -Authentication 'AuthExample' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/route1' -Role 'Developer' -Group 'Software' -Authentication 'AuthExample' -Access 'MergedExample' -ScriptBlock { Write-PodeJsonResponse -Value @{ 'Value' = 'Hello!' } } # create a route which only admins can access - Add-PodeRoute -Method Get -Path '/route2' -Role 'Admin' -Group 'Operations' -Authentication 'AuthExample' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/route2' -Role 'Admin' -Group 'Operations' -Authentication 'AuthExample' -Access 'MergedExample' -ScriptBlock { Write-PodeJsonResponse -Value @{ 'Value' = 'Hi!' } } } @@ -248,9 +248,9 @@ Start-PodeServer { ## Custom Access -Pode has inbuilt support for Roles, Groups, Scopes, and Users authorisation on Routes. However, if you need to setup a more Custom authorisation policy on Routes you can create an Access method with `-Type` "Custom", and add custom access values to a Route using [`Add-PodeAuthCustomAccess`](../../../Functions/Authentication/Add-PodeAuthCustomAccess). +Pode has inbuilt support for Roles, Groups, Scopes, and Users authorisation on Routes. However, if you need to setup a more Custom authorisation policy on Routes you can create a custom Access scheme by supplying `-Custom` to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme), and add custom access values to a Route using [`Add-PodeAccessCustom`](../../../Functions/Authentication/Add-PodeAccessCustom). -Custom access values on a User won't be automatically loaded from the User object, and a `-Path` or `-ScriptBlock` on [`Add-PodeAuthAccess`](../../../Functions/Authentication/Add-PodeAuthAccess) will be required. +Custom access values for a User won't be automatically loaded from the authenticated User object, and a `-Path` or `-ScriptBlock` on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme) will be required. For example, if you wanted to authorise access from a set of user attributes, and based on favourite colour, you could do the following: @@ -259,13 +259,13 @@ Start-PodeServer { Add-PodeEndpoint -Address * -Port 8080 -Protocol Http # create a simple role access method - Add-PodeAuthAccess -Name 'CustomExample' -Type Custom -Path 'Metadata.Attributes' -Validator { + New-PodeAccessScheme -Custom -Path 'Metadata.Attributes' | Add-PodeAccess -Name 'CustomExample' -ScriptBlock { param($userAttrs, $routeAttrs) return ($userAttrs.Colour -ieq $routeAttrs.Colour) } # setup Basic authentication - New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'AuthExample' -Sessionless -Access 'CustomExample' -ScriptBlock { + New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'AuthExample' -Sessionless -ScriptBlock { param($username, $password) # here you'd check a real user storage, this is just for example @@ -288,24 +288,24 @@ Start-PodeServer { } # create a route which only users who like the colour blue can access - Add-PodeRoute -Method Get -Path '/blue' -Authentication 'AuthExample' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/blue' -Authentication 'AuthExample' -Access 'CustomExample' -ScriptBlock { Write-PodeJsonResponse -Value @{ 'Value' = 'Hello!' } } -PassThru | - Add-PodeAuthCustomAccess -Name 'CustomExample' -Value @{ Colour = 'Blue' } + Add-PodeAccessCustom -Name 'CustomExample' -Value @{ Colour = 'Blue' } # create a route which only users who like the colour red can access - Add-PodeRoute -Method Get -Path '/red' -Authentication 'AuthExample' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/red' -Authentication 'AuthExample' -Access 'CustomExample' -ScriptBlock { Write-PodeJsonResponse -Value @{ 'Value' = 'Hi!' } } -PassThru | - Add-PodeAuthCustomAccess -Name 'CustomExample' -Value @{ Colour = 'Red' } + Add-PodeAccessCustom -Name 'CustomExample' -Value @{ Colour = 'Red' } } ``` ## Using Adhoc -It is possible to invoke the Access method validation in an adhoc manner, without (or while) using Authentication, using [`Test-PodeAuthAccess`](../../../Functions/Authentication/Test-PodeAuthAccess). +It is possible to invoke the Access method validation in an adhoc manner, without (or while) using Authentication, using [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess). -When using the Access methods outside of Authentication/Routes, the `-Type` doesn't really having any bearing. +When using the Access methods outside of Authentication/Routes, the `-Type` doesn't really have any bearing. For example, you could create a Roles Access method and verify some Users Roles within a TCP Verb: @@ -314,14 +314,15 @@ Start-PodeServer { Add-PodeEndpoint -Address * -Port 9000 -Protocol Tcp -CRLFMessageEnd # create a role access method get retrieves roles from a database - Add-PodeAuthAccess -Name 'RoleExample' -Type Role -ScriptBlock { + $scheme = New-PodeAccessScheme -Type Role -ScriptBlock { param($username) return Invoke-Sqlcmd -Query "SELECT Roles FROM UserRoles WHERE Username = '$($username)'" -ServerInstance '(local)' } + $scheme | Add-PodeAccess -Name 'RoleExample' # setup a Verb that only allows Developers Add-PodeVerb -Verb 'EXAMPLE :username' -ScriptBlock { - if (!(Test-PodeAuthAccess -Name 'RoleExample' -Destination 'Developer' -ArgumentList $TcpEvent.Parameters.username)) { + if (!(Test-PodeAccess -Name 'RoleExample' -Destination 'Developer' -ArgumentList $TcpEvent.Parameters.username)) { Write-PodeTcpClient -Message "Forbidden Access" return } @@ -331,4 +332,4 @@ Start-PodeServer { } ``` -The `-ArgumentList`, on [`Test-PodeAuthAccess`](../../../Functions/Authentication/Test-PodeAuthAccess), will supply values as the first set of parameters to the `-ScriptBlock` defined on [`Add-PodeAuthAccess`](../../../Functions/Authentication/Add-PodeAuthAccess). +The `-ArgumentList`, on [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess), will supply values as the first set of parameters to the `-ScriptBlock` defined on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme). diff --git a/examples/public/styles/simple.css b/examples/public/styles/simple.css index 7cf2d40c0..22ff031a3 100644 --- a/examples/public/styles/simple.css +++ b/examples/public/styles/simple.css @@ -1,3 +1,11 @@ body { background-color: rebeccapurple; +} + +a { + color: white; +} + +a:visited { + color: yellow; } \ No newline at end of file diff --git a/examples/tcp-server-auth.ps1 b/examples/tcp-server-auth.ps1 index 1678e8c3c..daea3c39e 100644 --- a/examples/tcp-server-auth.ps1 +++ b/examples/tcp-server-auth.ps1 @@ -14,7 +14,7 @@ Start-PodeServer -Threads 2 { New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # create a role access method get retrieves roles from a database - Add-PodeAuthAccess -Name 'RoleExample' -Type Role -ScriptBlock { + Add-PodeAccess -Name 'RoleExample' -Type Role -ScriptBlock { param($username) if ($username -ieq 'morty') { return @('Developer') @@ -25,7 +25,7 @@ Start-PodeServer -Threads 2 { # setup a Verb that only allows Developers Add-PodeVerb -Verb 'EXAMPLE :username' -ScriptBlock { - if (!(Test-PodeAuthAccess -Name 'RoleExample' -Destination 'Developer' -ArgumentList $TcpEvent.Parameters.username)) { + if (!(Test-PodeAccess -Name 'RoleExample' -Destination 'Developer' -ArgumentList $TcpEvent.Parameters.username)) { Write-PodeTcpClient -Message "Forbidden Access" 'Forbidden!' | Out-Default return diff --git a/examples/views/auth-about.pode b/examples/views/auth-about.pode new file mode 100644 index 000000000..1df74729c --- /dev/null +++ b/examples/views/auth-about.pode @@ -0,0 +1,23 @@ + + + Auth About + + + + + Hello, this is an about page! +
+ +

+ Home + Register +

+ +
+
+ +
+
+ + + \ No newline at end of file diff --git a/examples/views/auth-home.pode b/examples/views/auth-home.pode index 34c3c01e1..39af1f672 100644 --- a/examples/views/auth-home.pode +++ b/examples/views/auth-home.pode @@ -10,6 +10,11 @@ Your session will expire on $($data.Expiry)
+

+ About + Register +

+
diff --git a/examples/views/auth-register.pode b/examples/views/auth-register.pode new file mode 100644 index 000000000..d13719c68 --- /dev/null +++ b/examples/views/auth-register.pode @@ -0,0 +1,23 @@ + + + Auth Register + + + + + Hello, this is a register page! +
+ +

+ Home + About +

+ + +
+ +
+ + + + \ No newline at end of file diff --git a/examples/web-auth-basic-access.ps1 b/examples/web-auth-basic-access.ps1 index d6d92aecc..ca6ce591f 100644 --- a/examples/web-auth-basic-access.ps1 +++ b/examples/web-auth-basic-access.ps1 @@ -26,17 +26,20 @@ Start-PodeServer -Threads 2 { Add-PodeEndpoint -Address * -Port 8085 -Protocol Http # setup RBAC - Add-PodeAuthAccess -Type Role -Name 'TestRbac' - Add-PodeAuthAccess -Type Group -Name 'TestGbac' - # Add-PodeAuthAccess -Type Custom -Name 'TestRbac' -Path 'CustomAccess' -Validator { + # Add-PodeAccess -Type Role -Name 'TestRbac' + # Add-PodeAccess -Type Group -Name 'TestGbac' + New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'TestRbac' + New-PodeAccessScheme -Type Group | Add-PodeAccess -Name 'TestGbac' + # Add-PodeAccess -Type Custom -Name 'TestRbac' -Path 'CustomAccess' -Validator { # param($userRoles, $customValues) # return $userRoles.Example -iin $customValues.Example # } - Merge-PodeAuthAccess -Name 'TestMerged' -Access 'TestRbac', 'TestGbac' -Valid All + Merge-PodeAccess -Name 'TestMergedAll' -Access 'TestRbac', 'TestGbac' -Valid All + Merge-PodeAccess -Name 'TestMergedOne' -Access 'TestRbac', 'TestGbac' -Valid One # setup basic auth (base64> username:password in header) - New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Access 'TestMerged' -Sessionless -ScriptBlock { + New-PodeAuthScheme -Basic -Realm 'Pode Example Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { param($username, $password) # here you'd check a real user storage, this is just for example @@ -48,7 +51,7 @@ Start-PodeServer -Threads 2 { Type = 'Human' Username = 'm.orty' Roles = @('Developer') - Groups = @('Software') + Groups = @('Software', 'Admins') CustomAccess = @{ Example = 'test-val-1' } } } @@ -62,40 +65,81 @@ Start-PodeServer -Threads 2 { $WebEvent.Auth | Out-Default } - # 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' -Group 'Ops' -ScriptBlock { + # POST request to get list of users - there's no Access, 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 -Group Software -ScriptBlock { + Add-PodeRoute -Method Post -Path '/users-dev' -Authentication 'Validate' -Access 'TestRbac' -Role Developer -ScriptBlock { Write-PodeJsonResponse -Value @{ Users = @( @{ Name = 'Leeroy Jenkins' - Age = 1337 } ) } - } -PassThru | Add-PodeAuthCustomAccess -Name 'TestRbac' -Value @{ Example = 'test-val-1' } + } + + # POST request to get list of users - only QA roles can access + Add-PodeRoute -Method Post -Path '/users-qa' -Authentication 'Validate' -Access 'TestRbac' -Role QA -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Nikola Tesla' + } + ) + } + } + + # POST request to get list of users - only users in the SOftware group can access + Add-PodeRoute -Method Post -Path '/users-soft' -Authentication 'Validate' -Access 'TestGbac' -Group Software -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Smooth McGroove' + } + ) + } + } - # POST request to get list of users - only Admin roles can access - Add-PodeRoute -Method Post -Path '/users-admin' -Authentication 'Validate' -Role Admin -ScriptBlock { + # POST request to get list of users - only Developer role in the Admins group can access + Add-PodeRoute -Method Post -Path '/users-dev-admin' -Authentication 'Validate' -Access 'TestMergedAll' -Role Developer -Group Admins -ScriptBlock { Write-PodeJsonResponse -Value @{ Users = @( @{ Name = 'Arthur Dent' - Age = 30 } ) } - } -PassThru | Add-PodeAuthCustomAccess -Name 'TestRbac' -Value @{ Example = 'test-val-2' } + } + + # POST request to get list of users - either DevOps role or Admins group can access + Add-PodeRoute -Method Post -Path '/users-devop-admin' -Authentication 'Validate' -Access 'TestMergedOne' -Role DevOps -Group Admins -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Monkey D. Luffy' + } + ) + } + } + + # POST request to get list of users - either QA role or Support group can access + Add-PodeRoute -Method Post -Path '/users-qa-support' -Authentication 'Validate' -Access 'TestMergedOne' -Role QA -Group Support -ScriptBlock { + Write-PodeJsonResponse -Value @{ + Users = @( + @{ + Name = 'Donald Duck' + } + ) + } + } } \ No newline at end of file diff --git a/examples/web-auth-form-access.ps1 b/examples/web-auth-form-access.ps1 new file mode 100644 index 000000000..a7551dd30 --- /dev/null +++ b/examples/web-auth-form-access.ps1 @@ -0,0 +1,103 @@ +$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 examples shows how to use session persistant authentication with access. +The example used here is Form authentication and RBAC access on pages, sent from the
in HTML. + +Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' +page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you +back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and +take you back to the login page. + +- The Home and Login pages are accessible by all. +- The About page is only accessible by Developers (for morty it will load) +- The Register page is only accessible by QAs (for morty this will 403) +#> + +# create a server, and start listening on port 8085 +Start-PodeServer -Threads 2 { + + # listen on localhost:8085 + Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + + # set the view engine + Set-PodeViewEngine -Type Pode + + # enable error logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # setup session details + Enable-PodeSessionMiddleware -Duration 120 -Extend + + # setup form auth ( in HTML) + New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -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 = @{ + Name = 'Morty' + Roles = @('Developer') + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # set RBAC access + New-PodeAccessScheme -Type Role | Add-PodeAccess -Name 'Rbac' -Match One + + + # home page: + # redirects to login page if not authenticated + Add-PodeRoute -Method Get -Path '/' -Authentication Login -ScriptBlock { + $session:Views++ + + Write-PodeViewResponse -Path 'auth-home' -Data @{ + Username = $WebEvent.Auth.User.Name + Views = $session:Views + Expiry = Get-PodeSessionExpiry + } + } + + + # about page: + # only Developers can access this page + Add-PodeRoute -Method Get -Path '/about' -Authentication Login -Access Rbac -Role Developer -ScriptBlock { + Write-PodeViewResponse -Path 'auth-about' + } + + + # register page: + # only QAs can access this page + Add-PodeRoute -Method Get -Path '/register' -Authentication Login -Access Rbac -Role QA -ScriptBlock { + Write-PodeViewResponse -Path 'auth-register' + } + + + # login page: + # the login flag set below checks if there is already an authenticated session cookie. If there is, then + # the user is redirected to the home page. If there is no session then the login page will load without + # checking user authetication (to prevent a 401 status) + Add-PodeRoute -Method Get -Path '/login' -Authentication Login -Login -ScriptBlock { + Write-PodeViewResponse -Path 'auth-login' -FlashMessages + } + + + # login check: + # this is the endpoint the 's action will invoke. If the user validates then they are set against + # the session as authenticated, and redirect to the home page. If they fail, then the login page reloads + Add-PodeRoute -Method Post -Path '/login' -Authentication Login -Login + + + # logout check: + # when the logout button is click, this endpoint is invoked. The logout flag set below informs this call + # to purge the currently authenticated session, and then redirect back to the login page + Add-PodeRoute -Method Post -Path '/logout' -Authentication Login -Logout +} \ No newline at end of file diff --git a/examples/web-auth-form-session-auth.ps1 b/examples/web-auth-form-session-auth.ps1 new file mode 100644 index 000000000..aee830686 --- /dev/null +++ b/examples/web-auth-form-session-auth.ps1 @@ -0,0 +1,87 @@ +$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 examples shows how to use session persistant authentication, for things like logins on websites. +The example used here is Form authentication, sent from the in HTML. But also used is Session Authentication +on the main home page route and Form Auth on Login. + +Navigating to the 'http://localhost:8085' endpoint in your browser will auto-rediect you to the '/login' +page. Here, you can type the username (morty) and the password (pickle); clicking 'Login' will take you +back to the home page with a greeting and a view counter. Clicking 'Logout' will purge the session and +take you back to the login page. +#> + +# create a server, and start listening on port 8085 +Start-PodeServer -Threads 2 { + + # listen on localhost:8085 + Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + + # set the view engine + Set-PodeViewEngine -Type Pode + + # enable error logging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # setup session details + Enable-PodeSessionMiddleware -Duration 120 -Extend + + # setup form auth ( in HTML) + New-PodeAuthScheme -Form | Add-PodeAuth -Name 'Login' -FailureUrl '/login' -SuccessUrl '/' -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' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + + # setup session auth + Add-PodeAuthSession -Name 'SessionAuth' -FailureUrl '/login' + + + # home page: + # redirects to login page if not authenticated + Add-PodeRoute -Method Get -Path '/' -Authentication SessionAuth -ScriptBlock { + $session:Views++ + + Write-PodeViewResponse -Path 'auth-home' -Data @{ + Username = $WebEvent.Auth.User.Name + Views = $session:Views + Expiry = Get-PodeSessionExpiry + } + } + + + # login page: + # the login flag set below checks if there is already an authenticated session cookie. If there is, then + # the user is redirected to the home page. If there is no session then the login page will load without + # checking user authetication (to prevent a 401 status) + Add-PodeRoute -Method Get -Path '/login' -Authentication Login -Login -ScriptBlock { + Write-PodeViewResponse -Path 'auth-login' -FlashMessages + } + + + # login check: + # this is the endpoint the 's action will invoke. If the user validates then they are set against + # the session as authenticated, and redirect to the home page. If they fail, then the login page reloads + Add-PodeRoute -Method Post -Path '/login' -Authentication Login -Login + + + # logout check: + # when the logout button is click, this endpoint is invoked. The logout flag set below informs this call + # to purge the currently authenticated session, and then redirect back to the login page + Add-PodeRoute -Method Post -Path '/logout' -Authentication SessionAuth -Logout +} \ No newline at end of file diff --git a/examples/web-auth-merged.ps1 b/examples/web-auth-merged.ps1 index e6e664449..7dd84c36a 100644 --- a/examples/web-auth-merged.ps1 +++ b/examples/web-auth-merged.ps1 @@ -21,11 +21,11 @@ Start-PodeServer -Threads 2 { New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging # setup access - Add-PodeAuthAccess -Type Role -Name 'Rbac' - Add-PodeAuthAccess -Type Group -Name 'Gbac' + Add-PodeAccess -Type Role -Name 'Rbac' + Add-PodeAccess -Type Group -Name 'Gbac' # setup a merged access - Merge-PodeAuthAccess -Name 'MergedAccess' -Access 'Rbac', 'Gbac' -Valid All + Merge-PodeAccess -Name 'MergedAccess' -Access 'Rbac', 'Gbac' -Valid All # setup apikey auth New-PodeAuthScheme -ApiKey -Location Header | Add-PodeAuth -Name 'ApiKey' -Access 'Gbac' -Sessionless -ScriptBlock { diff --git a/src/Pode.psd1 b/src/Pode.psd1 index e5c776f3a..6ff02e503 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -223,18 +223,26 @@ 'Use-PodeAuth', 'ConvertFrom-PodeOIDCDiscovery', 'Test-PodeAuthUser', - 'Add-PodeAuthAccess', - 'Add-PodeAuthCustomAccess', - 'Get-PodeAuthAccess', - 'Test-PodeAuthAccessExists', - 'Test-PodeAuthAccess', - 'Test-PodeAuthAccessUser', - 'Test-PodeAuthAccessRoute', 'Merge-PodeAuth', 'Test-PodeAuth', 'Test-PodeAuthExists', - 'Merge-PodeAuthAccess', 'Get-PodeAuthUser', + 'Add-PodeAuthSession', + + # access + 'New-PodeAccessScheme', + 'Add-PodeAccess', + 'Add-PodeAccessCustom', + 'Get-PodeAccess', + 'Test-PodeAccessExists', + 'Test-PodeAccess', + 'Test-PodeAccessUser', + 'Test-PodeAccessRoute', + 'Merge-PodeAccess', + 'Remove-PodeAccess', + 'Clear-PodeAccess', + 'Add-PodeAccessMiddleware', + 'Use-PodeAccess', # logging 'New-PodeLoggingMethod', diff --git a/src/Private/Access.ps1 b/src/Private/Access.ps1 new file mode 100644 index 000000000..160dbcc5f --- /dev/null +++ b/src/Private/Access.ps1 @@ -0,0 +1,67 @@ +function Get-PodeAccessMiddlewareScript +{ + return { + param($opts) + + if ($null -eq $WebEvent.Auth) { + Set-PodeResponseStatus -Code 403 + return $false + } + + # test access + $WebEvent.Auth.IsAuthorised = Invoke-PodeAccessValidation -Name $opts.Name + + # 403 if unauthorised + if (!$WebEvent.Auth.IsAuthorised) { + Set-PodeResponseStatus -Code 403 + } + + # run next middleware or stop? + return $WebEvent.Auth.IsAuthorised + } +} + +function Invoke-PodeAccessValidation +{ + param( + [Parameter(Mandatory=$true)] + [string] + $Name + ) + + # get the access method + $access = $PodeContext.Server.Authorisations.Methods[$Name] + + # if it's a merged access, re-call this function and check against "succeed" value + if ($access.Merged) { + foreach ($accName in $access.Access) { + $result = Invoke-PodeAccessValidation -Name $accName + + # if the access passed, and we only need one access to pass, return true + if ($result -and $access.PassOne) { + return $true + } + + # if the access failed, but we need all to pass, return false + if (!$result -and !$access.PassOne) { + return $false + } + } + + # if the last access failed, and we only need one access to pass, return false + if (!$result -and $access.PassOne) { + return $false + } + + # if the last access succeeded, and we need all to pass, return true + if ($result -and !$access.PassOne) { + return $true + } + + # default failure + return $false + } + + # main access validation logic + return (Test-PodeAccessRoute -Name $Name) +} \ No newline at end of file diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index bbae97780..4adfbfaee 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -1072,7 +1072,7 @@ function Invoke-PodeAuthValidation # if it's a merged auth, re-call this function and check against "succeed" value if ($auth.Merged) { - $results = @() + $results = @{} foreach ($authName in $auth.Authentications) { $result = Invoke-PodeAuthValidation -Name $authName @@ -1094,12 +1094,7 @@ function Invoke-PodeAuthValidation # remember result if we need all to pass if (!$auth.PassOne) { - if ($result.Aggregated) { - $results += $result.Results - } - else { - $results += $result - } + $results[$authName] = $result } } @@ -1108,14 +1103,15 @@ function Invoke-PodeAuthValidation return $result } - # if the last auth succeeded, and we need all to pass, return aggregated results + # if the last auth succeeded, and we need all to pass, merge users/headers and return result if ($result.Success -and !$auth.PassOne) { - return @{ - Success = $true - Aggregated = $true - Results = $results - Auth = $results.Auth - } + # invoke scriptblock + $result = Invoke-PodeAuthInbuiltScriptBlock -User $results -ScriptBlock $auth.ScriptBlock.Script -UsingVariables $auth.ScriptBlock.UsingVariables + + # reset default properties and return + $result.Success = $true + $result.Auth = $results.Keys + return $result } # default failure @@ -1228,7 +1224,7 @@ function Test-PodeAuthValidation $result.Headers = @{} } - if (!$result.Headers.ContainsKey('WWW-Authenticate')) { + if (![string]::IsNullOrWhiteSpace($auth.Scheme.Name) -and !$result.Headers.ContainsKey('WWW-Authenticate')) { $authHeader = Get-PodeAuthWwwHeaderValue -Name $auth.Scheme.Name -Realm $auth.Scheme.Realm -Challenge $result.Challenge $result.Headers['WWW-Authenticate'] = $authHeader } @@ -1260,111 +1256,6 @@ function Test-PodeAuthValidation } } -function Test-PodeAuthValidationAccess -{ - param( - [Parameter(Mandatory=$true)] - [string] - $Name, - - [Parameter()] - [string] - $BaseName - ) - - # base name - if ([string]::IsNullOrEmpty($BaseName)) { - $BaseName = $Name - } - - # get auth method - $auth = $PodeContext.Server.Authentications.Methods[$Name] - $access = $null - - # cached access? - if ($null -ne $auth.Cache.Access) { - $access = $auth.Cache.Access - } - - # recursively find access - else { - # have access and/or parent? - $hasAccess = ![string]::IsNullOrEmpty($auth.Access) - $hasParent = ![string]::IsNullOrEmpty($auth.Parent) - - # no access - if (!$hasAccess) { - $PodeContext.Server.Authentications.Methods[$Name].Cache.Access = [string]::Empty - - # skip if no parent - if (!$hasParent) { - return $true - } - - # re-call on parent - else { - return (Test-PodeAuthValidationAccess -Name $auth.Parent -BaseName $BaseName) - } - } - - # set access - $access = $auth.Access - $PodeContext.Server.Authentications.Methods[$BaseName].Cache.Access = $access - } - - # check access - return ([string]::IsNullOrEmpty($access) -or (Invoke-PodeAuthValidationAccess -Name $access -Authentication $BaseName)) -} - -function Invoke-PodeAuthValidationAccess -{ - param( - [Parameter(Mandatory=$true)] - [string] - $Name, - - [Parameter(Mandatory=$true)] - [string] - $Authentication - ) - - # get the access method - $access = $PodeContext.Server.Authentications.Access[$Name] - - # if it's a merged access, re-call this function and check against "succeed" value - if ($access.Merged) { - foreach ($accName in $access.Access) { - $result = Invoke-PodeAuthValidationAccess -Name $accName -Authentication $Authentication - - # if the access passed, and we only need one access to pass, return true - if ($result -and $access.PassOne) { - return $true - } - - # if the access failed, but we need all to pass, return false - if (!$result -and !$access.PassOne) { - return $false - } - } - - # if the last access failed, and we only need one access to pass, return false - if (!$result -and $access.PassOne) { - return $false - } - - # if the last access succeeded, and we need all to pass, return true - if ($result -and !$access.PassOne) { - return $true - } - - # default failure - return $false - } - - # main access validation logic - return (Test-PodeAuthAccessRoute -Name $Name -Authentication $Authentication) -} - function Get-PodeAuthMiddlewareScript { return { @@ -1480,43 +1371,27 @@ function Test-PodeAuthInternal -NoFailureRedirect:($result.FailureRedirect)) } - # if auth passed, assign the user(s) to the session - $user = $result.User - if ($result.Aggregated) { - $user = [ordered]@{} - foreach ($r in $result.Results) { - $user[$r.Auth] = $r.User - } - } - + # if auth passed, assign the user to the session $WebEvent.Auth = [ordered]@{ - User = $user + User = $result.User IsAuthenticated = $true IsAuthorised = $true Store = !$auth.Sessionless Name = $result.Auth - Multiple = [bool]$result.Aggregated } - # check access - foreach ($authName in $result.Auth) { - $WebEvent.Auth.IsAuthorised = Test-PodeAuthValidationAccess -Name $authName - - if (!$WebEvent.Auth.IsAuthorised) { - return (Set-PodeAuthStatus ` - -StatusCode 403 ` - -Description $result.Description ` - -Headers $result.Headers ` - -Name $authName ` - -LoginRoute:$Login ` - -NoFailureRedirect:($result.FailureRedirect)) - } + # successful auth + $authName = $null + if ($auth.Merged -and !$auth.PassOne) { + $authName = $Name + } + else { + $authName = @($result.Auth)[0] } - # successful auth return (Set-PodeAuthStatus ` -Headers $result.Headers ` - -Name @($result.Auth)[0] ` + -Name $authName ` -LoginRoute:$Login) } diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 3d7cf1b79..1d648590c 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -351,10 +351,13 @@ function New-PodeContext } } - # authentication methods and access + # authentication and authorisation methods $ctx.Server.Authentications = @{ Methods = @{} - Access = @{} + } + + $ctx.Server.Authorisations = @{ + Methods = @{} } # create new cancellation tokens diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index ffbd77b37..ef924b43a 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -299,25 +299,10 @@ function Write-PodeRequestLog # set username - dot spaces if (Test-PodeAuthUser -IgnoreSession) { $userProps = (Get-PodeLogger -Name $name).Properties.Username.Split('.') - $user = $null - if (!$WebEvent.Auth.Multiple) { - $user = $WebEvent.Auth.User - foreach ($atom in $userProps) { - $user = $user.($atom) - } - } - else { - foreach ($u in $WebEvent.Auth.User.Values) { - $user = $u - foreach ($atom in $userProps) { - $user = $user.($atom) - } - - if (![string]::IsNullOrWhiteSpace($user)) { - break - } - } + $user = $WebEvent.Auth.User + foreach ($atom in $userProps) { + $user = $user.($atom) } if (![string]::IsNullOrWhiteSpace($user)) { diff --git a/src/Private/OpenApi.ps1 b/src/Private/OpenApi.ps1 index 68b4d7980..719921319 100644 --- a/src/Private/OpenApi.ps1 +++ b/src/Private/OpenApi.ps1 @@ -274,6 +274,10 @@ function Get-PodeOpenApiDefinitionInternal foreach ($authName in $PodeContext.Server.Authentications.Methods.Keys) { $authType = (Find-PodeAuth -Name $authName).Scheme + if ([string]::IsNullOrWhiteSpace($authType)) { + continue + } + $_authName = ($authName -replace '\s+', '') $_authObj = @{} diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 59a4c551d..bd8ae7cd9 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -244,7 +244,7 @@ function Restart-PodeInternalServer # clear up authentication methods $PodeContext.Server.Authentications.Methods.Clear() - $PodeContext.Server.Authentications.Access.Clear() + $PodeContext.Server.Authorisations.Methods.Clear() # clear up shared state $PodeContext.Server.State.Clear() diff --git a/src/Public/Access.ps1 b/src/Public/Access.ps1 new file mode 100644 index 000000000..2da389099 --- /dev/null +++ b/src/Public/Access.ps1 @@ -0,0 +1,705 @@ +<# +.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')] + 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)) { + throw "A Path or ScriptBlock is required for sourcing the Custom access values" + } + } + + # 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 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, + + [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' + ) + + # check name unique + if (Test-PodeAccessExists -Name $Name) { + throw "Access method already defined: $($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 + 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 "Access method already defined: $($Name)" + } + + # ensure all the access methods exist + foreach ($accName in $Access) { + if (!(Test-PodeAccessExists -Name $accName)) { + throw "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 "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()] + 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 +{ + [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()] + 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) + $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) + $Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -Return -Splat + } + } + + # check for custom validator, or use default match logic + if ($null -ne $access.ScriptBlock) { + $_args = @(,$Source) + @(,$Destination) + @($access.Arguments) + $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.ScriptBlock.UsingVariables) + return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.ScriptBlock.Script -Arguments $_args -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) + $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) + $userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -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 + ) + + $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 "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' +} \ No newline at end of file diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index b4987b541..e87cbcd4e 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -1,9 +1,9 @@ <# .SYNOPSIS -Create a new type of Authentication. +Create a new type of Authentication scheme. .DESCRIPTION -Create a new type of Authentication, which is used to parse the Request for user credentials for validating. +Create a new type of Authentication scheme, which is used to parse the Request for user credentials for validating. .PARAMETER Basic If supplied, will use the inbuilt Basic Authentication credentials retriever. @@ -123,7 +123,7 @@ function New-PodeAuthScheme { [CmdletBinding(DefaultParameterSetName='Basic')] [OutputType([hashtable])] - param ( + param( [Parameter(ParameterSetName='Basic')] [switch] $Basic, @@ -670,10 +670,7 @@ Adds a custom Authentication method for verifying users. A unique Name for the Authentication method. .PARAMETER Scheme -The Scheme to use for retrieving credentials (From New-PodeAuthScheme). - -.PARAMETER Access -An optional Access method Name to validate authorisation to Routes (From Add-PodeAuthAccess) +The authentication Scheme to use for retrieving credentials (From New-PodeAuthScheme). .PARAMETER ScriptBlock The ScriptBlock defining logic that retrieves and verifys a user. @@ -711,10 +708,6 @@ function Add-PodeAuth [hashtable] $Scheme, - [Parameter()] - [string] - $Access, - [Parameter(Mandatory=$true)] [ValidateScript({ if (Test-PodeIsEmpty $_) { @@ -759,11 +752,6 @@ function Add-PodeAuth throw "The supplied '$($Scheme.Name)' Scheme for the '$($Name)' authentication validator requires a valid ScriptBlock" } - # ensure the Access method exists - if (!(Test-PodeIsEmpty $Access) -and !(Test-PodeAuthAccessExists -Name $Access)) { - throw "Access method not found: $($Access)" - } - # if we're using sessions, ensure sessions have been setup if (!$Sessionless -and !(Test-PodeSessionsConfigured)) { throw 'Sessions are required to use session persistent authentication' @@ -776,7 +764,6 @@ function Add-PodeAuth $PodeContext.Server.Authentications.Methods[$Name] = @{ Name = $Name Scheme = $Scheme - Access = $Access ScriptBlock = $ScriptBlock UsingVariables = $usingVars Arguments = $ArgumentList @@ -820,13 +807,13 @@ Multiple Autentication method Names to be merged. .PARAMETER Valid How many of the Authentication methods are required to be valid, One or All. (Default: One) +.PARAMETER ScriptBlock +This is mandatory when Valid is All. A scriptblock to merge the mutliple users/headers returned by valid authentications into 1 user/header objects. +This scriptblock will receive a hashtable of all result objects returned from Authentication methods. The key for the hashtable will be the authentication names that passed. + .PARAMETER Default The Default Authentication method to use as a fallback for Failure URLs and other settings. -.PARAMETER Access -An optional Access method Name to validate authorisation to Routes. -This will be used as fallback for the merged Authentication methods if not set on them. - .PARAMETER FailureUrl The URL to redirect to when authentication fails. This will be used as fallback for the merged Authentication methods if not set on them. @@ -872,12 +859,12 @@ function Merge-PodeAuth $Valid = 'One', [Parameter()] - [string] - $Default, + [scriptblock] + $ScriptBlock, [Parameter()] [string] - $Access, + $Default, [Parameter()] [string] @@ -915,11 +902,6 @@ function Merge-PodeAuth throw "the Default Authentication '$($Default)' is not in the Authentication list supplied" } - # ensure the Access methods exists - if (!(Test-PodeIsEmpty $Access) -and !(Test-PodeAuthAccessExists -Name $Access)) { - throw "Access method not found: $($Access)" - } - # set default if ([string]::IsNullOrEmpty($Default)) { $Default = $Authentication[0] @@ -938,11 +920,6 @@ function Merge-PodeAuth throw 'Sessions are required to use session persistent authentication' } - # check access from default - if (Test-PodeIsEmpty $Access) { - $Access = $tmpAuth.Access - } - # check failure url from default if ([string]::IsNullOrEmpty($FailureUrl)) { $FailureUrl = $tmpAuth.Failure.Url @@ -963,6 +940,15 @@ function Merge-PodeAuth $SuccessUseOrigin = $tmpAuth.Success.UseOrigin } + # deal with using vars in scriptblock + if ($Valid -ieq 'all') { + if ($null -eq $ScriptBlock) { + throw "A Scriptblock for merging multiple authenticated users into 1 object is required When Valid is All" + } + + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + # set parent auth foreach ($authName in $Authentication) { $PodeContext.Server.Authentications.Methods[$authName].Parent = $Name @@ -973,8 +959,11 @@ function Merge-PodeAuth Name = $Name Authentications = @($Authentication) PassOne = ($Valid -ieq 'one') + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } Default = $Default - Access = $Access Sessionless = $Sessionless.IsPresent Failure = @{ Url = $FailureUrl @@ -1060,17 +1049,11 @@ The Name of the Authentication method. .PARAMETER IgnoreSession If supplied, authentication will be re-verified on each call even if a valid session exists on the request. -.PARAMETER CheckAccess -If supplied, an Authentication method's Access method will also be verified. - .EXAMPLE if (Test-PodeAuth -Name 'BasicAuth') { ... } .EXAMPLE if (Test-PodeAuth -Name 'FormAuth' -IgnoreSession) { ... } - -.EXAMPLE -if (Test-PodeAuth -Name 'BasicAuth' -CheckAccess) { ... } #> function Test-PodeAuth { @@ -1081,10 +1064,7 @@ function Test-PodeAuth $Name, [switch] - $IgnoreSession, - - [switch] - $CheckAccess + $IgnoreSession ) # if the session already has a user/isAuth'd, then skip auth - or allow anon @@ -1110,15 +1090,6 @@ function Test-PodeAuth return $false } - # check access - if ($CheckAccess) { - foreach ($authName in $result.Auth) { - if (!(Test-PodeAuthValidationAccess -Name $authName)) { - return $false - } - } - } - # successful auth return $true } @@ -1199,7 +1170,7 @@ New-PodeAuthScheme -Form | Add-PodeAuthWindowsAd -Name 'UnixAuth' -Server 'testd function Add-PodeAuthWindowsAd { [CmdletBinding(DefaultParameterSetName='Groups')] - param ( + param( [Parameter(Mandatory=$true)] [string] $Name, @@ -1339,6 +1310,139 @@ function Add-PodeAuthWindowsAd } } +<# +.SYNOPSIS +Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests. + +.DESCRIPTION +Adds the inbuilt Session Authentication method for verifying an authenticated session is present on Requests. + +.PARAMETER Name +A unique Name for the Authentication method. + +.PARAMETER FailureUrl +The URL to redirect to when authentication fails. + +.PARAMETER FailureMessage +An override Message to throw when authentication fails. + +.PARAMETER SuccessUrl +The URL to redirect to when authentication succeeds when logging in. + +.PARAMETER ScriptBlock +Optional ScriptBlock that is passed the found user object for further validation. + +.PARAMETER Middleware +An array of ScriptBlocks for optional Middleware to run before the Scheme's scriptblock. + +.PARAMETER SuccessUseOrigin +If supplied, successful authentication from a login page will redirect back to the originating page instead of the FailureUrl. + +.EXAMPLE +Add-PodeAuthSession -Name 'SessionAuth' -FailureUrl '/login' +#> +function Add-PodeAuthSession +{ + [CmdletBinding(DefaultParameterSetName='Groups')] + param( + [Parameter(Mandatory=$true)] + [string] + $Name, + + [Parameter()] + [string] + $FailureUrl, + + [Parameter()] + [string] + $FailureMessage, + + [Parameter()] + [string] + $SuccessUrl, + + [Parameter()] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [object[]] + $Middleware, + + [switch] + $SuccessUseOrigin + ) + + # if sessions haven't been setup, error + if (!(Test-PodeSessionsConfigured)) { + throw 'Sessions have not been configured' + } + + # ensure the name doesn't already exist + if (Test-PodeAuthExists -Name $Name) { + throw "Authentication method already defined: $($Name)" + } + + # if we have a scriptblock, deal with using vars + if ($null -ne $ScriptBlock) { + $ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + } + + # create the auth scheme for getting the session + $scheme = New-PodeAuthScheme -Custom -Middleware $Middleware -ScriptBlock { + param($options) + + # 401 if sessions not used + if (!(Test-PodeSessionsInUse)) { + Revoke-PodeSession + return @{ + Message = "Sessions are not being used" + Code = 401 + } + } + + # 401 if no authenticated user + if (!(Test-PodeAuthUser)) { + Revoke-PodeSession + return @{ + Message = "Session not authenticated" + Code = 401 + } + } + + # return user + return @($WebEvent.Session.Data.Auth) + } + + # add a custom auth method to return user back + $method = { + param($user, $options) + $result = @{ User = $user } + + # call additional scriptblock if supplied + if ($null -ne $options.ScriptBlock.Script) { + $result = Invoke-PodeAuthInbuiltScriptBlock -User $result.User -ScriptBlock $options.ScriptBlock.Script -UsingVariables $options.ScriptBlock.UsingVariables + } + + # return user back + return $result + } + + $scheme | Add-PodeAuth ` + -Name $Name ` + -ScriptBlock $method ` + -FailureUrl $FailureUrl ` + -FailureMessage $FailureMessage ` + -SuccessUrl $SuccessUrl ` + -SuccessUseOrigin:$SuccessUseOrigin ` + -ArgumentList @{ + ScriptBlock = @{ + Script = $ScriptBlock + UsingVariables = $usingVars + } + } +} + <# .SYNOPSIS Remove a specific Authentication method. @@ -1355,7 +1459,7 @@ Remove-PodeAuth -Name 'Login' function Remove-PodeAuth { [CmdletBinding()] - param ( + param( [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [string] $Name @@ -1494,7 +1598,7 @@ Add-PodeAuthIIS -Name 'IISAuth' -NoGroups function Add-PodeAuthIIS { [CmdletBinding(DefaultParameterSetName='Groups')] - param ( + param( [Parameter(Mandatory=$true)] [string] $Name, @@ -2277,10 +2381,6 @@ Test whether the current WebEvent or Session has an authenticated user. .DESCRIPTION Test whether the current WebEvent or Session has an authenticated user. Returns true if there is an authenticated user. -.PARAMETER Authentication -Only if multiple merged Authentication methods are being used, supply the Name of the Authentication method where the User object should be checked. -If not supplied, and using mutliple Authentication methods, true will be returned if there is a User object for any Authentication method. - .PARAMETER IgnoreSession If supplied, only the Auth object in the WebEvent will be checked and the Session will be skipped. @@ -2291,10 +2391,6 @@ function Test-PodeAuthUser { [CmdletBinding()] param( - [Parameter()] - [string] - $Authentication, - [switch] $IgnoreSession ) @@ -2314,13 +2410,7 @@ function Test-PodeAuthUser return $false } - # embedded auth - $user = $auth.User - if ($auth.Multiple -and ![string]::IsNullOrEmpty($Authentication)) { - $user = $user[$Authentication] - } - - return ($null -ne $user) + return ($null -ne $auth.User) } <# @@ -2330,10 +2420,6 @@ Get the authenticated user from the WebEvent or Session. .DESCRIPTION Get the authenticated user from the WebEvent or Session. This is similar to calling $Webevent.Auth.User. -.PARAMETER Authentication -Only if multiple merged Authentication methods are being used, supply the Name of the Authentication method where the User object should be retrieved. -If not supplied, and using mutliple Authentication methods, all User objects will be returned. - .PARAMETER IgnoreSession If supplied, only the Auth object in the WebEvent will be used and the Session will be skipped. @@ -2344,10 +2430,6 @@ function Get-PodeAuthUser { [CmdletBinding()] param( - [Parameter()] - [string] - $Authentication, - [switch] $IgnoreSession ) @@ -2367,572 +2449,5 @@ function Get-PodeAuthUser return $null } - # embedded auth - $user = $auth.User - if ($auth.Multiple -and ![string]::IsNullOrEmpty($Authentication)) { - $user = $user[$Authentication] - } - - return $user -} - -<# -.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 Type -The Type of Access this method is for: Role, Group, Scope, User, Custom. - -.PARAMETER ScriptBlock -An optional ScriptBlock for retrieving authorisation values for the authenticated user, useful if the values reside in an external data store. - -.PARAMETER ArgumentList -An optional array of arguments to supply to the Access's ScriptBlock and Validator. - -.PARAMETER Validator -An optional Validator scriptblock, which can be used to invoke custom validation logic to verify authorisation. - -.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; Username; or Custom. - -.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 -Add-PodeAuthAccess -Name 'Example' -Type Role - -.EXAMPLE -Add-PodeAuthAccess -Name 'Example' -Type Group -Path 'Metadata.Groups' -Match All - -.EXAMPLE -Add-PodeAuthAccess -Name 'Example' -Type Scope -Scriptblock { param($user) return @(Get-ExampleAccess -Username $user.Username) } - -.EXAMPLE -Add-PodeAuthAccess -Name 'Example' -Type Custom -Validator { param($userAccess, $customAccess) return $userAccess.Country -ieq $customAccess.Country } -#> -function Add-PodeAuthAccess -{ - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] - [string] - $Name, - - [Parameter(Mandatory=$true)] - [ValidateSet('Role', 'Group', 'Scope', 'User', 'Custom')] - [string] - $Type, - - [Parameter()] - [scriptblock] - $ScriptBlock = $null, - - [Parameter()] - [object[]] - $ArgumentList, - - [Parameter()] - [scriptblock] - $Validator = $null, - - [Parameter()] - [string] - $Path, - - [Parameter()] - [ValidateSet('All', 'One', 'None')] - [string] - $Match = 'One' - ) - - # check name unique - if (Test-PodeAuthAccessExists -Name $Name) { - throw "Access method already defined: $($Name)" - } - - # for custom access a validator is mandatory - if ($Type -ieq 'custom') { - if ([string]::IsNullOrEmpty($Path) -and ($null -eq $ScriptBlock)) { - throw "A Path or ScriptBlock is required for sourcing the Custom access values, for the Custom Access method '$($Name)'" - } - } - - # 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 - } - } - - # 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]::IsNullOrWhiteSpace($Path)) { - if ($Type -ieq 'user') { - $Path = 'Username' - } - else { - $Path = "$($Type)s" - } - } - - # add access object - $PodeContext.Server.Authentications.Access[$Name] = @{ - Name = $Name - Type = $Type - IsCustom = ($Type -ieq 'custom') - ScriptBlock = $scriptObj - Validator = $validObj - Arguments = $ArgumentList - Path = $Path - 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-PodeAuthAccess -Name MergedAccess -Access RbacAccess, GbacAccess -Valid All -#> -function Merge-PodeAuthAccess -{ - [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-PodeAuthAccessExists -Name $Name) { - throw "Access method already defined: $($Name)" - } - - # ensure all the access methods exist - foreach ($accName in $Access) { - if (!(Test-PodeAuthAccessExists -Name $accName)) { - throw "Access method does not exist for merging: $($accName)" - } - } - - # set parent access - foreach ($accName in $Access) { - $PodeContext.Server.Authentications.Access[$accName].Parent = $Name - } - - # add auth method to server - $PodeContext.Server.Authentications.Access[$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-PodeAuthCustomAccess -Name 'Example' -Value @{ Country = 'UK' } -#> -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 - } - } -} - -<# -.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-PodeAuthAccess - -.EXAMPLE -$methods = Get-PodeAuthAccess -Name 'Example' - -.EXAMPLE -$methods = Get-PodeAuthAccess -Name 'Example1', 'Example2' -#> -function Get-PodeAuthAccess -{ - [CmdletBinding()] - param( - [Parameter()] - [string[]] - $Name - ) - - # return all if no Name - if ([string]::IsNullOrEmpty($Name) -or ($Name.Length -eq 0)) { - return $PodeContext.Server.Authentications.Access.Values - } - - # return filtered - return @(foreach ($n in $Name) { - $PodeContext.Server.Authentications.Access[$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-PodeAuthAccessExists -Name 'Example') { } -#> -function Test-PodeAuthAccessExists -{ - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] - [string] - $Name - ) - - return $PodeContext.Server.Authentications.Access.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's methods ScriptBlock for retrieving access values. - -.EXAMPLE -if (Test-PodeAuthAccess -Name 'Example' -Source 'Developer' -Destination 'Admin') { } -#> -function Test-PodeAuthAccess -{ - [CmdletBinding()] - 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.Authentications.Access[$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.ScriptBlock) { - $_args = $ArgumentList + @($access.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scriptblock.UsingVariables) - $Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scriptblock.Script -Arguments $_args -Return -Splat - } - } - - # check for custom validator, or use default match logic - if ($null -ne $access.Validator) { - $_args = @(,$Source) + @(,$Destination) + @($access.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Validator.UsingVariables) - return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.Validator.Script -Arguments $_args -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. - -.PARAMETER Authentication -Only if multiple merged Authentication methods are being used, supply the Name of the Authentication method the Access being checked is for, -and where the User object should be retrieved. - -.EXAMPLE -if (Test-PodeAuthAccessUser -Name 'Example' -Value 'Developer', 'QA') { } - -.EXAMPLE -if (Test-PodeAuthAccessUser -Name 'Example' -Value 'Developer', 'QA' -Authentication 'BasicAuth') { } -#> -function Test-PodeAuthAccessUser -{ - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] - [string] - $Name, - - [Parameter(Mandatory=$true)] - [object[]] - $Value, - - [Parameter()] - [string] - $Authentication - ) - - # check if an auth name was passed for mutliple auths - if ($WebEvent.Auth.Multiple -and [string]::IsNullOrEmpty($Authentication)) { - throw "No Authentication name supplied to select User for Access testing, when mutliple authentications were used" - } - - # get the access method - $access = $PodeContext.Server.Authentications.Access[$Name] - - # get the user - $user = $WebEvent.Auth.User - if ($WebEvent.Auth.Multiple) { - $user = $user[$Authentication] - } - - # if there's no scriptblock, try the Path fallback - if ($null -eq $access.Scriptblock) { - $userAccess = $user - foreach ($atom in $access.Path.Split('.')) { - $userAccess = $userAccess.($atom) - } - } - - # otherwise, invoke scriptblock - else { - $_args = @($user) + @($access.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scriptblock.UsingVariables) - $userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scriptblock.Script -Arguments $_args -Return -Splat - } - - # is the user authorised? - return (Test-PodeAuthAccess -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. - -.PARAMETER Authentication -Only if multiple merged Authentication methods are being used, supply the Name of the Authentication method the Access being checked is for, -and where the User object should be retrieved. - -.EXAMPLE -if (Test-PodeAuthAccessRoute -Name 'Example') { } -#> -function Test-PodeAuthAccessRoute -{ - param( - [Parameter(Mandatory=$true)] - [string] - $Name, - - [Parameter()] - [string] - $Authentication - ) - - # check if an auth name was passed for mutliple auths - if ($WebEvent.Auth.Multiple -and [string]::IsNullOrEmpty($Authentication)) { - throw "No Authentication name supplied to select User for Access testing, when mutliple authentications were used" - } - - # get the access method - $access = $PodeContext.Server.Authentications.Access[$Name] - - # get route access values - if none then skip - $routeAccess = $WebEvent.Route.Access[$access.Type] - if ($access.IsCustom) { - $routeAccess = $routeAccess[$access.Name] - } - - if (($null -eq $routeAccess) -or ($routeAccess.Length -eq 0)) { - return $true - } - - # now test the user's access against the route's access - if ([string]::IsNullOrEmpty($Authentication)) { - $Authentication = $WebEvent.Route.Authentication - } - - return (Test-PodeAuthAccessUser -Name $Name -Value $routeAccess -Authentication $Authentication) + return $auth.User } \ No newline at end of file diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index 8f55dad3a..8b05f1886 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -938,7 +938,7 @@ Set-PodeResponseStatus -Code 500 -Exception $_.Exception -ContentType 'applicati function Set-PodeResponseStatus { [CmdletBinding()] - param ( + param( [Parameter(Mandatory=$true)] [int] $Code, diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index cfb4f38bd..d06199ade 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -38,6 +38,9 @@ An array of arguments to supply to the Route's ScriptBlock. .PARAMETER Authentication The name of an Authentication method which should be used as middleware on this Route. +.PARAMETER Access +The name of an Access method which should be used as middleware on this Route. + .PARAMETER AllowAnon If supplied, the Route will allow anonymous access for non-authenticated users. @@ -137,6 +140,10 @@ function Add-PodeRoute [string] $Authentication, + [Parameter()] + [string] + $Access, + [Parameter()] [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] [string] @@ -201,6 +208,10 @@ function Add-PodeRoute $Authentication = $RouteGroup.Authentication } + if ([string]::IsNullOrWhiteSpace($Access)) { + $Access = $RouteGroup.Access + } + if ($RouteGroup.AllowAnon) { $AllowAnon = $RouteGroup.AllowAnon } @@ -209,24 +220,24 @@ function Add-PodeRoute $IfExists = $RouteGroup.IfExists } - if ($null -ne $RouteGroup.Access.Role) { - $Role = $RouteGroup.Access.Role + $Role + if ($null -ne $RouteGroup.AccessMeta.Role) { + $Role = $RouteGroup.AccessMeta.Role + $Role } - if ($null -ne $RouteGroup.Access.Group) { - $Group = $RouteGroup.Access.Group + $Group + if ($null -ne $RouteGroup.AccessMeta.Group) { + $Group = $RouteGroup.AccessMeta.Group + $Group } - if ($null -ne $RouteGroup.Access.Scope) { - $Scope = $RouteGroup.Access.Scope + $Scope + if ($null -ne $RouteGroup.AccessMeta.Scope) { + $Scope = $RouteGroup.AccessMeta.Scope + $Scope } - if ($null -ne $RouteGroup.Access.User) { - $User = $RouteGroup.Access.User + $User + if ($null -ne $RouteGroup.AccessMeta.User) { + $User = $RouteGroup.AccessMeta.User + $User } - if ($null -ne $RouteGroup.Access.Custom) { - $CustomAccess = $RouteGroup.Access.Custom + if ($null -ne $RouteGroup.AccessMeta.Custom) { + $CustomAccess = $RouteGroup.AccessMeta.Custom } } @@ -275,6 +286,23 @@ function Add-PodeRoute # convert any middleware into valid hashtables $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) + # if an access name was supplied, setup access as middleware first to it's after auth middleware + if (![string]::IsNullOrWhiteSpace($Access)) { + if ([string]::IsNullOrWhiteSpace($Authentication)) { + throw "Access requires Authentication to be supplied on Routes" + } + + if (!(Test-PodeAccessExists -Name $Access)) { + throw "Access method does not exist: $($Access)" + } + + $options = @{ + Name = $Access + } + + $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) + } + # if an auth name was supplied, setup the auth as the first middleware if (![string]::IsNullOrWhiteSpace($Authentication)) { if (!(Test-PodeAuthExists -Name $Authentication)) { @@ -333,7 +361,8 @@ function Add-PodeRoute UsingVariables = $usingVars Middleware = $Middleware Authentication = $Authentication - Access = @{ + Access = $Access + AccessMeta = @{ Role = $Role Group = $Group Scope = $Scope @@ -421,6 +450,9 @@ The content type of any error pages that may get returned. .PARAMETER Authentication The name of an Authentication method which should be used as middleware on this Route. +.PARAMETER Access +The name of an Access method which should be used as middleware on this Route. + .PARAMETER AllowAnon If supplied, the static route will allow anonymous access for non-authenticated users. @@ -433,6 +465,18 @@ If supplied, the static route created will be returned so it can be passed throu .PARAMETER IfExists Specifies what action to take when a Static Route already exists. (Default: Default) +.PARAMETER Role +One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Group +One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Scope +One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER User +One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. + .EXAMPLE Add-PodeStaticRoute -Path '/assets' -Source './assets' @@ -484,11 +528,31 @@ function Add-PodeStaticRoute [string] $Authentication, + [Parameter()] + [string] + $Access, + [Parameter()] [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] [string] $IfExists = 'Default', + [Parameter()] + [string[]] + $Role, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $Scope, + + [Parameter()] + [string[]] + $User, + [switch] $AllowAnon, @@ -533,6 +597,10 @@ function Add-PodeStaticRoute $Authentication = $RouteGroup.Authentication } + if ([string]::IsNullOrWhiteSpace($Access)) { + $Access = $RouteGroup.Access + } + if (Test-PodeIsEmpty $Defaults) { $Defaults = $RouteGroup.Defaults } @@ -548,6 +616,26 @@ function Add-PodeStaticRoute if ($RouteGroup.IfExists -ine 'default') { $IfExists = $RouteGroup.IfExists } + + if ($null -ne $RouteGroup.AccessMeta.Role) { + $Role = $RouteGroup.AccessMeta.Role + $Role + } + + if ($null -ne $RouteGroup.AccessMeta.Group) { + $Group = $RouteGroup.AccessMeta.Group + $Group + } + + if ($null -ne $RouteGroup.AccessMeta.Scope) { + $Scope = $RouteGroup.AccessMeta.Scope + $Scope + } + + if ($null -ne $RouteGroup.AccessMeta.User) { + $User = $RouteGroup.AccessMeta.User + $User + } + + if ($null -ne $RouteGroup.AccessMeta.Custom) { + $CustomAccess = $RouteGroup.AccessMeta.Custom + } } # store the route method @@ -617,6 +705,23 @@ function Add-PodeStaticRoute # convert any middleware into valid hashtables $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) + # if an access name was supplied, setup access as middleware first to it's after auth middleware + if (![string]::IsNullOrWhiteSpace($Access)) { + if ([string]::IsNullOrWhiteSpace($Authentication)) { + throw "Access requires Authentication to be supplied on Static Routes" + } + + if (!(Test-PodeAccessExists -Name $Access)) { + throw "Access method does not exist: $($Access)" + } + + $options = @{ + Name = $Access + } + + $Middleware = (@(Get-PodeAccessMiddlewareScript | New-PodeMiddleware -ArgumentList $options) + $Middleware) + } + # if an auth name was supplied, setup the auth as the first middleware if (![string]::IsNullOrWhiteSpace($Authentication)) { if (!(Test-PodeAuthExists -Name $Authentication)) { @@ -646,6 +751,15 @@ function Add-PodeStaticRoute Method = $Method Defaults = $Defaults Middleware = $Middleware + Authentication = $Authentication + Access = $Access + AccessMeta = @{ + Role = $Role + Group = $Group + Scope = $Scope + User = $User + Custom = $CustomAccess + } Endpoint = @{ Protocol = $_endpoint.Protocol Address = $_endpoint.Address.Trim() @@ -874,6 +988,9 @@ The content type of any error pages that may get returned. .PARAMETER Authentication The name of an Authentication method which should be used as middleware on the Routes. +.PARAMETER Access +The name of an Access method which should be used as middleware on this Route. + .PARAMETER IfExists Specifies what action to take when a Route already exists. (Default: Default) @@ -933,6 +1050,10 @@ function Add-PodeRouteGroup [string] $Authentication, + [Parameter()] + [string] + $Access, + [Parameter()] [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] [string] @@ -999,6 +1120,10 @@ function Add-PodeRouteGroup $Authentication = $RouteGroup.Authentication } + if ([string]::IsNullOrWhiteSpace($Access)) { + $Access = $RouteGroup.Access + } + if ($RouteGroup.AllowAnon) { $AllowAnon = $RouteGroup.AllowAnon } @@ -1007,24 +1132,24 @@ function Add-PodeRouteGroup $IfExists = $RouteGroup.IfExists } - if ($null -ne $RouteGroup.Access.Role) { - $Role = $RouteGroup.Access.Role + $Role + if ($null -ne $RouteGroup.AccessMeta.Role) { + $Role = $RouteGroup.AccessMeta.Role + $Role } - if ($null -ne $RouteGroup.Access.Group) { - $Group = $RouteGroup.Access.Group + $Group + if ($null -ne $RouteGroup.AccessMeta.Group) { + $Group = $RouteGroup.AccessMeta.Group + $Group } - if ($null -ne $RouteGroup.Access.Scope) { - $Scope = $RouteGroup.Access.Scope + $Scope + if ($null -ne $RouteGroup.AccessMeta.Scope) { + $Scope = $RouteGroup.AccessMeta.Scope + $Scope } - if ($null -ne $RouteGroup.Access.User) { - $User = $RouteGroup.Access.User + $User + if ($null -ne $RouteGroup.AccessMeta.User) { + $User = $RouteGroup.AccessMeta.User + $User } - if ($null -ne $RouteGroup.Access.Custom) { - $CustomAccess = $RouteGroup.Access.Custom + if ($null -ne $RouteGroup.AccessMeta.Custom) { + $CustomAccess = $RouteGroup.AccessMeta.Custom } } @@ -1036,9 +1161,10 @@ function Add-PodeRouteGroup TransferEncoding = $TransferEncoding ErrorContentType = $ErrorContentType Authentication = $Authentication + Access = $Access AllowAnon = $AllowAnon IfExists = $IfExists - Access = @{ + AccessMeta = @{ Role = $Role Group = $Group Scope = $Scope @@ -1089,6 +1215,9 @@ The content type of any error pages that may get returned. .PARAMETER Authentication The name of an Authentication method which should be used as middleware on the Static Routes. +.PARAMETER Access +The name of an Access method which should be used as middleware on this Route. + .PARAMETER IfExists Specifies what action to take when a Static Route already exists. (Default: Default) @@ -1098,6 +1227,18 @@ If supplied, the Static Routes will allow anonymous access for non-authenticated .PARAMETER DownloadOnly When supplied, all static content on the Routes will be attached as downloads - rather than rendered. +.PARAMETER Role +One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Group +One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Scope +One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER User +One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. + .EXAMPLE Add-PodeStaticRouteGroup -Path '/static' -Routes { Add-PodeStaticRoute -Path '/images' -Etc } #> @@ -1147,11 +1288,31 @@ function Add-PodeStaticRouteGroup [string] $Authentication, + [Parameter()] + [string] + $Access, + [Parameter()] [ValidateSet('Default', 'Error', 'Overwrite', 'Skip')] [string] $IfExists = 'Default', + [Parameter()] + [string[]] + $Role, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $Scope, + + [Parameter()] + [string[]] + $User, + [switch] $AllowAnon, @@ -1204,6 +1365,10 @@ function Add-PodeStaticRouteGroup $Authentication = $RouteGroup.Authentication } + if ([string]::IsNullOrWhiteSpace($Access)) { + $Access = $RouteGroup.Access + } + if (Test-PodeIsEmpty $Defaults) { $Defaults = $RouteGroup.Defaults } @@ -1219,6 +1384,26 @@ function Add-PodeStaticRouteGroup if ($RouteGroup.IfExists -ine 'default') { $IfExists = $RouteGroup.IfExists } + + if ($null -ne $RouteGroup.AccessMeta.Role) { + $Role = $RouteGroup.AccessMeta.Role + $Role + } + + if ($null -ne $RouteGroup.AccessMeta.Group) { + $Group = $RouteGroup.AccessMeta.Group + $Group + } + + if ($null -ne $RouteGroup.AccessMeta.Scope) { + $Scope = $RouteGroup.AccessMeta.Scope + $Scope + } + + if ($null -ne $RouteGroup.AccessMeta.User) { + $User = $RouteGroup.AccessMeta.User + $User + } + + if ($null -ne $RouteGroup.AccessMeta.Custom) { + $CustomAccess = $RouteGroup.AccessMeta.Custom + } } $RouteGroup = @{ @@ -1231,9 +1416,17 @@ function Add-PodeStaticRouteGroup Defaults = $Defaults ErrorContentType = $ErrorContentType Authentication = $Authentication + Access = $Access AllowAnon = $AllowAnon DownloadOnly = $DownloadOnly IfExists = $IfExists + AccessMeta = @{ + Role = $Role + Group = $Group + Scope = $Scope + User = $User + Custom = $CustomAccess + } } # add routes @@ -1586,6 +1779,9 @@ Like normal Routes, an array of Middleware that will be applied to all generated .PARAMETER Authentication The name of an Authentication method which should be used as middleware on this Route. +.PARAMETER Access +The name of an Access method which should be used as middleware on this Route. + .PARAMETER AllowAnon If supplied, the Route will allow anonymous access for non-authenticated users. @@ -1595,6 +1791,18 @@ If supplied, the Command's Verb will not be included in the Route's path. .PARAMETER NoOpenApi If supplied, no OpenAPI definitions will be generated for the routes created. +.PARAMETER Role +One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Group +One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Scope +One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER User +One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. + .EXAMPLE ConvertTo-PodeRoute -Commands @('Get-ChildItem', 'Get-Host', 'Invoke-Expression') -Middleware { ... } @@ -1637,6 +1845,26 @@ function ConvertTo-PodeRoute [string] $Authentication, + [Parameter()] + [string] + $Access, + + [Parameter()] + [string[]] + $Role, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $Scope, + + [Parameter()] + [string[]] + $User, + [switch] $AllowAnon, @@ -1708,7 +1936,22 @@ function ConvertTo-PodeRoute $_path = ("$($Path)/$($Module)/$($name)" -replace '[/]+', '/') # create the route - $route = (Add-PodeRoute -Method $_method -Path $_path -Middleware $Middleware -Authentication $Authentication -AllowAnon:$AllowAnon -ArgumentList $cmd -ScriptBlock { + $params = @{ + Method = $_method + Path = $_path + Middleware = $Middleware + Authentication = $Authentication + Access = $Access + Role = $Role + Group = $Group + Scope = $Scope + User = $User + AllowAnon = $AllowAnon + ArgumentList = $cmd + PassThru = $true + } + + $route = Add-PodeRoute @params -ScriptBlock { param($cmd) # either get params from the QueryString or Payload @@ -1726,7 +1969,7 @@ function ConvertTo-PodeRoute if (!(Test-PodeIsEmpty $result)) { Write-PodeJsonResponse -Value $result -Depth 1 } - } -PassThru) + } # set the openapi metadata of the function, unless told to skip if ($NoOpenApi) { @@ -1790,12 +2033,27 @@ Like normal Routes, an array of Middleware that will be applied to all generated .PARAMETER Authentication The name of an Authentication method which should be used as middleware on this Route. +.PARAMETER Access +The name of an Access method which should be used as middleware on this Route. + .PARAMETER AllowAnon If supplied, the Page will allow anonymous access for non-authenticated users. .PARAMETER FlashMessages If supplied, Views will have any flash messages supplied to them for rendering. +.PARAMETER Role +One or more optional Roles that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Group +One or more optional Groups that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER Scope +One or more optional Scopes that will be authorised to access this Route, when using Authentication with an Access method. + +.PARAMETER User +One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. + .EXAMPLE Add-PodePage -Name Services -ScriptBlock { Get-Service } @@ -1843,6 +2101,26 @@ function Add-PodePage [string] $Authentication, + [Parameter()] + [string] + $Access, + + [Parameter()] + [string[]] + $Role, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $Scope, + + [Parameter()] + [string[]] + $User, + [switch] $AllowAnon, @@ -1912,14 +2190,22 @@ function Add-PodePage $_path = ("$($Path)/$($Name)" -replace '[/]+', '/') # create the route - Add-PodeRoute ` - -Method Get ` - -Path $_path ` - -Middleware $Middleware ` - -Authentication $Authentication ` - -AllowAnon:$AllowAnon ` - -ArgumentList $arg ` - -ScriptBlock $logic + $params = @{ + Method = 'Get' + Path = $_path + Middleware = $Middleware + Authentication = $Authentication + Access = $Access + Role = $Role + Group = $Group + Scope = $Scope + User = $User + AllowAnon = $AllowAnon + ArgumentList = $arg + ScriptBlock = $logic + } + + Add-PodeRoute @params } <# diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index f30715609..68d207ecb 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -130,7 +130,9 @@ Describe 'Restart-PodeInternalServer' { Sessions = @{ 'key' = 'value' } Authentications = @{ Methods = @{ 'key' = 'value' } - Access = @{ 'key' = 'value' } + } + Authorisations = @{ + Methods = @{ 'key' = 'value' } } State = @{ 'key' = 'value' } Output = @{