ObjectStackObjectStack

Rls

Rls protocol schemas

Row-Level Security (RLS) Protocol

Implements fine-grained record-level access control inspired by PostgreSQL RLS and Salesforce Criteria-Based Sharing Rules.

Overview

Row-Level Security (RLS) allows you to control which rows users can access in database tables based on their identity and positions. Unlike object-level permissions (CRUD), RLS provides record-level filtering.

Use Cases

  1. Multi-Tenant Data Isolation

    • Users only see records from their organization
    • using: "organization_id == current_user.organization_id"
  2. Ownership-Based Access

    • Users only see records they own
    • using: "owner_id == current_user.id"
  3. Organization Member Visibility

    • Users see fellow members of their active organization
    • using: "id in current_user.org_user_ids" (org_user_ids is pre-resolved by the runtime)
  4. Territory / Regional Access (§7.3.1 dynamic membership)

    • Sales reps only see accounts in their assigned territories
    • using: "account_id in current_user.territory_account_ids" (the runtime stages territory_account_ids in ExecutionContext.rlsMembership)
  5. Manager / Hierarchy Access (§7.3.1 dynamic membership)

    • Managers see records assigned to anyone they manage
    • using: "assigned_to_id in current_user.team_member_ids" (the runtime pre-resolves team_member_ids, no subquery needed)

PostgreSQL RLS Comparison

PostgreSQL RLS Example:

CREATE POLICY tenant_isolation ON accounts
  FOR SELECT
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

CREATE POLICY account_insert ON accounts
  FOR INSERT
  WITH CHECK (tenant_id = current_setting('app.current_tenant_id')::uuid);

ObjectStack RLS Equivalent:

{
  name: 'tenant_isolation',
  object: 'account',
  operation: 'select',
  using: 'organization_id == current_user.organization_id'
}

Salesforce Sharing Rules Comparison

Salesforce uses "Sharing Rules" and a visibility hierarchy for record-level access (our equivalent hierarchy is the business-unit tree, ADR-0090 D3). ObjectStack RLS provides similar functionality with more flexibility.

Salesforce:

  • Criteria-Based Sharing: Share records matching criteria with users/groups
  • Owner-Based Sharing: Share records based on who owns them
  • Manual Sharing: Individual record sharing

ObjectStack RLS:

  • A constrained CEL predicate grammar: comparisons and set-membership against literals or current_user.* values, composable with && / ||; anything that does not lower to a filter fails closed
  • Subquery-shaped needs are pre-resolved by the runtime (§7.3.1)
  • Multiple policies OR-combine for union (any-match-allows) semantics

Best Practices

  1. Always Define SELECT Policy: Control what users can view
  2. Define INSERT/UPDATE CHECK Policies: Prevent data leakage
  3. Use Position-Scoped Policies: Apply different rules to different positions
  4. Test Thoroughly: RLS can have complex interactions
  5. Monitor Performance: Complex RLS policies can impact query performance

Security Considerations

  1. Defense in Depth: RLS is one layer; use with object permissions
  2. Default Deny: If no policy matches, access is denied
  3. Policy Precedence: More permissive policy wins (OR logic)
  4. Context Variables: Ensure current_user context is always set

See also: https://www.postgresql.org/docs/current/ddl-rowsecurity.html

See also: https://help.salesforce.com/s/articleView?id=sf.security_sharing_rules.htm

Source: packages/spec/src/security/rls.zod.ts

TypeScript Usage

import { RLSEvaluationResultSchema, RLSOperation, RLSUserContextSchema, RowLevelSecurityPolicySchema } from '@objectstack/spec/security';
import type { RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security';

// Validate data
const result = RLSEvaluationResultSchema.parse(data);

RLSEvaluationResult

Properties

PropertyTypeRequiredDescription
policyNamestringPolicy name
grantedbooleanWhether access was granted
durationMsnumberoptionalEvaluation duration in milliseconds
errorstringoptionalError message if evaluation failed
usingResultbooleanoptionalUSING clause evaluation result
checkResultbooleanoptionalCHECK clause evaluation result

RLSOperation

Allowed Values

  • select
  • insert
  • update
  • delete
  • all

RLSUserContext

Properties

PropertyTypeRequiredDescription
idstringUser ID
emailstringoptionalUser email
tenantIdstringoptionalTenant/Organization ID
positionsstring[]optionalPositions held by the user
departmentstringoptionalUser department
attributesRecord<string, any>optionalAdditional custom user attributes

RowLevelSecurityPolicy

Properties

PropertyTypeRequiredDescription
namestringPolicy unique identifier (snake_case)
labelstringoptionalHuman-readable policy label
descriptionstringoptionalPolicy description and business justification
objectstringTarget object name
operationEnum<'select' | 'insert' | 'update' | 'delete' | 'all'>Database operation this policy applies to
usingstringoptionalFilter condition for SELECT/UPDATE/DELETE, authored in canonical CEL (ADR-0058 D1). It enforces when the predicate lowers to an ObjectQL filter: a field compared against a literal or a current_user.* context value using ==, !=, <, <=, > or >=; in against a current_user.* array or an inline literal list (e.g. status in ['draft', 'pending']); these combined with && / ||; or the bare allow-all true. Anything that does not lower fails closed — the policy matches zero rows. The legacy SQL-ish spellings are still accepted through a transitional bridge that rewrites = to == and IN to in (deprecated under ADR-0058 D1); SQL AND / OR / NOT IN / IS NULL / LIKE are NOT bridged and fail closed. Optional for INSERT-only policies.
checkstringoptionalValidation condition for INSERT/UPDATE (defaults to USING clause if not specified - enforced at application level)
positionsstring[]optionalPositions this policy applies to (omit for all)
enabledbooleanWhether this policy is active
priorityneveroptional[REMOVED] rowLevelSecurity[].priority was removed in @objectstack/spec 17.0.0 (#3896 security audit). It never had an effect and could not: applicable policies OR-combine (most permissive wins), so there is no conflict to order. Delete the key — policy outcomes are unchanged. Run os migrate meta --from 16 to rewrite existing sources automatically.
tagsstring[]optionalPolicy categorization tags

On this page