Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prisma / nestjs example #15

Open
ghost opened this issue Aug 29, 2021 · 3 comments
Open

Prisma / nestjs example #15

ghost opened this issue Aug 29, 2021 · 3 comments

Comments

@ghost
Copy link

ghost commented Aug 29, 2021

Hello, I am sorry if the question is dumb as I am pretty new here. I am not sure if I have done correctly.

I am using prisma with nestjs, and want to add dynamic permissions using casl. As far as I understand from the docs, json defined rules would be my choice as I want to manage and assign the permissions to users via dashboard.

So according to https://casl.js.org/v5/en/cookbook/roles-with-persisted-permissions, I would choose the second option to build a permission model in prisma like this:

enum Action {
  Manage
  Create
  Update
  Read
  Delete
}

model Permission {
  id         String   @id @default(cuid())
  action     Action
  subject    String
  fields     String[]
  conditions Json?
  inverted   Boolean? @default(false)
  reason     String?
  role       Role     @relation(fields: [roleId], references: [id])
  roleId     String
}

and use prisma's methods to CRUD rules:

permissions.service.ts

 async readPermissions(query: Prisma.PermissionFindManyArgs): Promise<Permission[]> {
        return await this.prisma.permission.findMany(query)
    }

 async createPermission(data: Prisma.PermissionCreateInput): Promise<Permission> {
        return await this.prisma.permission.create({
            data,
        })
    }
...

and need to dynamically generate ability instance with the defined rules for different prisma models.

according to https://docs.nestjs.com/security/authorization#integrating-casl and the above cookbook:

@Injectable()
export class AbilityFactory {
    constructor(private permissionsService: PermissionsService) {}
    async createAbility(modelName: Prisma.ModelName) {
        if (modelName === 'Permission') throw new HttpException('Error', HttpStatus.CONFLICT)

        const subjects = [modelName, 'all']
        const actions = Object.keys(Action)
        type Abilities = [
            typeof actions[number],
            typeof subjects[number] | ForcedSubject<Exclude<typeof subjects[number], 'all'>>,
        ]
        // type AppAbility = Ability<Abilities>

       // get the defined rules(permissions) via prisma
        const permissions = await this.permissionsService.readPermissions({
            where: {
                subject: modelName,
            },
            select: {
                action: true,
                subject: true,
                fields: true,
                conditions: true,
                inverted: true,
                reason: true,
                role: true,
                roleId: true,
            },
        })

        return new Ability<Abilities>(permissions)
    }
}

here when I found the @casl/prisma package, I could barely understand its usage from the docs.

So my question is that, is this correct if I use the @casl/prisma package instead of the above to generate a prisma specific ability instance, and pass the ability to accessibleBy in prisma methods?

the new ability builder would look like this, where the subject type conflicts :

async createPrismaAbility() {
       // need to import all models
        type PrismaModels = Subjects<{
            Address: Address
            Comment: Comment
            Country: Country
            File: File
            ...
            User: User
        }>
        type AppAbility = PrismaAbility<[string, PrismaModels]>

        const AppAbility = PrismaAbility as AbilityClass<AppAbility>

        // return new Ability<AppAbility>(rules)

        const { can, cannot, build } = new AbilityBuilder(AppAbility)
      // get all defined rules
        const permissions = await this.permissionsService.readPermissions({})

        permissions.map((permission) => {
            const { action, subject, fields, conditions } = permission
            if (permission.inverted) {
                return cannot(action, subject, fields, conditions) // Argument of type 'string' is not assignable to parameter of type 'SubjectTypeOf<AppAbility> | SubjectTypeOf<AppAbility>[]
            }
            return can(action, subject, subject, conditions)
        })
        return build()
    }

there are still a lot to do like guard and request context though. So I am wondering if there would be a minimal example. Thank you in advance.

@stalniy
Copy link
Owner

stalniy commented Aug 29, 2021

Hey,

the minimal example on its way but has a low priority :)

casl /prisma does 2 things:

  1. It provides custom PrismaAbility that is configured to use prisma query to define permissions (instead of mongodb)
  2. It provides accessibleBy helper that returns prisma conditions out of ability rules. You can use this conditions to get from db only what user can actually access

@SynergyEvolved
Copy link

I too would greatly appreciate a nestjs/prisma example or a link to a github repo that uses it.
Thanks!

@ZenSoftware
Copy link

My repo ⛩ Nest + Prisma + Angular 🏮 Full Stack GraphQL Starter Kit ⛩ is using @casl/prisma for our auth solution. I thought it might be useful if I shared my code as an example of a concrete implementation. Cheers! 🎐

casl.factory.ts

import { AbilityBuilder, PureAbility } from '@casl/ability';
import { Injectable } from '@nestjs/common';
import { Action } from '@zen/api-interfaces';
import { ICaslFactory, RequestUser } from '@zen/nest-auth';

import { PrismaQuery, createPrismaAbility } from './casl-prisma';
import { PrismaSubjects } from './generated';

/** @description A union of subjects to extend the ability beyond just Prisma models */
export type ExtendedSubjects = 'all';
export type AppAbility = PureAbility<[Action, PrismaSubjects | ExtendedSubjects], PrismaQuery>;

@Injectable()
export class CaslFactory implements ICaslFactory {
  async createAbility(user: RequestUser) {
    const { can, cannot, build } = new AbilityBuilder<AppAbility>(createPrismaAbility);

    if (user.roles.includes('Super')) {
      can('manage', 'all');
    }

    // Customize user permissions here

    return build();
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants