mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
d905911eab
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
223 lines
10 KiB
Markdown
223 lines
10 KiB
Markdown
# 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](https://hasura.io/docs/latest/graphql/core/auth/authorization/index.html)
|
|
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 <a name="footnote-1-ref"></a>[[1]](#footnote-1-def).
|
|
|
|
Last, **_Inherited Roles_** may compose permissions, see [User
|
|
docs](https://hasura.io/docs/latest/graphql/core/auth/authorization/inherited-roles.html#select-permissions).
|
|
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](permissions-mysql/Inherited%20roles%20permissions.png)
|
|
|
|
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`](https://github.com/hasura/graphql-engine-mono/blob/dfba245a4dbe1a71b1e3cc7c92914fc0a919c2b0/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.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
|
|
|
|
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`
|
|
<a name="footnote-2-ref"></a>[[2]](#footnote-2-def). 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` <a name="footnote-3-ref"></a>[[3]](#footnote-3-def)
|
|
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. <a name="footnote-4-ref"></a>[[4]](#footnote-4-def)
|
|
|
|
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
|
|
|
|
<a name="footnote-1-def"></a>[1][^](#footnote-1-ref): 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.
|
|
|
|
<a name="footnote-2-def"></a>[2][^](#footnote-2-ref): See [`server/src-lib/Hasura/RQL/IR/Select.hs`]( https://github.com/hasura/graphql-engine-mono/blob/dfba245a4dbe1a71b1e3cc7c92914fc0a919c2b0/server/src-lib/Hasura/RQL/IR/Select.hs#L67).
|
|
|
|
<a name="footnote-3-def"></a>[3][^](#footnote-3-ref): See [`server/src-lib/Hasura/RQL/IR/Select.hs`]( https://github.com/hasura/graphql-engine-mono/blob/dfba245a4dbe1a71b1e3cc7c92914fc0a919c2b0/server/src-lib/Hasura/RQL/IR/Select.hs#L305). Haddocks contain descriptions of use.
|
|
|
|
<a name="footnote-4-def"></a>[4][^](#footnote-4-ref): In PostgreSQL we exploit that `INSERT` supports a
|
|
`RETURNING` clause that lets us extract information from the affected rows.
|
|
MySQL does not support this.
|