6

I have the following Schema:

input CreateEventInput {
    userID: String!
    eventID: ID!
    type: String!
    data: String
    dateTime: AWSDateTime!
}

type Mutation {
    createEvent(input: CreateEventInput!): event
}

type Subscription {
    onCreateEvent(): event
        @aws_subscribe(mutations: ["createEvent"])

The createEvent resolver sets the userID like:

"key" : {
        "userID" : $util.dynamodb.toDynamoDBJson($context.identity.username),
        "eventID" : $util.dynamodb.toDynamoDBJson($util.autoId())
    }

I'd like to limit the subscription so that only records where the userID = $context.identity.username are returned to the user.

Does anyone know how to set this up? I think I need a resolver on the subscription, but I can't find a clear example of this where you have a Primary partition key (userID) and Primary sort key (eventID).

I would really appreciate any help or guidance. I can change the schema or DB if needed.

Update:

I believe I can set the Subscription Response Mapping Template to something like:

#if(${context.identity.username} != ${context.arguments.userID})
    $utils.unauthorized()
#else
##User is authorized, but we return null to continue
    null
#end

However, I'm at a loss what to put in the Request Mapping Template.

Drew
  • 701
  • 2
  • 10
  • 22

2 Answers2

5

I think the first step to filter subscriptions based on user is easiest done with a minor update to your schema, by breaking the 'input' shape into individual inputs to the mutation. Specifically:

type mutation {
  createEvent(userID: String!, eventID: ID!, type: String!, 
    data: String, dateTime: AWSDateTime!): event
}
... other stuff...
type Subscription {
  onCreateEvent(userId: String!): event
  @aws_subscribe(mutations: ["createEvent"])
}

A few notes on this:

1) This assumes you want that to be a requirement on the subscription. If not, if you want it to be an optional rule, remove the !. By your comment, I believe you would want it.

2) Subscription filters (which is what the userId parameter is in the subscription operation) require that the filters be in the response for the mutation. So be sure that when you're defining the operation on your client, you include userId in the response there.

3) This is required to apply the subscription filter. The service won't know what a userId is unless it's a direct input to the mutation, having it inside and input shape won't work.

Now, as far as nailing down that a user can't just subscribe to someone else's username. I believe you were looking at this docs page. This will work, is totally valid, and could be completed with the something close to the example in that docs page, but it's based around having a permissions lookup table and a Dynamo resolver. If you don't have one or would prefer to avoid using one, a little adjustment should be able to make it work with a none/local resolver. Without a permissions table or anything to check against, I'd strongly recommend a local/none resolver.

Specifically, I believe you could move what you have in the response mapping template to your new none/local resolver's mapping template...

#if(${context.identity.username} != ${context.arguments.userID})
    $utils.unauthorized()
#else
##User is authorized, but we return null to continue
    null
#end

...and have the response mapping template be a default response, then you'd have it without unnecessary infrastructure in a permissions table or dead code that sets up a dynamo interaction that does not happen. Instead, all this will do is check the username in the input against the username in the Cognito token.

Jeff Bailey
  • 5,655
  • 1
  • 22
  • 30
  • Breaking the schema apart was/is not necessary. Can you tell me what you think that would help? I was able to get this working by putting a _dummy_ Request Mapping Template in place - just a hardcoded "operation": "GetItem" - since this has no effect on the subscription and just needs to be there in order to save the Response mapping. Certainly a hack. I could add a permissions lookup table -- but I have no need for that either. – Drew Oct 30 '18 at 07:42
  • 1
    So, the way with AppSync to do something like subscribing to a user, an author, a post...etc. is with subscription filters. Those are the inputs to the subscription function. I just confirmed that the only way for those to apply is to have the various fields for the mutation be at the top level, not in an input shape. If you're not doing that and got it to work, how did you subscribe to a single user? – Jeff Bailey Oct 30 '18 at 17:16
  • the example of subscription filtering given in the [AWS docs](https://docs.aws.amazon.com/appsync/latest/devguide/real-time-data.html) (scroll down to _Using Subscription Arguments_) has the input to the mutation broken apart – Mike Fogel Mar 16 '20 at 14:07
3

Until Appsync improves here is how I accomplished a subscription that only allows a user to subscribe to events that match their own userID, using the schema I posted above:

Request Mapping Template:

{
    "version": "2017-02-28",
    "operation": "GetItem",
    "key": {
        "userID": $util.dynamodb.toDynamoDBJson($ctx.identity.username),
        "eventID": { "S" : "0bfe0d7c-b469-441e-95f6-788fe300f76d" }
    },
}

The request mapping template is only here for looks (The Appsync web console will not let you save without populating this with something valid) It does a hardcoded lookup every time someone makes a subscription request. This does nothing but succeed, and the data is thrown away. That is how subscriptions work in Appsync.

Subscription Response Mapping Template:

#if(${context.identity.username} != ${context.arguments.userID})
    $utils.unauthorized()
#else
##User is authorized, but we return null to continue
    null
#end

This is where the magic happens. This basically says if the user did not request a subscription to events with the same username as themselves -- return unauthorized. If the user did request a subscription to events with the same userID as the logged in account, null (null is a Response Mapping Template's way of continuing successfully (ie, not erroring).

For thoroughness, here is what the client request looks like:

const eventSub = `subscription eventSub($userID: String!) {
  onCreateEvent(userID: $userID) {
    userID
    email_hash
    eventID
    type
    data
    dateTime
  }
}`;
Drew
  • 701
  • 2
  • 10
  • 22
  • Thanks for this. Of note: I am using AWS_LAMBDA authorization for AppSync and could only get the authorization to work by placing the auth check in the Request Mapping Template. It was ignored entirely in the Response Mapping Template. FWIW. – Daniel Apr 24 '23 at 07:18