Skip to main content
Version: 3.x

Field-Level Policies

Available since v3.2.0
Preview Feature
Field-level policy is in preview and may be subject to breaking changes in future releases.

Field-level policies allow you to define access control rules at the individual field level within a model. This provides fine-grained control over who can read or write specific fields in your data models. To define field-level policies, use the @allow and @deny attributes directly on model fields (note the single @).

model User {
id Int @id

// email can be updated only by the user themselves
email String @allow('update', auth() == this)

// name cannot be read by anonymous users
name String @deny('read', auth() == null)
}

Field-level policies are similar to model-level ones, with the following key restrictions:

  • Only "read" and "update" operations are supported. You can use "all" to denote both.
  • They cannot be defined on relation fields or computed fields.

Read Behavior​

When reading a row, fields that violates "read" policies will be nullified in the result. Conceptually, the following form of SQL is generated to guard the fields:

SELECT
CASE WHEN <read_policy_for_field_1> THEN field_1 ELSE NULL END AS field_1,
...
FROM table
WHERE <model_level_policies> and <other_conditions>;

If read policies are defined on foreign key fields, they will also control the readability of the corresponding relations.

info

Setting unreadable fields null brings a caveat that you cannot tell whether a field is actually NULL in the database or just unreadable due to access control. So why don't we instead omit the fields from the result?

The concern is that a non-readable field should still have a valid SQL value, because it can be used to compute other data (computed columns, joins, etc.). With null values, the computation remain valid in SQL (e.g., NULL + 1 results in NULL), so the fields remain usable everywhere even though their actual values cannot be seen.

Update Behavior​

When updating data, if an update involves setting fields that violate "update" policies, the entire update operation will be rejected with an ORMError with reason set to REJECTED_BY_POLICY.

Samples​

Open in StackBlitz
field-level/zenstack/schema.zmodel
datasource db {
provider = 'sqlite'
}

plugin policy {
provider = '@zenstackhq/plugin-policy'
}

model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]

@@allow('all', true)
}

model Post {
id Int @id @default(autoincrement())
title String @allow('read', published) @allow('update', auth() == author)
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?

@@allow('all', true)
}
field-level/main.ts
import { PolicyPlugin } from '@zenstackhq/plugin-policy';
import { createClient } from '../db';
import { schema } from './zenstack/schema';

async function main() {
const db = await createClient(schema);

// create users and posts with raw client
const alice = await db.user.create({
data: {
email: 'alice@example.com',
posts: {
create: [
{ id: 1, title: 'Alice Published Post', published: true },
{ id: 2, title: 'Alice Draft Post', published: false }
]
}
}
});

const bob = await db.user.create({
data: {
email: 'bob@example.com'
}
});

// install policy plugin
const authDb = db.$use(new PolicyPlugin());

// create user-bound clients
const aliceDb = authDb.$setAuth(alice);
const bobDb = authDb.$setAuth(bob);

// query posts as Alice, only published posts' titles are visible
console.log('Alice sees posts:');
console.table(await aliceDb.post.findMany());

// updating title with Bob should fail
try {
await bobDb.post.update({
where: { id: 1 },
data: { title: 'Hacked Title' }
});
} catch (err) {
console.log(`Bob failed to update post title as expected, ${err}`);
}

// updating title with Alice should succeed
const updated = await aliceDb.post.update({
where: { id: 1 },
data: { title: 'Alice Updated Post' }
});
console.log('Alice successfully updated her post title.');
console.table(updated);
}

main();
Comments
Feel free to ask questions, give feedback, or report issues.

Don't Spam


You can edit/delete your comments by going directly to the discussion, clicking on the 'comments' link below