1 Granular Access Control
Anton Nesterov edited this page 2026-02-22 22:54:39 +01:00

Granular Access Control

VSKI provides field-level access control that allows you to control which fields are visible or editable by different user groups. This works in addition to the standard collection-level rules.

Overview

Granular access control gives you fine-grained control over:

  • View Rules (fieldRules) - Control which fields users can READ (view)
  • Edit Rules (fieldEditRules) - Control which fields users can WRITE (create/update)

Key Benefits

  1. Data Privacy - Hide sensitive fields (like passwords, internal notes) from certain user groups
  2. Data Integrity - Prevent users from modifying critical fields (like status, approval flags)
  3. Flexible Security - Override global rules with field-level restrictions
  4. Group-Based - Different access levels for different user roles

How Field Rules Work

Rule Priority

Field-level rules have higher priority than global collection rules:

Collection-level rules:
  ├─ listRule
  ├─ viewRule
  ├─ createRule
  ├─ updateRule
  ├─ deleteRule
  └─ executeRule

Field-level rules (HIGHER PRIORITY):
  ├─ fieldRules (VIEW)      → Overrides viewRule, listRule
  └─ fieldEditRules (EDIT)   → Overrides createRule, updateRule

When field rules are defined for a user's groups, global rules are bypassed.

Rule Format

Field rules use the following JSON format:

{
  "groupName1": ["field1", "field2", "field3"],
  "groupName2": ["*"],  // Wildcard = all fields
  "*": ["publicField"]    // Public group applies to all users
}

System Fields

The following system fields are always included in view rules and always read-only in edit rules:

  • id - Record identifier
  • created - Creation timestamp
  • updated - Last update timestamp

These fields cannot be removed from field rules.

View Rules (fieldRules)

View rules control READ access - which fields users can see when listing or viewing records.

Creating Collection with View Rules

await client.settings.collections.create({
  name: "user_profiles",
  type: "base",
  fields: [
    { id: crypto.randomUUID(), name: "username", type: "text", required: true },
    { id: crypto.randomUUID(), name: "email", type: "email", required: true },
    { id: crypto.randomUUID(), name: "phone", type: "text" },
    { id: crypto.randomUUID(), name: "ssn", type: "text" }, // Sensitive!
    { id: crypto.randomUUID(), name: "notes", type: "text" }, // Internal!
  ],
  listRule: "1=1",
  viewRule: "1=1",
  createRule: "1=0", // Admin only
  updateRule: "1=0",
  deleteRule: "1=0",

  // Field-level view rules
  fieldRules: {
    "public": ["username"], // Public can only see username
    "viewer": ["username", "email", "phone"], // Viewers can't see SSN or notes
    "admin": ["*"], // Admins see everything
  },
});

How View Rules Affect Queries

When a user queries records with field-level restrictions:

// User in "viewer" group
const records = await client.collection("user_profiles").getList(1, 10);

// Result - only username, email, phone, id, created, updated
// SSN and notes are filtered out automatically
[
  {
    id: "abc123",
    username: "john_doe",
    email: "john@example.com",
    phone: "+1234567890",
    created: "2026-02-22T10:00:00Z",
    updated: "2026-02-22T10:00:00Z",
    // ssn and notes are NOT included
  },
];

View Rules Bypass Global Rules

If a user has field-level view access, global viewRule and listRule are bypassed:

// Global rule says "no access"
viewRule: "1=0",  // No one can view (except admin)

// But field rules give access
fieldRules: {
  "viewer": ["title"]  // Viewers can see title
}

// Result: User in "viewer" group CAN view records
// despite global viewRule being "1=0"

Edit Rules (fieldEditRules)

Edit rules control WRITE access - which fields users can set when creating or updating records.

Creating Collection with Edit Rules

await client.settings.collections.create({
  name: "support_tickets",
  type: "base",
  fields: [
    { id: crypto.randomUUID(), name: "title", type: "text", required: true },
    {
      id: crypto.randomUUID(),
      name: "description",
      type: "text",
      required: true,
    },
    { id: crypto.randomUUID(), name: "status", type: "text", required: true }, // Workflow field
    { id: crypto.randomUUID(), name: "priority", type: "text", required: true }, // Workflow field
    { id: crypto.randomUUID(), name: "resolution", type: "text" },
  ],
  listRule: "1=1",
  viewRule: "1=1",
  createRule: "1=0",
  updateRule: "1=0",
  deleteRule: "1=0",

  // Field-level edit rules
  fieldEditRules: {
    "customer": ["title", "description"], // Customers can only set title, description
    "agent": ["title", "description", "resolution"], // Agents can't change status/priority
    "admin": ["*"], // Admins can edit everything
  },
});

