# RLS Policies - ERP Suite Shared Library Centralized Row-Level Security (RLS) policies for multi-tenant isolation across all ERP-Suite modules. ## Overview This module provides: - **5 generic RLS policy templates** (SQL) - **TypeScript functions** for dynamic policy application - **Helper utilities** for RLS management and context handling ## Files - `rls-policies.sql` - SQL policy templates and helper functions - `apply-rls.ts` - TypeScript utilities for applying RLS policies - `README.md` - This documentation - `usage-example.ts` - Usage examples ## RLS Policy Types ### 1. Tenant Isolation Policy **Purpose**: Ensures users can only access data from their own tenant. **Usage**: Apply to all tables with `tenant_id` column. ```sql -- SQL SELECT apply_tenant_isolation_policy('core', 'partners'); ``` ```typescript // TypeScript import { applyTenantIsolationPolicy } from '@erp-suite/core'; await applyTenantIsolationPolicy(pool, 'core', 'partners'); ``` ### 2. User Data Policy **Purpose**: Restricts access to data created by or assigned to the current user. **Usage**: Apply to tables with `created_by`, `assigned_to`, or `owner_id` columns. ```sql -- SQL SELECT apply_user_data_policy('projects', 'tasks', ARRAY['created_by', 'assigned_to']::TEXT[]); ``` ```typescript // TypeScript import { applyUserDataPolicy } from '@erp-suite/core'; await applyUserDataPolicy(pool, 'projects', 'tasks', ['created_by', 'assigned_to']); ``` ### 3. Read Own Data Policy **Purpose**: Allows users to read only their own data (SELECT only). **Usage**: Apply when users need read access to own data but restricted write. ```sql -- See rls-policies.sql for template -- No dedicated function yet - use custom SQL ``` ### 4. Write Own Data Policy **Purpose**: Allows users to insert/update/delete only their own data. **Usage**: Companion to READ_OWN_DATA_POLICY for write operations. ```sql -- See rls-policies.sql for template -- No dedicated function yet - use custom SQL ``` ### 5. Admin Bypass Policy **Purpose**: Allows admin users to bypass RLS restrictions for support/management. **Usage**: Apply as permissive policy to allow admin full access. ```sql -- SQL SELECT apply_admin_bypass_policy('financial', 'invoices'); ``` ```typescript // TypeScript import { applyAdminBypassPolicy } from '@erp-suite/core'; await applyAdminBypassPolicy(pool, 'financial', 'invoices'); ``` ## Quick Start ### 1. Apply RLS to Database First, run the SQL functions to create the helper functions: ```bash psql -d your_database -f apps/shared-libs/core/database/policies/rls-policies.sql ``` ### 2. Apply Policies to Tables #### Single Table ```typescript import { applyCompleteRlsPolicies } from '@erp-suite/core'; import { Pool } from 'pg'; const pool = new Pool({ /* config */ }); // Apply tenant isolation + admin bypass await applyCompleteRlsPolicies(pool, 'core', 'partners'); ``` #### Multiple Tables ```typescript import { applyCompletePoliciesForSchema } from '@erp-suite/core'; await applyCompletePoliciesForSchema(pool, 'inventory', [ 'products', 'warehouses', 'locations', 'lots' ]); ``` #### Batch Application ```typescript import { batchApplyRlsPolicies } from '@erp-suite/core'; await batchApplyRlsPolicies(pool, [ { schema: 'core', table: 'partners' }, { schema: 'core', table: 'addresses' }, { schema: 'inventory', table: 'products', includeAdminBypass: false }, { schema: 'projects', table: 'tasks', tenantColumn: 'company_id' }, ]); ``` ### 3. Set RLS Context Before querying data, set the session context: ```typescript import { setRlsContext, withRlsContext } from '@erp-suite/core'; // Manual context setting await setRlsContext(pool, { tenantId: 'uuid-tenant-id', userId: 'uuid-user-id', userRole: 'admin', }); // Or use helper for scoped context const result = await withRlsContext(pool, { tenantId: 'tenant-uuid', userId: 'user-uuid', userRole: 'user', }, async (client) => { return await client.query('SELECT * FROM core.partners'); }); ``` ## API Reference ### Policy Application | Function | Description | |----------|-------------| | `applyTenantIsolationPolicy()` | Apply tenant isolation policy to a table | | `applyAdminBypassPolicy()` | Apply admin bypass policy to a table | | `applyUserDataPolicy()` | Apply user data policy to a table | | `applyCompleteRlsPolicies()` | Apply complete policies (tenant + admin) | | `applyCompletePoliciesForSchema()` | Apply to multiple tables in a schema | | `batchApplyRlsPolicies()` | Batch apply policies to multiple tables | ### RLS Management | Function | Description | |----------|-------------| | `enableRls()` | Enable RLS on a table | | `disableRls()` | Disable RLS on a table | | `isRlsEnabled()` | Check if RLS is enabled | | `listRlsPolicies()` | List all policies on a table | | `dropRlsPolicy()` | Drop a specific policy | | `dropAllRlsPolicies()` | Drop all policies from a table | ### Status and Inspection | Function | Description | |----------|-------------| | `getSchemaRlsStatus()` | Get RLS status for all tables in a schema | ### Context Management | Function | Description | |----------|-------------| | `setRlsContext()` | Set session context for RLS | | `clearRlsContext()` | Clear RLS context from session | | `withRlsContext()` | Execute function within RLS context | ## Migration Guide ### From Vertical-Specific RLS to Centralized **Before** (in vertical migrations): ```sql -- apps/verticales/construccion/database/migrations/001-rls.sql CREATE OR REPLACE FUNCTION get_current_tenant_id() ...; CREATE POLICY tenant_isolation_projects ON projects.projects ...; ``` **After** (using shared-libs): ```typescript // apps/verticales/construccion/database/migrations/001-rls.ts import { applyCompleteRlsPolicies } from '@erp-suite/core'; import { pool } from '../db'; export async function up() { await applyCompleteRlsPolicies(pool, 'projects', 'projects'); } ``` ## Best Practices 1. **Always apply tenant isolation** to tables with `tenant_id` 2. **Include admin bypass** for support and troubleshooting (default: enabled) 3. **Use user data policies** for user-specific tables (tasks, notifications, etc.) 4. **Set RLS context** in middleware or at the application boundary 5. **Test RLS policies** thoroughly before deploying to production 6. **Document custom policies** if you deviate from the templates ## Testing RLS Policies ### Check if RLS is enabled ```typescript import { isRlsEnabled } from '@erp-suite/core'; const enabled = await isRlsEnabled(pool, 'core', 'partners'); console.log(`RLS enabled: ${enabled}`); ``` ### List policies ```typescript import { listRlsPolicies } from '@erp-suite/core'; const status = await listRlsPolicies(pool, 'core', 'partners'); console.log(`Policies on core.partners:`, status.policies); ``` ### Get schema-wide status ```typescript import { getSchemaRlsStatus } from '@erp-suite/core'; const statuses = await getSchemaRlsStatus(pool, 'core'); statuses.forEach(status => { console.log(`${status.table}: RLS ${status.rlsEnabled ? 'enabled' : 'disabled'}, ${status.policies.length} policies`); }); ``` ## Troubleshooting ### RLS blocking legitimate queries? 1. Check that RLS context is set correctly: ```typescript await setRlsContext(pool, { tenantId, userId, userRole }); ``` 2. Verify the policy USING clause matches your data: ```typescript const status = await listRlsPolicies(pool, 'schema', 'table'); console.log(status.policies); ``` 3. Test with admin bypass to isolate the issue: ```typescript await setRlsContext(pool, { userRole: 'admin' }); ``` ### Policy not being applied? 1. Ensure RLS is enabled: ```typescript const enabled = await isRlsEnabled(pool, 'schema', 'table'); ``` 2. Check that the policy exists: ```typescript const status = await listRlsPolicies(pool, 'schema', 'table'); ``` 3. Verify the session context is set: ```sql SELECT current_setting('app.current_tenant_id', true); ``` ## Examples See `usage-example.ts` for complete working examples. ## Contributing When adding new RLS policy types: 1. Add SQL template to `rls-policies.sql` 2. Add TypeScript function to `apply-rls.ts` 3. Export from `index.ts` 4. Update this README 5. Add example to `usage-example.ts` ## Support For questions or issues, contact the ERP-Suite core team or file an issue in the repository.