From 55fb854f4328cf8ef4e9ef0b541d541a3c9d026f Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi <0x777@users.noreply.github.com> Date: Wed, 24 Jan 2024 20:51:13 -0800 Subject: [PATCH 1/4] rfc for improvements to permissions in v3 --- rfcs/permission-improvements-v3.md | 303 +++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 rfcs/permission-improvements-v3.md diff --git a/rfcs/permission-improvements-v3.md b/rfcs/permission-improvements-v3.md new file mode 100644 index 0000000000000..182009c466882 --- /dev/null +++ b/rfcs/permission-improvements-v3.md @@ -0,0 +1,303 @@ +# Permissions roadmap in V3 + +We would like to improve on these known permission issues: + +## 1. Reusing parent permissions + +Let me explain this with an example: + +```graphql +user, user_channel, channel, message +user_channel: { user_id: { _eq: x-hasura-user-id } } +channel: { users: { user_id: { _eq: x-hasura-user-id } } } +message: { channel: { users: { user_id: { _eq: x-hasura-user-id } } } } +``` + +The pattern here is as follows: + +> The permission on a table is derived from the permission of its parent. + +To simplify this, v3 introduces this change (something equivalent to this example): + +```graphql +user_channel: { user_id: { _eq: x-hasura-user-id } } +channel: { users: { append_permission_predicate: true } } +message: { channel: { append_permission_predicate: true } } +``` + +## 2. Allowing multiple roles per request + +With role inheritance in v2, we introduced a way to compose permissions. You +could treat roles as entitlements and group together entitlements. + +However, a major downside of this system is that this composition has to be +done in the metadata and not in your auth system. i.e, you cannot specify +'x-hasura-role' to a set of roles without combining them in v3. + +We are planning to improve the system to enable support for multiple roles to +be specified in a request. + +For example, if a request's authz resolves to the following roles: + +```yaml +x-hasura-role: reader, editor +``` + +graphql-engine would combine the permissions of both reader and editor when +processing the request. + +### 2.1 Challenges: + +Composing permissions this way seems quite natural. Let's see how this gets +complicated. + +#### 2.1.1 No overlapping permissions +If there are no overlapping permissions for a table, the merge semantics +are straightforward. + +#### 2.1.2 Overlapping permissions + +i.e. permission on a table is defined for two different roles: + +1. For selects: + + 1. If the column sets are the same, this is easy. The 'filter' becomes + `filter(r1)` OR `filter(r2)`. + + 1. If the column sets are not the same, for example: + + ```yaml + - role: public + table: users + columns: id, username + filter: {} + + - role: private + table: users + columns: id, username, email + filter: + id: x-hasura-user-id + ``` + + What should the semantics of this query be? + + ```graphql + query { + users { + id + username + email + } + } + ``` + + If we were to union the columns and the rows, the users get to see + `email` column for all users and not just theirs. + + > The expected behaviour however is that `email` is returned as `null` + for all other users. + + This is fairly challenging to implement, v2 uses SQL `case` expressions to + suport this and this heavily relies on the database features. It may not be + possible to have these correct semantics for all databases. + +1. For mutations, if the presets are different, we do not know how which one to + use. + +### 2.2 Merge/Conflict strategies + +In v2, permission composition happens at the metadata layer, any invalid +combination of roles would result in a failed metadata update. + +Once we start allowing multiple roles per request, we need to figure out the +strategy for handling these overlapping permissions: + +1. Fail the request: + + If the operation being invoked has more than one permission, we fail the + request. This is quite straightforward but may not be the desired behaviour. + +2. Provide merge semantics: + + In the metadata, instead of composing a fixed set of permissions using + inherited roles, we could for example, allow specifying the permission + capabilities. For example, + + ```yaml + conflict_rules: + # admin insert permissions should be preferred over user role's insert + # permissions on all tables + - admin.insert > user.insert + + # manager's update permissions should be preferred over user role's select + # update permissions on employee_details table + - manager.update.employee_details > user.update.employee_details + ``` +3. Pick the first role in the request: + + If the request's `x-hasura-role` is `manager, employee`, we would pick + `manager` to execute an operation when there are conflicts. + +## 3. Policy Engine + +Allowing multiple roles in a request is *a solution* to improve the +expressivity of Hasura's permission system. This would put Hasura's permission +in the category of an RBAC sytem with elements from ReBAC and ABAC systems. + +A general solution could exist in the form of a policy engine where we don't +take a stance on a specific permission model. If we look at Hasura's query +processing engine, it is roughly as follows: + +1. Read 'x-hasura-roles' from the request body. +1. Traverse the GraphQL query and derive permissions from 'roles' obtained + above. This will involve merging and resolving conflicts using one of the + strategies described in 2.2. +1. Execute the operation with the permissions obtained from 2. + +With a policy engine, the pipeline would change as follows: +1. Read user's session, tables and columns referenced in the query. +1. Evaluate the user defined policies with user's session, tables + and columns referenced in the query to get a set of permissions that are + applicable for the query. +1. Execute the operation with the permissions obtained from 2. + +> The key change with the policy based system is that users can define +their own merge/conflict semantics in a policy language without Hasura +having to provide a ton of switches to customize the behaviour. + +We are not yet certain what this policy language could look like. Existing +policy languages such as Rego, Cedar, Biscuit, Polar derive their roots from +Datalog and/or Prolog. + +For example, the following rego policy implements an RBAC system where the +conflict resolution policy is to pick the permission of a role that is +specified first: + +```ruby +package rbac.authz + +import future.keywords.contains +import future.keywords.if +import future.keywords.in + +# Permissions defined per role +defined_permissions := { + "public": {"product": { + "columns": ["id", "name"], + "filter": {}, + }}, + "analyst": { + "product": { + "columns": ["*"], + "filter": {}, + }, + "product_analytics": { + "columns": ["*"], + "filter": {}, + }, + }, +} + +# Finds all the permissions defined for a table_name +find_permissions(query_table_name) := [table_permission | + # For each role in x-hasura-roles + some role in input.user.x_hasura_roles + + # For each table_name, table_permission of the role + some table_name, table_permission in defined_permissions[role] + + # Check if the permission is defined on the table + query_table_name = table_name +] + +# The permissions that are decided on +resolved_permissions[query_table_name] := resolved_permission if { + # For each table referenced in the query + some query_table_name in input.query.tables + + # assign permission to be returned + resolved_permission := find_permissions(query_table_name)[0] +} +``` + +You can try this policy out [here](https://play.openpolicyagent.org/p/M5kpAZsCMbl). If you +add the input as: + +```json +{ + "query": { + "table": "product", + "tables": [ + "product", + "product_analytics" + ] + }, + "user": { + "x_hasura_roles": [ + "public", + "analyst" + ] + } +} +``` + +the resolved permissions would be + +```json +{ + "product": { + "columns": [ + "id", + "name" + ], + "filter": {} + }, + "product_analytics": { + "columns": [ + "*" + ], + "filter": {} + } +} +``` + +If you change the ordering of the roles: + +```json +{ + "query": { + "table": "product", + "tables": [ + "product", + "product_analytics" + ] + }, + "user": { + "x_hasura_roles": [ + "analyst", + "public" + ] + } +} +``` + +the resolved permissions would be + +```json +{ + "product": { + "columns": [ + "*" + ], + "filter": {} + }, + "product_analytics": { + "columns": [ + "*" + ], + "filter": {} + } +} +``` + +i.e, `analyst`'s permissions are picked over `public`'s permissions From 033fc1f98e0ee8e6491e9bc6aa50c7696c1adadd Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi <0x777@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:04:16 -0800 Subject: [PATCH 2/4] abac style rego example --- rfcs/permission-improvements-v3.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/rfcs/permission-improvements-v3.md b/rfcs/permission-improvements-v3.md index 182009c466882..1e6ec53c35394 100644 --- a/rfcs/permission-improvements-v3.md +++ b/rfcs/permission-improvements-v3.md @@ -301,3 +301,32 @@ the resolved permissions would be ``` i.e, `analyst`'s permissions are picked over `public`'s permissions + +While the above demonstrates a simple example, a language such as Rego can be quite expressive. For example, +you may not even have a notion of roles and may want to define permissions in ABAC style. One such example: + +```rego +package authz.admin + +import future.keywords.if +import future.keywords.in + +# The permissions that are decided on +resolved_permissions[query_table_name] := resolved_permission if { + # For each table referenced in the query + some query_table_name in input.query.tables + + # if the user is admin + input.user.is_admin = true + + # assign permission to be returned + resolved_permission := { + "columns": ["*"], + "filter": {}, + } +} +``` + +You can experiment with the policy +[here](https://play.openpolicyagent.org/p/DrTGxLJuFM). Try changing the +`is_admin` attribute to `false` and check the returned permissions. From 934ee48197d8dd839e42d9e9379574ccc7c9dd2e Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi <0x777@users.noreply.github.com> Date: Wed, 24 Jan 2024 21:06:08 -0800 Subject: [PATCH 3/4] update links --- rfcs/permission-improvements-v3.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfcs/permission-improvements-v3.md b/rfcs/permission-improvements-v3.md index 1e6ec53c35394..91569e61b084a 100644 --- a/rfcs/permission-improvements-v3.md +++ b/rfcs/permission-improvements-v3.md @@ -220,7 +220,7 @@ resolved_permissions[query_table_name] := resolved_permission if { } ``` -You can try this policy out [here](https://play.openpolicyagent.org/p/M5kpAZsCMbl). If you +You can try this policy out [here](https://play.openpolicyagent.org/p/BuMPLAbuL0). If you add the input as: ```json From 10ded78802192ffc5801e0fdc5d48c9055403834 Mon Sep 17 00:00:00 2001 From: Vamshi Surabhi <0x777@users.noreply.github.com> Date: Thu, 25 Jan 2024 10:50:07 -0800 Subject: [PATCH 4/4] Update permission-improvements-v3.md --- rfcs/permission-improvements-v3.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/rfcs/permission-improvements-v3.md b/rfcs/permission-improvements-v3.md index 91569e61b084a..b90cae7674cf7 100644 --- a/rfcs/permission-improvements-v3.md +++ b/rfcs/permission-improvements-v3.md @@ -226,7 +226,6 @@ add the input as: ```json { "query": { - "table": "product", "tables": [ "product", "product_analytics" @@ -266,7 +265,6 @@ If you change the ordering of the roles: ```json { "query": { - "table": "product", "tables": [ "product", "product_analytics"