Skip to content

Access Control

Auto-generated from knowledge base TOML files by docs_gen.py. Do not edit manually; run dazzle docs generate to regenerate.

DAZZLE uses Cedar-style access rules with three layers: entity-level permit/forbid blocks, surface-level access restrictions, and workspace-level persona allow/deny lists. Default policy is deny. This page covers access rule syntax, authentication integration, and RBAC patterns.


Access Rules

Inline access control rules on entities. Cedar-style syntax uses two separate blocks: permit: (WHO may access — role checks only) and scope: (WHAT they can see — row-level field conditions with for: clauses). Scope rules compile to a formal predicate algebra with static FK graph validation. Legacy access: blocks are still supported. Evaluation order: FORBID > PERMIT > default-deny. Field conditions in permit: are a parser error — they must live in scope: blocks.

Syntax

# Two-block pattern (recommended): separate WHO from WHAT
permit:
  <action>: role(<name>)        # WHO — pure role checks only

scope:
  <action>: <field-condition>   # WHAT — row-level filter
    for: <role>, <role>         # which roles this filter applies to
  <action>: all                 # 'all' means no filter (full table access)
    for: <role>

# forbid: and audit: blocks
forbid:
  <action>: role(<name>)        # separation-of-duty constraints

audit:
  <action>: role(<name>)        # compliance logging

# permit: expressions — role checks only (field conditions are a parser error here):
# role(<name>) - User has the specified role
# role(a) or role(b) - Either role

# scope: expressions — field conditions only (compile to predicate algebra):
# field = current_user - Field matches logged-in user
# field = current_user.field - Field matches a property of the logged-in user
# field = value - Field equals literal value
# parent.field = current_user - FK path traversal (depth-N, nested subquery)
# via Entity(field = current_user, field = id) - EXISTS subquery (junction table)
# not via Entity(field = current_user, field = id) - NOT EXISTS (exclusion)
# not (condition) - Parenthesised negation
# Combine with: and, or — all boolean logic compiles to SQL

# Cedar-style actions: list, read, create, update, delete, approve, prescribe, etc.
# Cedar evaluation: FORBID rules override PERMIT; default is deny

# Legacy access: block (read/write permissions — still supported)
access:
  read: <condition>
  write: <condition>

Example

# Two-block pattern: permit: gates WHO, scope: gates WHAT
entity Shape "Shape":
  realm: ref Realm required
  creator: ref User required
  colour: enum[red,blue,green] required

  # WHO may access — role checks only
  permit:
    list: role(oracle)
    read: role(oracle)
    create: role(oracle) or role(forgemaster)
    update: role(oracle)
    delete: role(oracle)

  # Separation of duty
  forbid:
    delete: role(forgemaster)

  # WHAT they can see — row-level filters with for: clauses
  scope:
    list: all                              # oracle sees everything
      for: oracle
    read: all
      for: oracle
    create: all
      for: oracle
    create: realm = current_user.realm     # forgemaster scoped to their realm
      for: forgemaster
    update: all
      for: oracle
    delete: all
      for: oracle
    list: realm = current_user.realm or creator = current_user
      for: forgemaster
    read: realm = current_user.realm or creator = current_user
      for: forgemaster

# Cedar-style fine-grained RBAC with forbid: and audit:
entity Prescription "Prescription":
  patient: ref Patient required
  prescriber: ref Doctor required
  status: enum[draft,active,dispensed,cancelled]=draft

  permit:
    read: role(doctor) or role(pharmacist) or role(nurse)
    prescribe: role(doctor)
    dispense: role(pharmacist)

  forbid:
    prescribe: role(pharmacist)
    dispense: role(doctor)

  audit:
    read: role(admin)
    prescribe: role(compliance_officer)
    dispense: role(compliance_officer)

# Legacy style (still supported)
entity Document "Document":
  owner: ref User required
  is_public: bool = false

  access:
    read: owner = current_user or is_public = true or role(admin)
    write: owner = current_user or role(admin)

Best Practices

  • Use = for equality (not ==)
  • Start with restrictive rules, expand as needed
  • permit: contains role checks only — field conditions belong in scope:
  • scope: rules use for: clauses to bind row-level filters to specific roles
  • Use 'all' in scope: for roles that should see the full table (e.g., admins)
  • Use for: * in scope: to apply a filter to all permitted roles
  • Cedar evaluation order: FORBID > PERMIT > default-deny
  • Use audit: blocks for compliance logging of sensitive actions
  • Use forbid: for separation-of-duty constraints (e.g., prescriber cannot dispense)

Related: Entity, Persona, Invariant, Cedar Rbac


Ownership Pattern

Row-level ownership filtering for personal data. Uses scope: blocks (not permit:) because ownership is a row-level concern. The permit: block controls WHO (role checks), the scope: block controls WHICH ROWS (field conditions). There is no 'owner' keyword — use the actual field name with '= current_user'.

Syntax