Creating Records with Edit Restrictions

// User in "customer" group
const record = await client.collection("support_tickets").create({
  title: "Login issue",
  description: "I can't log in",
  status: "open",        // IGNORED - not in customer's edit list
  priority: "high",     // IGNORED - not in customer's edit list
  resolution: ""          // IGNORED - not in customer's edit list
});

// Result: Record created with only allowed fields
{
  id: "ticket-123",
  title: "Login issue",
  description: "I can't log in",
  status: "open",  // Set by DB default or trigger
  priority: "normal", // Set by DB default or trigger
  resolution: null,
  created: "2026-02-22T10:00:00Z",
  updated: "2026-02-22T10:00:00Z"
}

Updating Records with Edit Restrictions

// User in "agent" group
const updated = await client.collection("support_tickets").update(
  "ticket-123",
  {
    title: "Updated title", // ALLOWED
    description: "Updated description", // ALLOWED
    resolution: "Fixed by password reset", // ALLOWED
    status: "closed", // IGNORED - not in agent's edit list
    priority: "low", // IGNORED - not in agent's edit list
  },
);

// Result: Only title, description, resolution are updated
// status and priority remain unchanged

Edit Rules and Required Fields

If a required field is not in a user's edit field list, the server logs a warning but allows the operation. The field may still get a value through:

  1. Database default values
  2. Trigger hooks
  3. Migration defaults

Example:

// Field "status" is required
fields: [
  { name: "status", type: "text", required: true }
]

// But customer group can't edit it
fieldEditRules: {
  "customer": ["title", "description"]  // status NOT included
}

// Customer creates record - warning logged but allowed
await client.collection("tickets").create({
  title: "Issue",
  description: "Details"
  // status not provided, but DB has default "open"
});

// Server logs: "Creating record with required fields not in allowed edit fields: status"
// But record is created successfully with status = "open" (default)

Edit Rules Bypass Global Rules

If a user has field-level edit access, global createRule and updateRule are bypassed:

// Global rule says "no access"
createRule: "1=0",  // No one can create (except admin)
updateRule: "1=0",  // No one can update (except admin)

// But field rules give access
fieldEditRules: {
  "creator": ["title"]  // Creators can create/edit title
}

// Result: User in "creator" group CAN create/update records
// despite global createRule/updateRule being "1=0"

Using Wildcards

The * character has special meanings:

Wildcard in Group Name ("*")

The "*" group represents all users, including unauthenticated (guest) users:

fieldRules: {
  "*": ["title", "description"]  // Everyone can see these
  "admin": ["*"]  // Admins see everything
}

Wildcard in Field List (["*"])

A field list containing only ["*"] means all fields are allowed:

fieldEditRules: {
  "editor": ["*"]  // Editors can edit every field
  "viewer": ["title", "description"]  // Viewers restricted
}

Group Priority and Union

Users can belong to multiple groups. Field rules are combined using union logic:

fieldRules: {
  "*": ["title"],              // Public - everyone gets this
  "team_member": ["description"],  // Team members get this
  "admin": ["notes", "internal"]  // Admins get this
}

// User in "team_member" group:
// Can see: title (from *) + description (from team_member)
// Cannot see: notes, internal (only admin)

Practical Examples

HR System

Public can see name/title, HR sees salary, managers see all:

await client.settings.collections.create({
  name: "employees",
  type: "base",
  fields: [
    { id: crypto.randomUUID(), name: "name", type: "text", required: true },
    { id: crypto.randomUUID(), name: "title", type: "text", required: true },
    {
      id: crypto.randomUUID(),
      name: "department",
      type: "text",
      required: true,
    },
    { id: crypto.randomUUID(), name: "salary", type: "number" }, // Sensitive!
    { id: crypto.randomUUID(), name: "performance_notes", type: "text" }, // Internal!
  ],
  listRule: "1=0",
  viewRule: "1=0",
  createRule: "1=0",
  updateRule: "1=0",
  deleteRule: "1=0",

  fieldRules: {
    "public": ["name", "title"], // Anyone can see name/title
    "hr": ["name", "title", "department", "salary"], // HR can see salary
    "manager": ["*"], // Managers see everything
  },

  fieldEditRules: {
    "hr": ["name", "title", "department", "salary"], // HR can edit these
    "manager": ["*"], // Managers can edit everything
  },
});

