Skip to content

Latest commit

 

History

History
334 lines (272 loc) · 17 KB

File metadata and controls

334 lines (272 loc) · 17 KB
title Security Permissions Matrix
description 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):

Permission Flag Description Grants
Read allowRead View records owned by the user or shared with them View own records
Create allowCreate Create new records Insert records
Edit allowEdit Modify records owned by the user or shared with them Edit own records
Delete allowDelete Remove records owned by the user or shared with them Delete own records
Transfer allowTransfer Change record ownership Reassign owner
Restore allowRestore Undelete from trash Recover soft-deleted records
Purge allowPurge Permanently (hard) delete GDPR / compliance erase
View All viewAllRecords View all records regardless of ownership or sharing Read all records (bypass sharing)
Modify All modifyAllRecords Edit/delete all records regardless of ownership Full 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 partly pending:** the dedicated `transfer` / `restore` / `purge` ObjectQL operations do not exist yet (roadmap M2); their RBAC gate is already mapped in the permission evaluator, so the moment they ship they are denied unless the matching flag (or `modifyAllRecords`) is granted. One exception: `allowTransfer` is **already enforced today** through the ordinary `insert` / `update` door — `owner_id` is system-managed, so planting a record under another user or reassigning / disowning one is denied unless the caller holds `allowTransfer` (or `modifyAllRecords`, which implies it) (#3004). Authoring `allowRestore` / `allowPurge` today still 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

Object System Admin Sales Manager Sales Rep Marketing Read Only
account modifyAllRecords viewAllRecords allowRead allowCreate allowEdit allowRead allowRead
contact modifyAllRecords viewAllRecords allowRead allowCreate allowEdit allowRead allowCreate allowRead
opportunity modifyAllRecords modifyAllRecords allowRead allowCreate allowEdit allowRead allowRead
task modifyAllRecords viewAllRecords allowRead allowCreate allowEdit allowDelete allowRead allowCreate allowRead
report modifyAllRecords allowRead allowCreate allowRead allowRead allowCreate allowRead
user modifyAllRecords allowRead allowRead allowRead allowRead
audit_log viewAllRecords

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.

Field System Admin Sales Manager Sales Rep Marketing Read Only
account.name Visible, Editable Visible, Editable Visible, Editable Visible Visible
account.revenue Visible, Editable Visible, Editable Visible Hidden Hidden
contact.email Visible, Editable Visible, Editable Visible, Editable Visible Visible
contact.ssn Visible, Editable Hidden Hidden Hidden Hidden
opportunity.amount Visible, Editable Visible, Editable Visible, Editable Visible Visible
opportunity.margin Visible, Editable Visible Hidden Hidden Hidden

FLS States

State readable editable API Behavior
Visible, Editable true true Field is included in read and accepted in write
Visible, Read-Only true false Field is included in read; a write attempt to it is rejected with a PermissionDeniedError (403)
Hidden false false Field 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).

Mechanism type Description Example
Owner-Based owner Share records owned by a specific group/position with another recipient — [experimental — not enforced]: owner rules are skipped at seed time and materialize no shares yet All accounts owned by "West Region" team are shared with "Sales Directors"
Criteria-Based criteria Share records matching a CEL predicate over field values All 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](/docs/permissions/sharing-rules#owner-based-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`). **Matrix / territory-shaped access** is served today by multi-position assignment anchored on business units, plus pre-resolved membership sets: a registered `rls-membership-resolver` (ADR-0105 D11) stages e.g. `territory_account_ids` into `ExecutionContext.rlsMembership`, and a policy references it as `account_id IN (current_user.territory_account_ids)`. The aspirational `TerritorySchema` was removed in ADR-0105 D11 — it had no runtime object, stack field or resolver; a generalized dimension-security module will arrive with its own ADR. 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).

Default sharingModel Read Access Write Access Use When
Public Read/Write public_read_write All users All users Low-sensitivity data (e.g., tasks, wiki pages)
Public Read Only public_read All users Owner + shared Moderate sensitivity (e.g., accounts, contacts)
Private private Owner + shared Owner + shared High sensitivity (e.g., opportunities, HR records)
Controlled by Parent controlled_by_parent Derived from the master record Derived from the master record Master-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 mechanically rewrites `read` → `public_read` and `read_write` → `public_read_write` on upgrade; `full` has no lossless target (it implied access wider than any canonical value), so it is surfaced as a manual migration to `public_read_write` or explicit sharing rules. 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
Loading

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]
Loading
Step Layer What It Checks
1 Object Permission Does any resolved permission set grant this operation on the object?
2 OWD Is the object public? If so, grant access immediately
3 Record Ownership Does the user own this record?
4 Scope Depth Does a readScope / writeScope grant (own_and_reports / unit / unit_and_below / org) widen the owner-match to cover the record's owner?
5 Sharing Rules Did a criteria sharing rule materialize a share for this user on this record?
6 Manual Sharing Was this record explicitly shared with the user (sys_record_share, source: 'manual')?
7 Field-Level Security Which 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