dbAuth Setup - How to Add Role from Selected Tenant to getCurrentUser

I’m curious how others would implement the following…

  • Users can belong to more than one tenant
  • Users could have a different roles per tenant
  • Role authorization should be derived from the selected tenant
  • User can select tenant on login
  • The role from the selected tenant should then flow through authorization

I’ve messed around with multiple variations on this but haven’t successfully implemented any. That’s usually means I trying an anti-pattern or overlooking a simple solution. :grinning:

Should I…

  • Just add a currentTenant & currentRole field to the user model and manage via login/logout/change tenant
  • add the selected tenant as a client cookie, pass as a header and consume in redwood auth flow? If so… how?
  • Update the session/global context onSelectTenant and plug into the auth flow? <— this is where I started but I don’t see a way to persist the selected tenant…
  • Do something I’m overlooking?

A a simplified schema looks something like like…

model User {
  id                       Int            @id @default(autoincrement())
  createdAt                DateTime       @default(now())
  name             String
  tenantMemberships        TenantMember[]
}

model Tenant {
  id               Int            @id @default(autoincrement())
  name             String
  type             TenantType     @default(ORGANIZATION)
  members          TenantMember[]
}

enum TenantType {
  INDIVIDUAL
  ORGANIZATION
}

model TenantMember {
  id       Int         @id @default(autoincrement())
  userId   Int
  user     User        @relation(fields: [userId], references: [id])
  tenantId Int
  tenant   Tenant      @relation(fields: [tenantId], references: [id])
  role     TenantRoles @default(TENANT_MEMBER)

  @@unique([userId, tenantId])
}

enum TenantRoles {
  TENANT_OWNER
  TENANT_ADMIN
  TENANT_MEMBER
}

Hi,
first of all in graphql.ts you can add additional information to your global context for the backend. So if your graphql query has header information on the tenant you can do something like:

const setContext: ContextFunction = async ( { context } ) => {   
   context.tenant = await getTenant(context.event.header.tenant)
   return tenant
})

export const handler = createGraphQLHandler({
...
context: setContext,
... })

you can access this information within src/lib/auth in the hasRole function and relate it to the memberships of your user. Better orient yourself on how redwood is doing it, i only give a simplified example:

const tenant = context.tenant
const membership = context.currentUser.tenantMemberships.find((x)=>x.id === tenant.id)

return membership.role

i think you can do something similar on the web side in web/src/auth

And on your RedwoodApolloProvider you can add header information:

 graphQLClientConfig={{
                    link: (rwLinks) => {
                      const consoleLink = new ApolloLink((operation, forward) => {
                        const tenant = getTenantForExampleFromUrl()
                        operation.setContext(({ headers = {} }) => ({
                          headers: {
                            ...headers,
                            ['tenant']: tenant,
                          },
                        }))

                        return forward(operation)
                      })
                      const links = rwLinks.map(({ link }) => link)
// i think there was a reason for this splice, order was important. maybe its documented on redwood docs
                      links.splice(2, 0, consoleLink)
                      return ApolloLink.from(links)
                    },
                    httpLinkConfig: { credentials: 'include' },
                  }}

Hope this is helpful.

2 Likes

@Entelechy I thought this article on multi-tenant modeling interesting - Ultimate guide to multi-tenant SaaS data modeling

Its got some prisma examples sprinkled through the article

4 Likes