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_idsis 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 stagesterritory_account_idsinExecutionContext.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-resolvesteam_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
- 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 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
| 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, 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. |
| 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 | never | optional | [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. |
| tags | string[] | optional | Policy categorization tags |