Amplify-cli: Many to Many with @auth rules Schema Implementation

Created on 30 Oct 2018  路  7Comments  路  Source: aws-amplify/amplify-cli

I'm trying to create a React app with an Amplify API backend and Cognito User Pool and am not sure how to implement the schema for a particular use case. I'm new to Amplify, GraphQL and DynamoDB so any advice would be greatly appreciated.
Imagine a Project Management app that consists of Users, Goals and Tasks. I have written out what I'm trying to achieve below, with the auth rules also listed. I have read #91, #318, #352 and #356 but am having trouble applying it to this application.

My general approach is to list Goals and Tasks through the User model (eg. User.Goals or User.Tasks) to enforce viewing restrictions and use the auth rules on the Goal and Task models to provide the required restrictions for creating, updating and deleting.

# Any user can create Goals and Tasks
# A user can collaborate on 0 or more Goals
# A user can be assigned to 0 or more Tasks
# A user can only view Goals they are a collaborator on
# A user can only view Tasks they are Assigned to or Tasks that
# are part of a Goal they are a collaborator on

type User
  @model 
  @auth(rules: [
    { allow: owner }
  ]) {
  id: ID!
  name: String
  settings: String
  goals: [Goal!]! @connection(name: "UserGoals")
  tasks: [Task!]! @connection(name: "UserTasks")
}

# A goal can have 1 or more collaborators
# A collaborator can view/update/delete a Goal
# A Goal can have 0 or more Tasks
# A collaborator can create a Task

type Goal
  @model(queries: null)
  @auth(rules: [
    { allow: owner, ownerField: "collaborators"}
  ]) {
  id: ID!
  name: String!
  description: String
  status: String
  tasks: [Task] @connection(name: "GoalTasks")
  collaborators: [User!]! @connection(name: "UserGoals")
}

# A task must have 1 Goal
# A task can have 0 or more assignees
# An assignee can view/update/delete a Task
# A collaborator of the Goal should be able to create/view/update/delete a Task

type Task
  @model(queries: null)
  @auth(rules: [
    { allow: owner, ownerField: 鈥渁ssignees鈥潁
    # How to manage collaborators update/delete rights?
  ]) {
  id: ID!
  name: String!
  status: String
  goal: Goal! @connection(name: "GoalTasks")
  assignees: [User] @connection(name: "UserTasks")
}

If someone can point me in the right direction to make the above work that would be fantastic!

feature-request graphql-transformer

Most helpful comment

Hey just wanted to update this issue.. Now that pipeline resolvers have been released we can now start thinking about how to solve this use case. The work will take a fair amount of refactoring but in order to implement this we can use a pipeline resolver with two functions. The first function would look up membership in the many-to-many connection and either grant access to continue or fail. The second function would do what the current resolver does and actually create the object if the first function allows.

My current thinking is to create a new type of AuthStrategy such that you can protect mutations in many-to-many relationships like this:

type User @model {
  id: ID!
  chats: [ConvoLink] @connection(name: "UserConvoLinks")
}
type ConvoLink @model {
  id: ID!
  convo: Convo @connection(name: "ConvoLinks")
  user: User @connection(name: "UserConvoLinks")
}
type Convo @model {
  id: ID!
  messages: [Message] @connection(name: "ConvoMessages")
  links: [ConvoLink] @connection(name: "ConvoLinks")
}
# This would allow you to create messages only if you are enrolled in the conversation
# and would implemented using chain resolvers to do the DynamoDB lookup.
type Message 
  @model 
  @auth(rules: [{ allow: connection, path: ["convo", "links"], queries: null }]) 
{
  id: ID!
  content: String!
  convo: Convo @connection(name: "ConvoMessages")
}

Let me know what you think of this approach and any other approaches that you suggest. Thanks.

All 7 comments

Hey @DanNeish. This is a great question but this is not yet possible when using many-to-many relationships via the transform alone. To be more general, the problem is that there is currently no way to do an authorization check on an attribute that lives in a different table than the @model type itself. In this case, the many-to-many relationship would be managed in some UserTasks table and the desired authorization check would be to check that table for a record with userId = "id-of-logged-in-user" and taskId = "id-of-task-being-created". If the record exists, allow the operation; if it doesn't, fail. In the future, we will be able to achieve this but for now, you would need to use a lambda function to fully protect the Task and Goal mutations.

We are working on supporting custom resolvers from the Amplify CLI here #74, but you can add a lambda resolver today that implements the more complex auth use case via the AppSync console or your own CloudFormation stack that targets the API created by your Amplify project. To do so, you can turn off auto-generated mutations by using @model(mutations: null) and then define your own mutation fields in your schema.graphql.

type Mutation {
  customCreateTask(input: CreateTaskInput!): Task
  ... etc for other necessary mutations.
}

After deploying this, you can go to the AWS AppSync console and attach an AWS Lambda function to the Mutation.customCreateTask field and implement the authorization check in the lambda function.

Hi @mikeparisstuff thanks for the feedback. I will have a look into the option you proposed. Looking forward to that feature rolling out. Also fantastic work on this, it's very helpful!

Hey just wanted to update this issue.. Now that pipeline resolvers have been released we can now start thinking about how to solve this use case. The work will take a fair amount of refactoring but in order to implement this we can use a pipeline resolver with two functions. The first function would look up membership in the many-to-many connection and either grant access to continue or fail. The second function would do what the current resolver does and actually create the object if the first function allows.

My current thinking is to create a new type of AuthStrategy such that you can protect mutations in many-to-many relationships like this:

type User @model {
  id: ID!
  chats: [ConvoLink] @connection(name: "UserConvoLinks")
}
type ConvoLink @model {
  id: ID!
  convo: Convo @connection(name: "ConvoLinks")
  user: User @connection(name: "UserConvoLinks")
}
type Convo @model {
  id: ID!
  messages: [Message] @connection(name: "ConvoMessages")
  links: [ConvoLink] @connection(name: "ConvoLinks")
}
# This would allow you to create messages only if you are enrolled in the conversation
# and would implemented using chain resolvers to do the DynamoDB lookup.
type Message 
  @model 
  @auth(rules: [{ allow: connection, path: ["convo", "links"], queries: null }]) 
{
  id: ID!
  content: String!
  convo: Convo @connection(name: "ConvoMessages")
}

Let me know what you think of this approach and any other approaches that you suggest. Thanks.

@mikeparisstuff That would be awesome. When will allow: connection, path: [] be available for @auth directive? And is there a way to do this now?

@cocacrave We are working on a few design docs that will be released as RFCs on Github where the community will be given an opportunity to provide feedback on these features. In the meantime, you are able to use custom resolvers & custom stacks within your Amplify project to implement custom auth rules.

@mikeparisstuff Do you have any good references or examples on how to setup custom resolvers and custom stacks to implement custom auth rules?

@mikeparisstuff Is their any update to the ability to add auth on a many to many schema design? multiple users to multiple posts. User is always an individual so owner by Id will work there but a post could be owned by many users so it has to look for the postEditor type for the owner field. I believe this is what is being described above.

Was this page helpful?
0 / 5 - 0 ratings