entity ReadingProgress "Reading Progress":
  id: uuid pk
  user_id: ref User required
  work: ref Work required
  chapter: int = 1
  progress_pct: float = 0.0

  permit:
    create: role(reader) or role(author) or role(admin)
    read: role(reader) or role(author) or role(admin)
    update: role(reader) or role(author) or role(admin)
    delete: role(admin)

  scope:
    read: user_id = current_user
      for: reader, author
    update: user_id = current_user
      for: reader, author
    read: all
      for: admin

Best Practices

  • Always pair ownership permit: rules with a scope: block that filters by the owner field
  • Use 'all for: admin' in scope: to give admins visibility to all rows
  • Name the ownership field explicitly (user_id, not just 'user') for clarity
  • Add a ref constraint on the ownership field to link to the User entity

Related: Access Rules, Scope Runtime, Cedar Rbac


Visibility Rules

Row-level visibility rules on entities. Controls which records are visible based on authentication state (anonymous or authenticated). Evaluated before permission rules to determine which records a user can see in list/query results. Uses ConditionExpr for the filter condition.

Syntax

visible:
  when anonymous: <condition>
  when authenticated: <condition>

# Auth contexts:
#   anonymous     - User is not logged in
#   authenticated - Any logged-in user

# Condition is a standard ConditionExpr:
#   field = value, field != value, role(name), field = current_user
#   Combine with: and, or

Example

entity Document "Document":
  id: uuid pk
  title: str(200) required
  is_public: bool = false
  created_by: ref User

  visible:
    when anonymous: is_public = true
    when authenticated: is_public = true or created_by = current_user

entity Post "Blog Post":
  id: uuid pk
  published: bool = false
  author: ref User required

  visible:
    when anonymous: published = true
    when authenticated: published = true or author = current_user

Best Practices

  • Use anonymous visibility for public-facing content
  • Authenticated visibility should include the owner check (created_by = current_user)
  • Combine with permission rules for full access control
  • Visibility filters apply at query time - no data leaks in list views

Related: Access Rules, Entity, Persona, Surface Access


Surface Access

Access control on surfaces (UI screens). Controls who can view and interact with a surface. Three levels: public (no auth), authenticated (any logged-in user), or persona-restricted (named personas only). Used by E2E test generators to create protected-route tests.

Syntax

surface <name> "<Title>":
  ...
  access: public
  access: authenticated
  access: persona(<name1>, <name2>, ...)

# access: public          → require_auth=false, anyone can access
# access: authenticated   → require_auth=true, any logged-in user
# access: persona(Admin, Manager) → require_auth=true, only listed personas

Example

# Public surface - no login required
surface landing_page "Welcome":
  uses entity Page
  mode: view
  access: public

# Authenticated - any logged-in user
surface my_tasks "My Tasks":
  uses entity Task
  mode: list
  access: authenticated

# Persona-restricted - only these roles
surface admin_panel "Admin Panel":
  uses entity User
  mode: list
  access: persona(Admin, SuperAdmin)

Best Practices

  • Default to authenticated when auth is enabled globally
  • Use persona() for admin-only or role-specific screens
  • Public surfaces should only show non-sensitive data
  • Pair with entity-level access rules for defense in depth

Related: Access Rules, Surface, Persona, Workspace Access, Visibility Rules


Workspace Access

Access control on workspaces. Defines who can access a workspace dashboard. Three levels: public, authenticated, or persona-restricted. Default is authenticated when auth is enabled globally. Syntax is the same as surface access.

Syntax

workspace <name> "<Title>":
  ...
  access: public
  access: authenticated
  access: persona(<name1>, <name2>, ...)

# access: public          → level=public, no login required
# access: authenticated   → level=authenticated, any logged-in user (default)
# access: persona(Admin)  → level=persona, only listed personas can access

Example

# Public dashboard - visible without login
workspace public_metrics "Public Metrics":
  purpose: "Public-facing KPI dashboard"
  access: public

  metrics:
    aggregate:
      total_users: count(User)
    display: metrics

# Admin-only workspace
workspace admin_dashboard "Admin Dashboard":
  purpose: "System administration"
  access: persona(Admin, SuperAdmin)

  users:
    source: User
    display: list
    action: user_edit

# Default: authenticated (explicit for clarity)
workspace team_board "Team Board":
  purpose: "Team task overview"
  access: authenticated

  tasks:
    source: Task
    filter: status != closed
    sort: priority desc
    display: list

Best Practices

  • Use persona() for role-specific dashboards
  • Public workspaces should aggregate non-sensitive data only
  • Default is authenticated - omit access: if that is sufficient

Related: Access Rules, Workspace, Persona, Surface Access


Authentication

Session-based authentication system in Dazzle. Uses cookie-based sessions with SQLite storage. Auth is optional and can be enabled/disabled per project.

Example

# In DSL, use personas to define role-based access:
ux:
  for admin:
    scope: all
  for member:
    scope: owner = current_user

# API login:
POST /auth/login
Content-Type: application/json
{"username": "admin", "password": "secret"}

# Response sets session cookie automatically

Related: Persona, Scope