|
| 1 | +# Table Permissions in MySQL |
| 2 | + |
| 3 | +## Metadata |
| 4 | +``` |
| 5 | +--- |
| 6 | +authors: Philip Lykke Carlsen <[email protected]> |
| 7 | +discussion: |
| 8 | + https://github.com/hasura/graphql-engine-mono/pull/2183 |
| 9 | +state: published |
| 10 | +--- |
| 11 | +``` |
| 12 | + |
| 13 | +## Description |
| 14 | + |
| 15 | +We want to support the [role-based access control |
| 16 | +feature](https://hasura.io/docs/latest/graphql/core/auth/authorization/index.html) |
| 17 | +on MySQL in the same fasihion that it works on Postgres currently. |
| 18 | + |
| 19 | +The role-based access control feature, often referred to simply as "Permissions" |
| 20 | +allows Hasura users to restrict what data is returned by queries and admitted by |
| 21 | +mutations. Several flavors of permissions exist: |
| 22 | + |
| 23 | +**_Column Permissions_** censor the columns that cliens in a given role have access |
| 24 | +to (either in Queries or Mutations), by means of an explicit list of columns |
| 25 | +exposed. |
| 26 | + |
| 27 | +**_Row Permissions_** censor table rows returned or affected, on the basis of a |
| 28 | +boolean-returning SQL expression, which is allowed to reference the columns of |
| 29 | +the table as well as session variables. These are sometimes also known as |
| 30 | +_Filter Permissions_ in the server code. Both _Queries_ and _Mutations_ may be |
| 31 | +subjected to _Row Permissions_. |
| 32 | + |
| 33 | +The query semantics of _Column Permissions_ and _Row Permissions_ is that, |
| 34 | +in the context of a role: |
| 35 | + |
| 36 | +> _For all intents and purposes, the dataset that a query ranges over includes |
| 37 | + only those columns and rows that the **Column Permissions** and **Row |
| 38 | + Permissions** of that role allow._ |
| 39 | + |
| 40 | +For example: |
| 41 | +* A user is unable to delete rows that her role's delete-permissions do not |
| 42 | + include. It is not an error to try; from the point of view of the delete |
| 43 | + mutation the rows indicated by the query are just not there. |
| 44 | +* A query for an aggregation (say, the average `age` in a `persons` table) will |
| 45 | + only consider those rows that her role's select-permissions include. |
| 46 | + |
| 47 | +The **_Aggregation Permission_** (a single boolean) decides if the query root |
| 48 | +fields that relate to aggregations should appear in the schema. |
| 49 | + |
| 50 | +**_Limit Permissions_** (an integer) apply only to queries and limit the |
| 51 | +maximum number of rows any query may yield. Importantly, an active _Limit |
| 52 | +Permission_ does not influence the row domain of an aggregation query, only the |
| 53 | +maximum number of rows produced by the query, see <a name="footnote-1-ref"></a>[[1]](#footnote-1-def). |
| 54 | + |
| 55 | +Last, **_Inherited Roles_** may compose permissions, see [User |
| 56 | +docs](https://hasura.io/docs/latest/graphql/core/auth/authorization/inherited-roles.html#select-permissions). |
| 57 | +An _Inherited Role_ is an authorization role defined in terms of other |
| 58 | +pre-existing roles. The permissions that an _Inherited Role_ grants are _not_ |
| 59 | +just the point-wise union of each of the parent roles' permission syntax, but |
| 60 | +rather the _union of the query datasets_ that each parent role permits, in the |
| 61 | +sense described above. |
| 62 | + |
| 63 | +This introduces a complication: In isolation, a role's _Column Permissions_ and |
| 64 | +_Row Permissions_ describe a "rectangular" dataset, with columns along one side |
| 65 | +and rows along the other. For two or more roles however, when we union the |
| 66 | +datasets they permit we do not necessarily end up with a rectangular dataset: |
| 67 | + |
| 68 | + |
| 69 | + |
| 70 | +Our data universe however only permits "rectangular" data. In order to |
| 71 | +accomodate the complexity resulting from _Inherited Roles_ we make columns that |
| 72 | +are particular to a single parent role nullable. For example, in the diagram |
| 73 | +above we would return `null` for (`Row 5`, `Column B`) and (`Row 2`, `Column |
| 74 | +D`). |
| 75 | + |
| 76 | +## What does this concretely look like |
| 77 | + |
| 78 | +When this is implemented, it should be possible to set permissions on MySQL |
| 79 | +tables in exactly the same fashion as is possible on Postgres tables, and |
| 80 | +queries and mutations should respect those permissions. |
| 81 | + |
| 82 | +At the time of this writing, this means every tracked MySQL tables has a |
| 83 | +_Permissions_ tab in the Console, which allows a user to set permissions on: |
| 84 | + |
| 85 | +* Rows and Columns |
| 86 | + * For each of the CRUD actions |
| 87 | + * Using all the predicates supported as boolean operators in `_where: {..}` |
| 88 | + arguments in queries to MySQL tables. |
| 89 | +* Limit and Aggregation |
| 90 | + |
| 91 | +The tests in |
| 92 | +[`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) |
| 93 | +pertaining to permissions should be generalised to multiple backends and made to |
| 94 | +pass for MySQL. |
| 95 | + |
| 96 | +## How are we going to implement it |
| 97 | + |
| 98 | +The GraphQL-Engine applies permissions at three points of processing: |
| 99 | + |
| 100 | +1. When building the schema, where _Column Permissions_ may cause fields to be |
| 101 | + censored from the schema. |
| 102 | +2. When parsing an incoming GraphQL query into HGE IR, where _Column Permissions_ |
| 103 | + again influence the grammar parsed, and _Row Permissions_ influence the IR |
| 104 | + generated such that relevant permissions are included. |
| 105 | +3. When SQL is generated from the IR, where the translation needs to take the IR |
| 106 | + node fields containing permissions into account. |
| 107 | + |
| 108 | +Since parser/schema generation is a single unified abstraction in |
| 109 | +GraphQL-Engine, all a backend needs to do to support permissions is a suitable |
| 110 | +implementation of type class methods `MonadSchema.buildTableQueryFields`, |
| 111 | +`buildTableInsertMutationFields` etc.. |
| 112 | + |
| 113 | +`buildTableQueryFields` et al. are given as inputs a representation of the |
| 114 | +permissions for a table (in the context of some role), which for _Column |
| 115 | +Permissions_ list the exposed columns and for _Row Permissions_ contain |
| 116 | +backend-specific Boolean Expression IR fragments, which are supposed to end up |
| 117 | +in parser outputs. |
| 118 | + |
| 119 | +There are already backend-generic implementations of these methods in |
| 120 | +`Hasura.GraphQL.Schema.Build` which we may use unless a product requirement |
| 121 | +surfaces that require us to deviate from the (de facto) standard table schema. |
| 122 | + |
| 123 | +The inputting and storing of permissions in metadata is handled completely |
| 124 | +generically by the core infrastructure referencing only the backend-defined |
| 125 | +notions of column names and boolean expressions and how to (de-)serialize them. |
| 126 | +The only work that is required here is to expose API endpoints for the various |
| 127 | +CRUD-actions on permissions. |
| 128 | + |
| 129 | +### Development plan for Queries |
| 130 | + |
| 131 | +1. Implement the `instance MonadSchema 'MySQL` using the backend-generic default |
| 132 | + implementations. |
| 133 | + |
| 134 | +2. Enabling the API for manipulating permissions amounts to is adding |
| 135 | + `tablePermissionsCommands @MySQL` to the `metadataV1CommandParsers` |
| 136 | + implementation of the `BackendAPI MySQL` instance. |
| 137 | + |
| 138 | +3. For SQL generation of a _Query_, the case that translates an `AnnSelectG` |
| 139 | + <a name="footnote-2-ref"></a>[[2]](#footnote-2-def). Any applicable _Row |
| 140 | + Permissions_ and _Limit Permissions_ are found in |
| 141 | + the field `_asnPerm` and need to translate into `WHERE` and `LIMIT` clauses |
| 142 | + respectably. |
| 143 | + |
| 144 | +4. Also for SQL generation of a _Query_, the case that translates an |
| 145 | + `AnnColumnField` <a name="footnote-3-ref"></a>[[3]](#footnote-3-def) |
| 146 | + needs to observe the field `_acfCaseBoolExpression`, which decides |
| 147 | + whether the column value should be nullified, as resulting from |
| 148 | + inherited roles. |
| 149 | + |
| 150 | +### Notes for Mutations |
| 151 | + |
| 152 | +A GQL _Mutation_ however may result in either of `INSERT`, `UPDATE`, or `DELETE` |
| 153 | +statements. Of these, `INSERT` has no obvious point in which to include a permissions |
| 154 | +predicate over the rows inserted. |
| 155 | + |
| 156 | +As a consequence of this we need to translate an insert-mutation into a MySQL |
| 157 | +transaction, where performing the mutation and checking permissions on the |
| 158 | +affected rows is split over multiple statements. <a name="footnote-4-ref"></a>[[4]](#footnote-4-def) |
| 159 | + |
| 160 | +One suggested way to do this could be making a temporary table having the |
| 161 | +permissions as `CHECK`-constraints, inserting the new rows into this table |
| 162 | +(which fails if the permissions are not satisfied) and copying them over to the |
| 163 | +table actually targeted by the mutation. |
| 164 | + |
| 165 | +## Future |
| 166 | + |
| 167 | +This document is a product of its time, brought into existence by the |
| 168 | +contemporary need to elaborate on how permissons work because the development |
| 169 | +work on MySQL needs to incorporate them. |
| 170 | + |
| 171 | +An insight resulting from discussing this subject is that it would be more |
| 172 | +appropriate to treat permissions not as a distinct topic of a dedicated RFC |
| 173 | +document, but rather as associated concepts of the RFCs of the objects they |
| 174 | +apply to, i.e. variants of _Queries_ and _Mutations_. |
| 175 | + |
| 176 | +As it were, _permissions_ do not exist in a vacuum. In order to talk about them |
| 177 | +we need to also talk about what they apply to. As such it makes for a more |
| 178 | +elegant exposition to talk about permissons as associated aspects of the subject |
| 179 | +they act on. |
| 180 | + |
| 181 | +It it therefore expected that this document be superseded by dedicated RFCs on |
| 182 | +the subjects of _Queries_, _Mutations_. |
| 183 | + |
| 184 | +## Questions |
| 185 | + |
| 186 | +How does the feature of _Inherited Roles_ interact with the permissions-support |
| 187 | +in a backend? |
| 188 | + |
| 189 | +> The permissions that result from Inherited Roles are completely resolved into |
| 190 | +> base permissions before being handed over to schema building. So Inherited |
| 191 | +> Roles have no interaction with backend code. |
| 192 | +
|
| 193 | +Do _Limit Permissions_ only apply to root-fields or also to array relationships? |
| 194 | + |
| 195 | +> Yet unanswered. |
| 196 | +
|
| 197 | +## Footnotes |
| 198 | + |
| 199 | +<a name="footnote-1-def"></a>[1][^](#footnote-1-ref): For example, |
| 200 | + |
| 201 | +``` |
| 202 | +query { |
| 203 | + articles_aggregate { |
| 204 | + count |
| 205 | + nodes { .. } |
| 206 | + } |
| 207 | +} |
| 208 | +``` |
| 209 | + |
| 210 | +If the select permission on some role `r` specifies a limit of `5` and there are |
| 211 | +a total of `10` rows accessible to `r` (as per active _Row Permissions_), the |
| 212 | +`count` in the above query should return `10` while `nodes` should only return |
| 213 | +`5`. I.e, the _Limit Permission_ should only be applied when returning rows and |
| 214 | +not when computing aggregate data. |
| 215 | + |
| 216 | +<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). |
| 217 | + |
| 218 | +<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. |
| 219 | + |
| 220 | +<a name="footnote-4-def"></a>[4][^](#footnote-4-ref): In PostgreSQL we exploit that `INSERT` supports a |
| 221 | +`RETURNING` clause that lets us extract information from the affected rows. |
| 222 | +MySQL does not support this. |
0 commit comments