Authorization RBAC with dynamic roles

Hello! yes I have built it all custom roles / permissions / team access

model User {
  id Int @id @default(autoincrement())

  currentTeam   Team? @relation(fields: [currentTeamId], references: [id])
  currentTeamId Int?
  currentRole   TeamMemberRole? @relation(fields: [currentRoleId], references: [id])
  currentRoleId Int?
  email         String  @unique
  issuer        String  @unique

  teamMembers     TeamMember[]
  createdAt       DateTime         @default(now())
  updatedAt       DateTime         @updatedAt
}

model TeamMember {
  id Int @id @default(autoincrement())
  team   Team? @relation(fields: [teamId], references: [id])
  teamId Int?
  teamMemberRole      TeamMemberRole?      @relation(fields: [teamMemberRoleId], references: [id])
  teamMemberRoleId    Int?
}

model Team {
  id        Int     @id @default(autoincrement())
  name                    String 
  teamMemberRoles         TeamMemberRole[]
  teamMembers             TeamMember[]
}

model TeamMemberRole {
  id              Int                    @id @default(autoincrement())
  name            String                 @unique
  permissions     TeamMemberPermission[]
  teamMembers     TeamMember[]
  teams           Team[]
  users           User[]
  teamInvitations TeamInvitation[]
  uneditable      Boolean                @default(false)
  createdAt       DateTime               @default(now())
  updatedAt       DateTime               @updatedAt
}

model TeamMemberPermission {
  id        Int              @id @default(autoincrement())
  name      String           @unique
  createdAt DateTime         @default(now())
  updatedAt DateTime         @updatedAt
  teamRoles TeamMemberRole[]
}

This is my authfile

import { TeamMemberLogActionTypes } from '@prisma/client'
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
import { context } from '@redwoodjs/api/dist/globalContext'


import { db } from './db'

export const getCurrentUser = async (authToken, b) => {
  const mAdmin = new Magic(process.env.MAGICLINK_SECRET)

  const {
    email,
    publicAddress,
    issuer,
  } = await mAdmin.users
    .getMetadataByToken(authToken)
    .catch((e) => console.error(e))

  if (!email || !publicAddress || !issuer) {
    throw new AuthenticationError('Uh, oh')
  }

  const account = await db.user.findOne({
    where: { issuer },
    include: {
      currentRole: true,
      currentTeam: true,
      teamMembers: true,
      onboarding: true,
    },
  })

  if (!account) {
    return await db.user.create({
      data: {
        email,
        publicAddress,
        issuer,
      },
      include: {
        currentRole: true,
        currentTeam: true,
        teamMembers: true,
      },
    })
  }

  const currentUser = {
    auth: {
      issuer: account.issuer,
      publicAddress: account.publicAddress,
      email: account.email,
    },
    user: {
      userId: account.id,l,
      firstName: account.firstName,
      secondName: account.secondName,
      updatedAt: account.updatedAt,
      createdAt: account.createdAt,
    },

    currentRole: account.currentRole,
    currentTeam: account.currentTeam,
    roles: null,
    teamMembers: account.teamMembers,
  }

  if (currentUser.currentRole?.id) {
    const findTeamRole = await db.teamMemberRole.findOne({
      where: { id: currentUser.currentRole?.id },
      include: {
        permissions: true,
      },
    })
    const currentPermissions = findTeamRole.permissions.map((o) => o.name)
    return { ...currentUser, roles: currentPermissions }
  } else {
    return currentUser
  }
}

// Use this function in your services to check that a user is logged in, and
// optionally raise an error if they're not.
export const requireAuth = ({ permission }) => {
  if (!context.currentUser) {
    throw new AuthenticationError('You are not logged into a user.')
  }

  if (
    typeof permission !== 'undefined' &&
    !context.currentUser.roles?.includes(permission)
  ) {
    throw new ForbiddenError(
      `You role does not have access rights to '${permission}'`
    )
  }
}

export const checkTeam = (teamId) => {
  if (teamId !== context.currentUser.currentTeam.id) {
    throw new ForbiddenError("You don't have access to that team!")
  }
}

I can explain more if needed in a bit of a pinch of time right now :slight_smile:
this was for a custom solution with Magic.Links

how to make it dynamic? you attach and detatch the roles as needed from the teamMember. One user can have multiple be part of multiple teams and the connecting table in the middle is teamMember

7 Likes