question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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 @auth here?
  • 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:

  1. Call getRole in frontend React App to get role into memory.
  2. Validate the request. If role.canGet=false, return unauthorized error, otherwise go to next step.
  3. Call API.graphql(graphqlOperation(getTaskQuery, {params: ...})) to get the task from GraphQL backend, where getTaskQuery is a GraphQL query such as query {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:

  1. Call API.graphql(graphqlOperation(getTaskQuery, {params: ...})) with param taskId on GraphQL backend.
  2. In request template mapping, construct dynamoDB request
  3. In response template mapping:
    1. Get user info from $ctx.identity.
    2. Get project info from $ctx.result.
    3. Get role of the (User, Project) pair from the dynamoDB table backing up ProjectMembership model.
    4. If role.canGet=false, return $util.unauthorized(), otherwise return the result.

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:

  1. Can I leverage @auth in User and ProjectMembership ?
  2. Is it beneficial to use user sub from Cognito as User.id?
  3. Does my proposal to get an task sound good?
  4. If answer to 3 is yes, seems I need to use pipeline resolvers in response template mapping, is that correct?

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:4
  • Comments:23 (11 by maintainers)

github_iconTop GitHub Comments

5reactions
BrianAndersen78commented, Jan 24, 2019

@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…

3reactions
YikSanChancommented, Jan 8, 2019

Closed as I solved this already. Will update the thread once I have summarized the process in a post.

Read more comments on GitHub >

github_iconTop Results From Across the Web

AWS Cognito and DynamoDB, Authentication with Many to ...
I'm building an API in Node with Serverless on AWS. There is a Many to Many Relationship between Projects and Users. - Each...
Read more >
Achieve fine-grained data security with row-level access ...
With RLS, you can define policies to enforce fine-grained row-level access control ... making this implementation relationship many-to-many.
Read more >
The evolution of fine-grained access control for applications
The evolution of fine-grained access control ... Authentication describes the process of finding out that you are who you say you are.
Read more >
Get fine-grained access control of your server with AblyD
AblyD can be an incredibly powerful tool for extending your functionalities and having secure, powerful control and insight into your processes.
Read more >
Introducing Granular Permissions - Hygraph
The fine-grained permissions are now enabled on your projects, allowing for ... Post and Category with a many to many relation between them, ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found