PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4524 Co-authored-by: Auke Booij <164426+abooij@users.noreply.github.com> Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com> GitOrigin-RevId: 1cae7a1596825925da9e82c2675507482f41c3fb
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 apersons
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:
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:
- When building the schema, where Column Permissions may cause fields to be censored from the schema.
- 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.
- 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.buildTableQueryAndSubscriptionFields
,
buildTableInsertMutationFields
etc..
buildTableQueryAndSubscriptionFields
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
-
Implement the
instance MonadSchema 'MySQL
using the backend-generic default implementations. -
Enabling the API for manipulating permissions amounts to is adding
tablePermissionsCommands @MySQL
to themetadataV1CommandParsers
implementation of theBackendAPI MySQL
instance. -
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 intoWHERE
andLIMIT
clauses respectably. -
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.