How to model hierarchical access with AppSync
Tags: appsync, aws, graphql, serverless
Published: 2020-08-26 17:26:29

---
title: How to model hierarchical access with AppSync
published: true
date: 2020-08-26 10:54:51 UTC
tags: AppSync,AWS,GraphQL,Serverless
canonical_url: https://theburningmonk.com/2020/08/how-to-model-hierarchical-access-with-appsync/
---

I have been working with a US client to build a first-of-its-kind app for managing medical consents. It falls under HIPAA compliance and it’s paramount that we do not allow unauthorized access to user data.

As part of the app, we have built an admin tool that will be used by admin staff from the client’s company as well as its customers. Essentially, we have three roles in the admin tool:

- **Admin** : these are admin staffs from the client’s company. This group of user have access to all the functionalities, including adding new tenants.
- **Tenant** : these are admin staffs from the client’s customers which are typically Health Information Exchanges (HIEs). An HIE would collaborate with multiple healthcare providers (e.g. hospitals, pharmacies) to facilitate data exchange between them. An HIE’s admin staffs should therefore have access to information related to their own organization as well as any healthcare providers that it collaborates with.
- **Service Area** : these are admin staffs from the healthcare providers that collaborate with the HIEs above. They only have access to information related to their own organization as well as its patients.

This is a drastic oversimplification of the entities that are involved, but it serves to illustrate the different tiers of access that we need to model:

- `Admin` users can access everything.
- `Tenant` users can access its own data plus data that belongs to its subsidiary service areas.
- `Service Area` users can access only its own data.

