Support for row-level security (RLS)
See original GitHub issueProblem
Currently, if we want to guarantee that user’s requests have enough permissions to run a query, we have to keep coding where:{...}
clauses everywhere in our code. Depending on the size of the code base, it can be extremely hard to make sure that all query conditions are correct, updated and concise with the data model.
// check if the venue is the actual owner of the ticket
if (id) {
const ticket = await prisma.ticket.findFirst({
where: {
id,
venueId: context.venue.id,
},
})
if (!ticket) {
throw new Error('Ticket not found')
}
}
// there's no way to verify to make sure the upsert has enough permission to update or create
await prisma.ticket.upsert({
where: {
id,
},
update: data,
create: {
...data,
venueId: context.venue.id,
},
})
Suggested solution
We could have a way to implement some sort of @@security
directly on the schema.prisma
file. This way, we can check the user’s permissions directly on the database, saving some back-and-forth queries and also making sure that rules will always be respected anywhere in the codebase.
An API that follows the same where
principles applied to the models will allow a more flexible and powerful way to implement a granular security.
@@security(
name: 'Blah', // Name of the rule (important for debugging)
create: { // Command: update, delete, read, all...
row: { ... }, // where clause from the current row
context: { ... }, // where clause from the current context
// since it follows the same principle, we can use the same syntax for conditions
OR: [
{ row: { ... } },
{ context: { ... } },
]
}
)
Here’s a quick example:
// defines the context structure that should be sent by the prisma client
context {
id String
role Role
}
enum Role {
USER
ADMIN
}
model Cart {
id String @id @default(uuid())
customer Profile @relation(fields: [customerId], references: [id])
customerId String
@@security(
name: 'Carts are only visible by owners',
read: { row: { customerId: [id] } }
)
@@security(
name: 'Admins can do anything with carts',
all: { context: { role: ADMIN } }
)
}
(Note: a more in depth example can be found in the here)
The migration tool would then make sure that this RLS rule would be synced with the database
-- Policies
CREATE POLICY "Carts are only visible by owners"
on "Cart" for select
using ( customerId = current_setting('context.id') );
CREATE POLICY "Admins can do anything with carts"
on "Cart" for all
using ( current_setting('context.role') = 'ADMIN' );
and the prisma client would make sure that the context is provided to the query.
await prisma.cart.findMany({
context: { id: '...', role: 'ADMIN' },
data: {...},
})
await prisma.$transaction([
...
await prisma.cart.findMany({
// context: {...} should not exist
data: {...},
})
], {context: {...})
Alternatives
As suggested by @psugihara, the RLS could be implemented ‘virtually’ within prisma for unsupported databases.
Additional context
I’ve tried to implement it off prisma and ended up making a full report of the adventure here: https://github.com/prisma/prisma/issues/5128#issuecomment-1059814093 I’d extremely encourage going over all points I’ve made.
Issue Analytics
- State:
- Created a year ago
- Reactions:60
- Comments:30 (5 by maintainers)
Top GitHub Comments
(We are talking to Supabase about this as well, and currently looking in how their API layer (PostgREST, which the Supabase JS SDK talks to) implements this under the hood.)
What if the new security attribute worked more like a
where
query.I think this would be beneficial as:
Some rules for use:
context security
must be defined to use@@security
otherwise schema is invalid@@security
attribute must have a namecontext: {security}
can be added to queries, must matchcontext security
structureUsing the above examples and converting to this approach. It would look like:
This would generate for PostgreSQL
When querying, a context with security can optionally be added