loading...

GRANDstack Access Control - Checking out the MVP

imkleats profile image Ian Kleats ・5 min read

Hi! It's me again. Welcome to this fifth article in my series on discretionary access control with the GRANDstack. 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 logo imkleats / neo4j-graphql-deepauth

Directive-based support for fine-grained access control in neo4j-graphql-js GraphQL endpoints

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:

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:

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, "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:

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:

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:

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!

Posted on by:

imkleats profile

Ian Kleats

@imkleats

Graph enthusiast. Hobbyist developer. Philosopher. Economist. Daydreamer. (they/them/their)

Discussion

markdown guide