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
- Multi-Tenant Data Isolation
-
Users only see records from their organization
-
using: "organization_id == current_user.organization_id"
- Ownership-Based Access
-
Users only see records they own
-
using: "owner_id == current_user.id"
- 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)
- 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)
- 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 small, fixed expression grammar (equality, set-membership, always-true)
-
Subquery-shaped needs are pre-resolved by the runtime (§7.3.1)
-
Multiple policies OR-combine for union (any-match-allows) semantics
Best Practices
-
Always Define SELECT Policy: Control what users can view
-
Define INSERT/UPDATE CHECK Policies: Prevent data leakage
-
Use Position-Scoped Policies: Apply different rules to different positions
-
Test Thoroughly: RLS can have complex interactions
-
Monitor Performance: Complex RLS policies can impact query performance
Security Considerations
-
Defense in Depth: RLS is one layer; use with object permissions
-
Default Deny: If no policy matches, access is denied
-
Policy Precedence: More permissive policy wins (OR logic)
-
Context Variables: Ensure current_user context is always set
@see https://www.postgresql.org/docs/current/ddl-rowsecurity.html
@see https://help.salesforce.com/s/articleView?id=sf.security_sharing_rules.htm
Source: packages/spec/src/security/rls.zod.ts
TypeScript Usage
import { RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security';
import type { RLSEvaluationResult, RLSOperation, RLSUserContext, RowLevelSecurityPolicy } from '@objectstack/spec/security';
// Validate data
const result = RLSEvaluationResult.parse(data);RLSEvaluationResult
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| policyName | string | ✅ | Policy name |
| granted | boolean | ✅ | Whether access was granted |
| durationMs | number | optional | Evaluation duration in milliseconds |
| error | string | optional | Error message if evaluation failed |
| usingResult | boolean | optional | USING clause evaluation result |
| checkResult | boolean | optional | CHECK clause evaluation result |
RLSOperation
Allowed Values
selectinsertupdatedeleteall
RLSUserContext
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| id | string | ✅ | User ID |
string | optional | User email | |
| tenantId | string | optional | Tenant/Organization ID |
| positions | string[] | optional | Positions held by the user |
| department | string | optional | User department |
| attributes | Record<string, any> | optional | Additional custom user attributes |
RowLevelSecurityPolicy
Properties
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | ✅ | Policy unique identifier (snake_case) |
| label | string | optional | Human-readable policy label |
| description | string | optional | Policy description and business justification |
| object | string | ✅ | Target object name |
| operation | Enum<'select' | 'insert' | 'update' | 'delete' | 'all'> | ✅ | Database operation this policy applies to |
| using | string | optional | Filter condition for SELECT/UPDATE/DELETE. One of the four compiler-supported forms: field = current_user.<prop>, field = 'literal', field IN (current_user.<array>), or 1 = 1. Optional for INSERT-only policies. |
| check | string | optional | Validation condition for INSERT/UPDATE (defaults to USING clause if not specified - enforced at application level) |
| positions | string[] | optional | Positions this policy applies to (omit for all) |
| enabled | boolean | ✅ | Whether this policy is active |
| priority | integer | ✅ | Policy evaluation priority (higher = evaluated first) |
| tags | string[] | optional | Policy categorization tags |