AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Zenstack Access Control

skill-zenstackhq-skills-zenstack-access-control · by zenstackhq

Add access control to a ZenStack V3 schema. Use when writing access policies (@@allow / @@deny), the auth() function, field-level rules, post-update rules, or setting up the policy plugin and $setAuth runtime. (For data validation — @email/@length/@@validate — see zenstack-querying.)

No reviews yet
0 installs
29 views
0.0% view→install

Install

$ agentstack add skill-zenstackhq-skills-zenstack-access-control

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-zenstackhq-skills-zenstack-access-control)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Zenstack Access Control? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

ZenStack V3 — Access Control

ZenStack enforces access policies declared in ZModel at the ORM layer. Policies live next to your models (see zenstack-schema-modeling); enforcement is opt-in via a runtime plugin. For client/query basics — and for data validation — see zenstack-querying.

After editing policies, run zen generate to regenerate the client (it also validates the schema, policy expressions included). If you only want to validate without regenerating, run zen check.

Runtime setup (required for policies to apply)

Policies do nothing until you (1) declare the plugin in the schema and (2) install it on the client.

Schema:

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

Install the package: npm install @zenstackhq/plugin-policy.

Client:

import { ZenStackClient } from '@zenstackhq/orm';
import { PolicyPlugin } from '@zenstackhq/plugin-policy';

const db = new ZenStackClient(schema, { dialect });   // raw, unprotected
export const authDb = db.$use(new PolicyPlugin());     // access control enforced

ZenStackClient instances are immutable$use() and $setAuth() return new (cheap, shallow- clone) instances and never mutate the original. Query with db to bypass policies (e.g. trusted server code) and with the policy-enforced client for user-facing access.

The current user — auth() and $setAuth()

Bind the authenticated user per request; auth() in policies resolves to it.

const userDb = authDb.$setAuth(user);   // user-bound client
await userDb.post.findMany();           // policies evaluated against `user`
  • No $setAuth() → anonymous mode → auth() is null. $setAuth(undefined) forces anonymous.
  • Read the bound user back via client.$auth.

Define what auth() returns, by priority: a model/type annotated @@auth, otherwise a model literally named User, otherwise a compile error. Using a type decouples the auth shape from your tables:

type Auth {
    id   Int
    role String
    @@auth
}

Policy rules — @@allow / @@deny

@@allow(operation, condition)
@@deny(operation, condition)

Operations (comma-separate to combine): create, read, update, delete, post-update, and all (= create+read+update+delete, not post-update).

Evaluation (secure by default): any matching @@deny → denied; else any matching @@allow → allowed; else denied. read/update/delete silently filter out rows that fail, rather than erroring; create is checked before insert.

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

    @@allow('create,read', true)        // anyone can sign up; profiles public
    @@allow('all', auth().id == id)     // the user has full access to themselves
}

model Post {
    id        Int     @id @default(autoincrement())
    title     String
    published Boolean @default(false)
    author    User    @relation(fields: [authorId], references: [id])
    authorId  Int

    @@deny('all', auth() == null)       // no anonymous access
    @@allow('read', published)          // published posts readable by anyone
    @@allow('all', auth().id == authorId) // author has full access
}

Policy expressions

  • Operators: == != > >= < <=, && || !, and field references.
  • To-one relations: dot access — auth().city == profile.address.city.
  • To-many relations: collection predicates
  • relation?[cond]some match
  • relation![cond]all match
  • relation^[cond]none match

``zmodel @@deny('delete', posts?[published == true]) // can't delete a user with published posts ``

  • Use this to escape the collection scope: comments?[author.verified && this.published].
  • create rules can only reference owned relations (those whose FK the model holds) — other

relations don't exist yet at creation time.

  • check(relation [, op]) delegates the check to a to-one relation's policies, avoiding

duplication: @@allow('read', check(user)) / @@allow('update', check(user, 'update')).

  • Helper functions usable in expressions: contains, startsWith, endsWith (each takes an

optional case-insensitive flag), has, hasSome, hasEvery, isEmpty, currentModel(), currentOperation().

Field-level policies (v3.2.0+, preview)

Single-@ @allow / @deny on a field, operations read and update only.

model User {
    id    Int    @id
    email String @allow('update', auth() == this)  // only the owner can change it
    name  String @deny('read', auth() == null)     // hidden from anonymous readers
}
  • Failing a read policy nullifies the field (returned as null, still usable in SQL

computations) — you can't distinguish a real null from a denied one.

  • Failing an update policy rejects the whole update (ORMError, reason REJECTED_BY_POLICY).

Post-update rules

post-update expresses conditions that must hold after an update; field references see the new values, and before() reads the old values. (This replaces V2's future().)

model Post {
    published Boolean @default(false)
    authorId  Int

    @@deny('post-update', published == true && auth().id != authorId)  // only author can publish
    @@deny('post-update', before().authorId != authorId)               // authorId is immutable
}

If a model has no post-update rule, an update only needs to pass update. If it has any, at least one @@allow('post-update', ...) must pass.

Execution order & error reasons

Per mutation, policies are checked before the database is touched (after any input validation — see zenstack-querying). Policy-related ORMError.reason values: REJECTED_BY_POLICY (policy) and NOT_FOUND (missing or filtered out by a read policy). REJECTED_BY_POLICY further carries rejectedByPolicyReason: NO_ACCESS, CANNOT_READ_BACK (mutation allowed but result not readable back), or OTHER.

Limitations

  • Database-level cascade deletes/updates and triggers are not policed by ZenStack.
  • Raw SQL ($queryRaw/$executeRaw) and query-builder access bypass policies. Allow raw SQL while

the policy plugin is installed only via new PolicyPlugin({ dangerouslyAllowRawSql: true }) (v3.5.0+).

Reference docs

Full ZenStack documentation for this topic is bundled under [references/](references/):

  • [access-control-overview.md](references/access-control-overview.md) — how access control works
  • [write-policies.md](references/write-policies.md) — @@allow/@@deny, auth(), expressions
  • [enforcing-policies.md](references/enforcing-policies.md) — policy plugin + $setAuth runtime
  • [post-update.md](references/post-update.md) — post-update rules and before()
  • [field-level.md](references/field-level.md) — field-level read/update policies
  • [zmodel-expression-reference.md](references/zmodel-expression-reference.md) — policy expression reference
  • [policy-plugin-reference.md](references/policy-plugin-reference.md) — policy plugin options

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.