zenstackhq / zenstack

Fullstack TypeScript toolkit that enhances Prisma ORM with flexible Authorization layer for RBAC/ABAC/PBAC/ReBAC, offering auto-generated type-safe APIs and frontend hooks.
https://zenstack.dev
MIT License
2.05k stars 87 forks source link

[ Bug ] Comparison Between Fields of Different Models Not Supported in Model-Level #1544

Open benjollymore opened 3 months ago

benjollymore commented 3 months ago

Description and expected behavior Cross model comparison on models for 'read' checks works for models with a direct relation to the model backing auth() (User in the following example).

It does not, however, work on models that do not have a direct relation to auth, and an error is thrown. The following behaviour can be observed with the schema below.

Reproducable Schema

model User extends Base {
      name              String
      email             String
      username          String
      superAdmin        SuperAdmin?
      companyAdmin      CompanyAdmin?

      @@allow('all', auth() == this)
  }

  model SuperAdmin extends Base {
      userId String @unique
      user User @relation(fields: [userId], references: [id], onDelete: Cascade)

      @@allow('all', auth().superAdmin != null)
  }

    model Company extends Base {
        name String
        admins CompanyAdmin[]
        businessObjects BusinessObject[]

        @@allow('all', auth().superAdmin != null)
        @@allow('all', auth().companyAdmin.companyId == id) // cross model read error present here
    }

  model CompanyAdmin extends Base  {
      userId String @unique
      user User @relation(fields: [userId], references: [id], onDelete: Cascade)

      companyId String 
      company   Company @relation(fields: [companyId], references: [id])

      @@allow('all', auth().superAdmin != null)
      @@allow('all', auth().companyAdmin.companyId == companyId) // no error for cross model read here
  }

   model BusinessObject extends Base {
      company Company? @relation(fields: [companyId], references: [id])
      companyId String? 

      productNumber String

      @@allow('all', auth().superAdmin != null)
      @@allow('all', auth().companyAdmin.companyId == companyId) // cross model read error present here
 }

  abstract model Base {
      id String @id @default(uuid())
      createdAt DateTime @default(now())
      updatedAt DateTime @updatedAt
      deletedAt DateTime? @omit
  }

Environment (please complete the following information):

Additional context Add any other context about the problem here.

benjollymore commented 3 months ago

Looking at this for a bit longer (without a real understanding of Zenstack's inner machinations), is the following case where this is seen to be working actually cross model?

model CompanyAdmin extends Base  {
      userId String @unique
      user User @relation(fields: [userId], references: [id], onDelete: Cascade)

      companyId String 
      company   Company @relation(fields: [companyId], references: [id])

      @@allow('all', auth().superAdmin != null)
      @@allow('all', auth().companyAdmin.companyId == companyId) // no error for cross model read here
  }

I suppose we are comparing the companyAdmin related to auth() to other members of the CompanyAdmin model, so this may not really be a cross model comparison and the fact that it is related to auth() may be a red herring.

Eliav2 commented 2 months ago

There is no problem referring cross models. But its your responsibility to pass the right auth context to the getPrisma function. If you want to use nested related models (like in your example companyAdmin is a relation) you should query your db with unenhanced prisma client each request and pass it as auth context to the getPrisma function

benjollymore commented 2 months ago

@Eliav2 Thank you for the response and sorry for taking so long to get back to you myself.

I am making a query that picks up all the nested relations for the user and passes that to enhance.

That code looks something like this and is executed every request:

 const getPrisma() => {
   const user: TUserWithRelations = getSessionUser(); // user based on jwt with all relations fetched using vanilla prisma client
   return enhance(prisma, { user })
 }

The error that I was referring to gets picked up by the Zenstack VSCode plugin and also prevents pnpm zenstack generate from succeeding.

When running pnpm zenstack generate: image

VSCode plugin: image

The names of the following vary slightly from the example above but the structure is virtually identical.

The above is on v2.3.2.