ObjectStackObjectStack

Security Permissions Matrix

Visual reference for ObjectStack's security model — permission types, the object × permission-set matrix, field-level security, sharing rules, business-unit depth, and the access-matrix snapshot gate

Security Permissions Matrix

This page provides a comprehensive visual reference for ObjectStack's security model — from object-level permissions to field-level security, sharing rules, and business-unit depth.

Security Model: ObjectStack uses a layered security model inspired by Salesforce. Permissions are evaluated in order: Object Permission → Organization-Wide Defaults → Record Ownership / Scope Depth → Sharing (rules + manual shares) → Row-Level Security → Field-Level Security. Unlike Salesforce, there is no automatic hierarchy layer — the business-unit tree widens access only where a sharing rule's unit_and_subordinates recipient or a scope-depth grant explicitly invokes it (positions are flat, ADR-0090 D3).


1. Permission Types

ObjectStack's ObjectPermission schema defines these boolean flags for object access (CRUD + lifecycle + VAMA super-user grants):

PermissionFlagDescriptionGrants
ReadallowReadView records owned by the user or shared with themView own records
CreateallowCreateCreate new recordsInsert records
EditallowEditModify records owned by the user or shared with themEdit own records
DeleteallowDeleteRemove records owned by the user or shared with themDelete own records
TransferallowTransferChange record ownershipReassign owner
RestoreallowRestoreUndelete from trashRecover soft-deleted records
PurgeallowPurgePermanently (hard) deleteGDPR / compliance erase
View AllviewAllRecordsView all records regardless of ownership or sharingRead all records (bypass sharing)
Modify AllmodifyAllRecordsEdit/delete all records regardless of ownershipFull object access (bypass sharing)

Super-user bypass: When modifyAllRecords is set it satisfies write checks (allowEdit/allowDelete, and the lifecycle class allowTransfer/allowRestore/allowPurge) on any record; viewAllRecords (or modifyAllRecords) satisfies allowRead on any record — both bypass ownership and sharing. See packages/plugins/plugin-security/src/permission-evaluator.ts.

