Zap Studio

Conditions

Build complex authorization rules using and(), or(), not(), and has() condition combinators in permit.

Conditions are the building blocks for creating complex authorization rules. permit provides combinators to compose simple conditions into powerful, readable policies.

Understanding Conditions

A condition is a function that evaluates to true or false:

type ConditionFn<TContext, TAction, TResource> = (
  context: TContext,
  action: TAction,
  resource: TResource
) => boolean;

Conditions are used with when() to create policy rules:

when((ctx, action, resource) => /* condition logic */)

Condition Combinators

and()

The and() combinator creates a condition that passes only when all conditions are true. It short-circuits on the first false value.

import { and } from "@zap-studio/permit";

and(...conditions)

Example: Multiple Requirements

import { createPolicy, when, and } from "@zap-studio/permit";

// User must be authenticated AND be the owner AND post must be published
const canEditPublishedPost = and(
  (ctx) => ctx.user !== null,
  (ctx, _, post) => ctx.user?.id === post.authorId,
  (_, __, post) => post.status === "published"
);

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    post: {
      edit: when(canEditPublishedPost),
    },
  },
});

or()

The or() combinator creates a condition that passes when any condition is true. It short-circuits on the first true value.

import { or } from "@zap-studio/permit";

or(...conditions)

Example: Multiple Access Paths

import { createPolicy, when, or } from "@zap-studio/permit";

// User can access if they're the owner OR an admin OR explicitly shared
const canAccess = or(
  (ctx, _, doc) => ctx.user?.id === doc.ownerId,
  (ctx) => ctx.user?.role === "admin",
  (ctx, _, doc) => doc.sharedWith.includes(ctx.user?.id ?? "")
);

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    document: {
      read: when(canAccess),
    },
  },
});

not()

The not() combinator negates a condition.

import { not } from "@zap-studio/permit";

not(condition)

Example: Exclusion Rules

import { createPolicy, when, not, and } from "@zap-studio/permit";

// Cannot interact with your own content
const isNotOwner = not((ctx, _, resource) => ctx.user?.id === resource.authorId);

// Content is not archived
const isNotArchived = not((_, __, resource) => resource.status === "archived");

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    post: {
      // Can like any post except your own
      like: when(isNotOwner),

      // Can comment on posts that aren't archived
      comment: when(isNotArchived),

      // Can report posts that: you don't own AND aren't already reported by you
      report: when(
        and(
          isNotOwner,
          (ctx, _, post) => !post.reportedBy.includes(ctx.user?.id ?? "")
        )
      ),
    },
  },
});

has()

The has() helper creates a condition that checks if a context property equals a specific value.

import { has } from "@zap-studio/permit";

has(key, value)

Example: Simple Property Checks

import { createPolicy, when, has, or } from "@zap-studio/permit";

type AppContext = {
  role: "guest" | "user" | "admin";
  isVerified: boolean;
  plan: "free" | "pro" | "enterprise";
};

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    settings: {
      // Only admins can access settings
      read: when(has("role", "admin")),
    },
    billing: {
      // Only verified users can access billing
      read: when(has("isVerified", true)),
    },
    export: {
      // Pro or Enterprise users can export
      use: when(or(has("plan", "pro"), has("plan", "enterprise"))),
    },
  },
});

Combining Combinators

Combinators can be nested to create complex conditions:

import { createPolicy, when, and, or, not } from "@zap-studio/permit";

const policy = createPolicy<AppContext>({
  resources,
  actions,
  rules: {
    document: {
      // Can edit if:
      // (owner OR admin) AND (not archived) AND (not locked OR is admin)
      edit: when(
        and(
          or(
            (ctx, _, doc) => ctx.user?.id === doc.ownerId,
            (ctx) => ctx.user?.role === "admin"
          ),
          not((_, __, doc) => doc.status === "archived"),
          or(
            not((_, __, doc) => doc.isLocked),
            (ctx) => ctx.user?.role === "admin"
          )
        )
      ),
    },
  },
});

Extracting Reusable Conditions

For cleaner code, extract conditions into named functions:

import { and, or, not } from "@zap-studio/permit";
import type { ConditionFn } from "@zap-studio/permit/types";

type DocContext = {
  user: { id: string; role: string } | null;
};

type Document = {
  ownerId: string;
  status: string;
  isLocked: boolean;
};

// Reusable conditions
const isOwner: ConditionFn<DocContext, string, Document> = (ctx, _, doc) =>
  ctx.user?.id === doc.ownerId;

const isAdmin: ConditionFn<DocContext, string, Document> = (ctx) =>
  ctx.user?.role === "admin";

const isArchived: ConditionFn<DocContext, string, Document> = (_, __, doc) =>
  doc.status === "archived";

const isLocked: ConditionFn<DocContext, string, Document> = (_, __, doc) =>
  doc.isLocked;

// Composed conditions
const canEdit = and(
  or(isOwner, isAdmin),
  not(isArchived),
  or(not(isLocked), isAdmin)
);

// Use in policy
const policy = createPolicy<DocContext>({
  resources,
  actions,
  rules: {
    document: {
      edit: when(canEdit),
    },
  },
});

Best Practices

  1. Name your conditions — Extract complex logic into well-named functions
  2. Keep conditions simple — Each condition should check one thing
  3. Use short-circuit evaluation — Put fast/common checks first in and()/or()
  4. Avoid side effects — Conditions should be pure functions
  5. Document complex logic — Add comments explaining business rules
Edit on GitHub

Last updated on

On this page