GRANDstack Access Control - Checking out the MVP
Tags: graphql, grandstack, neo4j, node
Published: 2020-03-10 01:23:59

Hi! It's me again. Welcome to this fifth article in my series on discretionary access control with the [GRANDstack](https://grandstack.io). The past couple posts have ventured into some highly theoretical territory. After "losing" a weekend for some snowboarding (aka shredding the gnar), I have finally caught my code up to *actually do* all the things I *talked* about doing. I don't know about you, but I am super duper excited.

This article will cover the features implemented currently, lay out limitations that I intend to address with later enhancements (i.e. future articles), and demonstrate how this tool might be integrated into a `neo4j-graphql-js`-generated endpoint. First things first, let me show you the code:

{% github imkleats/neo4j-graphql-deepauth no-readme %}

#### Disclaimer and Reminder
The importance of data privacy cannot be overstated. Aside from any legal obligations, we have a moral responsibility as coders/developers to ensure the safety of those using our products. It is not hyperbole to say that poorly constructed access control can literally put people's lives at risk.

At this stage, please do not assume my work is production-ready. I make no guarantees of its quality or potential flaws. If you want to use this code, be responsible in writing your own unit and integration tests.

## @deepAuth MVP build
### **Minimum Viable Features**

* __Simplicity__: Anyone building a GraphQL backend using `neo4j-graphql-js` should be able to add fine-grained access control to their read-resources in three easy steps.
  1. Add schema definition for `@deepAuth` directive to your SDL.
  2. Add directive to user-defined types.
  3. Modify resolvers to replace the `resolveInfo.operation` and `resolveInfo.fragments` used by `neo4jgraphql()` with the pieces of your transformed query.
* __Powerful Security__: Clients should be able to access only the information for which they have been granted permission.
  + Leverage Neo4j's graph database capabilities to efficiently traverse arbitrarily complex access control relationships.
  + Prevents inference of unauthorized nested data by removing any client-defined filter arguments prior to execution. (Future enhancement to allow and dynamically modify client-defined filter arguments.)
* __Flexibility & Freedom__: In designing `@deepAuth`, a heavy premium was placed on _**extensibility**_.
  + Strive for great access control functionality out-of-the-box, but recognize that others might have different needs or ideas about what works for them.
  + Users are free to extend or modify the default behavior of `@deepAuth` by creating their own TranslationRules.
  + This TranslationRule pattern/approach is also not limited to directives. Get creative with it!

### **Enhancement Roadmap**

1. ~~Object-level `@deepAuth` directive support.~~ Complete
2. ~~Remove client-defined `filter` arguments on GraphQL queries~~ Complete
3. Field-level `@deepAuth` directive support.
  * Path argument will define path to a fieldPermissions node.
  * TranslationRule will add this fieldPermissions node to selectionSet.
  * Apollo tooling will be used to validate field-level permissions based on this extra data.
4. Nested filter support.
  * Restore client ability to supply filter arguments.
  * Use additional TranslationRule visitors to traverse existing filter arguments.
  * Wrap components of the existing filter argument with applicable `@deepAuth` filter.
5. Mutation support.
  * Attach newly-created nodes to a defined access control structure.
  * Use an `OperationDefinition` visitor in the TranslationRule to generate additional dependent mutations.
  * Submit all dependent mutations as a single database transaction.

### **Demonstration of Intended Flow**

#### 1. Add schema definition for `@deepAuth` directive to your SDL.

Your type definitions should include the following:

```js
const typeDefs = `
  # Other TypeDefs you defined before

  directive @deepAuth(
    path: String
    variables: [String]
  ) on OBJECT
`
```

Note that, under its current implementation, the behavior of `@deepAuth` will only be applied to Objects. Field-level access control will be the next topic I cover and implement. For forward-compatibility, you can safely use `on OBJECT | FIELD_DEFINITION`.

#### 2. Add directive to user-defined types.

Modify your previously-defined type definitions by including `@deepAuth` on any Object you want it to apply to. Using our To-Do example, that might look like:

```js
const typeDefs = `

