ObjectStackObjectStack

Permissions & Identity

Authentication, authorization, and record- and field-level access control in ObjectStack — a cross-protocol capability enforced by the ObjectStack runtime and declared as ObjectQL security metadata.

Permissions & Identity

This module covers authentication, authorization, and record- and field-level access control. It is a cross-protocol capability: enforced by the ObjectStack runtime, declared as ObjectQL security metadata.

The model has five concepts, one reading each (ADR-0090): additive permission sets are the only capability container; flat positions (岗位) distribute sets to people; the business-unit tree (plus the manager chain) is the one hierarchy and decides how deep you see; each object's Organization-Wide Default + sharing set the record baseline (sharing only widens); row-level security is the expert escape hatch that narrows. Field-level security masks and enforces field rules server-side (fields without a declared rule stay visible by default — see the FLS posture note). Object access scopes go from own through own_and_reports, unit, unit_and_below, to org. There is no Profile concept and the word "role" is reserved (D3) — if you knew the v1 model, start at Profiles (removed) and Positions.

Access rules are metadata like everything else — this is a real sharing rule from the CRM example app:

export const HighValueOpportunitySharingRule = defineSharingRule({
  type: 'criteria',
  name: 'share_high_value_opps_with_managers',
  label: 'High-Value Deals → Sales Managers',
  description: 'Automatically share opportunities over $100,000 with all Sales Managers.',
  object: 'crm_opportunity',
  condition: 'record.amount > 100000',
  accessLevel: 'edit',
  sharedWith: { type: 'position', value: 'sales_manager' },
  active: true,
});

Because AI agents act through the same permission-aware surface, these rules bound agent access exactly as they bound users (Actions as Tools).

Implementation status — Permission Model v2 (ADR-0090) is live. REST → ObjectQL propagates a populated ExecutionContext (userId, tenantId, positions, permissions, principalKind) into the SecurityPlugin middleware, so CRUD / FLS / RLS checks fire on every authenticated request; authenticated principals implicitly hold the everyone position and anonymous principals hold guest (D9). The member_default baseline is additive (D5 — no fallback cliff) and its owner-write policies are domained to org_member holders. The default member_default set ships the wildcard tenant-isolation RLS (organization_id == current_user.organization_id) with the same canonical-name contract as before; SecurityPlugin is the sole authority for tenant isolation, and analytics auto-bridges to security.getReadFilter. Anonymous traffic is denied by default (ADR-0056 D2), and public forms self-authorize via a declaration-derived publicFormGrant. An unset OWD fails closed to private (D1) and the D7 publish linter makes it a build error. Criteria sharing rules (with position / unit_and_subordinates recipients) are live and dogfood-proven; owner-type rules and group recipients are declared but not enforced — the seeder skips them with a warning (Sharing Rules). RBAC-table writes are governed by the delegated-admin gate (D12), and the security service answers explain(request) per evaluation layer (D6). The Studio RLS visual editor, per-user×org permission cache, and audit UI for denied access are queued. See Implementation Status for the latest matrix.

What's in this module

Spec: Security & Access Control · Schema reference: Security, Identity


Security Architecture

Authentication is the first layer. This guide covers authorization — who can see and do what once signed in (RBAC, RLS, FLS, sharing). Harden authentication first — password policy, enforced MFA, account lockout, session controls, and IP allowlist — in Enterprise Authentication Hardening.

ObjectStack implements a multi-layered security model:

┌─────────────────────────────────────┐
│   Application Visibility            │ ← Which apps can users access?
├─────────────────────────────────────┤
│   Object Permissions                │ ← CRUD on objects
├─────────────────────────────────────┤
│   Record-Level Security             │ ← Which records can be accessed?
│   - Organization-Wide Defaults      │
│   - Scope-Depth Grants (readScope)  │
│   - Sharing Rules                   │
│   - Manual Sharing                  │
├─────────────────────────────────────┤
│   Field-Level Security              │ ← Read/Write specific fields
└─────────────────────────────────────┘