Customer Portal

Customers can read limited data, support can read all:

await client.settings.collections.create({
  name: "orders",
  type: "base",
  fields: [
    {
      id: crypto.randomUUID(),
      name: "customerId",
      type: "text",
      required: true,
    },
    { id: crypto.randomUUID(), name: "items", type: "json", required: true },
    { id: crypto.randomUUID(), name: "total", type: "number", required: true },
    { id: crypto.randomUUID(), name: "status", type: "text", required: true },
    { id: crypto.randomUUID(), name: "internal_notes", type: "text" }, // Internal!
  ],
  listRule: "1=0",
  viewRule: "1=0",
  createRule: "1=0",
  updateRule: "1=0",
  deleteRule: "1=0",

  fieldRules: {
    "*": ["customerId", "items", "total", "status"], // Public can't see notes
    "support": ["*"], // Support sees everything
  },

  fieldEditRules: {
    "customer": [], // Customers can't create/edit (only view)
    "support": ["status", "internal_notes"], // Support can update these
  },
});

Content Management System

Editors can create content, editors can edit content, admins manage everything:

await client.settings.collections.create({
  name: "articles",
  type: "base",
  fields: [
    { id: crypto.randomUUID(), name: "title", type: "text", required: true },
    { id: crypto.randomUUID(), name: "content", type: "text", required: true },
    {
      id: crypto.randomUUID(),
      name: "published",
      type: "bool",
      required: true,
    },
    { id: crypto.randomUUID(), name: "author", type: "text", required: true },
    { id: crypto.randomUUID(), name: "featured", type: "bool" }, // Admin only!
  ],
  listRule: "published = true",
  viewRule: "published = true || author = @request.auth.id",
  createRule: null,
  updateRule: null,
  deleteRule: "1=0",

  fieldEditRules: {
    "author": ["title", "content", "published"], // Authors can't change author or featured
    "editor": ["title", "content", "published"], // Same for editors
    "admin": ["*"], // Admins control everything
  },
});

Web UI Configuration

Field-level permissions are configured in the Collection Settings page:

  1. Navigate to Settings → Collections
  2. Click on a collection to edit
  3. Go to Access tab
  4. Scroll to Field-Level Permissions section

UI Sections

The field permissions UI has two separate sections:

View Rules (blue)

Controls which fields users can READ:

  • Shows which fields are visible for each group
  • System fields (id, created, updated) are always included
  • Badge: "Controls READ visibility"

Edit Rules (orange)

Controls which fields users can CREATE/UPDATE:

  • Shows which fields are editable for each group
  • System fields are read-only
  • Warnings when required fields are missing from a group's allowed list
  • Badge: "Controls CREATE/UPDATE access"

Managing Group Rules

For each group (or * for public):

  1. Add Group Rule - Create a new permission group
  2. Toggle Fields - Click field buttons to add/remove from allowed list
  3. All Fields (*) - Click to allow all fields for this group
  4. Remove Group - Delete a permission group

Required Field Warnings

When a required field is not in a group's edit rules:

Required fields not editable: field1, field2

Users in this group won't be able to set these fields during create/update.

The record can still be created if the field has a default value or is set via triggers.

API Reference

Field rules are included in collection configuration:

interface CollectionConfig {
  // ... other properties
  fieldRules?: Record<string, string[]>; // View rules
  fieldEditRules?: Record<string, string[]>; // Edit rules
}

Creating Collection with Field Rules

await client.settings.collections.create({
  name: "my_collection",
  type: "base",
  fields: [...],
  fieldRules: {
    "viewer": ["title", "description"]
  },
  fieldEditRules: {
    "viewer": ["title"]
  }
});

Updating Field Rules

await client.settings.collections.update("my_collection", {
  fieldRules: {
    "viewer": ["title"], // More restrictive
    "editor": ["title", "description", "author"],
  },
  fieldEditRules: {
    "viewer": ["title"],
    "editor": ["title", "description"],
  },
});

