docs: add input validation docs (#9734)

* docs: add input validation docs

* Optimised images with calibre/image-actions

* remove input validation feature from experimental flag

* Docs edits

* Docs edits

* Docs edits

* implement review comments

* Optimised images with calibre/image-actions

* Apply suggestions from code review

Co-authored-by: Sean Park-Ross <94021366+seanparkross@users.noreply.github.com>

* fix brackets in request payload

* deduplicate data

* Update input-validations.mdx

* Update docs/docs/schema/postgres/data-validations.mdx

Co-authored-by: Tirumarai Selvan <tiru@hasura.io>

* Update input-validations.mdx

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sean Park-Ross <sean@hasura.io>
Co-authored-by: Sean Park-Ross <94021366+seanparkross@users.noreply.github.com>
Co-authored-by: Tirumarai Selvan <tiru@hasura.io>
GitOrigin-RevId: 5a36f61260c1a11f49f291ea6987192971d33239
This commit is contained in:
Naveen Naidu 2023-07-18 15:01:24 +05:30 committed by hasura-bot
parent ad64379876
commit 151a888289
5 changed files with 522 additions and 24 deletions

View File

@ -60,7 +60,16 @@ X-Hasura-Role: admin
"set":{
"id":"X-HASURA-USER-ID"
},
"columns":["name","author_id"]
"columns":["name","author_id"],
"validate_input": {
"type": "http",
"definition": {
"forward_client_headers": true,
"headers": [],
"timeout": 10,
"url": "http://www.somedomain.com/validateUser"
}
}
}
}
}
@ -310,6 +319,15 @@ X-Hasura-Role: admin
},
"set":{
"updated_at" : "NOW()"
},
"validate_input": {
"type": "http",
"definition": {
"forward_client_headers": true,
"headers": [],
"timeout": 10,
"url": "http://www.somedomain.com/validateUser"
}
}
}
}
@ -399,6 +417,15 @@ X-Hasura-Role: admin
"permission" : {
"filter" : {
"author_id" : "X-HASURA-USER-ID"
},
"validate_input": {
"type": "http",
"definition": {
"forward_client_headers": true,
"headers": [],
"timeout": 10,
"url": "http://www.somedomain.com/validateUser"
}
}
}
}

View File

