DEV Community

Ian Kleats
Ian Kleats

Posted on

GRANDstack Access Control - Generating the Filter Argument

Hi there. This is the fourth stop on my journey to implement discretionary access control for GRANDstack applications. Today, we are going to embark on a mission to generate the filter arguments we need for to modify our GraphQL request ASTs.

If you're joining me for the first time, welcome and thank you! I strongly encourage you to check out the previous articles in this series.

Last time on "As the Graph Turns"...

We started jumping right into applying the pattern I introduced in the second article for GraphQL AST translation/transformation (here's a link to the repo that lays this out):

  • We defined the @deepAuth directive on our schema.
  • We envisioned the structure of our post-traversal AstMap, which allowed us to define an AstCoalescer function to string our final, modified GraphQL request AST together.
  • We also established a skeleton of the TranslationRule that would lead to our idealized AstMap.

As I mentioned then, I'm outpacing the code I've developed. The bright side? We get to spend time digging a little more deeply into the actual implementation of the TranslationRule. Let's remind ourselves of that skeleton:

// 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 elicit special behavior.
      // In most cases, we just want to return undefined.
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Our goal for this leg of the journey is to work on step (2b) from the list above. We are going to lay the foundation for making the ACL filter referenced there.

Going back, way back

We should probably circle back to something we talked about in the first article. Our goal was to dynamically add filter arguments to our GraphQL queries so that they looked more like:

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

First of all, I need to say something: I hate that first part. If I were to stick true to that, I would need to open up my TranslationRule to visit those variable definitions and modify them. It sounds like more work than I should need to do, and I'm lazy.

But more importantly... where will this access control list path come from? And how do we handle the dynamic parts (i.e. user/group identifiers) if we aren't using query variables? We need some way for our backend developer to tell us that {visibleTo_some: {userId: $user_id}} is the right access control filter to apply to the Task object type and which part of that path is a variable.

Here's what I am going to propose. We require the user to provide the following arguments and data types for those arguments:

const deepAuthArgsForTask = {
  aclPath: "{visibleTo_some: {userId: $user_id}}", // String
  variables: ["$user_id"] // Array of String
}
Enter fullscreen mode Exit fullscreen mode

If we have this payload of arguments (still being agnostic about how we get them), we can do the following:

const MetaCode = """
  1) Pull the value of variable arguments from some available options/context.
      -- This can be accessible from our TranslationContext.

  2) Use regex to replace instances of the variable arguments in the aclPath
     string with the values obtained in Step 1.

  3) Use a string literal of a very simple GraphQL query to put the string
     generated by Step 2 into a filter argument.

  4) Use the `parse` function from graphql-js to parse that string into a
     Document AST.

  5) Returned the filter Argument node value from the parsed AST for our uses.
"""
Enter fullscreen mode Exit fullscreen mode

Now where do we get those arguments?

We've laid out the arguments we need and how those arguments will be used, but we've been agnostic as to how we are going to deliver those arguments. There might be more choices, but two of the most obvious are:

  • Attaching them to the same available options/context that we would be using in Step 1 of the meta-code I've written above.
    • This might have some benefit in that it obfuscates the access control structure from your GraphQL schema.
    • One possible downside is that we've continued to remain agnostic as to whether this query document transformation will occur as middleware prior to hitting the GraphQL server or within our root resolver functions. I don't know how that might complicate things.
  • Adding these as directive arguments in our GraphQL schema.
    • Basically the opposite pros-cons from the previous choice.

This isn't really an either-or choice. We can implement both, I think, as long as we are mindful of precedence when we get both. Since the process of adding an options object to a request context is fairly well documented, I will focus solely on the directive arguments. We need to revise our directive definition in our type definitions to be more like:

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

  directive @deepAuth(
    aclPath: String
    variables: [String]
  ) on OBJECT | FIELD_DEFINITION
`
Enter fullscreen mode Exit fullscreen mode

Wrapping up

Thanks for joining me in my digital white-boarding exercise. It was a little shorter than our last few adventures together, but I think we've made progress nonetheless, don't you? We can continue our efforts to implement the AuthorizationFilterRule with our heads held high.

As always, as you continue to digest this material, if you have any questions / comments / contributions, please do share them. Till next time!

Oldest comments (0)