The bypass is posture-gated for row-level security (ADR-0066 D2 ①): RLS policies are short-circuited only on objects whose posture permits it — access: { default: 'private' }, tenancy.enabled: false (platform-global), or better-auth-managed. On an ordinary tenant business object, authored and baseline RLS still applies (e.g. member_default's owner-scoped write policies keep org_member holders owner-scoped on update/delete even when another set grants modifyAllRecords), and the Layer 0 tenant wall always ANDs on top. See Sharing & OWD and plugin-security/src/security-plugin.ts (computeLayeredRlsFilter).

Lifecycle operations are pending: the transfer / restore / purge operations do not exist in ObjectQL yet (roadmap M2). Their RBAC gate is already mapped in the permission evaluator — the moment the operations ship they are denied unless the matching flag (or modifyAllRecords) is granted — but authoring these flags today grants nothing (#1883).


2. Object × Permission-Set Matrix

Permission sets are the only capability container (there is no Profile concept — ADR-0090 D2); a user's grants are the union of every set reached via positions, direct grants, the everyone anchor, and the additive baseline. This matrix shows which object permissions each set receives. Use it as a template for your own configuration.

Example: CRM Application

ObjectSystem AdminSales ManagerSales RepMarketingRead Only
accountmodifyAllRecordsviewAllRecordsallowRead allowCreate allowEditallowReadallowRead
contactmodifyAllRecordsviewAllRecordsallowRead allowCreate allowEditallowRead allowCreateallowRead
opportunitymodifyAllRecordsmodifyAllRecordsallowRead allowCreate allowEditallowReadallowRead
taskmodifyAllRecordsviewAllRecordsallowRead allowCreate allowEdit allowDeleteallowRead allowCreateallowRead
reportmodifyAllRecordsallowRead allowCreateallowReadallowRead allowCreateallowRead
usermodifyAllRecordsallowReadallowReadallowReadallowRead
audit_logviewAllRecords

Configuration Example

import { defineStack } from '@objectstack/spec';

export default defineStack({
  permissions: [
    {
      name: 'sales_rep_access',
      label: 'Sales Representative',
      objects: {
        account:     { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
        contact:     { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
        opportunity: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
        task:        { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true },
      },
    },
    {
      name: 'sales_manager_access',
      label: 'Sales Manager',
      objects: {
        account:     { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true, viewAllRecords: true },
        contact:     { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true, viewAllRecords: true },
        opportunity: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true, modifyAllRecords: true },
        task:        { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true, viewAllRecords: true },
      },
    },
  ],
});

3. Field-Level Security (FLS)

Field-level security controls visibility and editability of individual fields per permission set.

FieldSystem AdminSales ManagerSales RepMarketingRead Only
account.nameVisible, EditableVisible, EditableVisible, EditableVisibleVisible
account.revenueVisible, EditableVisible, EditableVisibleHiddenHidden
contact.emailVisible, EditableVisible, EditableVisible, EditableVisibleVisible
contact.ssnVisible, EditableHiddenHiddenHiddenHidden
opportunity.amountVisible, EditableVisible, EditableVisible, EditableVisibleVisible
opportunity.marginVisible, EditableVisibleHiddenHiddenHidden

FLS States

StatereadableeditableAPI Behavior
Visible, EditabletruetrueField is included in read and accepted in write
Visible, Read-OnlytruefalseField is included in read; a write attempt to it is rejected with a PermissionDeniedError (403)
HiddenfalsefalseField is stripped from read; a write attempt to it is rejected with a PermissionDeniedError (403)

Configuration Example

{
  name: 'sales_rep_access',
  objects: { /* ... */ },
  // Field-level security — keys are `<object>.<field>`
  fields: {
    'account.revenue':    { readable: true,  editable: false },
    'contact.ssn':        { readable: false, editable: false },
    'opportunity.margin': { readable: false, editable: false },
  },
}

4. Sharing Rule Types

Sharing rules extend access beyond ownership and the depth axis. The declarative SharingRule schema is a discriminated union with two type values — owner and criteria. Manual and territory sharing are separate mechanisms (see notes below the table).

MechanismtypeDescriptionExample
Owner-BasedownerShare records owned by a specific group/position with another recipient — [experimental — not enforced]: owner rules are skipped at seed time and materialize no shares yetAll accounts owned by "West Region" team are shared with "Sales Directors"
Criteria-BasedcriteriaShare records matching a CEL predicate over field valuesAll opportunities where record.amount > 100000 are shared with "VP Sales"

Enforcement status: only criteria rules are enforced today. Declared owner-type rules, and rules with group / guest recipients, are skipped at seed time with a warning (ADR-0049 — never silently advertised as live). See Sharing Rules.

Beyond declarative rules: Two other sharing mechanisms exist but are not SharingRule types. Manual sharing is a runtime grant — sys_record_share rows created with source: 'manual' (see packages/plugins/plugin-sharing/src/sharing-service.ts). Territories are a separate matrix model (TerritorySchema in packages/spec/src/security/territory.zod.ts) with their own account/opportunity/case access levels, parallel to the business-unit model. Owner/criteria rules are re-evaluated on insert/update via internal sharing rule hooks.

Configuration Example

{
  sharingRules: [
    {
      name: 'high_value_opps_to_vp',
      object: 'opportunity',
      type: 'criteria',
      // condition is a CEL predicate over the record
      condition: 'record.amount > 100000',
      sharedWith: { type: 'position', value: 'vp_sales' },
      accessLevel: 'read',
    },
    {
      name: 'west_accounts_to_directors',
      object: 'account',
      type: 'owner',
      // records owned by this group/position are the source set
      ownedBy: { type: 'position', value: 'west_region_rep' },
      sharedWith: { type: 'position', value: 'sales_director' },
      accessLevel: 'edit',
    },
  ],
}

5. Organization-Wide Defaults (OWD)

OWD sets the baseline access level for each object across the entire organization. The baseline is declared per object via the sharingModel field on the object definition (packages/spec/src/data/object.zod.ts).

DefaultsharingModelRead AccessWrite AccessUse When
Public Read/Writepublic_read_writeAll usersAll usersLow-sensitivity data (e.g., tasks, wiki pages)
Public Read Onlypublic_readAll usersOwner + sharedModerate sensitivity (e.g., accounts, contacts)
PrivateprivateOwner + sharedOwner + sharedHigh sensitivity (e.g., opportunities, HR records)
Controlled by Parentcontrolled_by_parentDerived from the master recordDerived from the master recordMaster-detail children (e.g., order lines)

These are the only four canonical values (ADR-0090 D4). The legacy spellings read / read_write / full were removed — the conversion pipeline rewrites them to public_read / public_read_write on upgrade, and an unknown value resolves to private (fail-closed). A custom object that omits sharingModel resolves to private at runtime (D1), and os validate's security-posture check requires every custom object to declare it explicitly. These models are enforced by plugin-sharing + plugin-security and dogfood-proven over the real HTTP stack.

Configuration Example

// OWD is declared on each object, not as a central map.
defineStack({
  objects: {
    account:     { sharingModel: 'public_read' /* ...fields */ },
    contact:     { sharingModel: 'public_read' },
    opportunity: { sharingModel: 'private' },
    task:        { sharingModel: 'public_read_write' },
    hr_record:   { sharingModel: 'private' },
  },
});

Opening Up Access: OWD restricts the baseline. Sharing rules, scope-depth grants, and manual sharing can only open up access — never restrict it further than the OWD.


6. Business-Unit Hierarchy & Positions

Positions (岗位) are flat distribution groups — they have no parent links and form no tree (ADR-0090 D3). The one hierarchy is the business-unit tree, and it grants nothing automatically: sitting above another unit does not by itself expose its records. Upward visibility is always an explicit, per-grant opt-in.

graph TD
    HQ[HQ]
    EAST[BU - East]
    WEST[BU - West]
    MKT[BU - Marketing]
    EAST_SALES[BU - East Sales]
    EAST_SVC[BU - East Service]

    HQ --> EAST
    HQ --> WEST
    HQ --> MKT
    EAST --> EAST_SALES
    EAST --> EAST_SVC

    style HQ fill:#4f46e5,color:#fff
    style EAST fill:#7c3aed,color:#fff
    style WEST fill:#7c3aed,color:#fff
    style MKT fill:#7c3aed,color:#fff

How the tree is consumed — three explicit mechanisms:

  • Scope depth — grant a permission set readScope: 'unit' / 'unit_and_below'; the owner-match widens to the caller's unit (or its subtree). A position assignment's business_unit_id anchor decides which unit that means for multi-unit users (ADR-0090 Addendum).
  • Sharing-rule recipients — share with { type: 'unit_and_subordinates', value: 'east' } to reach everyone in the East subtree, or { type: 'position', value: 'sales_manager' } to reach a job function across units.
  • Delegated administration — an adminScope bounds a subsidiary admin to a BU subtree (ADR-0090 D12).

A Sales Rep sees only their own records (plus shares) under a private OWD — that part needs no configuration.

Configuration Example

defineStack({
  positions: [
    // Flat job functions — no parent links.
    { name: 'vp_sales', label: 'VP Sales' },
    { name: 'sales_director', label: 'Sales Director' },
    { name: 'sales_rep', label: 'Sales Representative' },
  ],
  // The tree lives on business units (seeded as sys_business_unit rows).
});

Security Evaluation Order

When a user attempts to access a record, permissions are evaluated in this order:

flowchart TD
    A[User requests access] --> B{Object Permission?}
    B -->|No| DENY[❌ Access Denied]
    B -->|Yes| C{OWD allows?}
    C -->|Public Read/Write| ALLOW[✅ Access Granted]
    C -->|No| D{Is owner?}
    D -->|Yes| ALLOW
    D -->|No| E{Scope-depth grant? readScope}
    E -->|Yes| ALLOW
    E -->|No| F{Sharing rule matches?}
    F -->|Yes| ALLOW
    F -->|No| G{Manual share?}
    G -->|Yes| ALLOW
    G -->|No| DENY

    ALLOW --> H{Field-Level Security}
    H --> I[Filter visible fields]
    I --> J[Return filtered record]
StepLayerWhat It Checks
1Object PermissionDoes any resolved permission set grant this operation on the object?
2OWDIs the object public? If so, grant access immediately
3Record OwnershipDoes the user own this record?
4Scope DepthDoes a readScope / writeScope grant (own_and_reports / unit / unit_and_below / org) widen the owner-match to cover the record's owner?
5Sharing RulesDid a criteria sharing rule materialize a share for this user on this record?
6Manual SharingWas this record explicitly shared with the user (sys_record_share, source: 'manual')?
7Field-Level SecurityWhich fields is the user allowed to see/edit?

Performance: For reads, steps 2–6 are compiled into a query filter (owner-match ∪ materialized shares, AND-ed with RLS) at query time, not evaluated record-by-record. By-id writes are verified with a pre-image check: the target row is re-read through the write-scope filter before the mutation. This keeps security checks efficient even on tables with millions of rows.

See also

On this page