PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7467 GitOrigin-RevId: 915a963d3a237a7f95bed757e3cc5a5bf6c8134b
8.4 KiB
Roles and Permissions
What Is a Role?
A role is a label for a group of users, such as admin
, editor
, or even
anonymous
. Every API call from every graphql-engine
user will be executed
in the context of one of the defined roles, and each role can have some number
of defined permissions.
What Is a Permission?
A permission defines whether or not a particular part of the schema or data is accessible by the given role. For example, regular users may not be able to delete other users' blog posts, or we might want to hide certain results from users who aren't logged in. They may also only be able to select up to a given number of rows. These things are achieved through the two types of permission:
Schema Permissions
Schema permissions determine which parts of the GraphQL API are available to the given role1. This category includes things like column permissions, aggregation function permissions, and even table permissions2. We refer to these things as schema permissions because they change the schema for their role. If a user makes a request for information from a column they can't see, it's a compile error: the field doesn't exist in the schema.
Data Permissions
Data permissions do not change the schema, but filter the data that roles are allowed to interact with. This category includes row permissions and insert/update "check" permissions (see below). If a user makes a request for information from a row they can't see, it's not a syntax error, but no results will be returned.
This is an important distinction to make. If a role has no permissions to see any columns in a given table, the table won't exist in the schema. If a role has no permissions to see any rows in a given table (i.e. the row permission check is always false), the table will be visible, but all queries/mutations result in empty responses and no changes.
The Architecture of Permissions
Schema Permissions
When we combine metadata and database introspection together to build a
Hasura.RQL.Types.SchemaCache
,
each table in the SourceCache
contains a map from roles to their permissions
(a RolePermInfoMap
).
At this point, no distinction is made between schema and data permissions3.
Part of building the eventual SchemaCache
is building role contexts. The
context in this case contains parsers for queries, mutations, and
subscriptions.
In Hasura.GraphQL.Schema.Action.actionExecute
,
any field whose permitted roles do not include the current role will be discarded.
This is where schema permissions are applied: if the schema parser can only parse
fields we are allowed to see, then it follows that we can't see the fields we're not allowed to see.
At this point, each defined role has a context, and each context has a schema parser. We're ready to expose an API!
Data Permissions
With that out the way, permissions show up again when we start making actual
GraphQL requests. When a request is made and processed, we compile a request
down to a representation between GQL and (most likely, though not necessarily)
SQL. We call this our intermediate representation, or IR (defined under
Hasura.RQL.IR
).
The IR defines a collection of types for describing the various actions we
might want to perform as part of a request.
Hasura.RQL.IR.Select.AnnSelectG
,
for example, describes something like a SELECT
query that we could perform on
a given backend. Within this type, as well as the things we might expect (which
fields do we want, which table are we querying, what predicates must hold), we
also find TablePermG
:
a description of permissions as a boolean expression.
This is the important point to bear in mind with data permissions such as
row-level permissions: they are implemented as filters that will be placed in
the WHERE
clause of each query. Following the flow of code for AnnSelectG
,
we arrive at, for example,
Hasura.Backends.Postgres.Translate.Select
,
specifically .Internal.Process.processSelectParams
.
In here, we find (at time of writing) the following block of code:
finalWhere =
toSQLBoolExp (selectFromToQual selectFrom) $
maybe permFilter (andAnnBoolExps permFilter) whereM
Maybe
we have some WHERE
clause. If we do, we'll AND
it with the
permissions filter predicates. If we don't, we'll just use the permissions
filter predicates. This will be done for every query, and this is how row-level
permissions (and data permissions in general) are implemented.
Boolean predicates
Data permissions are defined (according to TablePermG
) as an
AnnBoolExp
:
a boolean algebra, whose core is shared across all backends. While we (currently)
consider AND
/OR
/NOT
/EXISTS
to be backend-agnostic, backends may vary in
other ways, such as whether aggregation predicates are available.
This is important when we think about a permission like, "you can only update
this blog post if you created it". In practice, how we achieve this varies
between backends: for example, with Postgres, we implement this by passing the
session variables as a prepared argument (notably the first one), and then
referring to them with $1
4. For other backends such as MSSQL, we may just
inline the values.
When do data permissions get applied?
SELECT
As discussed, SELECT
permissions are added to the final result set as a
filter in the WHERE
clause. In other words, rows will only be returned who
match both the user's query and the permission predicate. The convention in
the code is to refer to these permissions as a filter
.
INSERT
We specify permissions for INSERT
against every row we create. In other
words, we only commit the transaction to insert rows if they all pass the given
predicate. The convention in code is to refer to these permissions as a
check
.
Note that some INSERT
queries may also involve UPDATE
permissions, for
example in the case of INSERT ON CONFLICT
. In these cases, we use INSERT
permissions for created rows, and UPDATE
permissions for modified rows. If a
user has no permissions to perform an update, for example, on_conflict
will
be removed from the schema entirely.
UPDATE
These operations involve two sets of permissions: a filter
on the rows we can
update, and a check
on the rows that have been updated. These are, in effect,
pre- and post-conditions.
DELETE
Just like SELECT
, we require only a filter
precondition to determine
data permissions for deletions.
-
Note that
admin
is a little special here: if anadmin
role has no permissions, the default is that they can do anything. If another role has no permissions, the default is that they can do nothing. ↩︎ -
This is sort of an extension of column permissions: if a role has no permissions to view a given table's columns or aggregates, then that table is removed from the schema. ↩︎
-
Note that the
SchemaCache
is built in two phases: once we have resolved the metadata and introspection, we have a partial schema cache. We can then build the GraphQL schema, while simultaneously building the parsers, and the output is a complete schema cache. However, both are represented in code by the sameSchemaCache
type. ↩︎ -
Hasura.Backends.Postgres.Execute.Prepare.prepareWithPlan
has some nice documentation on how this is achieved. ↩︎