type User @deepAuth(
  path: """OR: [{userId: "$user_id"},
                {friends_some: {userId: "$user_id"}}]""",
  variables: ["$user_id"]
){
  userId: ID!
  firstName: String
  lastName: String
  email: String!
  friends: [User] @relation(name: "FRIENDS_WITH", direction: "OUT")
  taskList: [Task] @relation(name: "TO_DO", direction: "OUT")
  visibleTasks: [Task] @relation(name: "CAN_READ", direction: "IN")
}

type Task @deepAuth(
  path: """visibleTo_some: {userId: "$user_id"}"""
  variables: ["$user_id"]
) {
  taskId: ID!
  name: String!
  details: String
  location: Point
  complete: Boolean!
  assignedTo: User @relation(name: "TO_DO", direction: "IN")
  visibleTo: [User] @relation(name: "CAN_READ", direction: "OUT")
}

# ...Directive definition from above
`
```

Here we've limited access to Users if: a) the client is the `User`; or b) the client is friends with the `User`. And we've limited access to `Tasks` if and only if the client's `User` has a `CAN_READ` relationship to the `Task`.

Please note that, while the `path` argument generally corresponds to the filter argument that would define the existence of the ACL structure, it must be written without being enclosed by brackets at the outermost level (i.e. just `path` not `{ path }`). 

#### 3. Modify resolvers and request context

Unfortunately, unless or until `@deepAuth` is integrated as a broader feature into `neo4j-graphql-js`, we will not be able to rely on the automatically-generated resolvers. We will have to modify them ourselves.

Per the [GRANDstack docs](https://grandstack.io/docs/neo4j-graphql-js.html#translate-graphql-to-cypher), "inside each resolver, use neo4j-graphql() to generate the Cypher required to resolve the GraphQL query, passing through the query arguments, context and resolveInfo objects." This would normally look like:

```js
import { neo4jgraphql } from "neo4j-graphql-js";

const resolvers = {
  // entry point to GraphQL service
  Query: {
    User(object, params, ctx, resolveInfo) {
      return neo4jgraphql(object, params, ctx, resolveInfo);
    },
    Task(object, params, ctx, resolveInfo) {
      return neo4jgraphql(object, params, ctx, resolveInfo);
    },
  }
};
```

As alluded to above, we must modify these resolvers to replace the `resolveInfo.operation` and `resolveInfo.fragments` used by `neo4jgraphql()` with the pieces of your transformed query. That might look something like:

```js
import { neo4jgraphql } from "neo4j-graphql-js";
import { applyDeepAuth } from "../neo4j-graphql-deepauth";

const resolvers = {
  // entry point to GraphQL service
  Query: {
    User(object, params, ctx, resolveInfo) {
      const authResolveInfo = applyDeepAuth(params, ctx, resolveInfo);
      return neo4jgraphql(object, params, ctx, authResolveInfo);
    },
    Task(object, params, ctx, resolveInfo) {
      const authResolveInfo = applyDeepAuth(params, ctx, resolveInfo);
      return neo4jgraphql(object, params, ctx, authResolveInfo);
    },
  }
};
```

If you use any `variables` in your `@deepAuth` directives, you must define them within your request context with the key as it appears in your `variables` argument. Here is an example of how to add values to the `deepAuthParams` in the context using ApolloServer:

```js
const server = new ApolloServer({
  context: ({req}) => ({
    driver,
    deepAuthParams: {
      $user_id: req.user.id
    }
  })
})
```

### **Where do we go from here?**

Hmmm, good question. I still need to build a lot of tests for the code I've written. Of the three items on my "Enhancement Roadmap", getting nested filter functionality restored is probably the most important, but it's also the most technically challenging.

Field-level access control is probably the easiest, and mutations are fairly straightforward but to introduce database transactions requires re-implementing some parts of `neo4jgraphql()`. So who knows. I'm leaning towards field-level access control so I can focus on tests.

Thanks for joining me on my journey. We're in a pretty good spot, but there's a fair distance we have yet to travel. Till next time!