Fine-grained Authentication with many-to-many relation
See original GitHub issue** Which Category is your question related to? ** AppSync, Amplify, Cognito
** What AWS Services are you utilizing? ** AppSync, Amplify, Cognito
** Provide additional details e.g. code snippets ** The question is complicated. I write it down in the technical design doc to make it as clear as possible.
Project App - User Group Management
The app is for managing projects.
Relations
- Task - N : 1 - Project
- An project consists of many tasks
 
- Project - N : M - User
- An project is managed by many users
- A user manages many projects
 
- (Project, User) - 1 : 1 - Role
- A user can have different roles in different projects he/she manages
- For example, Jack manages both projects “Clean Jack’s home in SF” and “Clean Jack’s mom’s home in SEA”. Jack has super_user role in project “Clean Jack’s home in SF” so that he can CRUDL tasks in the project, while he has normal_user role in project “Jack’s mom’s home in SEA” where he cannot delete.
- This is the difficult problem I am trying to resolve.
 
I propose modeling the relations as followed.
Models
Modified from https://github.com/aws-amplify/amplify-cli/issues/318#issuecomment-431471227.
    type Task @model {
      id: ID!
      name: String!
      project: Project! @connection(name: "ProjectTasks")
    }
    
    type Project @model {
      id: ID!
      name: String!
      tasks: [Task] @connection(name: "ProjectTasks")
      users: [ProjectMembership] @connection(name: "ProjectMembership_Project")
    }
    
    type User @model {
      id: ID!
      name: String!
      projects: [ProjectMembership] @connection(name: "ProjectMembership_User")
    }
    
    # join model to encode many-to-many relationship
    type ProjectMembership @model {
      id: ID!
      user: User! @connection(name: "ProjectMembership_User")
      project: Project! @connection(name: "ProjectMembership_Project")
      role: Role!
    }
    
    type Role {
      canCreate: Boolean!
      canUpdate: Boolean!
      canDelete: Boolean!
      canGet: Boolean!
      canList: Boolean!
      canSearch: Boolean!
      // custom fields
      canX: Boolean!
    }
User and ProjectMembership
I have User and ProjectMembership type - I use data model to encode group membership rather than using the credential that comes from Cognito, because I don’t know how to leverage @auth  in this case.
Questions:
- Can I leverage @authhere?
- Also, I am thinking of rely on Cognito as much as possible, is it beneficial to use user sub from Cognito as User.id?
Role
It looks pretty much the same as { allow: ..., mutations: [...], queries: [...] }, but again I don’t know how to leverage @auth to reduce boilerplate.
APIs
I want to implement below APIs.
- listProjects(userId) => List[Project]
- ?getRole(userId, projectId) => Role
- listTasks(userId, projectId, role) => List[Task]
- getTask(taskId, role) => Task
- searchTask(filter, role) => Task
- createTask(userId, projectId, role) => Task
- updateTask(taskId, role) => Task
- deleteTask(taskId, role) => Task
listProjects
The implementation seems straightforward. I will rely on codegen getUser resolver and below GraphQL query:
    query {
      getUser(id: "user-id") {
        projects {
          project
        }
      }
    }
?getRole
getRole is to serve task methods that will be discussed later. Not sure if I want to expose this to frontend.
All task methods
Even though all task methods (i.e., list, get, search, create, update, delete) have an argument role, it does NOT necessarily mean, I want to get an task (let’s use getTask as an example) for my frontend React App like this:
- Call getRolein frontend React App to get role into memory.
- Validate the request. If role.canGet=false, return unauthorized error, otherwise go to next step.
- Call API.graphql(graphqlOperation(getTaskQuery, {params: ...}))to get the task from GraphQL backend, wheregetTaskQueryis a GraphQL query such asquery {getTask(…) {id name}}.
Instead, I am against authentication in the frontend and I hope it done implicitly in GraphQL backend. Ideally, to get an task:
- Call API.graphql(graphqlOperation(getTaskQuery, {params: ...}))with paramtaskIdon GraphQL backend.
- In request template mapping, construct dynamoDB request
- In response template mapping:
- Get user info from $ctx.identity.
- Get project info from $ctx.result.
- Get role of the (User, Project) pair from the dynamoDB table backing up ProjectMembershipmodel.
- If role.canGet=false, return$util.unauthorized(), otherwise return the result.
 
- Get user info from 
3.b can be saved if I also pass project info from React App to GraphQL backend, but here I prefer making the API simple to better clarify my problem.
Questions:
- Does my proposal to get an task sound good?
- If yes, seems I need to use pipeline resolvers in response template mapping, is that correct?
Alternatives Considered
Let’s assume super_user role equals to can get/list/search/create/update/delete, while normal_user role equals to cannot delete, look back to Jack’s example (see Relations), I can try this:
    type Task 
        @model 
        @auth(rules: [
            # super user is allowed all operations
            { allow: groups, groups: ["CleanJacksHomeInSfSuperUser", "CleanJacksMomsHomeInSeaSuperUser"] },
            # normal user is not allowed delete operation
            { allow: groups, groups: ["CleanJacksHomeInSfNormalUser", "CleanJacksMomsHomeInSeaNormalUser"], mutations: [create, update] }
        ]) {
        id: ID!
        name: String!
        project: Project! @connection(name: "ProjectTasks")
    }
    
    type Project 
        @model
        @auth(rules: [
            { allow: owner }
        ]) {
        id: ID!
        name: String!
        tasks: [Task] @connection(name: "ProjectTasks")
    }
And in AWS Cognito, I create 4 groups:
- CleanJacksHomeInSfSuperUser
- CleanJacksHomeInSfNormalUser
- CleanJacksMomsHomeInSeaSuperUser
- CleanJacksMomsHomeInSeaNormalUser
And assign user Jack to groups:
- CleanJacksHomeInSfSuperUser
- CleanJacksMomsHomeInSeaNormalUser
This should work. However, it doesn’t seem scalable. I will have to maintains #(project) * #(user type) of groups, which will hit group count limit mentioned at https://github.com/aws-amplify/amplify-cli/issues/318 easily. I also need to update the schema every time there is a new project or a new user type. To me, the cons dominates its pros: less and easier code.
Questions
In summary:
- Can I leverage @authinUserandProjectMembership?
- Is it beneficial to use user sub from Cognito as User.id?
- Does my proposal to get an task sound good?
- If answer to 3 is yes, seems I need to use pipeline resolvers in response template mapping, is that correct?
Issue Analytics
- State:
- Created 5 years ago
- Reactions:4
- Comments:23 (11 by maintainers)

 Top Related Medium Post
Top Related Medium Post Top Related StackOverflow Question
Top Related StackOverflow Question
@YikSanChan Any chance you have a little code to share… It doesn’t matter if it’s unstructered 😃 I’m sitting with the exact same problem, and having a hard time, figuring out what to add to my resolver…
Closed as I solved this already. Will update the thread once I have summarized the process in a post.