GRANDstack Access Control - Query Translation Strategy
Tags: graphql, grandstack, neo4j, backend
Published: 2020-02-27 02:09:11

Howdy! Welcome back to Part 3 of my series on discretionary access control in the [GRANDstack](https://grandstack.io). If you're still with me after that last article, *mil gracias*. As promised, we are going to define and empower a schema directive to lock down our assets.

#### Recapping our journey so far...

* We motivated our work by thinking about a GRANDstack To-do app.
  + Our first to-do? Keep Bob from down the street out of our  to-do list.
* We identified that the powerful nested/relational filtering from `neo4j-graphql-js` could be used to restrict our query results based on some arbitrary access control structure.
  + Our second to-do? Automate that filter behavior using a schema directive.
* Then, we needed to explore how GraphQL processes an operation (i.e. query or mutation) from request through result to understand potential implementations.
* We learned that the JavaScript reference implementation's `graphql/language` module and its `validate` function contain utilities and patterns that "fit" the query transformation problem we're trying to solve.

Today, we'll apply the pattern we described by writing a `TranslationRule` to add a filter argument to our Document ASTs for every type/field that we put this new directive on within our type definitions.

This endeavor will assume you're familiar with (or can reference on the fly) the repository I shared last time that lays out the pattern we'll be using:

{% github imkleats/graphql-ast-tools no-readme %}

#### Full disclosure:

I'm developing best practices as I write this, so feel free to offer different opinions in the comments or as issues in the above repo.

The other thing is that this is a hobby for me. My biggest well of time to work on this is the weekend... only after devoting time to family and household obligations. In the sake of keeping up the cadence of this series, though, I'm going to be out-pacing some of the code I've already written.

Some code samples might reference utility functions that I haven't created yet, others might be more like meta programming. If you see where I'm coming from / going to and want to fill in the blanks yourself, I welcome it. Just don't expect my code blocks to be copy-pastable and working.

### __Where do we start?__

In thinking through how to approach transformations, let's remind ourselves of the steps involved in the pattern we are going to apply:

1. Receive an incoming GraphQL request, `parse` it into an AST, and feed it into `translate`.
2. `translate` traverses the AST and applies a set of visitor functions that are supplied as `TranslationRules` to each AST node it visits.
3. These visitor functions, subject to whatever logic you define, emit information that is stored at a specified location in an `AstMap`.
  * This information may include references to pending values in other locations in the `AstMap`.
4. Upon traversal completion, a function is invoked to build a new AST (not necessarily a GraphQL AST) from the components of the `AstMap`.

I am going to propose that we should start with thinking about the state that needs to exist at the end of Step 3 before heading into Step 4.

In this exercise, we are going to be transforming one GraphQL query into another GraphQL query. Let's think about what that means for the structure of our finalized `AstMap`.

### __Scoping our AstMap and Final Coalescer__

The first thing that comes to mind as I think about a GraphQL-to-GraphQL transformation is that we could "clone" an entire Document AST with our `AstMap`.

Did I mention I'm lazy, though? Did I? To misquote one of my favorite mercantilist philosophers, C.J. Sparrow:

> You can always trust a lazy person to conduct your business with the utmost efficiency. Honestly. It's the industrious ones you want to watch out for.

(*Isn't there inherent value in knowing how to replicate the full-fledged structure of a GraphQL AST?!?* Of course there is! But meh...)

To save myself some work, I'm going to ask:

* What parts of the AST are we interested in changing?
  * Argument nodes with Field node parents.
* Do we need to care about any of the child nodes of those Argument nodes?
  * The Name and Value nodes tell us if there is an existing "filter" argument.
  * None of those children will hold a subsequent Field Node, so we can kind of treat the entire Argument Node as a leaf.

Ok, why did I ask those questions? It's because I wanted to save myself some work, silly. It sounds like we can get away only needing a list of objects that contain:
1. A new or modified Argument Node for our ACL filter; and
2. The location in the Document/Operation AST in which to insert that node.

Our final AstMap (that is generated by our visitor functions, so not actually a piece of code we have to define) might look like:

```js
const AstMap = {
  originalQuery: () => {
    return original_document_ast;
  },
  authFilters: () => {
    return [{path: [path_to_filter_arg_1], node: {new_Argument_Node_1},
            {path: [path_to_filter_arg_2], node: {new_Argument_Node_2},
            ...
            {path: [path_to_filter_arg_N], node: {new_Argument_Node_N}];
  },
} 
```

This could naturally lead to a final `AstCoalescer` function (which we do have to define for ourselves) that looks something like this:

```js
import set from 'lodash/set';

const finalCoalescer = (astmap) => {
  
  const requestAst = astmap['originalQuery'] ? astmap.originalQuery() : {};
  const authFilters = astmap['authFilters'] ? astmap.authFilters() : [];

  authFilters.map( authFilter => set(requestAst, authFilter.path, authFilter.node) );
  
  return requestAst;
}
```

### **Writing a TranslationRule to make that happen**

Mmkay, we know what we want at the end. Now we need to think about which visitor function or set of visitor functions will be necessary to create that structure. 

#### Populating originalQuery

If we were going to use a visitor function to produce a copy of the original Document AST, it might make sense to do that at the first node we enter.

Can we stop to ask ourselves why we would do this through a visitor function?

Honestly, having the capacity to produce an original version of our AST is probably going to be needed fairly frequently for other applications. Maybe we should just revise `translate` to populate this into our `AstMap` under a reserved key before any visitation.

#### Populating authFilters

If the `originalQuery` is taken care of without any `TranslationRule`, our primary concern is now limited to `authFilters`. Here's a part -- like I mentioned above -- that I am getting a little ahead of my actual codebase. This next part will be a little more conceptual, and I'll spend more time in the next article to complete the implementation.

Ok, let's take a second to think: Each visitor function could apply to a specific `Kind` of node. What kind of node(s) are we interested in?

(*Duh, we want to modify `Argument` nodes in the final AST, so we should write an `Argument` visitor!* ...and that is why I am writing this article and not you. Just kidding... mostly. ;p )

Back to reality. Why would we not use an `Argument` visitor? Because there are some `Fields` that we will visit which will not currently have any `Argument` nodes but will need the schema directive applied to them. Uh-oh, visitors can't visit a node that is undefined.

However, nothing prevents us from accessing the `Argument` node array when stopping at the `Field`. We want to do our work when visiting each `Field` node.

*Are there any other node `Kinds` we need to visit?* Nope.

So, let's put some bones in our `Field` visitor.

```js
// Definition of our Rule to add an authorization filter.

export function AuthorizationFilterRule(
  context   // The TranslationContext class we instantiate in translate().
) {
  // Returns an ASTVisitor
  return {
    Field(node, key, parent, path, ancestors) {
      
      const ToDoList = """
         1a) Check for directive on field's type in schema.
         1b) Check for filter arguments on Field node.
         2a) Modify or remove existing filter arguments.
         2b) If 1a is true, wrap 2a with the ACL filter.
         3)  Discern appropriate path for new/modified filter arguments.
         4a) Get current authFilters list from AstMap using `context`.
         4b) Append object with {path: results_of_3, node: results_of_2}
             to 4a (with a higher order function or something similar).
         4c) Post result of 4b to AstMap using `context`.
      """;

      // The @return value of visitor functions elicits special behavior.
      // In most cases, we just want to return undefined.
    }
  }
}
```

### **Adding the Directive to our Schema**

Alright, we kinda sorta have a skeleton for how we are going to implement our `AuthorizationFilterRule`. That was a doozy. Take a breath. We've got some momentously difficult work ahead of us ("srs bzns" if you're into the whole brevity thing).

```js
const typeDefs = `
  # Other TypeDefs you defined before
  directive @deepAuth on OBJECT | FIELD_DEFINITION
`
```

Done. That was it. Seriously. We might think about arguments for that directive, or we might not. I haven't decided yet.

### **Wrapping Up**

That's a wrap on this installment of the series. I wish I could say I wasn't a little bummed out. I really wanted to have an MVP `AuthorizationFilterRule` to share with you, and I fell short.

But hey! We *did* cover a lot anyway! Let's choose to be happy. It's probably all for the best in any case since we'll be able to take a deeper dive into those implementation details in the next post. Till next time!