/** * RLS Policy Application Utilities * * Centralized functions for applying Row-Level Security policies to database tables. * These utilities work in conjunction with the SQL policy templates in rls-policies.sql. * * @module @erp-suite/core/database/policies * * @example * ```typescript * import { applyTenantIsolationPolicy, applyCompleteRlsPolicies } from '@erp-suite/core'; * import { Pool } from 'pg'; * * const pool = new Pool({ ... }); * * // Apply tenant isolation to a single table * await applyTenantIsolationPolicy(pool, 'core', 'partners'); * * // Apply complete policies (tenant + admin) to multiple tables * await applyCompletePoliciesForSchema(pool, 'inventory', ['products', 'warehouses', 'locations']); * ``` */ import { Pool, PoolClient } from 'pg'; /** * RLS Policy Configuration Options */ export interface RlsPolicyOptions { /** Schema name */ schema: string; /** Table name */ table: string; /** Column name for tenant isolation (default: 'tenant_id') */ tenantColumn?: string; /** Include admin bypass policy (default: true) */ includeAdminBypass?: boolean; /** Custom user columns for user data policy */ userColumns?: string[]; } /** * RLS Policy Type */ export enum RlsPolicyType { TENANT_ISOLATION = 'tenant_isolation', USER_DATA = 'user_data', READ_OWN_DATA = 'read_own_data', WRITE_OWN_DATA = 'write_own_data', ADMIN_BYPASS = 'admin_bypass', } /** * RLS Policy Status */ export interface RlsPolicyStatus { schema: string; table: string; rlsEnabled: boolean; policies: Array<{ name: string; command: string; using: string | null; check: string | null; }>; } /** * Apply tenant isolation policy to a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @param tenantColumn - Column name for tenant isolation (default: 'tenant_id') * @returns Promise * * @example * ```typescript * await applyTenantIsolationPolicy(pool, 'core', 'partners'); * await applyTenantIsolationPolicy(pool, 'inventory', 'products', 'company_id'); * ``` */ export async function applyTenantIsolationPolicy( client: Pool | PoolClient, schema: string, table: string, tenantColumn: string = 'tenant_id', ): Promise { const query = `SELECT apply_tenant_isolation_policy($1, $2, $3)`; await client.query(query, [schema, table, tenantColumn]); } /** * Apply admin bypass policy to a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @returns Promise * * @example * ```typescript * await applyAdminBypassPolicy(pool, 'financial', 'invoices'); * ``` */ export async function applyAdminBypassPolicy( client: Pool | PoolClient, schema: string, table: string, ): Promise { const query = `SELECT apply_admin_bypass_policy($1, $2)`; await client.query(query, [schema, table]); } /** * Apply user data policy to a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @param userColumns - Array of column names to check (default: ['created_by', 'assigned_to', 'owner_id']) * @returns Promise * * @example * ```typescript * await applyUserDataPolicy(pool, 'projects', 'tasks'); * await applyUserDataPolicy(pool, 'crm', 'leads', ['created_by', 'assigned_to']); * ``` */ export async function applyUserDataPolicy( client: Pool | PoolClient, schema: string, table: string, userColumns: string[] = ['created_by', 'assigned_to', 'owner_id'], ): Promise { const query = `SELECT apply_user_data_policy($1, $2, $3)`; await client.query(query, [schema, table, userColumns]); } /** * Apply complete RLS policies to a table (tenant isolation + optional admin bypass) * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @param tenantColumn - Column name for tenant isolation (default: 'tenant_id') * @param includeAdminBypass - Whether to include admin bypass policy (default: true) * @returns Promise * * @example * ```typescript * await applyCompleteRlsPolicies(pool, 'core', 'partners'); * await applyCompleteRlsPolicies(pool, 'sales', 'orders', 'tenant_id', false); * ``` */ export async function applyCompleteRlsPolicies( client: Pool | PoolClient, schema: string, table: string, tenantColumn: string = 'tenant_id', includeAdminBypass: boolean = true, ): Promise { const query = `SELECT apply_complete_rls_policies($1, $2, $3, $4)`; await client.query(query, [schema, table, tenantColumn, includeAdminBypass]); } /** * Apply complete RLS policies to multiple tables in a schema * * @param client - Database client or pool * @param schema - Schema name * @param tables - Array of table names * @param options - Optional configuration * @returns Promise * * @example * ```typescript * await applyCompletePoliciesForSchema(pool, 'inventory', [ * 'products', * 'warehouses', * 'locations', * 'lots' * ]); * ``` */ export async function applyCompletePoliciesForSchema( client: Pool | PoolClient, schema: string, tables: string[], options?: { tenantColumn?: string; includeAdminBypass?: boolean; }, ): Promise { const { tenantColumn = 'tenant_id', includeAdminBypass = true } = options || {}; for (const table of tables) { await applyCompleteRlsPolicies( client, schema, table, tenantColumn, includeAdminBypass, ); } } /** * Check if RLS is enabled on a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @returns Promise - True if RLS is enabled * * @example * ```typescript * const isEnabled = await isRlsEnabled(pool, 'core', 'partners'); * console.log(`RLS enabled: ${isEnabled}`); * ``` */ export async function isRlsEnabled( client: Pool | PoolClient, schema: string, table: string, ): Promise { const query = `SELECT is_rls_enabled($1, $2) as enabled`; const result = await client.query(query, [schema, table]); return result.rows[0]?.enabled ?? false; } /** * List all RLS policies on a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @returns Promise - Policy status information * * @example * ```typescript * const status = await listRlsPolicies(pool, 'core', 'partners'); * console.log(`Policies on core.partners:`, status.policies); * ``` */ export async function listRlsPolicies( client: Pool | PoolClient, schema: string, table: string, ): Promise { const rlsEnabled = await isRlsEnabled(client, schema, table); const query = `SELECT * FROM list_rls_policies($1, $2)`; const result = await client.query(query, [schema, table]); return { schema, table, rlsEnabled, policies: result.rows.map((row) => ({ name: row.policy_name, command: row.policy_cmd, using: row.policy_using, check: row.policy_check, })), }; } /** * Enable RLS on a table (without applying any policies) * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @returns Promise * * @example * ```typescript * await enableRls(pool, 'core', 'custom_table'); * ``` */ export async function enableRls( client: Pool | PoolClient, schema: string, table: string, ): Promise { const query = `ALTER TABLE ${schema}.${table} ENABLE ROW LEVEL SECURITY`; await client.query(query); } /** * Disable RLS on a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @returns Promise * * @example * ```typescript * await disableRls(pool, 'core', 'temp_table'); * ``` */ export async function disableRls( client: Pool | PoolClient, schema: string, table: string, ): Promise { const query = `ALTER TABLE ${schema}.${table} DISABLE ROW LEVEL SECURITY`; await client.query(query); } /** * Drop a specific RLS policy from a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @param policyName - Policy name to drop * @returns Promise * * @example * ```typescript * await dropRlsPolicy(pool, 'core', 'partners', 'tenant_isolation_partners'); * ``` */ export async function dropRlsPolicy( client: Pool | PoolClient, schema: string, table: string, policyName: string, ): Promise { const query = `DROP POLICY IF EXISTS ${policyName} ON ${schema}.${table}`; await client.query(query); } /** * Drop all RLS policies from a table * * @param client - Database client or pool * @param schema - Schema name * @param table - Table name * @returns Promise - Number of policies dropped * * @example * ```typescript * const count = await dropAllRlsPolicies(pool, 'core', 'partners'); * console.log(`Dropped ${count} policies`); * ``` */ export async function dropAllRlsPolicies( client: Pool | PoolClient, schema: string, table: string, ): Promise { const status = await listRlsPolicies(client, schema, table); for (const policy of status.policies) { await dropRlsPolicy(client, schema, table, policy.name); } return status.policies.length; } /** * Batch apply RLS policies to tables based on configuration * * @param client - Database client or pool * @param configs - Array of policy configurations * @returns Promise * * @example * ```typescript * await batchApplyRlsPolicies(pool, [ * { schema: 'core', table: 'partners' }, * { schema: 'core', table: 'addresses' }, * { schema: 'inventory', table: 'products', includeAdminBypass: false }, * { schema: 'projects', table: 'tasks', tenantColumn: 'company_id' }, * ]); * ``` */ export async function batchApplyRlsPolicies( client: Pool | PoolClient, configs: RlsPolicyOptions[], ): Promise { for (const config of configs) { const { schema, table, tenantColumn = 'tenant_id', includeAdminBypass = true, } = config; await applyCompleteRlsPolicies( client, schema, table, tenantColumn, includeAdminBypass, ); } } /** * Get RLS status for all tables in a schema * * @param client - Database client or pool * @param schema - Schema name * @returns Promise - Array of policy statuses * * @example * ```typescript * const statuses = await getSchemaRlsStatus(pool, 'core'); * statuses.forEach(status => { * console.log(`${status.table}: RLS ${status.rlsEnabled ? 'enabled' : 'disabled'}, ${status.policies.length} policies`); * }); * ``` */ export async function getSchemaRlsStatus( client: Pool | PoolClient, schema: string, ): Promise { // Get all tables in schema const tablesQuery = ` SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE' ORDER BY table_name `; const tablesResult = await client.query(tablesQuery, [schema]); const statuses: RlsPolicyStatus[] = []; for (const row of tablesResult.rows) { const status = await listRlsPolicies(client, schema, row.table_name); statuses.push(status); } return statuses; } /** * Set session context for RLS (tenant_id, user_id, user_role) * * @param client - Database client or pool * @param context - Session context * @returns Promise * * @example * ```typescript * await setRlsContext(client, { * tenantId: 'uuid-tenant-id', * userId: 'uuid-user-id', * userRole: 'admin', * }); * ``` */ export async function setRlsContext( client: Pool | PoolClient, context: { tenantId?: string; userId?: string; userRole?: string; }, ): Promise { const { tenantId, userId, userRole } = context; if (tenantId) { await client.query(`SET app.current_tenant_id = $1`, [tenantId]); } if (userId) { await client.query(`SET app.current_user_id = $1`, [userId]); } if (userRole) { await client.query(`SET app.current_user_role = $1`, [userRole]); } } /** * Clear RLS context from session * * @param client - Database client or pool * @returns Promise * * @example * ```typescript * await clearRlsContext(client); * ``` */ export async function clearRlsContext(client: Pool | PoolClient): Promise { await client.query(`RESET app.current_tenant_id`); await client.query(`RESET app.current_user_id`); await client.query(`RESET app.current_user_role`); } /** * Helper: Execute function within RLS context * * @param client - Database client or pool * @param context - RLS context * @param fn - Function to execute * @returns Promise - Result of the function * * @example * ```typescript * const result = await withRlsContext(pool, { * tenantId: 'tenant-uuid', * userId: 'user-uuid', * userRole: 'user', * }, async (client) => { * return await client.query('SELECT * FROM core.partners'); * }); * ``` */ export async function withRlsContext( client: Pool | PoolClient, context: { tenantId?: string; userId?: string; userRole?: string; }, fn: (client: Pool | PoolClient) => Promise, ): Promise { await setRlsContext(client, context); try { return await fn(client); } finally { await clearRlsContext(client); } } /** * Export all RLS utility functions */ export default { // Policy application applyTenantIsolationPolicy, applyAdminBypassPolicy, applyUserDataPolicy, applyCompleteRlsPolicies, applyCompletePoliciesForSchema, batchApplyRlsPolicies, // RLS management enableRls, disableRls, isRlsEnabled, listRlsPolicies, dropRlsPolicy, dropAllRlsPolicies, // Status and inspection getSchemaRlsStatus, // Context management setRlsContext, clearRlsContext, withRlsContext, // Types RlsPolicyType, };