-- ============================================================================ -- SHARED RLS POLICIES - ERP-Suite Core Library -- ============================================================================ -- Purpose: Centralized Row-Level Security policies for multi-tenant isolation -- Location: apps/shared-libs/core/database/policies/rls-policies.sql -- Usage: Applied dynamically via apply-rls.ts functions -- ============================================================================ -- ============================================================================ -- HELPER FUNCTIONS FOR RLS -- ============================================================================ -- Function: Get current tenant ID from session context CREATE OR REPLACE FUNCTION get_current_tenant_id() RETURNS UUID AS $$ BEGIN RETURN NULLIF(current_setting('app.current_tenant_id', true), '')::UUID; EXCEPTION WHEN OTHERS THEN RETURN NULL; END; $$ LANGUAGE plpgsql STABLE SECURITY DEFINER; COMMENT ON FUNCTION get_current_tenant_id() IS 'Retrieves the tenant_id from the current session context for RLS policies. Returns NULL if not set. Used by all tenant isolation policies.'; -- Function: Get current user ID from session context CREATE OR REPLACE FUNCTION get_current_user_id() RETURNS UUID AS $$ BEGIN RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID; EXCEPTION WHEN OTHERS THEN RETURN NULL; END; $$ LANGUAGE plpgsql STABLE SECURITY DEFINER; COMMENT ON FUNCTION get_current_user_id() IS 'Retrieves the user_id from the current session context. Used for user-specific RLS policies (read/write own data).'; -- Function: Get current user role from session context CREATE OR REPLACE FUNCTION get_current_user_role() RETURNS TEXT AS $$ BEGIN RETURN current_setting('app.current_user_role', true); EXCEPTION WHEN OTHERS THEN RETURN 'guest'; END; $$ LANGUAGE plpgsql STABLE SECURITY DEFINER; COMMENT ON FUNCTION get_current_user_role() IS 'Retrieves the user role from the current session context. Used for role-based access control in RLS policies. Defaults to "guest".'; -- Function: Check if current user is admin CREATE OR REPLACE FUNCTION is_current_user_admin() RETURNS BOOLEAN AS $$ BEGIN RETURN get_current_user_role() IN ('admin', 'super_admin', 'system_admin'); END; $$ LANGUAGE plpgsql STABLE SECURITY DEFINER; COMMENT ON FUNCTION is_current_user_admin() IS 'Returns TRUE if the current user has an admin role. Used for admin bypass policies.'; -- ============================================================================ -- GENERIC RLS POLICY TEMPLATES -- ============================================================================ -- POLICY 1: TENANT_ISOLATION_POLICY -- Purpose: Ensures users can only access data from their own tenant -- Usage: Apply to all tables with tenant_id column -- ============================================================================ /* TEMPLATE FOR TENANT_ISOLATION_POLICY: ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_{table} ON {schema}.{table} FOR ALL TO authenticated USING (tenant_id = get_current_tenant_id()) WITH CHECK (tenant_id = get_current_tenant_id()); COMMENT ON POLICY tenant_isolation_{table} ON {schema}.{table} IS 'Multi-tenant isolation: Users can only access records from their own tenant. Applied to all operations (SELECT, INSERT, UPDATE, DELETE).'; */ -- ============================================================================ -- POLICY 2: USER_DATA_POLICY -- Purpose: Restricts access to data created by or assigned to the current user -- Usage: Apply to tables with created_by or assigned_to columns -- ============================================================================ /* TEMPLATE FOR USER_DATA_POLICY: ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY; CREATE POLICY user_data_{table} ON {schema}.{table} FOR ALL TO authenticated USING ( tenant_id = get_current_tenant_id() AND ( created_by = get_current_user_id() OR assigned_to = get_current_user_id() OR owner_id = get_current_user_id() ) ) WITH CHECK ( tenant_id = get_current_tenant_id() AND ( created_by = get_current_user_id() OR assigned_to = get_current_user_id() OR owner_id = get_current_user_id() ) ); COMMENT ON POLICY user_data_{table} ON {schema}.{table} IS 'User-level isolation: Users can only access their own records. Checks: created_by, assigned_to, or owner_id matches current user.'; */ -- ============================================================================ -- POLICY 3: READ_OWN_DATA_POLICY -- Purpose: Allows users to read only their own data (more permissive for SELECT) -- Usage: Apply when users need read access to own data but restricted write -- ============================================================================ /* TEMPLATE FOR READ_OWN_DATA_POLICY: ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY; CREATE POLICY read_own_data_{table} ON {schema}.{table} FOR SELECT TO authenticated USING ( tenant_id = get_current_tenant_id() AND ( created_by = get_current_user_id() OR assigned_to = get_current_user_id() OR owner_id = get_current_user_id() ) ); COMMENT ON POLICY read_own_data_{table} ON {schema}.{table} IS 'Read access: Users can view records they created, are assigned to, or own. SELECT only - write operations controlled by separate policies.'; */ -- ============================================================================ -- POLICY 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 -- ============================================================================ /* TEMPLATE FOR WRITE_OWN_DATA_POLICY: ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY; -- INSERT policy CREATE POLICY write_own_data_insert_{table} ON {schema}.{table} FOR INSERT TO authenticated WITH CHECK ( tenant_id = get_current_tenant_id() AND created_by = get_current_user_id() ); -- UPDATE policy CREATE POLICY write_own_data_update_{table} ON {schema}.{table} FOR UPDATE TO authenticated USING ( tenant_id = get_current_tenant_id() AND ( created_by = get_current_user_id() OR owner_id = get_current_user_id() ) ) WITH CHECK ( tenant_id = get_current_tenant_id() AND ( created_by = get_current_user_id() OR owner_id = get_current_user_id() ) ); -- DELETE policy CREATE POLICY write_own_data_delete_{table} ON {schema}.{table} FOR DELETE TO authenticated USING ( tenant_id = get_current_tenant_id() AND ( created_by = get_current_user_id() OR owner_id = get_current_user_id() ) ); COMMENT ON POLICY write_own_data_insert_{table} ON {schema}.{table} IS 'Write access (INSERT): Users can only create records for themselves.'; COMMENT ON POLICY write_own_data_update_{table} ON {schema}.{table} IS 'Write access (UPDATE): Users can only update their own records.'; COMMENT ON POLICY write_own_data_delete_{table} ON {schema}.{table} IS 'Write access (DELETE): Users can only delete their own records.'; */ -- ============================================================================ -- POLICY 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 -- ============================================================================ /* TEMPLATE FOR ADMIN_BYPASS_POLICY: ALTER TABLE {schema}.{table} ENABLE ROW LEVEL SECURITY; CREATE POLICY admin_bypass_{table} ON {schema}.{table} FOR ALL TO authenticated USING (is_current_user_admin()) WITH CHECK (is_current_user_admin()); COMMENT ON POLICY admin_bypass_{table} ON {schema}.{table} IS 'Admin bypass: Admin users (admin, super_admin, system_admin) have full access. Use for support, troubleshooting, and system management. Security: Only assign admin roles to trusted users.'; */ -- ============================================================================ -- UTILITY FUNCTION: Apply RLS Policies Dynamically -- ============================================================================ -- Function: Apply tenant isolation policy to a table CREATE OR REPLACE FUNCTION apply_tenant_isolation_policy( p_schema TEXT, p_table TEXT, p_tenant_column TEXT DEFAULT 'tenant_id' ) RETURNS VOID AS $$ DECLARE v_policy_name TEXT; BEGIN v_policy_name := 'tenant_isolation_' || p_table; -- Enable RLS EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', p_schema, p_table); -- Drop policy if exists EXECUTE format( 'DROP POLICY IF EXISTS %I ON %I.%I', v_policy_name, p_schema, p_table ); -- Create policy EXECUTE format( 'CREATE POLICY %I ON %I.%I FOR ALL TO authenticated USING (%I = get_current_tenant_id()) WITH CHECK (%I = get_current_tenant_id())', v_policy_name, p_schema, p_table, p_tenant_column, p_tenant_column ); -- Add comment EXECUTE format( 'COMMENT ON POLICY %I ON %I.%I IS %L', v_policy_name, p_schema, p_table, 'Multi-tenant isolation: Users can only access records from their own tenant.' ); RAISE NOTICE 'Applied tenant_isolation_policy to %.%', p_schema, p_table; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION apply_tenant_isolation_policy IS 'Applies tenant isolation RLS policy to a table. Parameters: - p_schema: Schema name - p_table: Table name - p_tenant_column: Column name for tenant isolation (default: tenant_id)'; -- Function: Apply admin bypass policy to a table CREATE OR REPLACE FUNCTION apply_admin_bypass_policy( p_schema TEXT, p_table TEXT ) RETURNS VOID AS $$ DECLARE v_policy_name TEXT; BEGIN v_policy_name := 'admin_bypass_' || p_table; -- Enable RLS (if not already enabled) EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', p_schema, p_table); -- Drop policy if exists EXECUTE format( 'DROP POLICY IF EXISTS %I ON %I.%I', v_policy_name, p_schema, p_table ); -- Create policy EXECUTE format( 'CREATE POLICY %I ON %I.%I FOR ALL TO authenticated USING (is_current_user_admin()) WITH CHECK (is_current_user_admin())', v_policy_name, p_schema, p_table ); -- Add comment EXECUTE format( 'COMMENT ON POLICY %I ON %I.%I IS %L', v_policy_name, p_schema, p_table, 'Admin bypass: Admin users have full access for support and management.' ); RAISE NOTICE 'Applied admin_bypass_policy to %.%', p_schema, p_table; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION apply_admin_bypass_policy IS 'Applies admin bypass RLS policy to a table. Admins can access all records regardless of tenant or ownership. Parameters: - p_schema: Schema name - p_table: Table name'; -- Function: Apply user data policy to a table CREATE OR REPLACE FUNCTION apply_user_data_policy( p_schema TEXT, p_table TEXT, p_user_columns TEXT[] DEFAULT ARRAY['created_by', 'assigned_to', 'owner_id']::TEXT[] ) RETURNS VOID AS $$ DECLARE v_policy_name TEXT; v_using_clause TEXT; v_column TEXT; v_conditions TEXT[] := ARRAY[]::TEXT[]; BEGIN v_policy_name := 'user_data_' || p_table; -- Build USING clause with provided user columns FOREACH v_column IN ARRAY p_user_columns LOOP v_conditions := array_append(v_conditions, format('%I = get_current_user_id()', v_column)); END LOOP; v_using_clause := 'tenant_id = get_current_tenant_id() AND (' || array_to_string(v_conditions, ' OR ') || ')'; -- Enable RLS EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', p_schema, p_table); -- Drop policy if exists EXECUTE format( 'DROP POLICY IF EXISTS %I ON %I.%I', v_policy_name, p_schema, p_table ); -- Create policy EXECUTE format( 'CREATE POLICY %I ON %I.%I FOR ALL TO authenticated USING (%s) WITH CHECK (%s)', v_policy_name, p_schema, p_table, v_using_clause, v_using_clause ); -- Add comment EXECUTE format( 'COMMENT ON POLICY %I ON %I.%I IS %L', v_policy_name, p_schema, p_table, 'User-level isolation: Users can only access their own records based on: ' || array_to_string(p_user_columns, ', ') ); RAISE NOTICE 'Applied user_data_policy to %.% using columns: %', p_schema, p_table, array_to_string(p_user_columns, ', '); END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION apply_user_data_policy IS 'Applies user data RLS policy to a table. Users can only access records they created, are assigned to, or own. Parameters: - p_schema: Schema name - p_table: Table name - p_user_columns: Array of column names to check (default: created_by, assigned_to, owner_id)'; -- Function: Apply complete RLS policies to a table (tenant + admin) CREATE OR REPLACE FUNCTION apply_complete_rls_policies( p_schema TEXT, p_table TEXT, p_tenant_column TEXT DEFAULT 'tenant_id', p_include_admin_bypass BOOLEAN DEFAULT TRUE ) RETURNS VOID AS $$ BEGIN -- Apply tenant isolation PERFORM apply_tenant_isolation_policy(p_schema, p_table, p_tenant_column); -- Apply admin bypass if requested IF p_include_admin_bypass THEN PERFORM apply_admin_bypass_policy(p_schema, p_table); END IF; RAISE NOTICE 'Applied complete RLS policies to %.%', p_schema, p_table; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION apply_complete_rls_policies IS 'Applies a complete set of RLS policies (tenant isolation + optional admin bypass). Parameters: - p_schema: Schema name - p_table: Table name - p_tenant_column: Column name for tenant isolation (default: tenant_id) - p_include_admin_bypass: Whether to include admin bypass policy (default: TRUE)'; -- ============================================================================ -- EXAMPLE USAGE -- ============================================================================ /* -- Example 1: Apply tenant isolation to a single table SELECT apply_tenant_isolation_policy('core', 'partners'); -- Example 2: Apply complete policies (tenant + admin) to a table SELECT apply_complete_rls_policies('inventory', 'products'); -- Example 3: Apply user data policy SELECT apply_user_data_policy('projects', 'tasks', ARRAY['created_by', 'assigned_to']::TEXT[]); -- Example 4: Apply admin bypass only SELECT apply_admin_bypass_policy('financial', 'invoices'); -- Example 5: Apply to multiple tables at once DO $$ DECLARE r RECORD; BEGIN FOR r IN SELECT table_name FROM information_schema.tables WHERE table_schema = 'core' AND table_name IN ('partners', 'addresses', 'notes', 'attachments') LOOP PERFORM apply_complete_rls_policies('core', r.table_name); END LOOP; END $$; */ -- ============================================================================ -- MIGRATION HELPERS -- ============================================================================ -- Function: Check if RLS is enabled on a table CREATE OR REPLACE FUNCTION is_rls_enabled(p_schema TEXT, p_table TEXT) RETURNS BOOLEAN AS $$ DECLARE v_enabled BOOLEAN; BEGIN SELECT relrowsecurity INTO v_enabled FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = p_schema AND c.relname = p_table; RETURN COALESCE(v_enabled, FALSE); END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION is_rls_enabled IS 'Check if RLS is enabled on a specific table. Returns TRUE if enabled, FALSE otherwise.'; -- Function: List all RLS policies on a table CREATE OR REPLACE FUNCTION list_rls_policies(p_schema TEXT, p_table TEXT) RETURNS TABLE(policy_name NAME, policy_cmd TEXT, policy_using TEXT, policy_check TEXT) AS $$ BEGIN RETURN QUERY SELECT pol.polname::NAME as policy_name, CASE pol.polcmd WHEN 'r' THEN 'SELECT' WHEN 'a' THEN 'INSERT' WHEN 'w' THEN 'UPDATE' WHEN 'd' THEN 'DELETE' WHEN '*' THEN 'ALL' END as policy_cmd, pg_get_expr(pol.polqual, pol.polrelid) as policy_using, pg_get_expr(pol.polwithcheck, pol.polrelid) as policy_check FROM pg_policy pol JOIN pg_class c ON c.oid = pol.polrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = p_schema AND c.relname = p_table; END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION list_rls_policies IS 'List all RLS policies configured on a specific table. Returns: policy_name, policy_cmd, policy_using, policy_check'; -- ============================================================================ -- END OF RLS POLICIES -- ============================================================================