graphql-engine/rfcs/permissions-mysql.md
Philip Lykke Carlsen 8549035d9b RFC: (Table) Permissions in MySQL
This PR introduces an RFC for permissions in MySQL. (solves #2097)

[Rendered](https://github.com/hasura/graphql-engine-mono/blob/plc/rfc/mysql-permissions/rfcs/permissions-mysql.md)

https://github.com/hasura/graphql-engine-mono/pull/2183

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
GitOrigin-RevId: e63fb9ddfd3a8752e28536818193b04403635f0d
2021-09-09 06:41:51 +00:00

10 KiB

Table Permissions in MySQL

Metadata

---
authors: Philip Lykke Carlsen <philip@hasura.io>
discussion:
  https://github.com/hasura/graphql-engine-mono/pull/2183
state: published
---

Description

We want to support the role-based access control feature on MySQL in the same fasihion that it works on Postgres currently.

The role-based access control feature, often referred to simply as "Permissions" allows Hasura users to restrict what data is returned by queries and admitted by mutations. Several flavors of permissions exist:

Column Permissions censor the columns that cliens in a given role have access to (either in Queries or Mutations), by means of an explicit list of columns exposed.

Row Permissions censor table rows returned or affected, on the basis of a boolean-returning SQL expression, which is allowed to reference the columns of the table as well as session variables. These are sometimes also known as Filter Permissions in the server code. Both Queries and Mutations may be subjected to Row Permissions.

The query semantics of Column Permissions and Row Permissions is that, in the context of a role:

For all intents and purposes, the dataset that a query ranges over includes only those columns and rows that the Column Permissions and Row Permissions of that role allow.

For example:

  • A user is unable to delete rows that her role's delete-permissions do not include. It is not an error to try; from the point of view of the delete mutation the rows indicated by the query are just not there.
  • A query for an aggregation (say, the average age in a persons table) will only consider those rows that her role's select-permissions include.

The Aggregation Permission (a single boolean) decides if the query root fields that relate to aggregations should appear in the schema.

Limit Permissions (an integer) apply only to queries and limit the maximum number of rows any query may yield. Importantly, an active Limit Permission does not influence the row domain of an aggregation query, only the maximum number of rows produced by the query, see [1].

Last, Inherited Roles may compose permissions, see User docs. An Inherited Role is an authorization role defined in terms of other pre-existing roles. The permissions that an Inherited Role grants are not just the point-wise union of each of the parent roles' permission syntax, but rather the union of the query datasets that each parent role permits, in the sense described above.

This introduces a complication: In isolation, a role's Column Permissions and Row Permissions describe a "rectangular" dataset, with columns along one side and rows along the other. For two or more roles however, when we union the datasets they permit we do not necessarily end up with a rectangular dataset:

Inherited roles diagram

Our data universe however only permits "rectangular" data. In order to accomodate the complexity resulting from Inherited Roles we make columns that are particular to a single parent role nullable. For example, in the diagram above we would return null for (Row 5, Column B) and (Row 2, Column D).

What does this concretely look like

When this is implemented, it should be possible to set permissions on MySQL tables in exactly the same fashion as is possible on Postgres tables, and queries and mutations should respect those permissions.

At the time of this writing, this means every tracked MySQL tables has a Permissions tab in the Console, which allows a user to set permissions on:

  • Rows and Columns
    • For each of the CRUD actions
    • Using all the predicates supported as boolean operators in _where: {..} arguments in queries to MySQL tables.
  • Limit and Aggregation

The tests in server/tests-py/test_graphql_queries.py#L575 pertaining to permissions should be generalised to multiple backends and made to pass for MySQL.

How are we going to implement it

The GraphQL-Engine applies permissions at three points of processing:

  1. When building the schema, where Column Permissions may cause fields to be censored from the schema.
  2. When parsing an incoming GraphQL query into HGE IR, where Column Permissions again influence the grammar parsed, and Row Permissions influence the IR generated such that relevant permissions are included.
  3. When SQL is generated from the IR, where the translation needs to take the IR node fields containing permissions into account.

Since parser/schema generation is a single unified abstraction in GraphQL-Engine, all a backend needs to do to support permissions is a suitable implementation of type class methods MonadSchema.buildTableQueryFields, buildTableInsertMutationFields etc..

buildTableQueryFields et al. are given as inputs a representation of the permissions for a table (in the context of some role), which for Column Permissions list the exposed columns and for Row Permissions contain backend-specific Boolean Expression IR fragments, which are supposed to end up in parser outputs.

There are already backend-generic implementations of these methods in Hasura.GraphQL.Schema.Build which we may use unless a product requirement surfaces that require us to deviate from the (de facto) standard table schema.

The inputting and storing of permissions in metadata is handled completely generically by the core infrastructure referencing only the backend-defined notions of column names and boolean expressions and how to (de-)serialize them. The only work that is required here is to expose API endpoints for the various CRUD-actions on permissions.

Development plan for Queries

  1. Implement the instance MonadSchema 'MySQL using the backend-generic default implementations.

  2. Enabling the API for manipulating permissions amounts to is adding tablePermissionsCommands @MySQL to the metadataV1CommandParsers implementation of the BackendAPI MySQL instance.

  3. For SQL generation of a Query, the case that translates an AnnSelectG [2]. Any applicable Row Permissions and Limit Permissions are found in the field _asnPerm and need to translate into WHERE and LIMIT clauses respectably.

  4. Also for SQL generation of a Query, the case that translates an AnnColumnField [3] needs to observe the field _acfCaseBoolExpression, which decides whether the column value should be nullified, as resulting from inherited roles.

Notes for Mutations

A GQL Mutation however may result in either of INSERT, UPDATE, or DELETE statements. Of these, INSERT has no obvious point in which to include a permissions predicate over the rows inserted.

As a consequence of this we need to translate an insert-mutation into a MySQL transaction, where performing the mutation and checking permissions on the affected rows is split over multiple statements. [4]

One suggested way to do this could be making a temporary table having the permissions as CHECK-constraints, inserting the new rows into this table (which fails if the permissions are not satisfied) and copying them over to the table actually targeted by the mutation.

Future

This document is a product of its time, brought into existence by the contemporary need to elaborate on how permissons work because the development work on MySQL needs to incorporate them.

An insight resulting from discussing this subject is that it would be more appropriate to treat permissions not as a distinct topic of a dedicated RFC document, but rather as associated concepts of the RFCs of the objects they apply to, i.e. variants of Queries and Mutations.

As it were, permissions do not exist in a vacuum. In order to talk about them we need to also talk about what they apply to. As such it makes for a more elegant exposition to talk about permissons as associated aspects of the subject they act on.

It it therefore expected that this document be superseded by dedicated RFCs on the subjects of Queries, Mutations.

Questions

How does the feature of Inherited Roles interact with the permissions-support in a backend?

The permissions that result from Inherited Roles are completely resolved into base permissions before being handed over to schema building. So Inherited Roles have no interaction with backend code.

Do Limit Permissions only apply to root-fields or also to array relationships?

Yet unanswered.

Footnotes

[1]^: For example,

query {
  articles_aggregate {
    count
    nodes { .. }
  }
}

If the select permission on some role r specifies a limit of 5 and there are a total of 10 rows accessible to r (as per active Row Permissions), the count in the above query should return 10 while nodes should only return 5. I.e, the Limit Permission should only be applied when returning rows and not when computing aggregate data.

[2]^: See server/src-lib/Hasura/RQL/IR/Select.hs.

[3]^: See server/src-lib/Hasura/RQL/IR/Select.hs. Haddocks contain descriptions of use.

[4]^: In PostgreSQL we exploit that INSERT supports a RETURNING clause that lets us extract information from the affected rows. MySQL does not support this.