Security Layers

  1. Object Permissions: Control create, read, update, delete on entire objects
  2. Record-Level Security: Control access to specific records
  3. Field-Level Security: Control visibility and editability of fields
  4. Row-Level Security: Control access based on data values

Best Practices

1. Permission Sets

DO:

  • Keep each set a coherent capability ("Advanced Reporting", "HR Self-Service")
  • Start restrictive — the model is additive, so grant by adding sets
  • Put org-wide defaults on the everyone anchor (low-privilege only)
  • Remove grants when no longer needed

DON'T:

  • Author "subtraction sets" — restriction is done by not granting
  • Grant viewAllRecords/modifyAllRecords "just in case"
  • Ship a '*' wildcard superuser in a package (the linter rejects it)

2. Positions

DO:

  • Model job functions (岗位), not departments — keep the list short and flat
  • Bind capability through sets; anchor assignments to a business unit
  • Use delegated adminScopes so subsidiaries manage their own assignments

DON'T:

  • Recreate a reporting tree on positions — the hierarchy lives on business units
  • Use positions as sharing groups — that is what teams are for

3. Business Units & Depth

DO:

  • Mirror your organizational structure on the BU tree
  • Widen visibility with readScope/writeScope depth grants
  • Review the tree after reorgs

DON'T:

  • Create overly deep hierarchies
  • Grant org depth when unit_and_below suffices

4. Sharing Rules

DO:

  • Start with most restrictive OWD
  • Use sharing rules to open up access
  • Document business justification
  • Test thoroughly

DON'T:

  • Set OWD to Public unless necessary
  • Create redundant sharing rules
  • Grant more access than needed

5. Field-Level Security

DO:

  • Protect sensitive data (SSN, salary, etc.)
  • Use read-only for calculated fields
  • Document security classifications
  • Audit regularly

DON'T:

  • Hide required fields
  • Restrict access unnecessarily
  • Forget about API access

Security Checklist

Initial Setup

  • Harden authentication (password policy, enforced MFA, lockout, session controls, IP allowlist) — see Authentication Hardening
  • Model the business-unit tree and positions (岗位)
  • Author permission sets and bind them to positions
  • Set each object's organization-wide default (unset = private, and a build error)
  • Configure field-level security
  • Create sharing rules
  • Commit access-matrix.json so capability changes become reviewable diffs

Ongoing Maintenance

  • Review user access quarterly
  • Audit permission changes
  • Remove inactive users promptly
  • Update sharing rules as needed
  • Monitor security health checks

Compliance

  • Document security model
  • Maintain access request process
  • Log security changes
  • Conduct security reviews
  • Train users on security policies

Real-World Example

Complete security setup for a sales team:

// 1. Organization-Wide Defaults (one OWDModel value per object)
const OrganizationDefaults = {
  account: 'private',
  opportunity: 'private',
  contact: 'controlled_by_parent',
};

// 2. Positions (flat — the hierarchy lives on business units)
const positions = [
  { name: 'sales_manager', label: 'Sales Manager' },
  { name: 'sales_rep', label: 'Sales Rep' },
];

// 3. Permission set (bound to the sales_rep position at install/setup)
const SalesRepAccess = {
  name: 'sales_rep_access',
  objects: {
    account: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false, readScope: 'unit' },
    opportunity: { allowCreate: true, allowRead: true, allowEdit: true, allowDelete: false },
  },
  fields: {
    'account.annual_revenue': { readable: true, editable: false }, // Read-only
  },
};

// 4. Sharing Rule (criteria type with a CEL condition)
const AccountSharingRule = {
  name: 'high_value_accounts',
  type: 'criteria',
  object: 'account',
  // Share high-value accounts with sales reps
  condition: P`record.annual_revenue >= 1000000`,
  sharedWith: { type: 'position', value: 'sales_rep' },
  accessLevel: 'read',
};

// 5. Permission Set (systemPermissions is a string array)
const AdvancedReportingPermissionSet = {
  name: 'advanced_reporting',
  objects: {},
  // For sales analysts
  systemPermissions: ['run_reports', 'export_reports', 'view_all_data'],
};

Next: Automation →

On this page