Projects STRLCPY graphql-engine Commits 50f1243a
🤬
  • docs: attempt to document roles / annotations (#358)

    <!-- Thank you for submitting this PR! :) -->
    
    ## Description
    
    This is an attempt to somewhat document how roles / annotations work in
    `v3-engine`. The main purpose of this exercise was to solidify my
    understanding, so I would very much welcome any corrections.
    
    V3_GIT_ORIGIN_REV_ID: 28600998c8a01ef7f95198b44b875f4f14873793
  • Loading...
  • Daniel Harvey committed with hasura-bot 1 month ago
    50f1243a
    1 parent 1269108b
  • ■ ■ ■ ■ ■ ■
    v3/docs/roles-and-annotations.md
     1 +# Roles / namespaces / annotations in `v3-engine`
     2 + 
     3 +In V2 we created a schema per role.
     4 + 
     5 +In V3 we create one big schema at build time, where some nodes are annotated with either
     6 +`generic` info, or `namespaced` info that only applies to specific roles.
     7 + 
     8 +The types for these annotations come from the `lang-graphql` crate, where they
     9 +are associated types on the `SchemaContext` trait.
     10 + 
     11 +This example used in tests for `lang-graphql` shows a schema with no
     12 +annotations. It uses a `Namespace` type to differentiate different scopes,
     13 +however since it has no annotations this won't do anything meaningful:
     14 + 
     15 +```rust
     16 +impl SchemaContext for SDL {
     17 + type Namespace = Namespace;
     18 + type GenericNodeInfo = ();
     19 + type NamespacedNodeInfo = ();
     20 +```
     21 + 
     22 +The `engine` itself uses the `GDS` type, and so we can use the following annotations:
     23 + 
     24 +```rust
     25 +impl gql_schema::SchemaContext for GDS {
     26 + type Namespace = Role;
     27 + type GenericNodeInfo = types::Annotation;
     28 + type NamespacedNodeInfo = Option<types::NamespaceAnnotation>;
     29 +```
     30 + 
     31 +Note that our `Namespace` type is `Role` - this means that the engine attached
     32 +useful information per role, so we can add select permissions for a field that
     33 +only apply to the `user-1` role, for instance.
     34 + 
     35 +(incidentally, there is a comment on the `SchemaContext` trait suggesting `Namespace` is
     36 +renamed to `Scope` or `Role` - this seems like a useful move to make all this
     37 +clearer, personally `Namespace` makes me think about different subgraphs or
     38 +something)
     39 + 
     40 +## What is an annotation then, concretely?
     41 + 
     42 +The change I have recently been working on is to add preset arguments to
     43 +commands. This means that given a `delete_user` command with a `user_id: Int`
     44 +argument, we can preset the value for certain roles.
     45 + 
     46 +For instance, we might want a `user-1` role to only be able to delete
     47 +themselves, so we'd preset `user_id` with `{ "sessionVariable":
     48 +"x-hasura-user-id" }`.
     49 + 
     50 +Previous to this change, when building the schema for the Command, we'd use
     51 +`builder.allow_all_namespaced` to create it, like
     52 + 
     53 +```rust
     54 +builder.allow_all_namespace(command_schema_stuff, None)
     55 +```
     56 + 
     57 +Instead, for our example, we'd use `conditional_namespaced` like this (excuse my pseudo-Rust):
     58 + 
     59 +```rust
     60 +let role_annotations = HashMap::new();
     61 + 
     62 +role_annotations.insert("user-1", Some(ArgumentPresets { "user_id":
     63 +"x-hasura-role-id" }));
     64 + 
     65 +role_annotations.insert("admin", None);
     66 + 
     67 +builder.conditional_namespaced(command_schema_stuff, role_annotations)
     68 +```
     69 + 
     70 +Here, we've added an annotation for `user-1` with some useful information we
     71 +can use later when receiving and running queries. We've also added a `None`
     72 +annotation for `admin` - this means there'll be no useful information later,
     73 +but we're still signalling that we want this `Command` to work for `admin`
     74 +users. If we had a `user-2` role in the schema, they wouldn't be able to use
     75 +the command.
     76 + 
     77 +## Removing items from the GraphQL schema, per role
     78 + 
     79 +By adding or omitting keys from the `role_annotations` above, we've made our
     80 +command appear or disappear from the GraphQL schema. This "snipping" as such,
     81 +happens in the `normalize_request` function in `lang-graphql`.
     82 + 
     83 +The important thing to know here is that `lang-graphql` code does not know
     84 +about `GDS` or our `NamespaceAnnotation` type. All it has to act on is whether
     85 +a `Role` (or `Namespace`, to it's eyes) has a key in any namespaced annotations
     86 +or not. Because we're using associated types the contents are "protected" from
     87 +`lang-graphql` and so it can't peek inside.
     88 + 
     89 +We're going to want the `user_id` argument to disappear from `user-1`'s schema
     90 +- we do this by using `conditional_namespaced` when contructing schema for the
     91 + `user_id` command argument itself:
     92 + 
     93 +```rust
     94 +let role_annotations = HashMap::new();
     95 + 
     96 +// insert an empty annotation for `admin` to make sure the argument remains in
     97 +the schema
     98 +role_annotations.insert("admin", None);
     99 + 
     100 +// don't add one for `user-1`, so it disappears from the schema (as it has been
     101 +replaced with the preset value)
     102 +builder.conditional_namespaced(command_argument_schema_stuff, role_annotations)
     103 +```
     104 + 
     105 +## Reading the annotations at request time
     106 + 
     107 +Everything before here happens at "compile time" for the engine, so we do it
     108 +all once at startup and it remains static for the lifetime of the application.
     109 + 
     110 +At some point we are going to need to [serve a
     111 +request](https://github.com/hasura/v3-engine/blob/main/engine/src/execute.rs#L170) though. The steps are as
     112 +follows:
     113 + 
     114 +- Receive request
     115 +- Parse into a GraphQL query
     116 +- Normalize the query
     117 +- Generate IR
     118 +- Construct a query plan
     119 +- Execute / explain the query plan
     120 + 
     121 +The `normalize` step was mentioned earlier - it takes place in `lang_graphql`
     122 +and combines the query with the role information to remove all irrelevant
     123 +namespace information from the query, and snip any parts of the schema that the
     124 +current role can't see.
     125 + 
     126 +Generally the helpful place for our annotations is in generating IR
     127 +(intermediate representation, compiler fans). For our commands change, we can
     128 +look up the `ArgumentPresets` we stored earlier like
     129 +[this](https://github.com/hasura/v3-engine/pull/340/files#diff-f01744b02938317df22c7bc991717ae20a397f623c387332f103a30d1c0d2dc9R104).
     130 + 
     131 +```rust
     132 +match field_call.info.namespaced {
     133 + None => {}
     134 + Some(NamespaceAnnotation::ArgumentPresets(argument_presets)) => {
     135 + // use `argument_presets` for current role to generate IR
     136 + ...
     137 + }
     138 +}
     139 +```
     140 + 
     141 +The IR will then be created from a mixture of the schema for the user's role,
     142 +and any arguments etc from their request.
     143 + 
     144 + 
     145 + 
Please wait...
Page is in error, reload to recover