-- c77_rbac--1.1.sql: Enhanced PostgreSQL extension for role-based access control (RBAC) -- Requires PostgreSQL 14 or later -- Production-ready version with bulk operations, removal functions, and enhanced error handling \echo Use "CREATE EXTENSION c77_rbac" to load this file. \quit -- Tables (unchanged from 1.0) CREATE TABLE public.c77_rbac_subjects ( subject_id BIGSERIAL PRIMARY KEY, external_id TEXT UNIQUE NOT NULL ); CREATE TABLE public.c77_rbac_roles ( role_id BIGSERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL ); CREATE TABLE public.c77_rbac_features ( feature_id BIGSERIAL PRIMARY KEY, name TEXT UNIQUE NOT NULL ); CREATE TABLE public.c77_rbac_subject_roles ( subject_id BIGINT REFERENCES public.c77_rbac_subjects(subject_id) ON DELETE CASCADE, role_id BIGINT REFERENCES public.c77_rbac_roles(role_id) ON DELETE CASCADE, scope_type TEXT NOT NULL, scope_id TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (subject_id, role_id, scope_type, scope_id) ); CREATE TABLE public.c77_rbac_role_features ( role_id BIGINT REFERENCES public.c77_rbac_roles(role_id) ON DELETE CASCADE, feature_id BIGINT REFERENCES public.c77_rbac_features(feature_id) ON DELETE CASCADE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (role_id, feature_id) ); -- Enhanced indexes for better performance CREATE INDEX idx_c77_rbac_subjects_external_id ON public.c77_rbac_subjects(external_id); CREATE INDEX idx_c77_rbac_subjects_external_id_hash ON public.c77_rbac_subjects USING hash(external_id); CREATE INDEX idx_c77_rbac_roles_name ON public.c77_rbac_roles(name); CREATE INDEX idx_c77_rbac_features_name ON public.c77_rbac_features(name); CREATE INDEX idx_c77_rbac_subject_roles_subject_id ON public.c77_rbac_subject_roles(subject_id); CREATE INDEX idx_c77_rbac_subject_roles_role_id ON public.c77_rbac_subject_roles(role_id); CREATE INDEX idx_c77_rbac_subject_roles_scope ON public.c77_rbac_subject_roles(scope_type, scope_id); CREATE INDEX idx_c77_rbac_subject_roles_composite ON public.c77_rbac_subject_roles(subject_id, scope_type, scope_id); -- Enhanced c77_rbac_assign_subject with better error handling CREATE OR REPLACE FUNCTION public.c77_rbac_assign_subject( p_external_id TEXT, p_role_name TEXT, p_scope_type TEXT, p_scope_id TEXT ) RETURNS VOID AS $$ DECLARE v_subject_id BIGINT; v_role_id BIGINT; BEGIN -- Comprehensive input validation with helpful error messages IF p_external_id IS NULL OR trim(p_external_id) = '' THEN RAISE EXCEPTION 'external_id cannot be NULL or empty' USING HINT = 'Provide a valid user identifier', ERRCODE = 'invalid_parameter_value'; END IF; IF p_role_name IS NULL OR trim(p_role_name) = '' THEN RAISE EXCEPTION 'role_name cannot be NULL or empty' USING HINT = 'Provide a valid role name like admin, manager, editor', ERRCODE = 'invalid_parameter_value'; END IF; IF p_scope_type IS NULL OR trim(p_scope_type) = '' THEN RAISE EXCEPTION 'scope_type cannot be NULL or empty' USING HINT = 'Use global, department, region, or custom scope type', ERRCODE = 'invalid_parameter_value'; END IF; IF p_scope_id IS NULL OR trim(p_scope_id) = '' THEN RAISE EXCEPTION 'scope_id cannot be NULL or empty' USING HINT = 'Use "all" for global scope or specific identifier for scoped access', ERRCODE = 'invalid_parameter_value'; END IF; -- Validate scope_type against common patterns (warning, not error) IF p_scope_type NOT IN ('global', 'department', 'region', 'court', 'program', 'project', 'team', 'customer', 'tenant') THEN RAISE NOTICE 'scope_type "%" is not in standard list. Proceeding anyway.', p_scope_type; END IF; -- Insert or get subject INSERT INTO public.c77_rbac_subjects (external_id) VALUES (trim(p_external_id)) ON CONFLICT (external_id) DO NOTHING RETURNING subject_id INTO v_subject_id; IF v_subject_id IS NULL THEN SELECT subject_id INTO v_subject_id FROM public.c77_rbac_subjects WHERE external_id = trim(p_external_id); END IF; -- Insert or get role INSERT INTO public.c77_rbac_roles (name) VALUES (trim(p_role_name)) ON CONFLICT (name) DO NOTHING RETURNING role_id INTO v_role_id; IF v_role_id IS NULL THEN SELECT role_id INTO v_role_id FROM public.c77_rbac_roles WHERE name = trim(p_role_name); END IF; -- Assign role to subject with scope INSERT INTO public.c77_rbac_subject_roles (subject_id, role_id, scope_type, scope_id) VALUES (v_subject_id, v_role_id, trim(p_scope_type), trim(p_scope_id)) ON CONFLICT (subject_id, role_id, scope_type, scope_id) DO NOTHING; RAISE NOTICE 'Assigned role "%" to subject "%" with scope "%/%"', p_role_name, p_external_id, p_scope_type, p_scope_id; EXCEPTION WHEN unique_violation THEN RAISE EXCEPTION 'Role assignment already exists' USING HINT = 'This user already has this role with this scope'; WHEN OTHERS THEN RAISE EXCEPTION 'Failed to assign role: %', SQLERRM USING HINT = 'Check that inputs are valid and role exists'; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Enhanced c77_rbac_grant_feature with better error handling CREATE OR REPLACE FUNCTION public.c77_rbac_grant_feature( p_role_name TEXT, p_feature_name TEXT ) RETURNS VOID AS $$ DECLARE v_role_id BIGINT; v_feature_id BIGINT; BEGIN -- Input validation IF p_role_name IS NULL OR trim(p_role_name) = '' THEN RAISE EXCEPTION 'role_name cannot be NULL or empty' USING HINT = 'Provide a valid role name', ERRCODE = 'invalid_parameter_value'; END IF; IF p_feature_name IS NULL OR trim(p_feature_name) = '' THEN RAISE EXCEPTION 'feature_name cannot be NULL or empty' USING HINT = 'Provide a valid feature/permission name', ERRCODE = 'invalid_parameter_value'; END IF; -- Insert or get role INSERT INTO public.c77_rbac_roles (name) VALUES (trim(p_role_name)) ON CONFLICT (name) DO NOTHING RETURNING role_id INTO v_role_id; IF v_role_id IS NULL THEN SELECT role_id INTO v_role_id FROM public.c77_rbac_roles WHERE name = trim(p_role_name); END IF; -- Insert or get feature INSERT INTO public.c77_rbac_features (name) VALUES (trim(p_feature_name)) ON CONFLICT (name) DO NOTHING RETURNING feature_id INTO v_feature_id; IF v_feature_id IS NULL THEN SELECT feature_id INTO v_feature_id FROM public.c77_rbac_features WHERE name = trim(p_feature_name); END IF; -- Grant feature to role INSERT INTO public.c77_rbac_role_features (role_id, feature_id) VALUES (v_role_id, v_feature_id) ON CONFLICT (role_id, feature_id) DO NOTHING; RAISE NOTICE 'Granted feature "%" to role "%"', p_feature_name, p_role_name; EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION 'Failed to grant feature: %', SQLERRM USING HINT = 'Check that role and feature names are valid'; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- NEW: Bulk assignment function for performance CREATE OR REPLACE FUNCTION public.c77_rbac_bulk_assign_subjects( p_external_ids TEXT[], p_role_name TEXT, p_scope_type TEXT, p_scope_id TEXT ) RETURNS TABLE(external_id TEXT, success BOOLEAN, error_message TEXT) AS $$ DECLARE v_external_id TEXT; v_success_count INTEGER := 0; v_error_count INTEGER := 0; BEGIN -- Validate inputs IF p_external_ids IS NULL OR array_length(p_external_ids, 1) IS NULL THEN RAISE EXCEPTION 'external_ids array cannot be NULL or empty'; END IF; IF p_role_name IS NULL OR trim(p_role_name) = '' THEN RAISE EXCEPTION 'role_name cannot be NULL or empty'; END IF; -- Process each external_id FOREACH v_external_id IN ARRAY p_external_ids LOOP BEGIN PERFORM public.c77_rbac_assign_subject(v_external_id, p_role_name, p_scope_type, p_scope_id); external_id := v_external_id; success := true; error_message := NULL; v_success_count := v_success_count + 1; RETURN NEXT; EXCEPTION WHEN OTHERS THEN external_id := v_external_id; success := false; error_message := SQLERRM; v_error_count := v_error_count + 1; RETURN NEXT; END; END LOOP; RAISE NOTICE 'Bulk assignment complete: % successful, % failed', v_success_count, v_error_count; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- NEW: Remove role assignment function CREATE OR REPLACE FUNCTION public.c77_rbac_revoke_subject_role( p_external_id TEXT, p_role_name TEXT, p_scope_type TEXT, p_scope_id TEXT ) RETURNS BOOLEAN AS $$ DECLARE v_removed BOOLEAN := false; v_rows_affected INTEGER; BEGIN -- Input validation IF p_external_id IS NULL OR trim(p_external_id) = '' THEN RAISE EXCEPTION 'external_id cannot be NULL or empty'; END IF; IF p_role_name IS NULL OR trim(p_role_name) = '' THEN RAISE EXCEPTION 'role_name cannot be NULL or empty'; END IF; -- Remove the role assignment DELETE FROM public.c77_rbac_subject_roles sr WHERE sr.subject_id = ( SELECT subject_id FROM public.c77_rbac_subjects WHERE external_id = trim(p_external_id) ) AND sr.role_id = ( SELECT role_id FROM public.c77_rbac_roles WHERE name = trim(p_role_name) ) AND sr.scope_type = trim(p_scope_type) AND sr.scope_id = trim(p_scope_id); GET DIAGNOSTICS v_rows_affected = ROW_COUNT; v_removed := (v_rows_affected > 0); IF v_removed THEN RAISE NOTICE 'Revoked role "%" from subject "%" with scope "%/%"', p_role_name, p_external_id, p_scope_type, p_scope_id; ELSE RAISE NOTICE 'No role assignment found to revoke for subject "%" role "%" scope "%/%"', p_external_id, p_role_name, p_scope_type, p_scope_id; END IF; RETURN v_removed; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- NEW: Remove feature from role function CREATE OR REPLACE FUNCTION public.c77_rbac_revoke_feature( p_role_name TEXT, p_feature_name TEXT ) RETURNS BOOLEAN AS $$ DECLARE v_removed BOOLEAN := false; v_rows_affected INTEGER; BEGIN -- Input validation IF p_role_name IS NULL OR trim(p_role_name) = '' THEN RAISE EXCEPTION 'role_name cannot be NULL or empty'; END IF; IF p_feature_name IS NULL OR trim(p_feature_name) = '' THEN RAISE EXCEPTION 'feature_name cannot be NULL or empty'; END IF; -- Remove the feature from role DELETE FROM public.c77_rbac_role_features rf WHERE rf.role_id = ( SELECT role_id FROM public.c77_rbac_roles WHERE name = trim(p_role_name) ) AND rf.feature_id = ( SELECT feature_id FROM public.c77_rbac_features WHERE name = trim(p_feature_name) ); GET DIAGNOSTICS v_rows_affected = ROW_COUNT; v_removed := (v_rows_affected > 0); IF v_removed THEN RAISE NOTICE 'Revoked feature "%" from role "%"', p_feature_name, p_role_name; ELSE RAISE NOTICE 'Feature "%" was not granted to role "%"', p_feature_name, p_role_name; END IF; RETURN v_removed; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- NEW: Admin sync functions (referenced in docs but missing) CREATE OR REPLACE FUNCTION public.c77_rbac_sync_admin_features() RETURNS INTEGER AS $$ DECLARE v_feature_record RECORD; v_features_synced INTEGER := 0; BEGIN -- Grant all existing features to 'admin' role FOR v_feature_record IN SELECT name FROM public.c77_rbac_features LOOP -- Use the existing grant function to ensure consistency PERFORM public.c77_rbac_grant_feature('admin', v_feature_record.name); v_features_synced := v_features_synced + 1; END LOOP; RAISE NOTICE 'Synced % features to admin role', v_features_synced; RETURN v_features_synced; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE OR REPLACE FUNCTION public.c77_rbac_sync_global_admin_features() RETURNS INTEGER AS $$ DECLARE v_role_record RECORD; v_feature_record RECORD; v_assignments_made INTEGER := 0; BEGIN -- Find all roles that have global/all scope assignments FOR v_role_record IN SELECT DISTINCT r.name FROM public.c77_rbac_roles r JOIN public.c77_rbac_subject_roles sr ON r.role_id = sr.role_id WHERE sr.scope_type = 'global' AND sr.scope_id = 'all' LOOP -- Grant all features to this role FOR v_feature_record IN SELECT name FROM public.c77_rbac_features LOOP PERFORM public.c77_rbac_grant_feature(v_role_record.name, v_feature_record.name); v_assignments_made := v_assignments_made + 1; END LOOP; END LOOP; RAISE NOTICE 'Synced features to % global admin roles', v_assignments_made; RETURN v_assignments_made; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Enhanced c77_rbac_can_access with performance optimizations CREATE OR REPLACE FUNCTION public.c77_rbac_can_access_optimized( p_feature_name TEXT, p_external_id TEXT, p_scope_type TEXT, p_scope_id TEXT ) RETURNS BOOLEAN AS $$ BEGIN -- Fast path for NULL external_id IF p_external_id IS NULL THEN RETURN FALSE; END IF; -- Use optimized query with better indexes RETURN EXISTS ( SELECT 1 FROM public.c77_rbac_subjects s JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id JOIN public.c77_rbac_role_features rf ON sr.role_id = rf.role_id JOIN public.c77_rbac_features f ON rf.feature_id = f.feature_id WHERE s.external_id = p_external_id AND f.name = p_feature_name AND ( (sr.scope_type = 'global' AND sr.scope_id = 'all') OR (sr.scope_type = p_scope_type AND sr.scope_id = p_scope_id) ) ); END; $$ LANGUAGE plpgsql SECURITY DEFINER STABLE; -- Keep the original function for backward compatibility CREATE OR REPLACE FUNCTION public.c77_rbac_can_access( p_feature_name TEXT, p_external_id TEXT, p_scope_type TEXT, p_scope_id TEXT ) RETURNS BOOLEAN AS $$ BEGIN -- Use the optimized version RETURN public.c77_rbac_can_access_optimized(p_feature_name, p_external_id, p_scope_type, p_scope_id); END; $$ LANGUAGE plpgsql SECURITY DEFINER STABLE; -- Enhanced apply_policy function (unchanged core logic, better error handling) CREATE OR REPLACE FUNCTION public.c77_rbac_apply_policy( p_table_name TEXT, p_feature_name TEXT, p_scope_type TEXT, p_scope_column TEXT ) RETURNS VOID AS $$ DECLARE v_schema_name TEXT; v_table_name TEXT; v_policy_exists BOOLEAN; v_table_exists BOOLEAN; BEGIN -- Input validation IF p_table_name IS NULL OR trim(p_table_name) = '' THEN RAISE EXCEPTION 'table_name cannot be NULL or empty'; END IF; IF p_feature_name IS NULL OR trim(p_feature_name) = '' THEN RAISE EXCEPTION 'feature_name cannot be NULL or empty'; END IF; -- Split schema and table name IF position('.' IN p_table_name) > 0 THEN v_schema_name := split_part(p_table_name, '.', 1); v_table_name := split_part(p_table_name, '.', 2); ELSE v_schema_name := 'public'; v_table_name := p_table_name; END IF; -- Check if table exists SELECT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_schema = v_schema_name AND table_name = v_table_name ) INTO v_table_exists; IF NOT v_table_exists THEN RAISE EXCEPTION 'Table %.% does not exist', v_schema_name, v_table_name USING HINT = 'Check table name and schema'; END IF; -- Check if column exists IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = v_schema_name AND table_name = v_table_name AND column_name = p_scope_column ) THEN RAISE EXCEPTION 'Column % does not exist in table %.%', p_scope_column, v_schema_name, v_table_name USING HINT = 'Check column name spelling'; END IF; -- Check if policy exists SELECT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = v_schema_name AND tablename = v_table_name AND policyname = 'c77_rbac_policy' ) INTO v_policy_exists; -- Drop existing policy if it exists IF v_policy_exists THEN EXECUTE format('DROP POLICY c77_rbac_policy ON %I.%I', v_schema_name, v_table_name); RAISE NOTICE 'Dropped existing policy on %.%', v_schema_name, v_table_name; END IF; -- Create the new policy EXECUTE format( 'CREATE POLICY c77_rbac_policy ON %I.%I FOR ALL TO PUBLIC USING ( public.c77_rbac_can_access( %L, current_setting(''c77_rbac.external_id'', true), %L, %I::text ) )', v_schema_name, v_table_name, p_feature_name, p_scope_type, p_scope_column ); -- Enable and force RLS EXECUTE format('ALTER TABLE %I.%I ENABLE ROW LEVEL SECURITY', v_schema_name, v_table_name); EXECUTE format('ALTER TABLE %I.%I FORCE ROW LEVEL SECURITY', v_schema_name, v_table_name); RAISE NOTICE 'Applied RLS policy to %.% requiring feature "%" with scope "%/%"', v_schema_name, v_table_name, p_feature_name, p_scope_type, p_scope_column; EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION 'Failed to apply policy to %.%: %', v_schema_name, v_table_name, SQLERRM USING HINT = 'Check table exists and you have sufficient privileges'; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- NEW: Utility functions for easier management CREATE OR REPLACE FUNCTION public.c77_rbac_get_user_roles(p_external_id TEXT) RETURNS TABLE(role_name TEXT, scope_type TEXT, scope_id TEXT, assigned_at TIMESTAMP) AS $$ BEGIN RETURN QUERY SELECT r.name, sr.scope_type, sr.scope_id, sr.created_at FROM public.c77_rbac_subjects s JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id JOIN public.c77_rbac_roles r ON sr.role_id = r.role_id WHERE s.external_id = p_external_id ORDER BY sr.created_at DESC; END; $$ LANGUAGE plpgsql SECURITY DEFINER STABLE; CREATE OR REPLACE FUNCTION public.c77_rbac_get_role_features(p_role_name TEXT) RETURNS TABLE(feature_name TEXT, granted_at TIMESTAMP) AS $$ BEGIN RETURN QUERY SELECT f.name, rf.created_at FROM public.c77_rbac_roles r JOIN public.c77_rbac_role_features rf ON r.role_id = rf.role_id JOIN public.c77_rbac_features f ON rf.feature_id = f.feature_id WHERE r.name = p_role_name ORDER BY f.name; END; $$ LANGUAGE plpgsql SECURITY DEFINER STABLE; -- Enhanced cleanup functions (keep existing ones, add new) CREATE OR REPLACE FUNCTION public.c77_rbac_remove_all_policies() RETURNS void AS $$ DECLARE policy_record RECORD; table_count INTEGER := 0; BEGIN -- Find and remove all c77_rbac policies FOR policy_record IN SELECT schemaname, tablename, policyname FROM pg_policies WHERE policyname = 'c77_rbac_policy' LOOP EXECUTE format('DROP POLICY IF EXISTS %I ON %I.%I', policy_record.policyname, policy_record.schemaname, policy_record.tablename); -- Optionally disable RLS on the table EXECUTE format('ALTER TABLE %I.%I DISABLE ROW LEVEL SECURITY', policy_record.schemaname, policy_record.tablename); table_count := table_count + 1; RAISE NOTICE 'Removed policy from %.%', policy_record.schemaname, policy_record.tablename; END LOOP; RAISE NOTICE 'Removed policies from % tables', table_count; RAISE NOTICE 'You can now run: DROP EXTENSION c77_rbac CASCADE;'; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE OR REPLACE FUNCTION public.c77_rbac_cleanup_for_removal( p_remove_data BOOLEAN DEFAULT false ) RETURNS void AS $$ BEGIN -- First remove all policies PERFORM public.c77_rbac_remove_all_policies(); -- Optionally clear all RBAC data IF p_remove_data THEN -- Clear in correct order due to foreign keys DELETE FROM public.c77_rbac_subject_roles; DELETE FROM public.c77_rbac_role_features; DELETE FROM public.c77_rbac_subjects; DELETE FROM public.c77_rbac_roles; DELETE FROM public.c77_rbac_features; RAISE NOTICE 'Cleared all RBAC data'; END IF; RAISE NOTICE 'Cleanup complete. You can now run: DROP EXTENSION c77_rbac CASCADE;'; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE OR REPLACE FUNCTION public.c77_rbac_show_dependencies() RETURNS TABLE( dependency_type TEXT, schema_name TEXT, object_name TEXT, details TEXT ) AS $$ BEGIN RETURN QUERY SELECT * FROM ( -- Find policies SELECT 'POLICY'::TEXT as dependency_type, schemaname::TEXT as schema_name, tablename::TEXT as object_name, policyname::TEXT as details FROM pg_policies WHERE policyname = 'c77_rbac_policy' UNION ALL -- Find tables with RLS enabled SELECT 'RLS_ENABLED'::TEXT as dependency_type, schemaname::TEXT as schema_name, tablename::TEXT as object_name, 'Row Level Security is enabled'::TEXT as details FROM pg_tables WHERE rowsecurity = true AND (schemaname, tablename) IN ( SELECT schemaname, tablename FROM pg_policies WHERE policyname = 'c77_rbac_policy' ) UNION ALL -- Find stored procedures that might use c77_rbac functions SELECT DISTINCT 'FUNCTION'::TEXT as dependency_type, n.nspname::TEXT as schema_name, p.proname::TEXT as object_name, 'May reference c77_rbac functions'::TEXT as details FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE p.prosrc LIKE '%c77_rbac%' AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND p.proname NOT LIKE 'c77_rbac%' ) AS dependencies ORDER BY dependency_type, schema_name, object_name; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- Grant permissions for all functions GRANT EXECUTE ON FUNCTION public.c77_rbac_assign_subject(TEXT, TEXT, TEXT, TEXT), public.c77_rbac_grant_feature(TEXT, TEXT), public.c77_rbac_can_access(TEXT, TEXT, TEXT, TEXT), public.c77_rbac_can_access_optimized(TEXT, TEXT, TEXT, TEXT), public.c77_rbac_apply_policy(TEXT, TEXT, TEXT, TEXT), public.c77_rbac_bulk_assign_subjects(TEXT[], TEXT, TEXT, TEXT), public.c77_rbac_revoke_subject_role(TEXT, TEXT, TEXT, TEXT), public.c77_rbac_revoke_feature(TEXT, TEXT), public.c77_rbac_sync_admin_features(), public.c77_rbac_sync_global_admin_features(), public.c77_rbac_get_user_roles(TEXT), public.c77_rbac_get_role_features(TEXT), public.c77_rbac_remove_all_policies(), public.c77_rbac_cleanup_for_removal(BOOLEAN), public.c77_rbac_show_dependencies() TO PUBLIC; -- Grant table permissions (read-only, modifications through functions only) GRANT SELECT ON public.c77_rbac_subjects, public.c77_rbac_roles, public.c77_rbac_features, public.c77_rbac_subject_roles, public.c77_rbac_role_features TO PUBLIC; -- Explicitly revoke direct modification access REVOKE INSERT, UPDATE, DELETE ON public.c77_rbac_subjects, public.c77_rbac_roles, public.c77_rbac_features, public.c77_rbac_subject_roles, public.c77_rbac_role_features FROM PUBLIC; -- Create view for easier role management and reporting CREATE OR REPLACE VIEW public.c77_rbac_user_permissions AS SELECT s.external_id, r.name as role_name, f.name as feature_name, sr.scope_type, sr.scope_id, sr.created_at as role_assigned_at, rf.created_at as feature_granted_at FROM public.c77_rbac_subjects s JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id JOIN public.c77_rbac_roles r ON sr.role_id = r.role_id JOIN public.c77_rbac_role_features rf ON r.role_id = rf.role_id JOIN public.c77_rbac_features f ON rf.feature_id = f.feature_id ORDER BY s.external_id, sr.scope_type, sr.scope_id, f.name; GRANT SELECT ON public.c77_rbac_user_permissions TO PUBLIC; -- Create summary view for administrators CREATE OR REPLACE VIEW public.c77_rbac_summary AS SELECT 'Subjects' as object_type, count(*)::text as count, 'Total users in RBAC system' as description FROM public.c77_rbac_subjects UNION ALL SELECT 'Roles' as object_type, count(*)::text as count, 'Total roles defined' as description FROM public.c77_rbac_roles UNION ALL SELECT 'Features' as object_type, count(*)::text as count, 'Total features/permissions defined' as description FROM public.c77_rbac_features UNION ALL SELECT 'Role Assignments' as object_type, count(*)::text as count, 'Total role assignments to users' as description FROM public.c77_rbac_subject_roles UNION ALL SELECT 'Feature Grants' as object_type, count(*)::text as count, 'Total features granted to roles' as description FROM public.c77_rbac_role_features UNION ALL SELECT 'Active Policies' as object_type, count(*)::text as count, 'Tables with c77_rbac policies applied' as description FROM pg_policies WHERE policyname = 'c77_rbac_policy'; GRANT SELECT ON public.c77_rbac_summary TO PUBLIC;