Easy GraphQL Access Control with GRANDstack
Tags: graphql, apollo, react, neo4j
Published: 2020-09-06 17:58:02

### This article might be for you if you're interested in...

**A quick and flexible development experience to build:**

- Multi-tenant apps
- Apps that let their users choose:
  - **WHAT** data they want to share _and_
  - **WHO** to share it with
- Collaboration apps

GRANDstack (i.e. GraphQL, React, Apollo, Neo4j Database) already lowers technical overhead for initial app development, but it can be complicated or difficult to implement the above access-control features yourself. I'd like to share a package that fills those gaps, making the GRANDstack one of the best options for getting your next MVP up and running.

### Long ago in a galaxy far, far away...
Exaggeration is fun, but seriously. A while back, I wrote a [series of articles](https://dev.to/imkleats/series/5036) exploring some thoughts about GRANDstack (i.e. GraphQL, React, Apollo, Neo4j Database) and how its nested relationship filtering could be applied to access control. It feels like forever ago. Something called 2020 happened, and it took a while for it to go from rough proof-of-concept code to something I could share.

That day has come.

## Introducing: `neo4j-deepauth` v0.2.0 release

Directive-based support for fine-grained access control in `neo4j-graphql-js` GraphQL endpoints (i.e. GRANDstack apps). Notable improvements from the early thoughts/code I shared include:

- Restoring baseline `filter` functionality.
- Adding support for `@deepAuth` to be applied to an Interface and any Object Type that implements it.

### Using the `neo4j-deepauth` package

#### 1. Install package via NPM or Yarn
`yarn add neo4j-deepauth` or `npm install neo4j-deepauth`

Link to NPM page: https://www.npmjs.com/package/neo4j-deepauth

#### 2. 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 | INTERFACE
`
```

Note that, under its current implementation, the behavior of `@deepAuth` will only be applied to Objects or Interface types. A hotfix is in the works for "Relationship Types" due to the way `neo4j-graphql-js` generates new Object type definitions  Field-level access control can be implemented (rather inelegantly but simply) by moving restricted fields onto their own Object with a one-to-one relationship to the primary Type.

#### 3. 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`. **This is not the only or best authorization structure, just a simple example.**

Please note that the `path` argument strongly corresponds to the filter argument Input Types that would define the existence of the ACL structure. Declaring a `path` argument that does not conform to the correct filter Input Type is a potential cause of errors when `applyDeepAuth` attempts to coerce the argument value to that type.

#### 4. Modify resolvers and request context

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. Additionally, it should be noted that the top-level filter is obtained by `neo4jgraphql()` from the `params` argument, while subsequent filters are obtained from the `resolveInfo`. That might look something like:

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

const resolvers = {
  // entry point to GraphQL service
  Query: {
    User(object, params, ctx, resolveInfo) {
      const { authParams, authResolveInfo } = applyDeepAuth(params, ctx, resolveInfo);
      return neo4jgraphql(object, authParams, ctx, authResolveInfo);
    },
    Task(object, params, ctx, resolveInfo) {
      const { authParams, authResolveInfo } = applyDeepAuth(params, ctx, resolveInfo);
      return neo4jgraphql(object, authParams, 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 `express-graphql` (*Note: issues with ApolloServer have been diagnosed and resolved in the v0.2.1 release, but we can still give express-graphql some love.*):

```js
const app = express();
app.use('/', graphqlHTTP((request) => ({
  schema,
  context: {
    driver,
    deepAuthParams: {
      $user_id: request.user.id
    }
  },
  ...
})));
```

#### 5. Update Custom Mutations
The automatically-generated mutations will not currently respect or enforce the authorization paths provided on `@deepAuth`. Also, it will often be helpful or necessary to create/delete additional authorization nodes/relationships in the same transaction as a `Create`/`Delete` mutation.

For these reasons, you will need to create your own custom mutation resolvers for pretty much any Type that has `@deepAuth` applied or has a relationship to a `@deepAuth`ed Type.

## Examples
An example of `neo4j-deepauth` use can be found at [github.com/imkleats/neo4j-deepauth-example](https://github.com/imkleats/neo4j-deepauth-example)

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

## Issues and Contributions
As an early version number release, I'm still working on identifying all edge cases and continually fleshing out the test suite.  If you run into any bugs or have ideas for future feature releases, please open up an Issue on the Github repository.

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

Thanks for listening! Hope you find it useful, and I look forward to hearing from you!