GRANDstack Access Control - Basics and Concepts

imkleats

Ian Kleats

Posted on February 21, 2020

GRANDstack Access Control - Basics and Concepts

Hey there. Thank you for joining me on a journey of exploration and discovery into unlocking some of the most powerful features of the GRANDstack! By the end of this series, we'll be able to implement fine-grained discretionary access control features into the GraphQL endpoint generated by neo4j-graphql-js.

Cool, right? I thought so too.

Before we dive in...

First off, this series assumes some basic familiarity with GraphQL concepts and the GRANDstack itself (GraphQL, React, Apollo, Neo4j Database). Most important of those GRANDstack topics will be its support for complex nested filtering. Luckily, there's a good blog post to get you up to speed.

Second, this is not a full-fledged tutorial. . . yet. The posts in this series are as much a learning log to document these concepts being developed in real time as they are to invite others to think about and share their own approaches. Learning can be messy. Let's get messy together.

And back to the action...

Ok, let's start small. You know what's small? A boring old To-Do app.

(Wait, you promised an epic journey of awesomeness and are giving me some crappy To-Do app?!?!? For now at least, yes.)

We've heard about this thing called the GRANDstack. It has a lot of synergy out of the box. All you really need to get your backend up is your GraphQL type definitions (i.e. the data model). neo4j-graphql-js will generate the executable schema from there, which can be served by apollo-server.

Ignoring the custom mutation you might use for user login, your type definitions might look like:

const typeDefs = `
type User {
  ID: ID!
  firstName: String
  lastName: String
  email: String!
  todoList: [Task] @relation(name: "TO_DO", direction: "OUT")
}
type Task {
  ID: ID!
  name: String!
  details: String
  location: Point
  complete: Boolean!
  assignedTo: User @relation(name: "TO_DO", direction: "IN")
}
`;
Enter fullscreen mode Exit fullscreen mode

Cool beans. We have Users that can be assigned Tasks. Our tasks even take advantage of neo4j-graphql-js Spatial Types that could be useful in the future!

Let's run it and...

What went wrong?

Oh, your app works great. That is, if you wanted Bob down the street to see that you need to stop by the pharmacy to pick up some hemorrhoid cream.

We could use the @additionalLabels directive on Task to keep them accessible to only one User, but that's kind of limited. What if your mom was going to the pharmacy anyway? Maybe you want certain people to be able to see certain tasks.

Maybe you want discretionary access control.

Unfortunately, I am not aware of any clear cut fine-grained access control options for GRANDstack out of the box. If I were, this post would not exist. On the bright side, we get to explore the possibilities together!

Filter to the rescue!

I might have mentioned how GRANDstack does have out-of-the-box support for complex nested filtering. Could this be the answer we seek? (HINT: I think so.)

Nested filtering means that we can filter the results of any field within our query by the fields of its related types. Those fields of its related types could lead to yet other filterable related types. Ad infinitum.

I don't actually think we need to go on forever. We just need to realize that the access control list for our business data is itself a graph connected to our primary data model.

We could do this with an arbitrarily complex authorization layer, but instead we're going to keep it simple. Let's reduce the access control structure to a single relationship that sits between the User and Task types. Our updated type definitions might look like:

const typeDefs = `
type User {
  userId: ID!
  firstName: String
  lastName: String
  email: String!
  taskList: [Task] @relation(name: "TO_DO", direction: "OUT")
  visibleTasks: [Task] @relation(name: "CAN_READ", direction: "IN")
}
type Task {
  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")
}
`;
Enter fullscreen mode Exit fullscreen mode

The following filter arguments could then form the basis for locking down our assets:

query aclTasks($user_id: ID!){
  Task(filter: {visibleTo_some: {userId: $user_id}}) {
    ...task fields
  }
  User {
    taskList(filter: {visibleTo_some: {userId: $user_id}} {
      ...task fields
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If there are other filters that need to be applied, we can wrap them all with an AND clause:

query aclTasks($user_id: ID!){
  Task(filter: {AND: [{visibleTo_some: {userId: $user_id}},
                     {location_distance_lt: {...}}]}) {
    ...task fields
  }
}
Enter fullscreen mode Exit fullscreen mode

Moving ahead in our journey

Oh, I'm sorry. Did I miss something? Your nosy neighbor Bob can still see your pharmaceutical needs can't he because he's savvy enough to submit his own queries without those filters. That dog!

Next time we'll need to figure out how to use a new schema directive to automate the transformation of our GraphQL filter arguments. This will do more to keep Bob out and also keep the queries on the client side a little cleaner. Till then!

💖 💪 🙅 🚩
imkleats
Ian Kleats

Posted on February 21, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related