You are viewing the preview version of this book
Click here for the full version.

Access control for subscriptions

Try it yourself

You can find code example for this chapter here.

So far we've talked about why access control is important for subscriptions, but we haven't looked into how to implement it. For this, we'll use a simple setup with 3 users in 2 different groups and we'll make sure that each user can only get events from its own group but not from the other group. management

Users in groups

For subscription events, we'll use the example from the previous chapter where there is a todo subscription that gets a userId and a groupId argument.

type TodoEvent {
  userId: ID!
  groupId: ID!
  todoId: ID!
  todo: Todo
}

type Subscription {
  todo(userId: ID, groupId: ID): TodoEvent
  @aws_subscribe(mutations: ["notifyTodo"])
}

To secure this subscription, we'll need to implement a couple of restrictions:

  • Either the userId or the groupId argument is required
  • The userId must be in the same group as the caller User
  • The groupId must be the caller's group

Everything related to subscriptions is implemented in the response mapping template for the subscription field's resolver. So every code we write will be for the Subscription.todo field.

We'll cover a universal approach based on validating the arguments that works with any filtering method, then we'll see a different way that require enhanced filtering.

Argument validation

The idea behing this access control implementation is to check the arguments the client sent and reject the subscription if it is for events that should not be sent to that client. In our example, if the clients sends a subscription with a different group, then it should be rejected.

For example, let's log in with user1, which is in group1 (the password is Password.1):

Login with user1/Password.1

Then a subscription with groupId: "group2":

subscription MySubscription {
  todo(groupId: "group2") {
    todoId
    userId
    groupId
  }
}

The subscription is rejected:

Error: {
  "errors": [
    {
      "message": "Connection failed: {
        \"errors\":[{
          \"errorType\":\"Unauthorized\",
          \"message\":\"Not Authorized to access todo on type Subscription\"
        }]
      }"
    }
  ]
}

To implement this and the other requirements, we need a pipeline resolver for the Subscription.todo field as it needs to fetch two users from the database.

Subscription.todo implementation

Since the implementation needs to handle all cases, let's break it down according to the three cases!

  • When there is neither a userId nor a groupId supplied, the result is an error
  • If there is a userId argument, fetch that and the current user, then compare the groupIds
  • If there is only a groupId argument, fetch the current user then compare the groupIds
Different cases separated
No-arguments case

Since a subscription without any arguments receives all events, it is crucial to handle this case. Usually, a simple check in the first function with an explicit reject is enough.

The first function's implementation is rather simple:

#if(
  $util.isNull($ctx.args.userId) &&
  $util.isNull($ctx.args.groupId)
)
  $util.unauthorized()
#end
#if($util.isNull($ctx.args.userId))
  #return
#else
{
  "version" : "2018-05-29",
  "operation" : "GetItem",
  "key" : {
    "id": {"S": $util.toJson($ctx.args.userId)}
  }
}
#end

The first #if checks the "everything missing" case. Then the second #if checks if it needs to fetch the user or not.

The second function is a bit more complicated as it needs to make the access decision. The request mapping template fetches the user by ID:

{
  "version" : "2018-05-29",
  "operation" : "GetItem",
  "key" : {
    "id": {"S": $util.toJson($ctx.identity.username)}
  }
}

Then the response mapping template has everything it needs to cover all cases:

#if($util.isNull($ctx.args.userId))
  ## filter by groupId
  #if($ctx.args.groupId != $ctx.result.groupId)
    $util.unauthorized()
  #end
#else
  ## filter by userId, use result from previous step
  #if($ctx.prev.result.groupId != $ctx.result.groupId)
    $util.unauthorized()
  #end
#end

The outer #if checks if filtering is done by groupId or userId. In the former case the $ctx.args.groupId is defined and the $ctx.result is the currently logged in user object. If the groupIds don't match, it throws an error.

The latter case is when there is a userId argument. Here, the $ctx.prev.result is the argument user object, while the $ctx.result is the currently logged in user object. The access decision is whether the groupIds match or not.

Testing the implementation

Let's see the different cases and whether the subscription is accepted or not! In all these cases, we'll use user1 and it should have access to events for user2 (as they are in the same group) and group1.

No filters, rejected:

subscription MySubscription {
  todo {
    todoId
  }
}

Events for itself, accepted:

subscription MySubscription {
  todo(userId: "user1") {
    todoId
  }
}

There is more, but you've reached the end of this preview
Read this and all other chapters in full and get lifetime access to:
  • all future updates
  • full web-based access
  • PDF and Epub versions