![](https://theburningmonk.com/wp-content/uploads/2020/08/img_5f463e4d2afdf.png)

The good news is that AppSync makes it easy to implement group-based authentication with Cognito. Which is [one of the reasons](https://lumigo.io/aws-serverless-ecosystem/aws-appsync-five-reasons-you-should-consider-it-over-api-gateway/) why we decided to go with AppSync over API Gateway for this project.

However, modelling these overlapping actions in the GraphQL schema was still a big challenge in and of itself.

### The problem

The root of the problem is that there are many overlapping actions that each group can perform.

For example, `Admin` users can add new service areas and associate them with any `Tenant`. A `Tenant` user can also add new service areas, but cannot associate the service area with anyone but itself.

As such, we need two separate mutations with different inputs and lock down their access to different Cognito groups. As you can see from below, as a `Tenant` user, I only have access to the `addServiceAreaAsTenant` mutation where I cannot override the `tenantId` the service area is associated with.

```
type Mutation {
  addServiceAreaAsAdmin(tenantId: ID!, name: String!): ServiceArea!
  @aws_auth(cognito_groups: ["Admin"])

  addServiceAreaAsTenant(name: String!): ServiceArea!
  @aws_auth(cognito_groups: ["Tenant"])
}
```

As we add more and more queries and mutations this quickly becomes messy.

There are a lot of duplications for the `@aws_auth(cognito_groups: …)` clauses, and it’s not easy to see at a glance what each group of users can do. Also, we have to get increasingly creative about how we name these queries and mutations in order to distinguish them from one another.

Copy-and-paste errors can easily creep in (it almost did), and it’s also confusing for the frontend developers to keep track of which queries and mutations they should use for which users.

```
schema {
  query: Query
  mutation: Mutation
}

type Query {
  getTenantById(id: ID!): Tenant!
  @aws_auth(cognito_groups: ["Admin"])

  getServiceAreaById(id: ID!): ServiceArea!
  @aws_auth(cognito_groups: ["Admin"])

  getMyProfileAsTenant: Tenant!
  @aws_auth(cognito_groups: ["Tenant"])

  getMyServiceArea(id: ID!): ServiceArea!
  @aws_auth(cognito_groups: ["Tenant"])

  getMyProfileAsServiceArea: ServiceArea!
  @aws_auth(cognito_groups: ["ServiceArea"])
}

type Mutation {
  addTenant(name: String!): Tenant!
  @aws_auth(cognito_groups: ["Admin"])

  addServiceAreaAsAdmin(tenantId: ID!, name: String!): ServiceArea!
  @aws_auth(cognito_groups: ["Admin"])

  updateMyProfileAsTenant(name: String!): Tenant!
  @aws_auth(cognito_groups: ["Tenant"])

  addServiceAreaAsTenant(name: String!): ServiceArea!
  @aws_auth(cognito_groups: ["Tenant"])

  updateMyProfileAsServiceArea(name: String!): ServiceArea!
  @aws_auth(cognito_groups: ["ServiceArea"])
}

type Tenant {
  id: ID!
  name: String!
}

type ServiceArea {
  id: ID!
  name: String!
  tenantId: ID!
}
```

### Using nested Query/Mutation types

To make it easier to scale the complexity of this project, we decided to encapsulate each group into its own `Query` and `Mutation` types.

As you can see below, each group of users gets its own `Query` and `Mutation` types. Access is controlled in a single place, and it’s easy to see everything a group of users can do at a glance. Also, we don’t need to rely on naming conventions on overlapping actions anymore.

```
schema { 
  query: Query
  mutation: Mutation
}

type Query {
  asAdmin: AdminQuery
  @aws_auth(cognito_groups: ["Admin"])
  
  asTenant: TenantQuery
  @aws_auth(cognito_groups: ["Tenant"])

  asServiceArea: ServiceAreaQuery
  @aws_auth(cognito_groups: ["ServiceArea"])
}

type Mutation {
  asAdmin: AdminMutation
  @aws_auth(cognito_groups: ["Admin"])

  asTenant: TenantMutation
  @aws_auth(cognito_groups: ["Tenant"])

  asServiceArea: ServiceAreaMutation
  @aws_auth(cognito_groups: ["ServiceArea"])
}

type AdminQuery {
  getTenant(id: ID!): Tenant!
  getServiceArea(id: ID!): ServiceArea!
}

type TenantQuery {
  getMyProfile: Tenant!
  getMyServiceArea(id: ID!): ServiceArea!
}

type ServiceAreaQuery {
  getMyProfile: ServiceArea!
}

type AdminMutation {
  addTenant(name: String!): Tenant!
  addServiceArea(tenantId: ID!, name: String!): ServiceArea!
}

type TenantMutation {
  updateMyProfile(name: String!): Tenant!
  addServiceArea(name: String!): ServiceArea!
}

type ServiceAreaMutation {
  updateMyProfile(name: String!): ServiceArea!
}

type Tenant {
  id: ID!
  name: String!
}

type ServiceArea {
  id: ID!
  name: String!
  tenantId: ID!
}
```

This makes the GraphQL schema much easier to maintain as the project continues to grow, without breaking the existing security model.

As an `Admin` user, I’m able to perform the queries and mutations specified in the `AdminQuery` and `AdminMutation` types.

![](https://theburningmonk.com/wp-content/uploads/2020/08/img_5f463ed4ad28e.png)

![](https://theburningmonk.com/wp-content/uploads/2020/08/img_5f463edb52920.png)

As a `Tenant` user, the `tenantId` of my organization is captured as a custom attribute on my Cognito user.

![](https://theburningmonk.com/wp-content/uploads/2020/08/img_5f463ef044194.png)

The queries and mutations for a `Tenant` user never accept `tenantId` as an argument, and it’s always taken from `$context.identity`. This ensures tenants can only access its own data, without us having to litter our code with custom validation logic.

And since I don’t belong to the `Admin` group, I’m not authorized to access its queries and mutations.

![](https://theburningmonk.com/wp-content/uploads/2020/08/img_5f463f012095d.png)

But I’m able to act on my own data, as well as access data for service areas that are associated with my organization.

![](https://theburningmonk.com/wp-content/uploads/2020/08/img_5f463f1151f7b.png)

![](https://theburningmonk.com/wp-content/uploads/2020/08/img_5f463f19aec3f.png)

And the same goes to `Service Area` users, whose access will be restricted to its own data.

In addition to this, HIPAA compliance also requires us to have an audit trail of the data a user has accessed. So we also had to implement full request-response logging, which sadly does not come out-of-the-box with AppSync. We’ll dive into this particular topic in a later post.

In the meantime, if you want to see how this hierarchical model works for yourself, then check out [**this repo**](https://github.com/theburningmonk/appsync-hierarchical-permissions-demo) and try it out.

If you want to learn GraphQL and AppSync with a hands-on tutorial then please check out my [**new AppSync course**](https://appsyncmasterclass.com/) where you will build a Twitter clone using a combination of AppSync, Lambda, DynamoDB, Cognito and Vue.js. You will learn about different GraphQL modelling techniques, how to debug resolvers, CI/CD, monitoring and alerting and much more.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/nxcjh78qvkz95mrojx89.png)

---

The post [How to model hierarchical access with AppSync](https://theburningmonk.com/2020/08/how-to-model-hierarchical-access-with-appsync/) appeared first on [theburningmonk.com](https://theburningmonk.com).