Behavior Summary

View Rules (fieldRules)

Scenario Behavior
No rules defined All fields visible (default open)
User has field access Global viewRule/listRule bypassed, filtered fields returned
User has no field access Access denied (empty result)
Wildcard in field list All fields visible
User in multiple groups Union of all group fields visible
Admin user All fields visible (bypasses all rules)

Edit Rules (fieldEditRules)

Scenario Behavior
No rules defined All fields editable (default open)
User has field access Global createRule/updateRule bypassed, only allowed fields updated
User has no field access Access denied (create/update blocked)
Wildcard in field list All fields editable
User in multiple groups Union of all group fields editable
Required field not in list Warning logged, operation proceeds (field may have default)
System fields Always read-only, cannot be edited
Admin user All fields editable (bypasses all rules)

Security Considerations

1. Defense in Depth

Field rules are an additional layer of security:

  • Use field rules in addition to global rules
  • Don't rely solely on field rules for security

2. Default to Deny

When configuring field rules:

  • Start with empty field list (deny all)
  • Add only necessary fields (allow by exception)
  • More secure than starting with ["*"] and removing

3. Required Field Handling

If a required field is not in edit rules:

  • The field may get a default value from DB schema
  • Consider using triggers to set appropriate defaults
  • Or add the field to the user's allowed list

4. System Field Protection

System fields (id, created, updated):

  • Are always read-only for edit rules
  • Cannot be removed from view rules
  • This prevents record tampering

5. Test Access Patterns

Before deploying:

  1. Test with users in each group
  2. Verify field visibility (read operations)
  3. Verify field editing (create/update operations)
  4. Test global rule bypass scenarios
  5. Verify admin user behavior (should bypass all)

6. Monitor Warnings

Server logs warnings when:

  • Required fields are not in edit rules
  • Users attempt to edit restricted fields (silently ignored)
  • Monitor these logs to catch configuration issues

Common Patterns

Public View, Private Edit

{
  fieldRules: {
    "*": ["title", "description"]  // Public can read
  },
  fieldEditRules: {
    "editor": ["title", "description"]  // Only editors can write
  }
}

Progressive Disclosure

{
  fieldRules: {
    "viewer": ["title"],              // Basic info
    "member": ["title", "description"],  // More info
    "admin": ["*"]  // Full access
  }
}

Field Lifecycle Control

{
  fieldRules: {
    "*": ["*"]  // Everyone can read everything
  },
  fieldEditRules: {
    "creator": ["title", "content"],  // Can create initial content
    "editor": ["content"],  // Can edit content only
    "publisher": ["published"]  // Can only change status
  }
}

Troubleshooting

"Access Denied" on List/Get

Problem: User gets access denied error when listing or viewing records.

Solution: Check if user's group is in fieldRules:

// User groups
const userGroups = ["viewer"];

// Check field rules
const collection = await client.settings.collections.get("my_collection");
console.log("Field rules:", collection.fieldRules);

// Does user's group exist in fieldRules?
console.log("Has access:", userGroups.some((g) => collection.fieldRules?.[g]));

Fields Not Saving on Update

Problem: User updates a record but some fields don't change.

Solution: Check fieldEditRules - those fields may not be in allowed list:

// User in "viewer" group
await client.collection("my_collection").update("record-id", {
  title: "Updated", // Saved
  status: "active", // NOT saved - not in viewer's edit rules
});

Required Fields Null After Create

Problem: After creating a record, required fields are null.

Solution: Field may have default value. Check:

  1. Field default in schema
  2. Trigger that sets the value
  3. Or add field to user's fieldEditRules

Wildcard Not Working

Problem: User with "*" in field list can't see all fields.

Solution: Ensure wildcard is in array, not string:

// Wrong
fieldRules: {
  "viewer": "*"
}

// Correct
fieldRules: {
  "viewer": ["*"]
}

Global Rules Still Applying

Problem: Global rules (createRule, updateRule) still apply even with field rules defined.

Solution: Ensure user has ANY field-level access. Field rules override global rules only when user has field-level permission:

// User in "any_group" has field access → global createRule bypassed
fieldRules: {
  "any_group": ["title"]
}

// User in "no_access" has NO field access → global createRule applies
fieldRules: {
  "admin_only": ["*"]
}

See Also