@ -337,14 +337,31 @@ Configuration properties for particular column, as specified on [ColumnConfig](#
| function | false | `String` | Customize the `<function-name>` root field |
| function_aggregate | false | `String` | Customize the `<function-name>_aggregate` root field |
## InputValidationDefinition {#input-validation-definition}
| Key | Required | Schema | Description |
| ----------- | -------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| url | true | [WebhookURL](#webhookurl) | The input validations's webhook URL |
| headers | false | \[[HeaderFromValue](#headerfromvalue) \| [HeaderFromEnv](#headerfromenv) \] | List of defined headers to be sent to the handler |
| forward_client_headers | false | boolean | If set to `true` the client headers are forwarded to the webhook handler (default: `false`) |
| timeout | false | Integer | Number of seconds to wait for response before timing out. Default: 10 |
## InputValidation {#input-validation}
| Key | Required | Schema | Description |
| ---------------------- | -------- | -------- | -------------------------------------------------------------------- |
| type | true | `String` | The interface for input validation. (Currently only supports "http") |
| definition | true | [InputValidationDefinition](#input-validation-definition) | The definition for the input validation |
## InsertPermission {#insertpermission}
| Key | Required | Schema | Description |
| ------------ | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| check | true | [BoolExp](#boolexp) | This expression has to hold true for every new row that is inserted |
| set | false | [ColumnPresetsExp](#columnpresetexp) | Preset values for columns that can be sourced from session variables or static values |
| columns | false | [PGColumn](#pgcolumn) array (or) `'*'` | Can insert into only these columns (or all when `'*'` is specified) |
| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured |
| Key | Required | Schema | Description |
| ------------ | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| check | true | [BoolExp](#boolexp) | This expression has to hold true for every new row that is inserted |
| set | false | [ColumnPresetsExp](#columnpresetexp) | Preset values for columns that can be sourced from session variables or static values |
| columns | false | [PGColumn](#pgcolumn) array (or) `'*'` | Can insert into only these columns (or all when `'*'` is specified) |
| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured |
| validate_input | false | [InputValidation](#input-validation) | The input validation definition for the insert mutation. |
## SelectPermission {#selectpermission}
@ -366,20 +383,23 @@ The `query_root_fields` and the `subscription_root_fields` are only available in
## UpdatePermission {#updatepermission}
| Key | Required | Schema | Description |
| ------------ | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| columns | true | [PGColumn](#pgcolumn) array (or) `'*'` | Only these columns are selectable (or all when `'*'` is specified) |
| filter | true | [BoolExp](#boolexp) | Only the rows where this precondition holds true are updatable |
| check | false | [BoolExp](#boolexp) | Postcondition which must be satisfied by rows which have been updated |
| set | false | [ColumnPresetsExp](#columnpresetexp) | Preset values for columns that can be sourced from session variables or static values. |
| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured |
| Key | Required | Schema | Description |
| ------------ | -------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| columns | true | [PGColumn](#pgcolumn) array (or) `'*'` | Only these columns are selectable (or all when `'*'` is specified) |
| filter | true | [BoolExp](#boolexp) | Only the rows where this precondition holds true are updatable |
| check | false | [BoolExp](#boolexp) | Postcondition which must be satisfied by rows which have been updated |
| set | false | [ColumnPresetsExp](#columnpresetexp) | Preset values for columns that can be sourced from session variables or static values. |
| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured |
| validate_input | false | [InputValidation](#input-validation) | The input validation definition for the insert mutation. |
## DeletePermission {#deletepermission}
| Key | Required | Schema | Description |
| ------------ | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| filter | true | [BoolExp](#boolexp) | Only the rows where this expression holds true are deletable |
| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured |
| Key | Required | Schema | Description |
| ------------ | -------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| filter | true | [BoolExp](#boolexp) | Only the rows where this expression holds true are deletable |
| backend_only | false | Boolean | When set to `true` the mutation is accessible only if the `x-hasura-use-backend-only-permissions` session variable exists and is set to `true` and the request is made with `x-hasura-admin-secret` set if any auth is configured |
| validate_input | false | [InputValidation](#input-validation) | The input validation definition for the insert mutation. |
## LogicalModelSelectPermission {#logicalmodelselectpermission}

View File

@ -245,11 +245,130 @@ Learn more about [Postgres triggers](https://www.postgresql.org/docs/current/sql
## Using Hasura permissions
If the validation logic can be expressed **declaratively** using static values and data from the database, then you can
use [row level permissions](/auth/authorization/permissions/row-level-permissions.mdx) to perform the validations.
(Read more about [Authorization](/auth/authorization/index.mdx)).
Hasura permissions provides two different ways to validate data:
**Example 1:** Validate that an `article` can be inserted only if `title` is not empty.
1. If input arguments of a mutations needs to be validated, you can use the
[input validation](/docs/schema/postgres/input-validations.mdx) feature. This
allows you to write custom validation logic that is run in an external webhook before hitting the
database to execute the mutation.
2. If the validation logic can be expressed **declaratively** using static
values and data from the database, then you can use [row level
permissions](/auth/authorization/permissions/row-level-permissions.mdx) to
perform the validations. (Read more
about[Authorization](/auth/authorization/index.mdx)).
**Example 1:** Validate that a valid email is being inserted
Suppose, we have the following table in our schema:
```sql
customer (id uuid, name text, city text, email text)
```
Now, we can create a role `user` and add an input validation rule as follows:
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
<Thumbnail
src="/img/schema/input-validation-create-permission.png"
alt="Using boolean expressions to build rules"
width="500px"
/>
</TabItem>
<TabItem value="cli" label="CLI">
You can define the input validation in the `metadata -> databases -> [database-name] -> tables -> [table-name].yaml`
file, eg:
```yaml {9-14}
- table:
schema: public
name: customer
insert_permissions:
- role: user
permission:
columns: []
filter: {}
validate_input:
type: http
definition:
url: http://www.somedomain.com/validateCustomerMutation
forward_client_headers: true
timeout: 5
```
Apply the Metadata by running:
```bash
hasura metadata apply
```
</TabItem>
<TabItem value="api" label="API">
You can define the input validations when using the [permissions Metadata API](/api-reference/metadata-api/permission.mdx).
Example with a Postgres DB:
```http {14-21}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "pg_create_insert_permission",
"args": {
"source": "<db_name>",
"table": "customer",
"role": "user",
"permission": {
"columns": "*",
"filter": {},
"validate_input": {
"type": "http",
"definition": {
"forward_client_headers": true,
"headers": [],
"timeout": 5,
"url": "http://www.somedomain.com/validateCustomerMutation"
}
}
}
}
}
```
</TabItem>
</Tabs>
If we try to insert a customer with an invalid email, we will get a `validation-failed` error:
<GraphiQLIDE
query={`mutation {
insert_customer(
objects: {
name: "customer 1"
email: "random-email",
}
) {
affected_rows
}
}`}
response={`{
"errors": [
{
"message": "customer email id is not valid",
"extensions": {
"path": "$",
"code": "validation-dfailed"
}
}
]
}`}
/>
**Example 2:** Validate that an `article` can be inserted only if `title` is not empty.
Suppose, we have the following table in our schema:
@ -325,7 +444,7 @@ If we try to insert an article with `title = ""`, we will get a `permission-erro
query={`mutation {
insert_article(
objects: {
title: "
title: ""
content: "Lorem ipsum dolor sit amet",
}
) {
@ -345,7 +464,7 @@ If we try to insert an article with `title = ""`, we will get a `permission-erro
}`}
/>
**Example 2:** Validate that an `article` can be inserted only if its `author` is active.
**Example 3:** Validate that an `article` can be inserted only if its `author` is active.
Suppose, we have 2 tables:

View File

@ -0,0 +1,332 @@
---
sidebar_label: Input validations
sidebar_position: 11
description: Input validations in Hasura over Postgres
sidebar_class_name: beta-tag
keywords:
- hasura
- docs
- postgres
- schema
- mutations
- input validation
---
import ProductBadge from '@site/src/components/ProductBadge';
import GraphiQLIDE from '@site/src/components/GraphiQLIDE';
import Thumbnail from '@site/src/components/Thumbnail';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Postgres: Input Validations
<ProductBadge ce free standard pro ee self />
## Introduction
:::tip Supported from
Input validations are supported for versions `v2.29.0` and above.
:::
Many times, we need to perform validations on the input arguments of a GraphQL mutation before inserting, deleting
or updating the data.
Hasura provides a way to add input validations to your GraphQL mutations which route the input arguments of a
GraphQL mutation to an HTTP webhook to perform complex validation logic.
### Example
Consider you have the following GraphQL mutation:
```graphql
mutation insert_users {
insert_users(objects: [{name: "John", phone: "999"}]) {
affected_rows
}
}
mutation update_users {
update_users(where: {id: {_eq: 1}}, _set: {name: "John", email : "random email"}) {
affected_rows
}
}
```
You can define the input validations for the above mutations to perform the following checks:
1. Check that the `name` field is not empty
2. Check that the `phone` field is a valid phone number
3. Check that the `email` field is a valid email address
4. Do not allow more than 5 insertions in a single insert mutation
## Setting up validation permissions
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
You can define the input validations for `insert/update/delete` permissions on the Hasura Console in **Data -> [table]
-> Permissions -> (insert/update/delete)**.
<Thumbnail
src="/img/schema/input-validation-create-permission.png"
alt="Using boolean expressions to build rules"
width="500px"
/>
</TabItem>
<TabItem value="cli" label="CLI">
You can define the input validations in the `metadata -> databases -> [database-name] -> tables -> [table-name].yaml`
file, eg:
```yaml {9-17}
- table:
schema: public
name: products
insert_permissions:
- role: user
permission:
columns: []
filter: {}
validate_input:
type: http
definition:
url: http://www.somedomain.com/validateProducts
headers:
- name: X-Validate-Input-API-Key
value_from_env: VALIDATION_HOOK_API_KEY
forward_client_headers: true
timeout: 5
```
Apply the Metadata by running:
```bash
hasura metadata apply
```
</TabItem>
<TabItem value="api" label="API">
You can define the input validations when using the [permissions Metadata API](/api-reference/metadata-api/permission.mdx).
Example with a Postgres DB:
```http {14-21}
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "pg_create_(insert|update|delete)_permission",
"args": {
"source": "<db_name>",
"table": "products",
"role": "user",
"permission": {
"columns": "*",
"filter": {},
"validate_input": {
"type": "http",
"definition": {
"forward_client_headers": true,
"headers": [],
"timeout": 10,
"url": "http://www.somedomain.com/validateUser"
}
}
}
}
}
```
The `type` determines the interface for the input validation, which initially only supports an `http` webhook URL.
The `definition` field provides the necessary context for communicating and submitting the data for input validation.
It is an object with the following fields.
- `url` - *Required*, a [string value](https://hasura.io/docs/latest/api-reference/syntax-defs/#webhookurl) which
supports templating environment variables.
- `headers` - *Optional*, a list of headers to be sent to the URL.
- `forward_client_headers` - *Optional*, default is `false`. If set to `true` the client headers are forwarded to
the URL.
- `timeout` - *Optional*, default is `10`. The number of seconds to wait for a response before timing out.
</TabItem>
</Tabs>
:::caution Reduced performance
Mutations that involve input validation **may exhibit slower performance** compared to mutations without
validation. The execution time of the webhook URL can become a bottleneck, potentially reaching the maximum limit
specified by the `timeout` configuration value, the default of which is 10 seconds.
:::
## How it works
Following are the steps that are performed by mutations types (`insert/update/delete`)
When an mutation arrives with a role, the following steps are performed:
1. First, "collect" all the tables that the mutation targets (because there could be more than one table involved via
nested inserts)
2. If there is a `validate_input` permission defined on a table, then any mutation arguments targeting that table are
sent to the validation URL. This is done for all tables.
3. If all handlers validate the mutation arguments, then the request proceeds. **A transaction with
the database will only be started after the validation is completed and successful.**
4. If any webhook rejects the mutation data, then the request is aborted. An `error` message from the URL can also
be forwarded to the client.
Consider the following sample mutation:
```graphql
mutation insertAuthorWithArticles($name: String, $email:String, $articles_content:[article_insert_input!]!) {
insert_author(objects: {name: $name, email: $email, articles: {data: $articles_content}}){
returning {
first_name
articles {
id
}
}
}
}
```
The above mutation targets the `author` and `article` tables, involving a nested insert of an article into the
author model. Assuming that the `validate_input` permission is defined for both tables, the validation process
unfolds as follows:
1. The validation webhook specified for the `author` table is contacted first, including the inserted row with
`articles`
2. Subsequently, the validation webhook designated for the `article` table is contacted with `$articles_content` rows.
3. If both of the above webhook calls result in successful validation, a database transaction is started to insert the
rows into the respective tables.
## Webhook specification per mutation type
### Request
When a mutation on a table with `validate_input` configuration is executed, before making a database
transaction, Hasura sends the mutation argument data to the validation HTTP webhook using a `POST` request.
The request payload is of the format:
```json
{
"version": "<version-integer>",
"role": "<role-name>",
"session_variables": {
"x-hasura-user-id": "<session-user-id>",
"x-hasura-user-name": "<session-user-name>"
},
"data": {
"input": <mutation specific schema>
}
}
```
- `version`: An integer version serves to indicate the request format. Whenever a breaking update occurs in the
request payload, the version will be incremented. The initial version is set to `1`.
- `role`: Hasura session role on which permissions are enforced.
- `session_variables`: Session variables that aid in enforcing permissions. Session variable names always starts with
`x-hasura-*`.
- `data.input`: The schema of `data.input` varies per mutation type. This schema is defined below.
#### Insert Mutations
```json
{
"version": "<version-integer>",
"role": "<role-name>",
"session_variables": {<session-variables>},
"data": {
"input": [{JSON-fied <model_name>_insert_input!}]
}
}
```
- `data.input`: List of rows to be inserted which are specified in the `objects` input field of insert mutation.
Also includes nested data of relationships. The structure of this field will be similar to the JSONified structure
of the `<model_name>_insert_input!` graphql type.
Note that, in `data.input`, if the data to be inserted contains nested inserts, then:
1. The `data.input` for the root model will have the type `JSON-fied <model_name>_insert_input!`, i.e: the nested
inserts will be present as `JSON-fied <model_name>_(arr|obj)_rel_insert_input!`
2. The `data.input` for the nested inserts payload will have the type `JSON-fied <nested_model_name>_insert_input!`
#### Update Mutations
The user may want to validate the input values in the `where`, `_set`, `_inc` etc clause and `pk_columns`. So, the
upstream webhook is expected to receive those values in the payload.
```json
{
"version": "<version-integer>",
"role": "<role-name>",
"session_variables": {<session-variables>},
"data": {
"input": [
{
JSON-fied <model_name>_updates!,
"pk_columns": JSON-fied <model_name>_pk_columns_input! (only included for update_<mode_name>_by_pk)
}
]
}
}
```
- `data.input`: List of the multiple updates to run. The structure of this field will be similar to the JSONified
structure of the `<model_name>_updates!` graphql type. If it is an update mutation by primary key, then it will
also contain the `<model_name>_pk_columns_input!`
#### Delete Mutations
The user may want to validate the input values in the `where` clause and `pk_columns`. So the upstream webhook is
expected to receive those values in the payload.
```json
{
"version": "<version-integer>",
"role": "<role-name>",
"session_variables": {<session-variables>},
"data": {
"input": [
{
JSON-fied <model_name>_bool_exp!,
"pk_columns": JSON-fied <model_name>_pk_columns_input! (only included for delete_<mode_name>_by_pk)
}
]
}
}
```
- `data.input`: The delete condition. The structure of this field will be similar to the JSONified structure of the
`<model_name>_bool_exp!` graphql type. If it is a delete mutation by primary key, then it will also contain the
`<model_name>_pk_columns_input!`
### Response
The following is the expected response from the upstream webhook for all mutation types (`insert/update/delete`).
1. **Successful Response**
The HTTP validation URL should return a `200` status code to represent a successful validation.
```http
200 OK
```
2. **Failed Response**
The HTTP validation URL should return an optional JSON object with `400` status code to represent a failed validation.
The object should contain a `message` field whose value is a string and this message is forwarded to client.
If no JSON object is returned then no message is forwarded to client.
```http
400 BAD REQUEST
{
"message": "Phone number invalid"
}
```
**When an unexpected response format is received, Hasura raises internal exception**

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB