The-Devoyage / subgraph

A POC written in rust to generate a functional API based on a simple config/schema.
GNU General Public License v3.0
6 stars 0 forks source link

Authorization Guards + Custom Errors #44

Closed nickisyourfan closed 1 year ago

nickisyourfan commented 1 year ago

Should be able to guard the service/authorize access down to the field level of an entity. Guards will be boolean evaluators that can be applied to sections of the config file in order to "guard" accordingly.

Guards

Application

Applying a guard will be simple, using the guard property. The guard property will be of type string, which can handle evaluations.

For example, blocking acceess to the entire service can be done by providing a true value to the guard option.

guard = "true" # No Access To Entire Service

Using an eval lib, we will be able to provide conditions to the guard.

guard = "0 == 1"  # This false eval will result in the service not being guarded/the request is allowed through.

Guard Locations

The application will be able to have high level guards as well as low level guards effectively allowing you to block access from the entire service down to access to a specific field of a specific entity.

Global Accessors

Boolean evaluators will work with globally available variables that can allow you to access dynamic data points. These dynamic data points come from the context within the GraphQL Request.

For example, block access to the user entity if the requesting user is not an admin by checking the request headers for auth.role. If auth.role === Admin then allow the request to go forward, else guard the entity.

[[service.entities]]
name = "user"
guard = "Hearders(\"auth.role\") != \'Admin\'"

Or, guard the input value that the user has submitted. The following example will stop the query from being executed if the user has provided an value of less than 18 for the age input.

[[service.entities.fields]]
name = "age"
scalar = "Int"
guard = "Input(\"age\") < 18"

You will also be able to compare Globals. Assuming the entity has a id and password fields:

[[service.entities.fields]]
name = "password"
scalar =  "String"
guard = "Entity(\"id\")!= Headers(\"user.id\")"

Errors

By default, guards that are evaluated as truthy will result in a default "403 Forbidden" like error. That being said, custom error messages will be easy to apply in the result of a successful guard. Passing a string as the last argument results in a custom message.

[service]
guard = "true, \"You May Not Pass\""

In this case, the error message will be extracted from the guard and returned in response of the request.

Complex Error Handling

In such even that a guard becomes too complex for the configuration file, the guard may be moved to a file allowing for multi guard checks.

[service]
name = "dogs_api"
guard = "./service.guard"

This guard will read the content from the assigned file path. You may assign a general message as the first line in the file. Individual error messages may be assigned per error key allowing you to provide detailed error messages with ease.

# ./service.guard
"Forbidden. You do not have permission to access this service."
invalid_role                =   "Headers(\"auth.role\") == \"Admin\", \"Admin role is required.\""
invalid_access_key    =   "Headers(\"x_access_key\") == $X_ACCESS_KEY, \"Access Key is invalid.\""
missing_access_key  =   "!Headers(\"x_access_key\"), \"Missing access key\""

Error Execution and Response

  1. Service guards will be evaluated and returned before resolver execution.
  2. Resolver guards are second to be evaluated and returned.
  3. Entity guards are executed based on the resolver type. In the case of find operations, the Entity guard happens after the database execution. In the case of update guards, additional find operations will be implemented before the update query in order to validate the request. In this case, the guard is checked before performing the update. In the case of create or delete mutations, entity guards are not performed.

Once evaluated, if guarded, an error will be thrown.

The Error Response

The error response is a key value map that allows each error to be displayed. All errors will be mapped to their appropriate key value pair.

{
  message: "Forbidden. You do not have permission to access this service.",
  "extensions": {
      "invalid_role": "Admin role is required.",
      "invalid_access_key": "Access key is required.",
      "missing_access_key": "Missing access key."
    }
}
nickisyourfan commented 1 year ago

New syntax for implementing:

[[service.guards]]
name = "invalid_id"
if_expr = "input(\"cities\") == (\"Dallas\")"
then_msg = "Invalid ID - Permission Denied"
nickisyourfan commented 1 year ago

First version implemented. Closing for rescope of second version.