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

[Feature Request] add @@filter annotation to enable/disable access #1421

Open
bvkimball opened this issue May 9, 2024 · 1 comment
Open

Comments

@bvkimball
Copy link

Is your feature request related to a problem? Please describe.

Sometimes i want baked in logic like soft delete mechanism into my model access policy. But sometimes the user/client can still access the data but only in implicit circumstances. Most of this could be implemented through the access policy and passing the parameter to the auth() but then you would need to create a new enhanced client. i believe you might want to enable/disable the access at the transaction level.

To rephrase, sometimes a single user of elevated "role" wants to view the application data the same as other users but by toggling an "flag" they can now include rows previously excluded (ie. archived, deleted)

Describe the solution you'd like

    attribute @@filter(_  name: String, _ condition: Boolean, _ enabled: Boolean?)
Name Description Default
name  Key to be passed to 'enhanced' client to enabled/disable filter behavior  
condition Boolean expression to be evaluated/inject if the fitler is enabled  
enabled Boolean indicating if the filter is enabled by default, should not have "args()" false
model Post {
  id String @id @default(cuid())
  title String
  owner User @relation(fields: [ownerId], references: [id])
  ownerId Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  publishedAt DateTime
  archivedAt DateTime
  createdAt DateTime

  // Filter Annotation will always inject this where clause when accessing model,
  @@filter('excludeArchived', archivedAt != null, true)
  @@filter('existingAsOf', createdAt = args().date)
  @@filter('publishedBetween', publishedAt > args().min && publishedAt < args().max)

  @@deny('all', auth() == null)
  @@allow('all', auth() == owner)
  @@allow('read', auth() != null)
}

This could then be used as such:

import { PrismaClient } from '@prisma/client';
import { enhance } from '@zenstackhq/runtime';

const prisma = new PrismaClient();
const db = enhance(prisma);

// then elsewhere ...

// this will excludeArchived by default
await db.post.findMany();

// this will disable exclude archived logic
await db.post.findMany({
   filters: {
     excludeArchived: false
   }
});

// this will enable the other filters, multiple should be allowed
await db.post.findMany({
   filters: {
     existingAsOf: {date: new Date() },
     publishedBetween: { min: new Date(), max: new Date()}
   }
});

Describe alternatives you've considered

Now some the examples above are contrived and arguably pointless, for example the publishedBetween could/should just be written in the where clause and this layer of abstraction is complicated. I agree, the example was to illustrate the concept.

The real benefit here is for complex use-cases like effectiveDated/Versioned/Temporal models and "version" control, where a filter enabled at the top level will be enabled for relations that invoke the same filter name.

model Post {
  //...
  effectiveStart DateTime
  effectiveEnd DateTime
  comments Comment[]
  @@filter('asOf', effectiveStart < args().timestamp && (effectiveEnd > args().timestamp || effectiveEnd == null))
}

model Comment {
  //...
  effectiveStart DateTime
  effectiveEnd DateTime
  @@filter('asOf', effectiveStart < args().timestamp && (effectiveEnd > args().timestamp || effectiveEnd == null))
}

Then query the tree -->

await db.post.findMany({ 
  filter: { asOf: { timestamp: '2024-12-24' } },
  select: { 
    title: true, 
    comments: {
      select: {
        id: true,
        message: true,
      },
    },
  },
});


**Additional context**
Based on concept from Hibernate: https://thorben-janssen.com/hibernate-filter/

possibly related to #1402 and #520
@ymc9
Copy link
Member

ymc9 commented May 20, 2024

Hi @bvkimball , thank you for filing this FR with a great explanation! If I'm understanding it correctly, the proposal covers two aspects:

  1. A way of passing extra variables to policy rules at runtime
  2. A way of conditionally enabling/disabling some rules (on multiple models)

# 1 is similar to #1402. We can probably start by allowing the args() construct at the client level and then extend it to the query level for better flexibility. The latter will involve extending PrismaClient's current TS interface, but we're already doing it in V2 anyway 😄.

# 2 may be related to another thing that I've been thinking about for a while. Let me try to explain it here.

The current way policies are modeled in ZenStack is probably not good enough for modeling applications with multiple "sides". For example, an EC app can have a storefront "side" and a fulfillment "side". Authoring their rather different authorization rules in a "flat" way can be quite cumbersome and hard to maintain. So maybe we should introduce something like "profile" to segregate different sets of rules. (Please ignore the syntax, as it's just for showing the idea).

model Order {
  ...

  @@profile('storefront', [
    allow('read', ...),
    deny('update', ...)
  ])

  @@profile('fulfillment', [
    allow('read', ...),
    deny('update', ...)
  ])

}

So at enhancement time we can do:

const storefrontDb = enhance(prisma, ..., { profiles: ['storefront'] });

We can also allow overriding profiles at query time.

Back to your proposal, the equivalent will probably be something like:

model Post {
  //...
  effectiveStart DateTime
  effectiveEnd DateTime
  comments Comment[]
  @@profile('asOf', [
    deny('read', !(effectiveStart < args().timestamp && (effectiveEnd > args().timestamp || effectiveEnd == null))))
  ])
}
await db.post.findMany({ 
  select: { 
    title: true, 
    comments: {
      select: {
        id: true,
        message: true,
      },
    },
  },
  profiles: [ 'asOf' ],
  args: { timestamp: '2024-12-24' }
});

I'm still not really sure to what extent this really overlaps with your original thoughts, but just wanted to throw out some wild ideas 😄.

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

2 participants