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 anAstCoalescer
function to string our final, modified GraphQL request AST together. - We also established a skeleton of the
TranslationRule
that would lead to our idealizedAstMap
.
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.
}
}
}
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
}
}
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
}
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.
"""
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
`
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!
Top comments (0)