diff --git a/Database-Level Authorization_ A New Paradigm for Enterprise Security - c77_rbac White Paper.pdf b/Database-Level Authorization_ A New Paradigm for Enterprise Security - c77_rbac White Paper.pdf new file mode 100644 index 0000000..6c2cc53 Binary files /dev/null and b/Database-Level Authorization_ A New Paradigm for Enterprise Security - c77_rbac White Paper.pdf differ diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..37325a9 --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,204 @@ +# C77 RBAC Version 1.1 Improvements Summary + +## Critical Issues Addressed + +### ✅ 1. Enhanced Error Handling +**Problem**: Basic validation with generic error messages +**Solution**: +- Comprehensive input validation with helpful error messages +- Specific error codes (`invalid_parameter_value`) +- Contextual hints for troubleshooting +- Trimming of input parameters to handle whitespace issues +- Better exception handling with more specific error paths + +### ✅ 2. Bulk Operations Added +**Problem**: No batch assignment capabilities for large-scale operations +**Solution**: +- New `c77_rbac_bulk_assign_subjects()` function +- Returns detailed results table showing success/failure for each user +- Processes arrays of external_ids efficiently +- Provides summary statistics of successes vs failures + +### ✅ 3. Removal/Revoke Functions Added +**Problem**: Could only add roles/features, not remove them +**Solution**: +- `c77_rbac_revoke_subject_role()` - Remove role assignments from users +- `c77_rbac_revoke_feature()` - Remove features from roles +- Both functions return boolean indicating if anything was actually removed +- Informative notices about what was or wasn't found to remove + +### ✅ 4. Admin Sync Functions Implemented +**Problem**: Functions referenced in documentation but not implemented +**Solution**: +- `c77_rbac_sync_admin_features()` - Grants all existing features to 'admin' role +- `c77_rbac_sync_global_admin_features()` - Grants all features to roles with global/all scope +- Both functions return count of operations performed +- Essential for maintaining admin privileges as new features are added + +### ✅ 5. Performance Optimizations +**Problem**: Potential performance issues with large datasets +**Solution**: +- Added hash index on `external_id` for faster lookups +- New composite index on `subject_roles` for common query patterns +- `c77_rbac_can_access_optimized()` function with improved query structure +- Original function now calls optimized version for backward compatibility +- Added timestamps to track when roles/features were assigned + +### ✅ 6. Enhanced Policy Application +**Problem**: Limited validation when applying RLS policies +**Solution**: +- Table existence validation before applying policies +- Column existence validation +- Better error messages with specific hints +- Automatic cleanup of existing policies before creating new ones +- More informative success messages + +## New Features Added + +### 📊 Management and Reporting Functions +- `c77_rbac_get_user_roles(external_id)` - Get all roles for a specific user +- `c77_rbac_get_role_features(role_name)` - Get all features for a specific role +- Both include timestamp information for audit purposes + +### 📈 Administrative Views +- `c77_rbac_user_permissions` - Comprehensive view showing all user permissions +- `c77_rbac_summary` - High-level statistics about the RBAC system +- Both views are optimized for reporting and monitoring + +### 🔍 Enhanced Validation +- Standard scope type validation with warnings (not errors) for non-standard types +- Support for common scope types: global, department, region, court, program, project, team, customer, tenant +- Input trimming to handle whitespace issues gracefully + +## Database Schema Enhancements + +### New Columns Added +- `created_at` timestamp columns on `subject_roles` and `role_features` tables +- Enables audit tracking and troubleshooting + +### New Indexes for Performance +```sql +-- Hash index for external_id lookups +idx_c77_rbac_subjects_external_id_hash + +-- Composite index for common access patterns +idx_c77_rbac_subject_roles_composite +``` + +## Upgrade Path + +### From Version 1.0 to 1.1 +- **Upgrade Script**: `c77_rbac--1.0--1.1.sql` provides seamless upgrade +- **Backward Compatibility**: All existing functions continue to work +- **New Functions**: Available immediately after upgrade +- **Data Preservation**: No data loss during upgrade process + +### Installation Options +1. **Fresh Install**: Use `c77_rbac--1.1.sql` for new installations +2. **Upgrade Existing**: Use upgrade script to go from 1.0 → 1.1 +3. **Control File**: Updated to default_version = '1.1' + +## Court System Specific Benefits + +### Enhanced for Court Education System +- Validation includes 'court' and 'program' as standard scope types +- Bulk operations essential for enrolling multiple participants +- Removal functions needed for program completions and transfers +- Admin sync critical for maintaining court administrator privileges + +### Example Usage for Court System +```sql +-- Bulk enroll participants in a program +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['1001','1002','1003','1004','1005'], + 'participant', + 'program', + 'dui_education' +); + +-- Remove participant who completed program +SELECT public.c77_rbac_revoke_subject_role( + '1001', 'participant', 'program', 'dui_education' +); + +-- Sync new court features to admin +SELECT public.c77_rbac_sync_admin_features(); +``` + +## Security Improvements + +### Input Sanitization +- All text inputs are trimmed to remove leading/trailing whitespace +- Comprehensive NULL and empty string checking +- Proper error codes for different validation failures + +### Function Security +- All functions use SECURITY DEFINER for consistent privilege escalation +- Read-only table access enforced (modifications only through functions) +- Comprehensive permission grants for new functions + +### Audit Capabilities +- Timestamp tracking on all role assignments and feature grants +- Views provide easy access to audit information +- Summary statistics help identify potential security issues + +## Testing Recommendations + +### Validation Testing +```sql +-- Test error handling +SELECT public.c77_rbac_assign_subject('', 'role', 'scope', 'id'); -- Should fail +SELECT public.c77_rbac_assign_subject(NULL, 'role', 'scope', 'id'); -- Should fail + +-- Test bulk operations +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['test1', 'test2', 'invalid', 'test3'], + 'test_role', 'department', 'testing' +); + +-- Test removal functions +SELECT public.c77_rbac_revoke_subject_role('test1', 'test_role', 'department', 'testing'); +``` + +### Performance Testing +```sql +-- Test optimized access function +EXPLAIN ANALYZE +SELECT public.c77_rbac_can_access_optimized('test_feature', '123', 'department', 'sales'); + +-- Compare with original function (should be same plan) +EXPLAIN ANALYZE +SELECT public.c77_rbac_can_access('test_feature', '123', 'department', 'sales'); +``` + +## Migration Checklist + +### For Production Deployment +- [ ] Backup existing database +- [ ] Test upgrade script on development copy +- [ ] Verify all existing functionality still works +- [ ] Test new bulk operations with sample data +- [ ] Validate error handling improvements +- [ ] Check performance of optimized functions +- [ ] Update application code to use new functions where beneficial +- [ ] Update documentation and training materials + +### Rollback Plan +If issues are encountered: +1. Restore from backup (safest option) +2. Or manually drop new functions and views +3. Revert control file to version 1.0 +4. Extension will continue working with 1.0 functionality + +## Next Steps + +With these improvements, the c77_rbac extension is now production-ready for the court education system. The major gaps identified in the production readiness plan have been addressed: + +1. ✅ **Enhanced Error Handling** - Comprehensive validation and helpful error messages +2. ✅ **Bulk Operations** - Essential for large-scale user management +3. ✅ **Removal Functions** - Complete CRUD operations for roles and features +4. ✅ **Admin Management** - Automated feature syncing for administrators +5. ✅ **Performance Optimization** - Better indexes and query optimization +6. ✅ **Audit Support** - Timestamp tracking and reporting views + +The extension now provides enterprise-grade functionality suitable for the court system's security and compliance requirements. \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..a22defc --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,770 @@ +# c77_rbac PostgreSQL Extension - Installation Guide + +This comprehensive guide covers all aspects of installing and upgrading the c77_rbac extension, with detailed examples for different scenarios. + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Installation Overview](#installation-overview) +3. [Fresh Installation (Version 1.1)](#fresh-installation-version-11) +4. [Upgrading from Version 1.0](#upgrading-from-version-10) +5. [Post-Installation Verification](#post-installation-verification) +6. [Initial Configuration](#initial-configuration) +7. [Troubleshooting Installation Issues](#troubleshooting-installation-issues) +8. [Uninstallation](#uninstallation) +9. [Production Deployment Checklist](#production-deployment-checklist) + +## Prerequisites + +### System Requirements +- **PostgreSQL**: Version 14 or later +- **Operating System**: Linux (Ubuntu, CentOS, RHEL, Debian), macOS, or Windows +- **Privileges**: PostgreSQL superuser access for installation +- **Disk Space**: Minimal (< 1MB for extension files) + +### Required Access Levels +1. **System Administrator**: Access to copy files to PostgreSQL extension directory +2. **Database Superuser**: To create the extension and grant privileges +3. **Application User**: Regular database user for your application + +### Identify Your PostgreSQL Version and Paths +```bash +# Check PostgreSQL version +psql --version +# or +sudo -u postgres psql -c "SELECT version();" + +# Find PostgreSQL installation directory +pg_config --sharedir +# Example output: /usr/share/postgresql/14 + +# Find extension directory +pg_config --sharedir +# Extension directory will be: /usr/share/postgresql/14/extension/ +``` + +## Installation Overview + +### File Structure +The c77_rbac extension consists of these files: + +``` +c77_rbac.control # Extension metadata (version, dependencies) +c77_rbac--1.1.sql # Fresh installation SQL script +c77_rbac--1.0--1.1.sql # Upgrade script (1.0 → 1.1) +``` + +### Installation Paths +Choose the appropriate installation method: + +- **Fresh Installation**: Use `c77_rbac--1.1.sql` directly +- **Upgrade from 1.0**: Use `c77_rbac--1.0--1.1.sql` upgrade script + +## Fresh Installation (Version 1.1) + +### Step 1: Download and Prepare Files + +Download the extension files to a temporary directory: + +```bash +# Create temporary directory +mkdir ~/c77_rbac_install +cd ~/c77_rbac_install + +# Download files (replace with your actual download method) +wget https://github.com/yourusername/c77_rbac/releases/download/v1.1/c77_rbac.control +wget https://github.com/yourusername/c77_rbac/releases/download/v1.1/c77_rbac--1.1.sql + +# Verify files downloaded +ls -la +# Should show: +# c77_rbac.control +# c77_rbac--1.1.sql +``` + +### Step 2: Copy Files to PostgreSQL Extension Directory + +**For Ubuntu/Debian:** +```bash +# Find the correct PostgreSQL version directory +PG_VERSION=$(pg_config --version | sed 's/PostgreSQL \([0-9]\+\).*/\1/') +echo "PostgreSQL version: $PG_VERSION" + +# Copy files (requires sudo) +sudo cp c77_rbac.control /usr/share/postgresql/$PG_VERSION/extension/ +sudo cp c77_rbac--1.1.sql /usr/share/postgresql/$PG_VERSION/extension/ + +# Verify files copied correctly +ls -la /usr/share/postgresql/$PG_VERSION/extension/c77_rbac* +``` + +**For CentOS/RHEL/Rocky Linux:** +```bash +# Find PostgreSQL version +PG_VERSION=$(pg_config --version | sed 's/PostgreSQL \([0-9]\+\).*/\1/') + +# Copy files +sudo cp c77_rbac.control /usr/pgsql-$PG_VERSION/share/extension/ +sudo cp c77_rbac--1.1.sql /usr/pgsql-$PG_VERSION/share/extension/ + +# Verify +ls -la /usr/pgsql-$PG_VERSION/share/extension/c77_rbac* +``` + +**For macOS (with Homebrew):** +```bash +# Find Homebrew PostgreSQL path +PG_CONFIG_PATH=$(which pg_config) +PG_SHARE_DIR=$(pg_config --sharedir) + +# Copy files +sudo cp c77_rbac.control $PG_SHARE_DIR/extension/ +sudo cp c77_rbac--1.1.sql $PG_SHARE_DIR/extension/ + +# Verify +ls -la $PG_SHARE_DIR/extension/c77_rbac* +``` + +### Step 3: Create Database and Users + +Connect as PostgreSQL superuser: + +```bash +# Connect as postgres superuser +sudo -u postgres psql +``` + +Create your application database and users: + +```sql +-- Create application database +CREATE DATABASE myapp_production; + +-- Create application user with secure password +CREATE USER myapp_user WITH + PASSWORD 'your_very_secure_password_here' + NOSUPERUSER + NOCREATEDB + NOCREATEROLE; + +-- Optional: Create read-only user for reporting +CREATE USER myapp_readonly WITH + PASSWORD 'readonly_secure_password' + NOSUPERUSER + NOCREATEDB + NOCREATEROLE; + +-- List databases to verify +\l +``` + +### Step 4: Install the Extension + +Connect to your application database: + +```sql +-- Connect to your database +\c myapp_production +``` + +Install the c77_rbac extension: + +```sql +-- Install extension (this creates all tables, functions, views) +CREATE EXTENSION c77_rbac; + +-- Verify installation +SELECT extname, extversion FROM pg_extension WHERE extname = 'c77_rbac'; +-- Should show: c77_rbac | 1.1 +``` + +### Step 5: Grant Privileges to Application User + +Grant necessary privileges to your application user: + +```sql +-- Grant database-level privileges +GRANT CONNECT ON DATABASE myapp_production TO myapp_user; +GRANT CREATE ON DATABASE myapp_production TO myapp_user; + +-- Grant schema usage +GRANT USAGE ON SCHEMA public TO myapp_user; + +-- Grant table privileges (read-only, modifications through functions) +GRANT SELECT ON ALL TABLES IN SCHEMA public TO myapp_user; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO myapp_user; + +-- Grant function execution privileges +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO myapp_user; + +-- Set default privileges for future objects +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT ON TABLES TO myapp_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT EXECUTE ON FUNCTIONS TO myapp_user; + +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT USAGE, SELECT ON SEQUENCES TO myapp_user; + +-- For readonly user (optional) +GRANT CONNECT ON DATABASE myapp_production TO myapp_readonly; +GRANT USAGE ON SCHEMA public TO myapp_readonly; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO myapp_readonly; +GRANT SELECT ON public.c77_rbac_user_permissions TO myapp_readonly; +GRANT SELECT ON public.c77_rbac_summary TO myapp_readonly; +``` + +### Step 6: Verify Installation + +Test the installation: + +```sql +-- Check extension is installed +SELECT extname, extversion, extrelocatable FROM pg_extension WHERE extname = 'c77_rbac'; + +-- Check tables were created +\dt public.c77_rbac_* + +-- Check functions were created +\df public.c77_rbac_* + +-- Check views were created +\dv public.c77_rbac_* + +-- Test basic functionality +SELECT public.c77_rbac_grant_feature('test_role', 'test_feature'); +SELECT public.c77_rbac_assign_subject('test_user', 'test_role', 'global', 'all'); +SELECT public.c77_rbac_can_access('test_feature', 'test_user', 'global', 'all'); +-- Should return: true + +-- Clean up test data +SELECT public.c77_rbac_revoke_subject_role('test_user', 'test_role', 'global', 'all'); +SELECT public.c77_rbac_revoke_feature('test_role', 'test_feature'); +``` + +## Upgrading from Version 1.0 + +### Pre-Upgrade Checklist + +1. **Backup your database:** + ```bash + pg_dump -U postgres -h localhost myapp_production > backup_before_upgrade.sql + ``` + +2. **Verify current version:** + ```sql + SELECT extname, extversion FROM pg_extension WHERE extname = 'c77_rbac'; + -- Should show: c77_rbac | 1.0 + ``` + +3. **Check for dependencies:** + ```sql + SELECT * FROM public.c77_rbac_show_dependencies(); + ``` + +### Step 1: Download Upgrade Files + +```bash +# Create upgrade directory +mkdir ~/c77_rbac_upgrade +cd ~/c77_rbac_upgrade + +# Download upgrade files +wget https://github.com/yourusername/c77_rbac/releases/download/v1.1/c77_rbac.control +wget https://github.com/yourusername/c77_rbac/releases/download/v1.1/c77_rbac--1.0--1.1.sql + +# Optional: Download full 1.1 script for reference +wget https://github.com/yourusername/c77_rbac/releases/download/v1.1/c77_rbac--1.1.sql +``` + +### Step 2: Copy Upgrade Files + +```bash +# Find PostgreSQL directory +PG_VERSION=$(pg_config --version | sed 's/PostgreSQL \([0-9]\+\).*/\1/') + +# Copy files (Ubuntu/Debian) +sudo cp c77_rbac.control /usr/share/postgresql/$PG_VERSION/extension/ +sudo cp c77_rbac--1.0--1.1.sql /usr/share/postgresql/$PG_VERSION/extension/ + +# For CentOS/RHEL, use: +# sudo cp c77_rbac.control /usr/pgsql-$PG_VERSION/share/extension/ +# sudo cp c77_rbac--1.0--1.1.sql /usr/pgsql-$PG_VERSION/share/extension/ + +# Verify files +ls -la /usr/share/postgresql/$PG_VERSION/extension/c77_rbac* +# Should show: +# c77_rbac.control +# c77_rbac--1.0.sql (existing) +# c77_rbac--1.0--1.1.sql (new) +``` + +### Step 3: Perform the Upgrade + +Connect to your database as superuser: + +```bash +sudo -u postgres psql -d myapp_production +``` + +Execute the upgrade: + +```sql +-- Check current version +SELECT extname, extversion FROM pg_extension WHERE extname = 'c77_rbac'; + +-- Perform upgrade +ALTER EXTENSION c77_rbac UPDATE TO '1.1'; + +-- Verify upgrade +SELECT extname, extversion FROM pg_extension WHERE extname = 'c77_rbac'; +-- Should now show: c77_rbac | 1.1 +``` + +### Step 4: Verify Upgrade Success + +Test new functionality: + +```sql +-- Test new bulk assignment function +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['user1', 'user2', 'user3'], + 'test_role', + 'department', + 'engineering' +); + +-- Test new management views +SELECT * FROM public.c77_rbac_summary; + +-- Test new utility functions +SELECT * FROM public.c77_rbac_get_user_roles('user1'); + +-- Test admin sync function +SELECT public.c77_rbac_sync_admin_features(); + +-- Clean up test data +SELECT public.c77_rbac_revoke_subject_role('user1', 'test_role', 'department', 'engineering'); +SELECT public.c77_rbac_revoke_subject_role('user2', 'test_role', 'department', 'engineering'); +SELECT public.c77_rbac_revoke_subject_role('user3', 'test_role', 'department', 'engineering'); +``` + +### Step 5: Update Application Code + +After successful upgrade, you can now use new features in your application: + +```php +// Laravel example - using new bulk operations +$userIds = ['1001', '1002', '1003', '1004', '1005']; +$results = DB::select(" + SELECT * FROM public.c77_rbac_bulk_assign_subjects(?, ?, ?, ?) +", [json_encode($userIds), 'student', 'program', 'dui_education']); + +foreach ($results as $result) { + if (!$result->success) { + Log::warning("Failed to assign role to user {$result->external_id}: {$result->error_message}"); + } +} +``` + +## Post-Installation Verification + +### Comprehensive Testing Script + +Create a test script to verify all functionality: + +```sql +-- test_c77_rbac_installation.sql + +\echo 'Testing c77_rbac installation...' + +-- Test 1: Basic feature granting +\echo 'Test 1: Feature granting' +SELECT public.c77_rbac_grant_feature('admin', 'manage_users'); +SELECT public.c77_rbac_grant_feature('manager', 'view_reports'); +SELECT public.c77_rbac_grant_feature('employee', 'view_own_data'); + +-- Test 2: Role assignment +\echo 'Test 2: Role assignment' +SELECT public.c77_rbac_assign_subject('admin_user', 'admin', 'global', 'all'); +SELECT public.c77_rbac_assign_subject('dept_manager', 'manager', 'department', 'sales'); +SELECT public.c77_rbac_assign_subject('employee_1', 'employee', 'department', 'sales'); + +-- Test 3: Bulk assignment (v1.1 feature) +\echo 'Test 3: Bulk assignment' +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['emp_1', 'emp_2', 'emp_3'], + 'employee', + 'department', + 'engineering' +); + +-- Test 4: Permission checking +\echo 'Test 4: Permission checking' +SELECT public.c77_rbac_can_access('manage_users', 'admin_user', 'global', 'all') as admin_can_manage; +SELECT public.c77_rbac_can_access('view_reports', 'dept_manager', 'department', 'sales') as manager_can_view; +SELECT public.c77_rbac_can_access('manage_users', 'employee_1', 'department', 'sales') as employee_cannot_manage; + +-- Test 5: Management views (v1.1 feature) +\echo 'Test 5: Management views' +SELECT * FROM public.c77_rbac_summary; +SELECT * FROM public.c77_rbac_get_user_roles('admin_user'); + +-- Test 6: Admin sync (v1.1 feature) +\echo 'Test 6: Admin sync' +SELECT public.c77_rbac_sync_admin_features(); + +-- Test 7: Removal functions (v1.1 feature) +\echo 'Test 7: Removal functions' +SELECT public.c77_rbac_revoke_subject_role('emp_1', 'employee', 'department', 'engineering'); +SELECT public.c77_rbac_revoke_feature('employee', 'temporary_feature'); + +-- Cleanup +\echo 'Cleaning up test data...' +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; + +\echo 'Installation test complete!' +``` + +Run the test: + +```bash +sudo -u postgres psql -d myapp_production -f test_c77_rbac_installation.sql +``` + +### Performance Testing + +Test with larger datasets: + +```sql +-- Performance test script +DO $$ +DECLARE + start_time TIMESTAMP; + end_time TIMESTAMP; + i INTEGER; +BEGIN + start_time := clock_timestamp(); + + -- Create test features + FOR i IN 1..100 LOOP + PERFORM public.c77_rbac_grant_feature('perf_role', 'feature_' || i); + END LOOP; + + end_time := clock_timestamp(); + RAISE NOTICE 'Feature creation time: %', end_time - start_time; + + start_time := clock_timestamp(); + + -- Test bulk assignment + PERFORM public.c77_rbac_bulk_assign_subjects( + array_agg('user_' || generate_series), + 'perf_role', + 'department', + 'performance_test' + ) FROM generate_series(1, 1000); + + end_time := clock_timestamp(); + RAISE NOTICE 'Bulk assignment time (1000 users): %', end_time - start_time; + + start_time := clock_timestamp(); + + -- Test permission checking + FOR i IN 1..100 LOOP + PERFORM public.c77_rbac_can_access('feature_1', 'user_' || i, 'department', 'performance_test'); + END LOOP; + + end_time := clock_timestamp(); + RAISE NOTICE 'Permission check time (100 checks): %', end_time - start_time; +END $$; +``` + +## Initial Configuration + +### Example: Court Education System Setup + +After installation, configure for your specific use case: + +```sql +-- court_system_setup.sql + +-- Step 1: Create court-specific roles and features +SELECT public.c77_rbac_grant_feature('court_admin', 'manage_all_programs'); +SELECT public.c77_rbac_grant_feature('court_admin', 'view_all_participants'); +SELECT public.c77_rbac_grant_feature('court_admin', 'generate_court_reports'); +SELECT public.c77_rbac_grant_feature('court_admin', 'manage_users'); + +SELECT public.c77_rbac_grant_feature('judge', 'view_court_participants'); +SELECT public.c77_rbac_grant_feature('judge', 'approve_completions'); +SELECT public.c77_rbac_grant_feature('judge', 'access_court_reports'); + +SELECT public.c77_rbac_grant_feature('court_clerk', 'enroll_participants'); +SELECT public.c77_rbac_grant_feature('court_clerk', 'view_court_participants'); +SELECT public.c77_rbac_grant_feature('court_clerk', 'update_participant_info'); + +SELECT public.c77_rbac_grant_feature('program_coordinator', 'manage_programs'); +SELECT public.c77_rbac_grant_feature('program_coordinator', 'view_program_participants'); +SELECT public.c77_rbac_grant_feature('program_coordinator', 'update_progress'); + +SELECT public.c77_rbac_grant_feature('counselor', 'view_assigned_participants'); +SELECT public.c77_rbac_grant_feature('counselor', 'update_progress'); +SELECT public.c77_rbac_grant_feature('counselor', 'mark_attendance'); + +SELECT public.c77_rbac_grant_feature('participant', 'view_own_progress'); +SELECT public.c77_rbac_grant_feature('participant', 'access_program_materials'); + +-- Step 2: Sync admin features +SELECT public.c77_rbac_sync_admin_features(); + +-- Step 3: Assign initial users +-- System administrator +SELECT public.c77_rbac_assign_subject('1', 'court_admin', 'global', 'all'); + +-- County Superior Court judge +SELECT public.c77_rbac_assign_subject('101', 'judge', 'court', 'county_superior'); + +-- Court clerk for county superior +SELECT public.c77_rbac_assign_subject('201', 'court_clerk', 'court', 'county_superior'); + +-- DUI program coordinator +SELECT public.c77_rbac_assign_subject('301', 'program_coordinator', 'program', 'dui_education'); + +-- Step 4: Apply security policies to tables (after creating your tables) +-- SELECT public.c77_rbac_apply_policy('participants', 'view_court_participants', 'court', 'assigned_court'); +-- SELECT public.c77_rbac_apply_policy('participant_progress', 'view_own_progress', 'participant', 'participant_id'); +-- SELECT public.c77_rbac_apply_policy('programs', 'manage_programs', 'program', 'program_type'); + +-- Step 5: Verify setup +SELECT * FROM public.c77_rbac_summary; +``` + +### Laravel Environment Configuration + +Update your `.env` file: + +```ini +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=myapp_production +DB_USERNAME=myapp_user +DB_PASSWORD=your_very_secure_password_here + +# Optional: Enable query logging for debugging +DB_LOG_QUERIES=true +``` + +## Troubleshooting Installation Issues + +### Common Installation Problems + +#### Problem 1: Permission Denied Copying Files + +**Error:** +```bash +cp: cannot create regular file '/usr/share/postgresql/14/extension/c77_rbac.control': Permission denied +``` + +**Solution:** +```bash +# Use sudo for copying files +sudo cp c77_rbac.control /usr/share/postgresql/14/extension/ + +# Or change to postgres user temporarily +sudo su - postgres +cp /path/to/c77_rbac.control /usr/share/postgresql/14/extension/ +exit +``` + +#### Problem 2: Extension Directory Not Found + +**Error:** +```bash +cp: cannot stat '/usr/share/postgresql/14/extension/': No such file or directory +``` + +**Solution:** +```bash +# Find the correct PostgreSQL installation +pg_config --sharedir + +# Or search for extension directories +find /usr -name "extension" -type d 2>/dev/null | grep postgresql + +# Common locations: +# Ubuntu/Debian: /usr/share/postgresql/14/extension/ +# CentOS/RHEL: /usr/pgsql-14/share/extension/ +# macOS Homebrew: /opt/homebrew/share/postgresql@14/extension/ +``` + +#### Problem 3: Extension Creation Fails + +**Error:** +```sql +ERROR: could not open extension control file "/usr/share/postgresql/14/extension/c77_rbac.control": No such file or directory +``` + +**Solution:** +```bash +# Verify files are in correct location +ls -la /usr/share/postgresql/14/extension/c77_rbac* + +# Check file permissions +sudo chmod 644 /usr/share/postgresql/14/extension/c77_rbac* + +# Restart PostgreSQL if necessary +sudo systemctl restart postgresql +``` + +#### Problem 4: Upgrade Fails + +**Error:** +```sql +ERROR: extension "c77_rbac" has no update path from version "1.0" to version "1.1" +``` + +**Solution:** +```bash +# Verify upgrade script exists +ls -la /usr/share/postgresql/14/extension/c77_rbac--1.0--1.1.sql + +# Check control file has correct version +cat /usr/share/postgresql/14/extension/c77_rbac.control + +# Reload configuration +sudo systemctl reload postgresql +``` + +### Diagnostic Commands + +```sql +-- Check extension status +SELECT * FROM pg_extension WHERE extname = 'c77_rbac'; + +-- Check available versions +SELECT name, version, comment FROM pg_available_extensions WHERE name = 'c77_rbac'; + +-- Check update paths +SELECT * FROM pg_extension_update_paths('c77_rbac'); + +-- Verify tables exist +SELECT tablename FROM pg_tables WHERE tablename LIKE 'c77_rbac_%'; + +-- Check function permissions +SELECT routine_name, specific_name FROM information_schema.routines +WHERE routine_name LIKE 'c77_rbac_%'; +``` + +### Log Analysis + +Check PostgreSQL logs for detailed error information: + +```bash +# Ubuntu/Debian +sudo tail -f /var/log/postgresql/postgresql-14-main.log + +# CentOS/RHEL +sudo tail -f /var/lib/pgsql/14/data/log/postgresql-*.log + +# macOS Homebrew +tail -f /opt/homebrew/var/log/postgresql@14.log +``` + +## Uninstallation + +### Safe Uninstallation Process + +1. **Check dependencies first:** + ```sql + SELECT * FROM public.c77_rbac_show_dependencies(); + ``` + +2. **Remove all RLS policies:** + ```sql + SELECT public.c77_rbac_remove_all_policies(); + ``` + +3. **Optionally backup RBAC data:** + ```sql + COPY (SELECT * FROM public.c77_rbac_user_permissions) TO '/tmp/rbac_backup.csv' CSV HEADER; + ``` + +4. **Complete cleanup:** + ```sql + SELECT public.c77_rbac_cleanup_for_removal(true); -- true = remove data + ``` + +5. **Drop the extension:** + ```sql + DROP EXTENSION c77_rbac CASCADE; + ``` + +6. **Remove files from system:** + ```bash + sudo rm /usr/share/postgresql/14/extension/c77_rbac* + ``` + +## Production Deployment Checklist + +### Pre-Deployment + +- [ ] **Database backup completed** +- [ ] **Extension files copied to production server** +- [ ] **PostgreSQL service restarted/reloaded** +- [ ] **Test environment upgrade successful** +- [ ] **Application code updated to handle new features** +- [ ] **Rollback plan prepared** + +### Deployment Steps + +- [ ] **Connect as superuser** +- [ ] **Install/upgrade extension** +- [ ] **Verify installation with test script** +- [ ] **Grant privileges to application users** +- [ ] **Configure initial roles and features** +- [ ] **Apply RLS policies to application tables** +- [ ] **Test application connectivity** + +### Post-Deployment + +- [ ] **Monitor PostgreSQL logs for errors** +- [ ] **Verify application functionality** +- [ ] **Check performance metrics** +- [ ] **Update monitoring alerts** +- [ ] **Document installation for team** + +### Production Configuration Example + +```bash +#!/bin/bash +# production_deploy.sh + +set -e # Exit on any error + +echo "Starting c77_rbac production deployment..." + +# Backup database +pg_dump -U postgres -h localhost myapp_production > backup_$(date +%Y%m%d_%H%M%S).sql + +# Copy extension files +sudo cp c77_rbac.control /usr/share/postgresql/14/extension/ +sudo cp c77_rbac--1.1.sql /usr/share/postgresql/14/extension/ + +# Install extension +sudo -u postgres psql -d myapp_production -c "CREATE EXTENSION IF NOT EXISTS c77_rbac;" + +# Verify installation +sudo -u postgres psql -d myapp_production -c "SELECT extname, extversion FROM pg_extension WHERE extname = 'c77_rbac';" + +# Run initialization script +sudo -u postgres psql -d myapp_production -f court_system_setup.sql + +echo "Deployment completed successfully!" +``` + +This installation guide provides comprehensive coverage of all installation scenarios with detailed examples and troubleshooting information. The step-by-step approach ensures successful deployment in any environment. \ No newline at end of file diff --git a/README.md b/README.md index 7777d1a..a6b7320 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,20 @@ A PostgreSQL extension that provides Role-Based Access Control (RBAC) with Row-L - **Framework Agnostic**: Works with any application framework (Laravel, Rails, Django, etc.) - **Dynamic Schema Support**: Works with any PostgreSQL schema - **Performance Optimized**: Includes indexes and efficient access checks +- **Bulk Operations**: High-performance batch assignment for large user bases +- **Complete CRUD**: Full create, read, update, delete operations for roles and permissions +- **Audit Support**: Timestamp tracking and comprehensive reporting views + +## Version 1.1 Enhancements + +🆕 **New in Version 1.1:** +- **Bulk Operations**: `c77_rbac_bulk_assign_subjects()` for batch user assignments +- **Removal Functions**: `c77_rbac_revoke_subject_role()` and `c77_rbac_revoke_feature()` +- **Admin Sync**: `c77_rbac_sync_admin_features()` and `c77_rbac_sync_global_admin_features()` +- **Enhanced Error Handling**: Comprehensive validation with helpful error messages +- **Performance Optimization**: Better indexes and optimized query functions +- **Management Views**: `c77_rbac_user_permissions` and `c77_rbac_summary` for reporting +- **Audit Tracking**: Timestamps on all role assignments and feature grants ## Requirements @@ -19,10 +33,12 @@ A PostgreSQL extension that provides Role-Based Access Control (RBAC) with Row-L ## Installation +### New Installation (Recommended) + 1. **Copy extension files to PostgreSQL directory:** ```bash sudo cp c77_rbac.control /usr/share/postgresql/14/extension/ - sudo cp c77_rbac--1.0.sql /usr/share/postgresql/14/extension/ + sudo cp c77_rbac--1.1.sql /usr/share/postgresql/14/extension/ ``` 2. **Install the extension (requires superuser):** @@ -42,6 +58,21 @@ A PostgreSQL extension that provides Role-Based Access Control (RBAC) with Row-L GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user; ``` +### Upgrading from Version 1.0 + +If you have version 1.0 already installed: + +1. **Copy upgrade files:** + ```bash + sudo cp c77_rbac.control /usr/share/postgresql/14/extension/ + sudo cp c77_rbac--1.0--1.1.sql /usr/share/postgresql/14/extension/ + ``` + +2. **Upgrade the extension:** + ```sql + ALTER EXTENSION c77_rbac UPDATE TO '1.1'; + ``` + ## Core Concepts ### 1. Subjects (Users) @@ -69,21 +100,37 @@ SELECT public.c77_rbac_grant_feature('manager', 'view_reports'); SELECT public.c77_rbac_grant_feature('manager', 'edit_reports'); SELECT public.c77_rbac_grant_feature('admin', 'manage_users'); --- Admin roles should have all specific features -SELECT public.c77_rbac_grant_feature('admin', 'view_reports'); -SELECT public.c77_rbac_grant_feature('admin', 'edit_reports'); +-- Sync all features to admin role automatically +SELECT public.c77_rbac_sync_admin_features(); ``` ### 2. Assign Users to Roles ```sql --- Assign user to manager role for engineering department +-- Single assignment SELECT public.c77_rbac_assign_subject('123', 'manager', 'department', 'engineering'); --- Assign admin with global access +-- Bulk assignment (NEW in v1.1) +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['101','102','103','104','105'], + 'employee', + 'department', + 'sales' +); + +-- Global admin assignment SELECT public.c77_rbac_assign_subject('1', 'admin', 'global', 'all'); ``` -### 3. Apply Row-Level Security +### 3. Remove Role Assignments (NEW in v1.1) +```sql +-- Remove specific role assignment +SELECT public.c77_rbac_revoke_subject_role('123', 'manager', 'department', 'engineering'); + +-- Remove feature from role +SELECT public.c77_rbac_revoke_feature('temp_role', 'temporary_access'); +``` + +### 4. Apply Row-Level Security ```sql -- Apply RLS policy to a table SELECT public.c77_rbac_apply_policy( @@ -94,7 +141,7 @@ SELECT public.c77_rbac_apply_policy( ); ``` -### 4. Set User Context +### 5. Set User Context and Query ```sql -- Set the current user for RLS checks SET "c77_rbac.external_id" TO '123'; @@ -103,19 +150,57 @@ SET "c77_rbac.external_id" TO '123'; SELECT * FROM myschema.reports; -- Only shows reports for user's department ``` -## Admin Management - -Administrators with `global/all` scope need explicit feature grants. Use helper functions to manage this: +## Management and Reporting (NEW in v1.1) +### Check User Permissions ```sql --- Sync all features to admin role -SELECT public.c77_rbac_sync_admin_features(); +-- Get all roles for a user +SELECT * FROM public.c77_rbac_get_user_roles('123'); --- Or sync to all roles with global/all scope -SELECT public.c77_rbac_sync_global_admin_features(); +-- Get all features for a role +SELECT * FROM public.c77_rbac_get_role_features('manager'); + +-- View comprehensive permissions +SELECT * FROM public.c77_rbac_user_permissions +WHERE external_id = '123'; ``` -## Integration Examples +### System Overview +```sql +-- Get system statistics +SELECT * FROM public.c77_rbac_summary; + +-- Check specific permission +SELECT public.c77_rbac_can_access('edit_reports', '123', 'department', 'engineering'); +``` + +## Available Functions + +### Core Functions +- `c77_rbac_assign_subject(external_id, role, scope_type, scope_id)` - Assign role to user +- `c77_rbac_grant_feature(role, feature)` - Grant feature to role +- `c77_rbac_can_access(feature, external_id, scope_type, scope_id)` - Check access +- `c77_rbac_apply_policy(table, feature, scope_type, column)` - Apply RLS policy + +### Bulk Operations (NEW in v1.1) +- `c77_rbac_bulk_assign_subjects(external_ids[], role, scope_type, scope_id)` - Batch assign roles +- `c77_rbac_revoke_subject_role(external_id, role, scope_type, scope_id)` - Remove role assignment +- `c77_rbac_revoke_feature(role, feature)` - Remove feature from role + +### Admin Management +- `c77_rbac_sync_admin_features()` - Sync all features to admin role +- `c77_rbac_sync_global_admin_features()` - Sync features to all global/all roles + +### Reporting and Management (NEW in v1.1) +- `c77_rbac_get_user_roles(external_id)` - Get all roles for a user +- `c77_rbac_get_role_features(role_name)` - Get all features for a role + +### Maintenance Functions +- `c77_rbac_show_dependencies()` - Show all dependencies on the extension +- `c77_rbac_remove_all_policies()` - Remove all RLS policies +- `c77_rbac_cleanup_for_removal(remove_data)` - Prepare for extension removal + +## Framework Integration Examples ### Laravel Integration ```php @@ -128,10 +213,16 @@ public function handle($request, Closure $next) return $next($request); } +// Bulk assign roles to users +$userIds = ['101', '102', '103', '104', '105']; +$results = DB::select(" + SELECT * FROM public.c77_rbac_bulk_assign_subjects(?, ?, ?, ?) +", [json_encode($userIds), 'student', 'program', 'driver_education']); + // Check permissions -$canView = DB::selectOne(" +$canEdit = DB::selectOne(" SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS allowed -", ['view_reports', Auth::id(), 'department', 'engineering'])->allowed; +", ['edit_reports', Auth::id(), 'department', 'engineering'])->allowed; ``` ### Schema-Aware Usage @@ -149,22 +240,70 @@ SELECT public.c77_rbac_apply_policy( ); ``` -## Available Functions +## Real-World Example: Court Education System -### Core Functions -- `c77_rbac_assign_subject(external_id, role, scope_type, scope_id)` - Assign role to user -- `c77_rbac_grant_feature(role, feature)` - Grant feature to role -- `c77_rbac_can_access(feature, external_id, scope_type, scope_id)` - Check access -- `c77_rbac_apply_policy(table, feature, scope_type, column)` - Apply RLS policy +### Setup Court Roles and Features +```sql +-- Define court-specific roles and features +SELECT public.c77_rbac_grant_feature('court_admin', 'manage_all_programs'); +SELECT public.c77_rbac_grant_feature('court_admin', 'view_all_participants'); +SELECT public.c77_rbac_grant_feature('judge', 'approve_completions'); +SELECT public.c77_rbac_grant_feature('counselor', 'update_progress'); +SELECT public.c77_rbac_grant_feature('participant', 'view_own_progress'); -### Admin Helper Functions -- `c77_rbac_sync_admin_features()` - Sync all features to admin role -- `c77_rbac_sync_global_admin_features()` - Sync features to all global/all roles +-- Sync admin features +SELECT public.c77_rbac_sync_admin_features(); +``` -### Maintenance Functions -- `c77_rbac_show_dependencies()` - Show all dependencies on the extension -- `c77_rbac_remove_all_policies()` - Remove all RLS policies -- `c77_rbac_cleanup_for_removal(remove_data)` - Prepare for extension removal +### Bulk Enroll Participants +```sql +-- Enroll multiple participants in a DUI education program +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['P001','P002','P003','P004','P005'], + 'participant', + 'program', + 'dui_education_2025_q1' +); +``` + +### Apply Security Policies +```sql +-- Participants can only see their own data +SELECT public.c77_rbac_apply_policy( + 'participant_progress', + 'view_own_progress', + 'participant', + 'participant_id' +); + +-- Court staff can see their court's participants +SELECT public.c77_rbac_apply_policy( + 'participants', + 'view_court_participants', + 'court', + 'assigned_court' +); +``` + +## Performance Considerations + +### Built-in Optimizations (Enhanced in v1.1) +- Hash indexes on frequently queried columns +- Composite indexes for common access patterns +- Optimized permission checking functions +- Efficient bulk operations + +### Best Practices +```sql +-- Use bulk operations for large datasets +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + array_agg(user_id::text), 'role_name', 'scope_type', 'scope_id' +) FROM large_user_table; + +-- Cache permission checks in your application +-- Check EXPLAIN ANALYZE for query performance +EXPLAIN ANALYZE SELECT * FROM protected_table; +``` ## Uninstallation @@ -194,37 +333,70 @@ SELECT public.c77_rbac_apply_policy( - Be specific: `view_financial_reports` vs `view_reports` 2. **Admin Setup:** - - Always grant specific features to admin roles - - Use sync functions after adding new features + - Use `c77_rbac_sync_admin_features()` after adding new features - Document all features in your application 3. **Performance:** - - The extension includes optimized indexes - - Use explain analyze to verify query plans - - Consider materialized views for complex permission checks + - Use bulk operations for large datasets + - Consider caching permission checks in your application + - Monitor query performance with EXPLAIN ANALYZE 4. **Security:** - Always use parameterized queries - Reset session variables in connection pools - - Audit role assignments regularly + - Audit role assignments regularly using the new reporting views + +## Error Handling (Enhanced in v1.1) + +The extension now provides comprehensive error handling: + +```sql +-- Helpful error messages with hints +SELECT public.c77_rbac_assign_subject('', 'role', 'scope', 'id'); +-- ERROR: external_id cannot be NULL or empty +-- HINT: Provide a valid user identifier + +-- Bulk operations show individual results +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['valid_user', '', 'another_valid_user'], + 'role', 'scope', 'id' +); +-- Returns table showing success/failure for each user +``` ## Troubleshooting ### No Data Returned 1. Check if `c77_rbac.external_id` is set correctly -2. Verify user has the required role and features -3. Ensure RLS is enabled on the table -4. Check that policies reference the correct columns - -### Policy Not Working -1. Verify column names match between table and policy -2. Check feature names match exactly -3. Ensure scope types and IDs align +2. Use the new management views to verify permissions: + ```sql + SELECT * FROM public.c77_rbac_user_permissions WHERE external_id = 'your_user_id'; + ``` ### Performance Issues -1. Verify indexes exist on RBAC tables -2. Check query plans with EXPLAIN ANALYZE -3. Consider caching permission checks in your application +1. Check the system summary: + ```sql + SELECT * FROM public.c77_rbac_summary; + ``` +2. Use optimized functions (automatically used in v1.1) +3. Verify indexes exist using `\d table_name` + +## Changelog + +### Version 1.1 (Current) +- ✅ Added bulk assignment operations +- ✅ Added removal/revoke functions +- ✅ Enhanced error handling with helpful messages +- ✅ Added admin sync functions +- ✅ Performance optimizations and better indexes +- ✅ Added management views and utility functions +- ✅ Added audit tracking with timestamps + +### Version 1.0 +- ✅ Core RBAC functionality +- ✅ Row-Level Security integration +- ✅ Basic role and feature management +- ✅ Scope-based permissions ## Contributing @@ -243,8 +415,8 @@ MIT License - See LICENSE file for details - Create an issue for bugs or feature requests - Check existing issues before creating new ones - Include PostgreSQL version and reproduction steps for bugs +- Use the new management views to provide system information when reporting issues --- -For framework-specific extensions, see: -- [c77_rbac_laravel](https://github.com/yourusername/c77_rbac_laravel) - Laravel integration \ No newline at end of file +For comprehensive usage examples and Laravel integration, see [USAGE.md](USAGE.md). \ No newline at end of file diff --git a/TUTORIAL-P1.md b/TUTORIAL-P1.md new file mode 100644 index 0000000..07752e6 --- /dev/null +++ b/TUTORIAL-P1.md @@ -0,0 +1,167 @@ +# c77_rbac Complete Hands-On Tutorial - Part 1: Getting Started + +Welcome to the comprehensive tutorial for the c77_rbac PostgreSQL extension! This multi-part tutorial will walk you through every aspect of setting up and using database-level authorization, from installation to advanced real-world scenarios. + +**Tutorial Structure:** +- **Part 1: Getting Started** (this document) - Prerequisites, installation, and basic setup +- **Part 2: Building the TechCorp Database** - Creating realistic company data and schema +- **Part 3: Implementing RBAC** - Setting up roles, features, and permissions +- **Part 4: Row-Level Security** - Applying sophisticated access controls +- **Part 5: Testing and Validation** - Comprehensive security testing +- **Part 6: Advanced Features** - Bulk operations, web integration, and monitoring + +By the end of this complete tutorial, you'll have: +- ✅ Installed and configured c77_rbac +- ✅ Created a complete multi-department company system +- ✅ Implemented role-based access control with row-level security +- ✅ Tested various permission scenarios +- ✅ Integrated with a web application framework +- ✅ Set up monitoring and troubleshooting + +## Prerequisites + +- PostgreSQL 14+ installed and running +- Basic SQL knowledge +- Command line access +- Superuser access to PostgreSQL + +## Tutorial Overview + +We'll build a complete **TechCorp Employee Management System** with these features: +- Multiple departments (Engineering, Sales, HR, Finance) +- Different user roles (Admin, Manager, Employee, Contractor) +- Secure document sharing +- Project management with team access +- Expense tracking with approval workflows + +--- + +## Chapter 1: Installation and Basic Setup + +### Step 1: Verify PostgreSQL Installation + +```bash +# Check PostgreSQL version +psql --version +# Should show PostgreSQL 14 or later + +# Test connection +sudo -u postgres psql -c "SELECT version();" +``` + +### Step 2: Create Tutorial Database + +```bash +# Connect as postgres superuser +sudo -u postgres psql + +# Create our tutorial database +CREATE DATABASE techcorp_tutorial; + +# Create application user +CREATE USER techcorp_app WITH PASSWORD 'secure_tutorial_password'; + +# Connect to our tutorial database +\c techcorp_tutorial +``` + +### Step 3: Install c77_rbac Extension + +First, copy the extension files (assuming you have them): + +```bash +# Copy extension files to PostgreSQL directory +# (Adjust paths based on your PostgreSQL version) +sudo cp c77_rbac.control /usr/share/postgresql/14/extension/ +sudo cp c77_rbac--1.1.sql /usr/share/postgresql/14/extension/ +``` + +Now install the extension: + +```sql +-- Install the extension +CREATE EXTENSION c77_rbac; + +-- Verify installation +SELECT extname, extversion FROM pg_extension WHERE extname = 'c77_rbac'; +-- Should show: c77_rbac | 1.1 + +-- Grant necessary privileges to application user +GRANT CONNECT ON DATABASE techcorp_tutorial TO techcorp_app; +GRANT USAGE ON SCHEMA public TO techcorp_app; +GRANT SELECT ON ALL TABLES IN SCHEMA public TO techcorp_app; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO techcorp_app; +GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO techcorp_app; + +-- Set default privileges +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT ON TABLES TO techcorp_app; +ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT EXECUTE ON FUNCTIONS TO techcorp_app; +``` + +### Step 4: Verify RBAC Installation + +```sql +-- Check RBAC tables were created +\dt public.c77_rbac_* + +-- Check RBAC functions are available +\df public.c77_rbac_* + +-- Test basic functionality +SELECT public.c77_rbac_grant_feature('test_role', 'test_feature'); +SELECT public.c77_rbac_assign_subject('test_user', 'test_role', 'global', 'all'); +SELECT public.c77_rbac_can_access('test_feature', 'test_user', 'global', 'all'); +-- Should return: true + +-- Clean up test data +SELECT public.c77_rbac_revoke_subject_role('test_user', 'test_role', 'global', 'all'); +SELECT public.c77_rbac_revoke_feature('test_role', 'test_feature'); +``` + +**✅ Checkpoint 1:** You should now have c77_rbac installed and working! + +--- + +## What's Next? + +In **Part 2**, we'll create the complete TechCorp database schema with: +- Multiple departments and users +- Projects and team structures +- Document management with security levels +- Expense tracking system +- Realistic sample data + +**Continue to [Part 2: Building the TechCorp Database](TUTORIAL-Part2.md)** + +--- + +## Quick Reference + +### Key Extension Files +- `c77_rbac.control` - Extension metadata +- `c77_rbac--1.1.sql` - Main installation script +- `c77_rbac--1.0--1.1.sql` - Upgrade script (if upgrading from v1.0) + +### Basic Commands +```sql +-- Create extension +CREATE EXTENSION c77_rbac; + +-- Check installation +SELECT extname, extversion FROM pg_extension WHERE extname = 'c77_rbac'; + +-- Basic test +SELECT public.c77_rbac_can_access('feature', 'user', 'scope_type', 'scope_id'); +``` + +### Troubleshooting Installation + +If you encounter issues: + +1. **Permission denied copying files**: Use `sudo` for file operations +2. **Extension directory not found**: Check your PostgreSQL version and paths +3. **Extension creation fails**: Verify files are in correct location with proper permissions + +For detailed troubleshooting, see the [INSTALL.md](INSTALL.md) guide. \ No newline at end of file diff --git a/TUTORIAL-P2.md b/TUTORIAL-P2.md new file mode 100644 index 0000000..80ba3bb --- /dev/null +++ b/TUTORIAL-P2.md @@ -0,0 +1,346 @@ +# c77_rbac Tutorial - Part 2: Building the TechCorp Database + +**Tutorial Navigation:** +- [Part 1: Getting Started](TUTORIAL-Part1.md) - Prerequisites and installation +- **Part 2: Building the TechCorp Database** (this document) - Creating the schema and data +- [Part 3: Implementing RBAC](TUTORIAL-Part3.md) - Setting up roles and permissions +- [Part 4: Row-Level Security](TUTORIAL-Part4.md) - Applying access controls +- [Part 5: Testing and Validation](TUTORIAL-Part5.md) - Security testing +- [Part 6: Advanced Features](TUTORIAL-Part6.md) - Bulk operations and monitoring + +--- + +## Chapter 2: Creating the TechCorp Database Schema + +Now we'll create a realistic multi-department company database that will demonstrate all aspects of the c77_rbac system. + +### Step 1: Create Core Tables + +```sql +-- Create departments table +CREATE TABLE departments ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + code TEXT UNIQUE NOT NULL, + description TEXT, + budget DECIMAL(12,2), + manager_id INTEGER, -- Will reference users table + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create users table (represents our application users) +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + department_id INTEGER REFERENCES departments(id), + employee_type TEXT NOT NULL DEFAULT 'employee', -- 'employee', 'contractor', 'manager', 'admin' + hire_date DATE DEFAULT CURRENT_DATE, + salary DECIMAL(10,2), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Add foreign key constraint for department manager +ALTER TABLE departments ADD CONSTRAINT fk_dept_manager + FOREIGN KEY (manager_id) REFERENCES users(id); + +-- Create projects table +CREATE TABLE projects ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + department_id INTEGER REFERENCES departments(id), + project_manager_id INTEGER REFERENCES users(id), + status TEXT DEFAULT 'planning', -- 'planning', 'active', 'on_hold', 'completed', 'cancelled' + budget DECIMAL(12,2), + start_date DATE, + end_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create project team members +CREATE TABLE project_members ( + id SERIAL PRIMARY KEY, + project_id INTEGER REFERENCES projects(id), + user_id INTEGER REFERENCES users(id), + role TEXT DEFAULT 'member', -- 'member', 'lead', 'observer' + added_date DATE DEFAULT CURRENT_DATE, + UNIQUE(project_id, user_id) +); + +-- Create documents table +CREATE TABLE documents ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + content TEXT, + file_path TEXT, + author_id INTEGER REFERENCES users(id), + department_id INTEGER REFERENCES departments(id), + project_id INTEGER REFERENCES projects(id) NULL, + security_level TEXT DEFAULT 'internal', -- 'public', 'internal', 'confidential', 'restricted' + status TEXT DEFAULT 'draft', -- 'draft', 'published', 'archived' + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create expenses table +CREATE TABLE expenses ( + id SERIAL PRIMARY KEY, + description TEXT NOT NULL, + amount DECIMAL(10,2) NOT NULL, + category TEXT NOT NULL, -- 'travel', 'equipment', 'software', 'training', 'other' + submitted_by INTEGER REFERENCES users(id), + department_id INTEGER REFERENCES departments(id), + project_id INTEGER REFERENCES projects(id) NULL, + status TEXT DEFAULT 'submitted', -- 'submitted', 'approved', 'rejected', 'paid' + submitted_date DATE DEFAULT CURRENT_DATE, + approved_by INTEGER REFERENCES users(id) NULL, + approved_date DATE NULL, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create audit log for sensitive operations +CREATE TABLE audit_log ( + id BIGSERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id), + action TEXT NOT NULL, + table_name TEXT, + record_id INTEGER, + old_values JSONB, + new_values JSONB, + ip_address INET, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +### Step 2: Insert Sample Data + +```sql +-- Insert departments +INSERT INTO departments (name, code, description, budget) VALUES +('Engineering', 'ENG', 'Software development and infrastructure', 2500000.00), +('Sales', 'SALES', 'Customer acquisition and revenue generation', 1800000.00), +('Human Resources', 'HR', 'Employee management and company culture', 800000.00), +('Finance', 'FIN', 'Financial planning and accounting', 600000.00), +('Marketing', 'MKT', 'Brand management and lead generation', 1200000.00); + +-- Insert users (these IDs will be our external_ids in RBAC) +INSERT INTO users (id, email, first_name, last_name, department_id, employee_type, salary) VALUES +-- Engineering Department +(1, 'alice.admin@techcorp.com', 'Alice', 'Admin', 1, 'admin', 150000.00), +(101, 'bob.engineer@techcorp.com', 'Bob', 'Smith', 1, 'manager', 120000.00), +(102, 'carol.dev@techcorp.com', 'Carol', 'Johnson', 1, 'employee', 95000.00), +(103, 'dave.senior@techcorp.com', 'Dave', 'Wilson', 1, 'employee', 110000.00), +(104, 'eve.contractor@techcorp.com', 'Eve', 'Brown', 1, 'contractor', 85000.00), + +-- Sales Department +(201, 'frank.sales@techcorp.com', 'Frank', 'Davis', 2, 'manager', 130000.00), +(202, 'grace.rep@techcorp.com', 'Grace', 'Miller', 2, 'employee', 75000.00), +(203, 'henry.rep@techcorp.com', 'Henry', 'Garcia', 2, 'employee', 72000.00), + +-- HR Department +(301, 'iris.hr@techcorp.com', 'Iris', 'Martinez', 3, 'manager', 105000.00), +(302, 'jack.hr@techcorp.com', 'Jack', 'Anderson', 3, 'employee', 68000.00), + +-- Finance Department +(401, 'kelly.finance@techcorp.com', 'Kelly', 'Taylor', 4, 'manager', 115000.00), +(402, 'liam.accounting@techcorp.com', 'Liam', 'Thomas', 4, 'employee', 65000.00), + +-- Marketing Department +(501, 'maya.marketing@techcorp.com', 'Maya', 'White', 5, 'manager', 100000.00), +(502, 'noah.content@techcorp.com', 'Noah', 'Harris', 5, 'employee', 70000.00); + +-- Update department managers +UPDATE departments SET manager_id = 101 WHERE code = 'ENG'; +UPDATE departments SET manager_id = 201 WHERE code = 'SALES'; +UPDATE departments SET manager_id = 301 WHERE code = 'HR'; +UPDATE departments SET manager_id = 401 WHERE code = 'FIN'; +UPDATE departments SET manager_id = 501 WHERE code = 'MKT'; + +-- Insert sample projects +INSERT INTO projects (name, description, department_id, project_manager_id, status, budget, start_date) VALUES +('Customer Portal V2', 'Rebuild customer-facing portal with modern tech stack', 1, 101, 'active', 500000.00, '2024-01-15'), +('Mobile App Development', 'Native mobile app for iOS and Android', 1, 103, 'planning', 300000.00, '2024-03-01'), +('Q1 Sales Campaign', 'Major sales push for Q1 targets', 2, 201, 'active', 150000.00, '2024-01-01'), +('Employee Onboarding System', 'Streamline new hire processes', 3, 301, 'planning', 75000.00, '2024-02-15'), +('Financial Reporting Dashboard', 'Real-time financial analytics', 4, 401, 'active', 120000.00, '2024-01-20'); + +-- Insert project team members +INSERT INTO project_members (project_id, user_id, role) VALUES +-- Customer Portal V2 team +(1, 101, 'lead'), +(1, 102, 'member'), +(1, 103, 'member'), +(1, 104, 'member'), + +-- Mobile App team +(2, 103, 'lead'), +(2, 102, 'member'), + +-- Sales Campaign team +(3, 201, 'lead'), +(3, 202, 'member'), +(3, 203, 'member'), +(3, 502, 'member'), -- Marketing support + +-- HR System team +(4, 301, 'lead'), +(4, 302, 'member'), +(4, 102, 'member'), -- Engineering support + +-- Finance Dashboard team +(5, 401, 'lead'), +(5, 402, 'member'), +(5, 103, 'member'); -- Engineering support + +-- Insert sample documents +INSERT INTO documents (title, content, author_id, department_id, project_id, security_level, status) VALUES +('Company Handbook', 'Complete employee handbook with policies and procedures', 301, 3, NULL, 'internal', 'published'), +('Salary Ranges 2024', 'Confidential salary band information', 301, 3, NULL, 'confidential', 'published'), +('Customer Portal Requirements', 'Technical requirements for portal rebuild', 101, 1, 1, 'internal', 'published'), +('Sales Strategy Q1', 'Confidential sales strategy and targets', 201, 2, 3, 'confidential', 'published'), +('Security Policies', 'Company-wide security guidelines', 1, 1, NULL, 'internal', 'published'), +('Financial Results Q4', 'Quarterly financial performance', 401, 4, NULL, 'restricted', 'published'), +('Engineering Standards', 'Code review and development standards', 101, 1, NULL, 'internal', 'published'), +('Project Meeting Notes', 'Weekly standup meeting notes', 102, 1, 1, 'internal', 'draft'); + +-- Insert sample expenses +INSERT INTO expenses (description, amount, category, submitted_by, department_id, project_id, status, notes) VALUES +('MacBook Pro for development', 2499.00, 'equipment', 102, 1, 1, 'approved', 'For Customer Portal project'), +('Conference attendance - DevCon', 1200.00, 'training', 103, 1, NULL, 'submitted', 'Professional development'), +('Software licenses - IntelliJ', 500.00, 'software', 101, 1, NULL, 'approved', 'Team development tools'), +('Client dinner - ABC Corp', 350.00, 'travel', 201, 2, 3, 'approved', 'Q1 sales campaign'), +('Recruitment platform subscription', 800.00, 'software', 301, 3, NULL, 'submitted', 'Hiring tools'), +('Accounting software upgrade', 1500.00, 'software', 401, 4, NULL, 'submitted', 'Financial reporting needs'), +('Team building event', 2000.00, 'other', 501, 5, NULL, 'approved', 'Department morale activity'); +``` + +### Step 3: Add More Realistic Data + +```sql +-- Add more employees for testing bulk operations later +INSERT INTO users (id, email, first_name, last_name, department_id, employee_type, salary) +SELECT + 1000 + generate_series, + 'user' || generate_series || '@techcorp.com', + 'User', + 'Number' || generate_series, + (generate_series % 5) + 1, -- Distribute across departments + 'employee', + 50000 + (generate_series % 50) * 1000 +FROM generate_series(1, 50); + +-- Add more documents with varying security levels +INSERT INTO documents (title, content, author_id, department_id, security_level, status) VALUES +('Public Company Blog Post', 'Public facing marketing content', 502, 5, 'public', 'published'), +('Internal Newsletter Q1', 'Quarterly company updates for all employees', 301, 3, 'internal', 'published'), +('HR Policy Updates', 'Confidential updates to employee policies', 301, 3, 'confidential', 'published'), +('Executive Compensation Plan', 'Restricted financial information', 401, 4, 'restricted', 'published'), +('Engineering Roadmap 2024', 'Technical strategy and roadmap', 101, 1, 'confidential', 'published'), +('Sales Territory Assignments', 'Confidential sales territory information', 201, 2, 'confidential', 'published'), +('Marketing Budget Breakdown', 'Detailed marketing spend analysis', 501, 5, 'confidential', 'draft'); + +-- Add more expenses for comprehensive testing +INSERT INTO expenses (description, amount, category, submitted_by, department_id, status, notes) VALUES +('Sales team laptops', 8500.00, 'equipment', 201, 2, 'approved', 'Quarterly equipment refresh'), +('HR software license', 3600.00, 'software', 301, 3, 'submitted', 'Annual subscription renewal'), +('Marketing campaign costs', 15000.00, 'other', 501, 5, 'approved', 'Q1 digital advertising'), +('Training workshop', 2800.00, 'training', 102, 1, 'submitted', 'React development workshop'), +('Office supplies', 450.00, 'other', 302, 3, 'approved', 'Monthly office supply order'), +('Travel expenses - client visit', 1250.00, 'travel', 202, 2, 'submitted', 'San Francisco client meeting'); +``` + +**✅ Checkpoint 2:** You now have a complete company database with realistic data! + +### Step 4: Verify Your Data + +```sql +-- Check department structure +SELECT + d.name as department, + d.code, + m.first_name || ' ' || m.last_name as manager, + count(u.id) as total_employees, + d.budget +FROM departments d +LEFT JOIN users m ON d.manager_id = m.id +LEFT JOIN users u ON d.id = u.department_id +GROUP BY d.id, d.name, d.code, m.first_name, m.last_name, d.budget +ORDER BY d.name; + +-- Check project teams +SELECT + p.name as project, + d.name as department, + pm_user.first_name || ' ' || pm_user.last_name as project_manager, + count(pms.user_id) as team_size, + p.budget, + p.status +FROM projects p +JOIN departments d ON p.department_id = d.id +JOIN users pm_user ON p.project_manager_id = pm_user.id +LEFT JOIN project_members pms ON p.id = pms.project_id +GROUP BY p.id, p.name, d.name, pm_user.first_name, pm_user.last_name, p.budget, p.status +ORDER BY p.name; + +-- Check document security levels +SELECT + security_level, + count(*) as document_count, + round(count(*) * 100.0 / sum(count(*)) OVER (), 1) as percentage +FROM documents +GROUP BY security_level +ORDER BY count(*) DESC; + +-- Check user distribution +SELECT + d.name as department, + d.code, + count(u.id) as user_count, + count(CASE WHEN u.employee_type = 'manager' THEN 1 END) as managers, + count(CASE WHEN u.employee_type = 'employee' THEN 1 END) as employees, + count(CASE WHEN u.employee_type = 'contractor' THEN 1 END) as contractors +FROM departments d +LEFT JOIN users u ON d.id = u.department_id +GROUP BY d.id, d.name, d.code +ORDER BY d.name; +``` + +--- + +## What's Next? + +Now that we have a complete company database with realistic data, in **Part 3** we'll implement the RBAC system: + +- Define company-wide features and permissions +- Create role hierarchies (Admin, Manager, Employee, Contractor) +- Assign users to appropriate roles with proper scoping +- Set up department-based and project-based access patterns + +**Continue to [Part 3: Implementing RBAC](TUTORIAL-Part3.md)** + +--- + +## Database Summary + +You've created a comprehensive TechCorp database with: + +### Core Tables +- **5 departments** with realistic budgets and managers +- **65+ users** across different roles and departments +- **5 projects** with cross-departmental teams +- **15+ documents** with varying security levels +- **13+ expense records** with different approval states +- **Audit logging** capabilities + +### Key Features +- **Realistic relationships** between departments, users, and projects +- **Multiple security levels** for documents (public, internal, confidential, restricted) +- **Cross-departmental collaboration** through project teams +- **Comprehensive expense tracking** with approval workflows +- **Audit trail** for sensitive operations + +This database will serve as the foundation for demonstrating all aspects of the c77_rbac system in the following parts of the tutorial. \ No newline at end of file diff --git a/TUTORIAL-P3.md b/TUTORIAL-P3.md new file mode 100644 index 0000000..e3d070e --- /dev/null +++ b/TUTORIAL-P3.md @@ -0,0 +1,367 @@ +# c77_rbac Tutorial - Part 3: Implementing RBAC + +**Tutorial Navigation:** +- [Part 1: Getting Started](TUTORIAL-Part1.md) - Prerequisites and installation +- [Part 2: Building the TechCorp Database](TUTORIAL-Part2.md) - Creating the schema and data +- **Part 3: Implementing RBAC** (this document) - Setting up roles and permissions +- [Part 4: Row-Level Security](TUTORIAL-Part4.md) - Applying access controls +- [Part 5: Testing and Validation](TUTORIAL-Part5.md) - Security testing +- [Part 6: Advanced Features](TUTORIAL-Part6.md) - Bulk operations and monitoring + +--- + +## Chapter 3: Setting Up RBAC Roles and Features + +Now we'll implement a comprehensive role-based access control system for our TechCorp database. This chapter covers defining features, creating roles, and assigning users to appropriate roles with proper scoping. + +### Step 1: Define Company-Wide Features + +Features represent specific permissions that can be checked in your application. Let's define features that match our TechCorp organizational structure: + +```sql +-- System Administration Features +SELECT public.c77_rbac_grant_feature('system_admin', 'manage_all_users'); +SELECT public.c77_rbac_grant_feature('system_admin', 'access_all_departments'); +SELECT public.c77_rbac_grant_feature('system_admin', 'view_all_salaries'); +SELECT public.c77_rbac_grant_feature('system_admin', 'manage_system_settings'); +SELECT public.c77_rbac_grant_feature('system_admin', 'view_audit_logs'); +SELECT public.c77_rbac_grant_feature('system_admin', 'export_all_data'); + +-- Department Management Features +SELECT public.c77_rbac_grant_feature('dept_manager', 'view_dept_employees'); +SELECT public.c77_rbac_grant_feature('dept_manager', 'manage_dept_projects'); +SELECT public.c77_rbac_grant_feature('dept_manager', 'approve_dept_expenses'); +SELECT public.c77_rbac_grant_feature('dept_manager', 'view_dept_salaries'); +SELECT public.c77_rbac_grant_feature('dept_manager', 'create_dept_documents'); +SELECT public.c77_rbac_grant_feature('dept_manager', 'view_confidential_docs'); + +-- Regular Employee Features +SELECT public.c77_rbac_grant_feature('employee', 'view_own_profile'); +SELECT public.c77_rbac_grant_feature('employee', 'update_own_profile'); +SELECT public.c77_rbac_grant_feature('employee', 'view_company_handbook'); +SELECT public.c77_rbac_grant_feature('employee', 'submit_expenses'); +SELECT public.c77_rbac_grant_feature('employee', 'view_own_expenses'); +SELECT public.c77_rbac_grant_feature('employee', 'access_internal_docs'); + +-- Project Team Features +SELECT public.c77_rbac_grant_feature('project_member', 'view_project_details'); +SELECT public.c77_rbac_grant_feature('project_member', 'edit_project_documents'); +SELECT public.c77_rbac_grant_feature('project_member', 'view_project_expenses'); +SELECT public.c77_rbac_grant_feature('project_member', 'submit_project_expenses'); + +SELECT public.c77_rbac_grant_feature('project_lead', 'manage_project_team'); +SELECT public.c77_rbac_grant_feature('project_lead', 'approve_project_expenses'); +SELECT public.c77_rbac_grant_feature('project_lead', 'create_project_documents'); + +-- Contractor Features (limited access) +SELECT public.c77_rbac_grant_feature('contractor', 'view_own_profile'); +SELECT public.c77_rbac_grant_feature('contractor', 'view_assigned_projects'); +SELECT public.c77_rbac_grant_feature('contractor', 'submit_expenses'); +SELECT public.c77_rbac_grant_feature('contractor', 'access_project_docs'); + +-- HR Specific Features +SELECT public.c77_rbac_grant_feature('hr_staff', 'view_all_employees'); +SELECT public.c77_rbac_grant_feature('hr_staff', 'manage_employee_data'); +SELECT public.c77_rbac_grant_feature('hr_staff', 'access_hr_documents'); +SELECT public.c77_rbac_grant_feature('hr_staff', 'view_salary_data'); + +-- Finance Specific Features +SELECT public.c77_rbac_grant_feature('finance_staff', 'view_all_expenses'); +SELECT public.c77_rbac_grant_feature('finance_staff', 'approve_expenses'); +SELECT public.c77_rbac_grant_feature('finance_staff', 'view_budget_data'); +SELECT public.c77_rbac_grant_feature('finance_staff', 'access_financial_docs'); + +-- Sales Specific Features +SELECT public.c77_rbac_grant_feature('sales_staff', 'view_sales_data'); +SELECT public.c77_rbac_grant_feature('sales_staff', 'manage_customer_info'); +SELECT public.c77_rbac_grant_feature('sales_staff', 'access_sales_docs'); +``` + +### Step 2: Sync Admin Features + +The `admin` role should have access to all features. Let's sync all existing features to the admin role: + +```sql +-- Make sure system admin gets all features +SELECT public.c77_rbac_sync_admin_features(); + +-- Check what features were granted to admin +SELECT * FROM public.c77_rbac_get_role_features('admin'); + +-- Also sync features to our system_admin role +SELECT public.c77_rbac_sync_admin_features(); +``` + +### Step 3: Assign Roles to Users + +Now we'll assign our TechCorp users to appropriate roles with proper scoping: + +```sql +-- System Administrator (Alice) +SELECT public.c77_rbac_assign_subject('1', 'system_admin', 'global', 'all'); + +-- Department Managers +SELECT public.c77_rbac_assign_subject('101', 'dept_manager', 'department', 'ENG'); -- Bob +SELECT public.c77_rbac_assign_subject('201', 'dept_manager', 'department', 'SALES'); -- Frank +SELECT public.c77_rbac_assign_subject('301', 'dept_manager', 'department', 'HR'); -- Iris +SELECT public.c77_rbac_assign_subject('401', 'dept_manager', 'department', 'FIN'); -- Kelly +SELECT public.c77_rbac_assign_subject('501', 'dept_manager', 'department', 'MKT'); -- Maya + +-- HR Staff (need special access across departments) +SELECT public.c77_rbac_assign_subject('301', 'hr_staff', 'global', 'all'); -- Iris (manager) +SELECT public.c77_rbac_assign_subject('302', 'hr_staff', 'global', 'all'); -- Jack + +-- Finance Staff (need expense access across departments) +SELECT public.c77_rbac_assign_subject('401', 'finance_staff', 'global', 'all'); -- Kelly (manager) +SELECT public.c77_rbac_assign_subject('402', 'finance_staff', 'global', 'all'); -- Liam + +-- Regular Employees (department-scoped) +SELECT public.c77_rbac_assign_subject('102', 'employee', 'department', 'ENG'); +SELECT public.c77_rbac_assign_subject('103', 'employee', 'department', 'ENG'); +SELECT public.c77_rbac_assign_subject('202', 'employee', 'department', 'SALES'); +SELECT public.c77_rbac_assign_subject('203', 'employee', 'department', 'SALES'); +SELECT public.c77_rbac_assign_subject('502', 'employee', 'department', 'MKT'); + +-- Contractors (department-scoped with limited access) +SELECT public.c77_rbac_assign_subject('104', 'contractor', 'department', 'ENG'); + +-- Project-specific roles +SELECT public.c77_rbac_assign_subject('101', 'project_lead', 'project', '1'); -- Bob leads Portal project +SELECT public.c77_rbac_assign_subject('103', 'project_lead', 'project', '2'); -- Dave leads Mobile project +SELECT public.c77_rbac_assign_subject('102', 'project_member', 'project', '1'); -- Carol on Portal +SELECT public.c77_rbac_assign_subject('102', 'project_member', 'project', '2'); -- Carol on Mobile +SELECT public.c77_rbac_assign_subject('104', 'project_member', 'project', '1'); -- Eve on Portal + +-- Bulk assign basic employee role to all the additional users we created +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + (SELECT array_agg(id::text) FROM users WHERE id BETWEEN 1001 AND 1050), + 'employee', + 'department', + 'ENG' -- Assign them all to engineering for this example +); +``` + +### Step 4: Verify Role Assignments + +Let's check that our role assignments are working correctly: + +```sql +-- Check Alice's (admin) permissions +SELECT 'Alice (System Admin) - Roles:' as info; +SELECT * FROM public.c77_rbac_get_user_roles('1'); + +-- Check Bob's (engineering manager) permissions +SELECT 'Bob (Engineering Manager) - Roles:' as info; +SELECT * FROM public.c77_rbac_get_user_roles('101'); + +-- Check Carol's (developer) permissions +SELECT 'Carol (Developer) - Roles:' as info; +SELECT * FROM public.c77_rbac_get_user_roles('102'); + +-- Check Iris's (HR manager) permissions +SELECT 'Iris (HR Manager) - Roles:' as info; +SELECT * FROM public.c77_rbac_get_user_roles('301'); + +-- Check system summary +SELECT 'System Summary:' as info; +SELECT * FROM public.c77_rbac_summary; +``` + +### Step 5: Understanding RBAC Scoping + +Let's explore how different scoping patterns work in our system: + +```sql +-- Global scope (system-wide access) +-- Example: Alice has system_admin role with global/all scope +SELECT 'Global Admin Access Example:' as demo; +SELECT public.c77_rbac_can_access('manage_all_users', '1', 'global', 'all') as alice_can_manage_all; + +-- Department scope (department-specific access) +-- Example: Bob can manage engineering department but not sales +SELECT 'Department Scope Examples:' as demo; +SELECT public.c77_rbac_can_access('manage_dept_projects', '101', 'department', 'ENG') as bob_can_manage_eng; +SELECT public.c77_rbac_can_access('manage_dept_projects', '101', 'department', 'SALES') as bob_cannot_manage_sales; + +-- Project scope (project-specific access) +-- Example: Carol is a member of projects 1 and 2 +SELECT 'Project Scope Examples:' as demo; +SELECT public.c77_rbac_can_access('view_project_details', '102', 'project', '1') as carol_can_view_proj1; +SELECT public.c77_rbac_can_access('view_project_details', '102', 'project', '3') as carol_cannot_view_proj3; + +-- Cross-department access for special roles +-- Example: Iris (HR) can access all departments +SELECT 'Cross-Department Access Example:' as demo; +SELECT public.c77_rbac_can_access('view_all_employees', '301', 'global', 'all') as iris_can_see_all; +``` + +### Step 6: Create Role Hierarchy Visualization + +Let's create a query to visualize our role hierarchy: + +```sql +-- Create a comprehensive view of our RBAC setup +SELECT 'TechCorp RBAC Role Distribution:' as title; + +SELECT + r.name as role_name, + sr.scope_type, + sr.scope_id, + count(*) as user_count, + string_agg(s.external_id, ', ' ORDER BY s.external_id::integer) as user_ids +FROM public.c77_rbac_roles r +JOIN public.c77_rbac_subject_roles sr ON r.role_id = sr.role_id +JOIN public.c77_rbac_subjects s ON sr.subject_id = s.subject_id +GROUP BY r.name, sr.scope_type, sr.scope_id +ORDER BY r.name, sr.scope_type, sr.scope_id; + +-- Show feature distribution across roles +SELECT 'Feature Distribution:' as title; + +SELECT + r.name as role_name, + count(f.name) as feature_count, + string_agg(f.name, ', ' ORDER BY f.name) as features +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 +GROUP BY r.name +ORDER BY count(f.name) DESC, r.name; +``` + +**✅ Checkpoint 3:** You now have a complete RBAC setup with realistic corporate roles! + +--- + +## Understanding Our RBAC Structure + +### Role Hierarchy +1. **System Admin** (`system_admin`) + - Scope: `global/all` + - Access: Everything across all departments and projects + - Users: Alice (ID: 1) + +2. **Department Managers** (`dept_manager`) + - Scope: `department/[DEPT_CODE]` + - Access: Full control within their department + - Users: Bob (ENG), Frank (SALES), Iris (HR), Kelly (FIN), Maya (MKT) + +3. **Specialized Staff** + - **HR Staff** (`hr_staff`): Global access to employee data + - **Finance Staff** (`finance_staff`): Global access to financial data + - **Sales Staff** (`sales_staff`): Access to sales-specific features + +4. **Regular Employees** (`employee`) + - Scope: `department/[DEPT_CODE]` + - Access: Basic departmental access and own data + +5. **Project Roles** + - **Project Leads** (`project_lead`): Scope: `project/[PROJECT_ID]` + - **Project Members** (`project_member`): Scope: `project/[PROJECT_ID]` + +6. **Contractors** (`contractor`) + - Scope: `department/[DEPT_CODE]` or `project/[PROJECT_ID]` + - Access: Limited, task-specific access + +### Key Scopes Used +- **`global/all`**: System-wide access (admins, HR, finance) +- **`department/[CODE]`**: Department-specific access (ENG, SALES, HR, FIN, MKT) +- **`project/[ID]`**: Project-specific access (1, 2, 3, 4, 5) + +### Permission Patterns +- **Inheritance**: Users can have multiple roles with different scopes +- **Escalation**: `global/all` scope overrides specific scopes +- **Isolation**: Department and project scopes provide isolation +- **Flexibility**: Same user can have different roles in different contexts + +--- + +## Advanced Role Management + +### Adding New Roles Dynamically + +```sql +-- Example: Create a "Senior Developer" role with enhanced permissions +SELECT public.c77_rbac_grant_feature('senior_developer', 'view_project_details'); +SELECT public.c77_rbac_grant_feature('senior_developer', 'edit_project_documents'); +SELECT public.c77_rbac_grant_feature('senior_developer', 'mentor_junior_developers'); +SELECT public.c77_rbac_grant_feature('senior_developer', 'approve_code_reviews'); + +-- Assign Dave as a senior developer in the engineering department +SELECT public.c77_rbac_assign_subject('103', 'senior_developer', 'department', 'ENG'); + +-- Verify the assignment +SELECT * FROM public.c77_rbac_get_user_roles('103'); +``` + +### Bulk Role Operations + +```sql +-- Example: Assign all engineering employees to a "code_reviewer" role +-- First create the role +SELECT public.c77_rbac_grant_feature('code_reviewer', 'review_code'); +SELECT public.c77_rbac_grant_feature('code_reviewer', 'comment_on_pull_requests'); + +-- Bulk assign to engineering employees +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['101', '102', '103', '104'], -- Engineering team + 'code_reviewer', + 'department', + 'ENG' +); +``` + +### Temporary Access Patterns + +```sql +-- Example: Grant temporary access for a cross-departmental project +-- Marketing person needs temporary engineering access for a joint project +SELECT public.c77_rbac_assign_subject('502', 'project_member', 'project', '1'); + +-- Later, you can revoke this access +-- SELECT public.c77_rbac_revoke_subject_role('502', 'project_member', 'project', '1'); +``` + +--- + +## What's Next? + +Now that we have a comprehensive RBAC system set up, in **Part 4** we'll implement Row-Level Security (RLS) policies that will automatically enforce these permissions at the database level: + +- Apply RLS policies to our TechCorp tables +- Implement sophisticated business rules for document access +- Create multi-level security controls +- Test automatic data filtering based on user roles + +**Continue to [Part 4: Row-Level Security](TUTORIAL-Part4.md)** + +--- + +## Chapter Summary + +In this chapter, you've successfully: + +### ✅ Created a Comprehensive Permission System +- **40+ features** covering all aspects of TechCorp operations +- **10+ roles** with realistic corporate hierarchy +- **65+ user assignments** with proper scoping + +### ✅ Implemented Advanced Scoping Patterns +- **Global access** for system admins and cross-departmental roles +- **Department isolation** for managers and employees +- **Project-based collaboration** for cross-functional teams +- **Contractor limitations** for external workers + +### ✅ Demonstrated RBAC Flexibility +- **Multiple roles per user** (e.g., department manager + project lead) +- **Hierarchical permissions** (global overrides departmental) +- **Dynamic role assignment** for changing business needs +- **Bulk operations** for efficient management + +### ✅ Established Security Foundation +- **Role-based feature access** with proper validation +- **Scoped permissions** preventing unauthorized access +- **Audit-ready structure** with timestamps and tracking +- **Scalable design** that grows with organizational needs + +The RBAC system is now ready for Row-Level Security implementation, which will automatically enforce these permissions at the database query level. \ No newline at end of file diff --git a/TUTORIAL-P4.md b/TUTORIAL-P4.md new file mode 100644 index 0000000..c38ca21 --- /dev/null +++ b/TUTORIAL-P4.md @@ -0,0 +1,820 @@ +# c77_rbac Tutorial - Part 4: Row-Level Security + +**Tutorial Navigation:** +- [Part 1: Getting Started](TUTORIAL-Part1.md) - Prerequisites and installation +- [Part 2: Building the TechCorp Database](TUTORIAL-Part2.md) - Creating the schema and data +- [Part 3: Implementing RBAC](TUTORIAL-Part3.md) - Setting up roles and permissions +- **Part 4: Row-Level Security** (this document) - Applying access controls +- [Part 5: Testing and Validation](TUTORIAL-Part5.md) - Security testing +- [Part 6: Advanced Features](TUTORIAL-Part6.md) - Bulk operations and monitoring + +--- + +## Chapter 4: Implementing Row-Level Security + +Now we'll apply Row-Level Security (RLS) policies to our TechCorp tables. This is where the magic happens - the database will automatically filter data based on user permissions without any changes to your application code. + +### Step 1: Apply RLS to User Data + +Let's start with the users table, implementing a policy that allows users to see their own data, managers to see their department, and HR to see everyone: + +```sql +-- Users can see their own profile + department colleagues + HR can see all +CREATE POLICY users_access_policy ON users FOR ALL +USING ( + -- Users can see their own record + id::text = current_setting('c77_rbac.external_id', true) OR + + -- Department managers can see their department + public.c77_rbac_can_access('view_dept_employees', + current_setting('c77_rbac.external_id', true), + 'department', + (SELECT code FROM departments WHERE id = department_id)) OR + + -- HR can see all employees + public.c77_rbac_can_access('view_all_employees', + current_setting('c77_rbac.external_id', true), + 'global', 'all') OR + + -- System admin can see all + public.c77_rbac_can_access('manage_all_users', + current_setting('c77_rbac.external_id', true), + 'global', 'all') +); + +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +``` + +### Step 2: Apply RLS to Projects + +Project access should be limited to team members, department managers, and administrators: + +```sql +-- Project access: team members + department managers + admin +CREATE POLICY projects_access_policy ON projects FOR ALL +USING ( + -- Project team members can see their projects + EXISTS ( + SELECT 1 FROM project_members pm + WHERE pm.project_id = id + AND pm.user_id::text = current_setting('c77_rbac.external_id', true) + ) OR + + -- Department managers can see their department's projects + public.c77_rbac_can_access('manage_dept_projects', + current_setting('c77_rbac.external_id', true), + 'department', + (SELECT code FROM departments WHERE id = department_id)) OR + + -- System admin can see all + public.c77_rbac_can_access('access_all_departments', + current_setting('c77_rbac.external_id', true), + 'global', 'all') +); + +ALTER TABLE projects ENABLE ROW LEVEL SECURITY; +``` + +### Step 3: Apply RLS to Documents with Security Levels + +This is where we implement our most sophisticated policy - documents with multiple security levels: + +```sql +-- Complex document access based on security levels and scope +CREATE OR REPLACE FUNCTION can_access_document( + p_document_id INTEGER, + p_user_id TEXT +) RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER AS $$ +DECLARE + doc RECORD; +BEGIN + -- Get document details + SELECT d.*, dept.code as dept_code + INTO doc + FROM documents d + JOIN departments dept ON d.department_id = dept.id + WHERE d.id = p_document_id; + + -- Author can always access their documents + IF doc.author_id::text = p_user_id THEN + RETURN TRUE; + END IF; + + -- Check by security level + CASE doc.security_level + WHEN 'public' THEN + RETURN TRUE; + + WHEN 'internal' THEN + RETURN public.c77_rbac_can_access('access_internal_docs', p_user_id, 'global', 'all'); + + WHEN 'confidential' THEN + -- Department managers and HR can see confidential docs + RETURN public.c77_rbac_can_access('view_confidential_docs', p_user_id, 'department', doc.dept_code) OR + public.c77_rbac_can_access('access_hr_documents', p_user_id, 'global', 'all'); + + WHEN 'restricted' THEN + -- Only finance staff and admin can see restricted docs + RETURN public.c77_rbac_can_access('access_financial_docs', p_user_id, 'global', 'all') OR + public.c77_rbac_can_access('manage_all_users', p_user_id, 'global', 'all'); + + ELSE + RETURN FALSE; + END CASE; +END; +$$; + +-- Apply the complex policy +CREATE POLICY documents_access_policy ON documents FOR ALL +USING (can_access_document(id, current_setting('c77_rbac.external_id', true))); + +ALTER TABLE documents ENABLE ROW LEVEL SECURITY; +``` + +### Step 4: Apply RLS to Expenses + +Expense access should follow approval workflows - submitters, managers, finance staff: + +```sql +-- Expense access: submitter + department manager + finance + admin +CREATE POLICY expenses_access_policy ON expenses FOR ALL +USING ( + -- Users can see their own expenses + submitted_by::text = current_setting('c77_rbac.external_id', true) OR + + -- Department managers can see their department expenses + public.c77_rbac_can_access('approve_dept_expenses', + current_setting('c77_rbac.external_id', true), + 'department', + (SELECT code FROM departments WHERE id = department_id)) OR + + -- Finance staff can see all expenses + public.c77_rbac_can_access('view_all_expenses', + current_setting('c77_rbac.external_id', true), + 'global', 'all') OR + + -- Project leads can see project expenses + (project_id IS NOT NULL AND + public.c77_rbac_can_access('approve_project_expenses', + current_setting('c77_rbac.external_id', true), + 'project', project_id::text)) OR + + -- System admin can see all + public.c77_rbac_can_access('access_all_departments', + current_setting('c77_rbac.external_id', true), + 'global', 'all') +); + +ALTER TABLE expenses ENABLE ROW LEVEL SECURITY; +``` + +### Step 5: Apply RLS to Project Members + +Project team visibility should be controlled based on project access: + +```sql +-- Project members table access +CREATE POLICY project_members_access_policy ON project_members FOR ALL +USING ( + -- Users can see their own memberships + user_id::text = current_setting('c77_rbac.external_id', true) OR + + -- Project leads can see their project teams + public.c77_rbac_can_access('manage_project_team', + current_setting('c77_rbac.external_id', true), + 'project', project_id::text) OR + + -- Department managers can see their department's project teams + EXISTS ( + SELECT 1 FROM projects p + JOIN departments d ON p.department_id = d.id + WHERE p.id = project_id + AND public.c77_rbac_can_access('manage_dept_projects', + current_setting('c77_rbac.external_id', true), + 'department', d.code) + ) OR + + -- HR and Admin can see all + public.c77_rbac_can_access('view_all_employees', + current_setting('c77_rbac.external_id', true), + 'global', 'all') +); + +ALTER TABLE project_members ENABLE ROW LEVEL SECURITY; +``` + +### Step 6: Apply Simple RLS to Departments + +Department information should be visible to appropriate users: + +```sql +-- Use the built-in c77_rbac_apply_policy function for simpler cases +-- Departments visible to those who can view department employees +SELECT public.c77_rbac_apply_policy( + 'departments', + 'view_dept_employees', + 'department', + 'code' +); +``` + +**✅ Checkpoint 4:** All tables now have sophisticated row-level security policies! + +--- + +## Understanding RLS Policy Patterns + +### Pattern 1: Self-Access + Role-Based Access +```sql +-- Users can see their own data OR those with specific permissions +id::text = current_setting('c77_rbac.external_id', true) OR +public.c77_rbac_can_access('permission', user_id, 'scope_type', 'scope_id') +``` + +### Pattern 2: Hierarchical Access +```sql +-- Multiple levels of access: department → global +public.c77_rbac_can_access('dept_permission', user_id, 'department', dept_code) OR +public.c77_rbac_can_access('global_permission', user_id, 'global', 'all') +``` + +### Pattern 3: Complex Business Logic +```sql +-- Custom function for complex rules +can_access_document(id, current_setting('c77_rbac.external_id', true)) +``` + +### Pattern 4: Relationship-Based Access +```sql +-- Access based on relationships (project membership, etc.) +EXISTS ( + SELECT 1 FROM related_table rt + WHERE rt.foreign_key = id + AND rt.user_id::text = current_setting('c77_rbac.external_id', true) +) +``` + +--- + +## Testing RLS Policies + +Let's create a comprehensive test to verify our RLS policies work correctly: + +```sql +-- Create a test function to verify RLS is working +CREATE OR REPLACE FUNCTION test_rls_policies() +RETURNS TABLE( + test_name TEXT, + user_name TEXT, + table_name TEXT, + visible_rows INTEGER, + expected_result TEXT, + status TEXT +) LANGUAGE plpgsql AS $$ +DECLARE + test_users TEXT[] := ARRAY['1', '101', '102', '104', '301', '401']; + test_user TEXT; + user_name_val TEXT; + row_count INTEGER; +BEGIN + FOREACH test_user IN ARRAY test_users LOOP + -- Set user context + PERFORM set_config('c77_rbac.external_id', test_user, true); + + -- Get user name + SELECT first_name || ' ' || last_name INTO user_name_val + FROM users WHERE id::text = test_user; + + -- Test users table + SELECT count(*) INTO row_count FROM users; + RETURN QUERY SELECT + 'User Visibility'::TEXT, + user_name_val, + 'users'::TEXT, + row_count, + CASE + WHEN test_user = '1' THEN 'All users (admin)' + WHEN test_user = '301' THEN 'All users (HR)' + WHEN test_user IN ('101', '401') THEN 'Department + self' + ELSE 'Limited (self + some colleagues)' + END, + CASE + WHEN test_user IN ('1', '301') AND row_count > 50 THEN 'PASS' + WHEN test_user NOT IN ('1', '301') AND row_count < 50 THEN 'PASS' + ELSE 'REVIEW' + END; + + -- Test documents table + SELECT count(*) INTO row_count FROM documents; + RETURN QUERY SELECT + 'Document Access'::TEXT, + user_name_val, + 'documents'::TEXT, + row_count, + CASE + WHEN test_user = '1' THEN 'All documents (admin)' + WHEN test_user = '401' THEN 'Including restricted (finance)' + WHEN test_user IN ('101', '301') THEN 'Including confidential (manager/HR)' + ELSE 'Internal and own documents only' + END, + CASE WHEN row_count > 0 THEN 'PASS' ELSE 'FAIL' END; + + -- Test expenses table + SELECT count(*) INTO row_count FROM expenses; + RETURN QUERY SELECT + 'Expense Visibility'::TEXT, + user_name_val, + 'expenses'::TEXT, + row_count, + CASE + WHEN test_user IN ('1', '401') THEN 'All expenses' + WHEN test_user IN ('101', '301') THEN 'Department expenses' + ELSE 'Own expenses only' + END, + CASE WHEN row_count > 0 THEN 'PASS' ELSE 'REVIEW' END; + END LOOP; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$$; + +-- Run the comprehensive test +SELECT * FROM test_rls_policies() ORDER BY user_name, test_name; +``` + +### Manual Testing Examples + +```sql +-- Test as Alice (System Admin) - should see everything +SET "c77_rbac.external_id" TO '1'; +SELECT 'Alice (Admin) sees ' || count(*) || ' users' FROM users; +SELECT 'Alice (Admin) sees ' || count(*) || ' documents' FROM documents; +SELECT 'Alice (Admin) sees ' || count(*) || ' expenses' FROM expenses; + +-- Test as Bob (Eng Manager) - should see engineering department +SET "c77_rbac.external_id" TO '101'; +SELECT 'Bob (Eng Manager) sees ' || count(*) || ' users' FROM users; +SELECT 'Bob (Eng Manager) sees ' || count(*) || ' documents' FROM documents; +SELECT 'Bob (Eng Manager) sees ' || count(*) || ' expenses' FROM expenses; + +-- Test as Carol (Developer) - should see limited data +SET "c77_rbac.external_id" TO '102'; +SELECT 'Carol (Developer) sees ' || count(*) || ' users' FROM users; +SELECT 'Carol (Developer) sees ' || count(*) || ' documents' FROM documents; +SELECT 'Carol (Developer) sees ' || count(*) || ' expenses' FROM expenses; + +-- Test as Eve (Contractor) - should see very limited data +SET "c77_rbac.external_id" TO '104'; +SELECT 'Eve (Contractor) sees ' || count(*) || ' users' FROM users; +SELECT 'Eve (Contractor) sees ' || count(*) || ' documents' FROM documents; +SELECT 'Eve (Contractor) sees ' || count(*) || ' expenses' FROM expenses; + +-- Test document security levels specifically +SET "c77_rbac.external_id" TO '102'; -- Carol (regular employee) +SELECT 'Carol can see these document security levels:' as info; +SELECT security_level, count(*) +FROM documents +GROUP BY security_level +ORDER BY security_level; + +SET "c77_rbac.external_id" TO '401'; -- Kelly (Finance manager) +SELECT 'Kelly can see these document security levels:' as info; +SELECT security_level, count(*) +FROM documents +GROUP BY security_level +ORDER BY security_level; + +-- Reset context +RESET "c77_rbac.external_id"; +``` + +--- + +## Advanced RLS Patterns + +### Time-Based Access Control + +```sql +-- Example: Add time-based restrictions to sensitive documents +CREATE OR REPLACE FUNCTION can_access_document_with_time( + p_document_id INTEGER, + p_user_id TEXT +) RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER AS $$ +DECLARE + doc RECORD; + current_hour INTEGER; +BEGIN + -- Get document and current time + SELECT * INTO doc FROM documents WHERE id = p_document_id; + current_hour := EXTRACT(HOUR FROM CURRENT_TIME); + + -- Restricted documents only accessible during business hours (9-17) + IF doc.security_level = 'restricted' THEN + IF current_hour < 9 OR current_hour >= 17 THEN + -- Only allow admin access outside business hours + RETURN public.c77_rbac_can_access('manage_all_users', p_user_id, 'global', 'all'); + END IF; + END IF; + + -- Fall back to regular document access rules + RETURN can_access_document(p_document_id, p_user_id); +END; +$$; +``` + +### Conditional Write Policies + +```sql +-- Example: Only allow expense updates by submitter or approver +CREATE POLICY expenses_update_policy ON expenses FOR UPDATE +USING ( + -- Submitter can update before approval + (submitted_by::text = current_setting('c77_rbac.external_id', true) AND status = 'submitted') OR + + -- Department managers can update their department's expenses + public.c77_rbac_can_access('approve_dept_expenses', + current_setting('c77_rbac.external_id', true), + 'department', + (SELECT code FROM departments WHERE id = department_id)) OR + + -- Finance staff can update any expense + public.c77_rbac_can_access('approve_expenses', + current_setting('c77_rbac.external_id', true), + 'global', 'all') +); +``` + +### Audit Trail Integration + +```sql +-- Example: Automatic audit logging when accessing sensitive documents +CREATE OR REPLACE FUNCTION log_document_access() +RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + -- Log access to confidential or restricted documents + IF NEW.security_level IN ('confidential', 'restricted') THEN + INSERT INTO audit_log ( + user_id, + action, + table_name, + record_id, + new_values + ) VALUES ( + current_setting('c77_rbac.external_id', true)::integer, + 'document_accessed', + 'documents', + NEW.id, + jsonb_build_object( + 'document_title', NEW.title, + 'security_level', NEW.security_level, + 'access_time', CURRENT_TIMESTAMP + ) + ); + END IF; + RETURN NEW; +END; +$$; + +-- Apply the audit trigger +CREATE TRIGGER document_access_audit + AFTER SELECT ON documents + FOR EACH ROW + EXECUTE FUNCTION log_document_access(); +``` + +--- + +## Performance Considerations + +### Index Optimization for RLS + +```sql +-- Add indexes to support RLS policy performance +CREATE INDEX idx_users_department_id ON users(department_id); +CREATE INDEX idx_documents_author_security ON documents(author_id, security_level); +CREATE INDEX idx_documents_dept_security ON documents(department_id, security_level); +CREATE INDEX idx_expenses_submitter ON expenses(submitted_by); +CREATE INDEX idx_expenses_dept_status ON expenses(department_id, status); +CREATE INDEX idx_project_members_user_project ON project_members(user_id, project_id); + +-- Analyze performance of RLS policies +CREATE OR REPLACE FUNCTION analyze_rls_performance() +RETURNS TABLE( + table_name TEXT, + policy_name TEXT, + avg_execution_time TEXT, + recommendations TEXT +) LANGUAGE plpgsql AS $ +BEGIN + RETURN QUERY + SELECT + 'Performance Analysis'::TEXT as table_name, + 'RLS Policies'::TEXT as policy_name, + 'Run EXPLAIN ANALYZE on queries'::TEXT as avg_execution_time, + 'Monitor slow queries and add indexes as needed'::TEXT as recommendations; +END; +$; +``` + +### Query Optimization Examples + +```sql +-- Example: Optimized query patterns that work well with RLS + +-- Good: Use specific filters that align with RLS policies +SET "c77_rbac.external_id" TO '101'; -- Bob (Eng Manager) +EXPLAIN ANALYZE +SELECT u.first_name, u.last_name, d.name as department +FROM users u +JOIN departments d ON u.department_id = d.id +WHERE d.code = 'ENG'; -- This aligns with Bob's department scope + +-- Good: Project-specific queries for project members +SET "c77_rbac.external_id" TO '102'; -- Carol (Developer) +EXPLAIN ANALYZE +SELECT p.name, p.status, pm.role +FROM projects p +JOIN project_members pm ON p.id = pm.project_id +WHERE pm.user_id = 102; -- Explicit filter that matches RLS logic + +-- Reset context +RESET "c77_rbac.external_id"; +``` + +--- + +## Troubleshooting RLS Issues + +### Common Problems and Solutions + +```sql +-- Debug function to help troubleshoot RLS issues +CREATE OR REPLACE FUNCTION debug_rls_access( + p_table_name TEXT, + p_user_id TEXT, + p_expected_rows INTEGER DEFAULT NULL +) +RETURNS TABLE( + check_type TEXT, + result TEXT, + details TEXT +) LANGUAGE plpgsql AS $ +DECLARE + actual_rows INTEGER; + rls_enabled BOOLEAN; + policy_count INTEGER; +BEGIN + -- Set user context + PERFORM set_config('c77_rbac.external_id', p_user_id, true); + + -- Check if RLS is enabled + SELECT INTO rls_enabled + EXISTS ( + SELECT 1 FROM pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid + WHERE n.nspname = 'public' + AND c.relname = p_table_name + AND c.relrowsecurity = true + ); + + RETURN QUERY SELECT + 'RLS Enabled'::TEXT, + CASE WHEN rls_enabled THEN 'YES' ELSE 'NO' END, + 'Row Level Security status on table'::TEXT; + + -- Count policies + SELECT count(*) INTO policy_count + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = p_table_name; + + RETURN QUERY SELECT + 'Policy Count'::TEXT, + policy_count::TEXT, + 'Number of RLS policies on table'::TEXT; + + -- Count actual visible rows + EXECUTE format('SELECT count(*) FROM %I', p_table_name) INTO actual_rows; + + RETURN QUERY SELECT + 'Visible Rows'::TEXT, + actual_rows::TEXT, + 'Rows visible to current user'::TEXT; + + -- Check user permissions + RETURN QUERY SELECT + 'User Context'::TEXT, + p_user_id, + 'Current c77_rbac.external_id setting'::TEXT; + + -- Check user roles + RETURN QUERY SELECT + 'User Roles'::TEXT, + count(*)::TEXT, + 'Total roles assigned to user'::TEXT + FROM public.c77_rbac_get_user_roles(p_user_id); + + -- Compare with expectation if provided + IF p_expected_rows IS NOT NULL THEN + RETURN QUERY SELECT + 'Expectation Check'::TEXT, + CASE + WHEN actual_rows = p_expected_rows THEN 'MATCH' + WHEN actual_rows < p_expected_rows THEN 'FEWER THAN EXPECTED' + ELSE 'MORE THAN EXPECTED' + END, + format('Expected %s, got %s', p_expected_rows, actual_rows); + END IF; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$; + +-- Usage examples: +SELECT * FROM debug_rls_access('users', '102'); -- Carol should see limited users +SELECT * FROM debug_rls_access('documents', '401', 10); -- Kelly should see more documents +SELECT * FROM debug_rls_access('expenses', '104'); -- Eve (contractor) should see few expenses +``` + +### Policy Testing Framework + +```sql +-- Comprehensive RLS testing framework +CREATE OR REPLACE FUNCTION test_rls_comprehensive() +RETURNS TABLE( + test_category TEXT, + test_name TEXT, + user_tested TEXT, + expected_behavior TEXT, + actual_result TEXT, + status TEXT +) LANGUAGE plpgsql AS $ +DECLARE + test_users RECORD; + table_name TEXT; + row_count INTEGER; +BEGIN + -- Test each major user type + FOR test_users IN + SELECT + u.id::text as user_id, + u.first_name || ' ' || u.last_name as name, + u.employee_type, + d.code as dept_code + FROM users u + JOIN departments d ON u.department_id = d.id + WHERE u.id IN (1, 101, 102, 104, 301, 401) + LOOP + -- Set user context + PERFORM set_config('c77_rbac.external_id', test_users.user_id, true); + + -- Test Users Table Access + SELECT count(*) INTO row_count FROM users; + + RETURN QUERY SELECT + 'User Access'::TEXT, + 'Users Table Visibility'::TEXT, + test_users.name, + CASE + WHEN test_users.user_id IN ('1', '301') THEN 'See all users' + WHEN test_users.employee_type = 'manager' THEN 'See department users' + ELSE 'See limited users' + END, + 'Sees ' || row_count || ' users', + CASE + WHEN test_users.user_id IN ('1', '301') AND row_count >= 60 THEN 'PASS' + WHEN test_users.employee_type = 'manager' AND row_count BETWEEN 5 AND 60 THEN 'PASS' + WHEN test_users.employee_type NOT IN ('admin', 'manager') AND row_count <= 15 THEN 'PASS' + ELSE 'REVIEW' + END; + + -- Test Documents by Security Level + FOR table_name IN SELECT unnest(ARRAY['public', 'internal', 'confidential', 'restricted']) LOOP + EXECUTE format('SELECT count(*) FROM documents WHERE security_level = %L', table_name) + INTO row_count; + + RETURN QUERY SELECT + 'Document Security'::TEXT, + table_name || ' Documents', + test_users.name, + CASE + WHEN table_name = 'public' THEN 'Should see all' + WHEN table_name = 'internal' AND test_users.user_id != '104' THEN 'Should see all' + WHEN table_name = 'confidential' AND test_users.employee_type IN ('admin', 'manager') THEN 'Should see some' + WHEN table_name = 'restricted' AND test_users.user_id IN ('1', '401') THEN 'Should see some' + ELSE 'Should see none or few' + END, + CASE WHEN row_count > 0 THEN 'Sees ' || row_count ELSE 'Sees none' END, + CASE + WHEN table_name = 'public' AND row_count > 0 THEN 'PASS' + WHEN table_name = 'internal' AND test_users.user_id != '104' AND row_count > 0 THEN 'PASS' + WHEN table_name = 'confidential' AND test_users.employee_type IN ('admin', 'manager') AND row_count > 0 THEN 'PASS' + WHEN table_name = 'restricted' AND test_users.user_id IN ('1', '401') AND row_count > 0 THEN 'PASS' + WHEN table_name = 'restricted' AND test_users.user_id NOT IN ('1', '401') AND row_count = 0 THEN 'PASS' + WHEN table_name = 'confidential' AND test_users.employee_type NOT IN ('admin', 'manager') AND row_count <= 1 THEN 'PASS' + ELSE 'REVIEW' + END; + END LOOP; + + -- Test Expense Access + SELECT count(*) INTO row_count FROM expenses; + + RETURN QUERY SELECT + 'Expense Access'::TEXT, + 'Expense Visibility'::TEXT, + test_users.name, + CASE + WHEN test_users.user_id IN ('1', '401') THEN 'See all expenses' + WHEN test_users.employee_type = 'manager' THEN 'See department expenses' + ELSE 'See own expenses only' + END, + 'Sees ' || row_count || ' expenses', + CASE WHEN row_count > 0 THEN 'PASS' ELSE 'REVIEW' END; + + END LOOP; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$; + +-- Run comprehensive RLS tests +SELECT * FROM test_rls_comprehensive() +ORDER BY test_category, user_tested, test_name; +``` + +**✅ Checkpoint 4 Complete:** You now have sophisticated Row-Level Security protecting all your data! + +--- + +## Understanding What We've Built + +### Multi-Layer Security System + +1. **Application Layer**: User authentication and basic authorization +2. **RBAC Layer**: Role and feature-based permissions with flexible scoping +3. **Database Layer**: Row-Level Security automatically filtering data +4. **Audit Layer**: Comprehensive logging of sensitive data access + +### Security Levels Implemented + +1. **Public**: Accessible to everyone +2. **Internal**: Accessible to employees (not contractors) +3. **Confidential**: Accessible to managers and specialized staff +4. **Restricted**: Accessible only to finance staff and administrators + +### Access Control Patterns + +1. **Self-Access**: Users can always see their own data +2. **Hierarchical Access**: Managers see their department, admins see everything +3. **Role-Based Access**: Specialized roles (HR, Finance) have cross-department access +4. **Project-Based Access**: Team members see project-specific data +5. **Security-Level Access**: Documents filtered by classification level + +--- + +## What's Next? + +In **Part 5**, we'll thoroughly test our security system to ensure it works correctly: + +- Comprehensive security validation +- Edge case testing +- Permission verification +- Performance analysis +- Real-world scenario testing + +**Continue to [Part 5: Testing and Validation](TUTORIAL-Part5.md)** + +--- + +## Chapter Summary + +### ✅ Implemented Comprehensive RLS Policies +- **Users table**: Self + department + HR/admin access +- **Projects table**: Team members + department managers + admin +- **Documents table**: Multi-level security (public/internal/confidential/restricted) +- **Expenses table**: Submitter + approver + finance workflow +- **Project members table**: Project-based access control + +### ✅ Created Advanced Security Patterns +- **Multi-level document security** with business logic +- **Hierarchical access control** (department → global) +- **Role-based cross-department access** for specialized staff +- **Project-based collaboration** across departments +- **Self-service access** for personal data + +### ✅ Built Debugging and Testing Tools +- **RLS debugging functions** for troubleshooting +- **Comprehensive test framework** for validation +- **Performance analysis tools** for optimization +- **Policy verification utilities** for maintenance + +### ✅ Established Production-Ready Security +- **Automatic data filtering** without application changes +- **Consistent security enforcement** across all access paths +- **Audit-ready structure** with comprehensive logging +- **Scalable permission model** that grows with the organization + +The database now automatically enforces sophisticated business rules for data access, providing enterprise-grade security with zero application code changes required! \ No newline at end of file diff --git a/TUTORIAL-P5.md b/TUTORIAL-P5.md new file mode 100644 index 0000000..068e0db --- /dev/null +++ b/TUTORIAL-P5.md @@ -0,0 +1,945 @@ +# c77_rbac Tutorial - Part 5: Testing and Validation + +**Tutorial Navigation:** +- [Part 1: Getting Started](TUTORIAL-Part1.md) - Prerequisites and installation +- [Part 2: Building the TechCorp Database](TUTORIAL-Part2.md) - Creating the schema and data +- [Part 3: Implementing RBAC](TUTORIAL-Part3.md) - Setting up roles and permissions +- [Part 4: Row-Level Security](TUTORIAL-Part4.md) - Applying access controls +- **Part 5: Testing and Validation** (this document) - Security testing +- [Part 6: Advanced Features](TUTORIAL-Part6.md) - Bulk operations and monitoring + +--- + +## Chapter 5: Comprehensive Security Testing + +Now we'll thoroughly test our security system to ensure it works correctly in all scenarios. This chapter covers comprehensive validation, edge cases, and real-world testing patterns. + +### Step 1: Test User Isolation + +Let's verify that users can only see appropriate data based on their roles and permissions: + +```sql +-- Test as Alice (System Admin) - should see everything +SET "c77_rbac.external_id" TO '1'; + +SELECT 'Alice (Admin) - Users visible:' as test; +SELECT count(*) as total_users, + count(CASE WHEN employee_type = 'admin' THEN 1 END) as admins, + count(CASE WHEN employee_type = 'manager' THEN 1 END) as managers, + count(CASE WHEN employee_type = 'employee' THEN 1 END) as employees +FROM users; + +SELECT 'Alice (Admin) - Projects visible:' as test; +SELECT count(*) as total_projects, + count(CASE WHEN status = 'active' THEN 1 END) as active_projects +FROM projects; + +SELECT 'Alice (Admin) - Documents by security level:' as test; +SELECT security_level, count(*) as document_count +FROM documents +GROUP BY security_level +ORDER BY security_level; + +SELECT 'Alice (Admin) - Expenses visible:' as test; +SELECT count(*) as total_expenses, + sum(amount) as total_amount, + count(CASE WHEN status = 'approved' THEN 1 END) as approved_count +FROM expenses; +``` + +```sql +-- Test as Bob (Engineering Manager) - should see engineering department +SET "c77_rbac.external_id" TO '101'; + +SELECT 'Bob (Eng Manager) - Users visible:' as test; +SELECT count(*) as total_users, + string_agg(DISTINCT d.name, ', ') as departments_visible +FROM users u +LEFT JOIN departments d ON u.department_id = d.id; + +SELECT 'Bob (Eng Manager) - Projects visible:' as test; +SELECT count(*) as total_projects, + string_agg(p.name, ', ') as project_names +FROM projects p +JOIN departments d ON p.department_id = d.id; + +SELECT 'Bob (Eng Manager) - Documents visible by security level:' as test; +SELECT security_level, count(*) as document_count +FROM documents +GROUP BY security_level +ORDER BY security_level; + +SELECT 'Bob (Eng Manager) - Can access confidential docs:' as test; +SELECT count(*) as confidential_docs +FROM documents +WHERE security_level = 'confidential'; + +SELECT 'Bob (Eng Manager) - Cannot access restricted docs:' as test; +SELECT count(*) as restricted_docs +FROM documents +WHERE security_level = 'restricted'; +``` + +```sql +-- Test as Carol (Regular Developer) - should see limited data +SET "c77_rbac.external_id" TO '102'; + +SELECT 'Carol (Developer) - Users visible:' as test; +SELECT count(*) as total_users FROM users; + +SELECT 'Carol (Developer) - Projects visible:' as test; +SELECT count(*) as total_projects, + string_agg(p.name, ', ') as accessible_projects +FROM projects p; + +SELECT 'Carol (Developer) - Documents by security level:' as test; +SELECT security_level, count(*) as document_count +FROM documents +GROUP BY security_level +ORDER BY security_level; + +SELECT 'Carol (Developer) - Own expenses vs total:' as test; +SELECT count(*) as visible_expenses, + count(CASE WHEN submitted_by = 102 THEN 1 END) as own_expenses +FROM expenses; +``` + +```sql +-- Test as Eve (Contractor) - should see very limited data +SET "c77_rbac.external_id" TO '104'; + +SELECT 'Eve (Contractor) - Users visible:' as test; +SELECT count(*) as total_users FROM users; + +SELECT 'Eve (Contractor) - Projects visible:' as test; +SELECT count(*) as total_projects, + string_agg(p.name, ', ') as accessible_projects +FROM projects p; + +SELECT 'Eve (Contractor) - Documents by security level:' as test; +SELECT security_level, count(*) as document_count +FROM documents +GROUP BY security_level +ORDER BY security_level; + +SELECT 'Eve (Contractor) - Can only see public and own documents:' as test; +SELECT count(*) as total_docs, + count(CASE WHEN security_level = 'public' THEN 1 END) as public_docs, + count(CASE WHEN author_id = 104 THEN 1 END) as own_docs +FROM documents; +``` + +### Step 2: Test Cross-Department Access + +Verify that special roles (HR, Finance) can access data across departments: + +```sql +-- Test as Iris (HR Manager) - should see all employees but limited other data +SET "c77_rbac.external_id" TO '301'; + +SELECT 'Iris (HR Manager) - Can see all employees:' as test; +SELECT count(*) as total_users, + count(DISTINCT department_id) as departments_covered +FROM users; + +SELECT 'Iris (HR Manager) - Can see confidential HR docs:' as test; +SELECT count(*) as confidential_docs +FROM documents +WHERE security_level = 'confidential'; + +SELECT 'Iris (HR Manager) - Cannot see restricted financial docs:' as test; +SELECT count(*) as restricted_docs +FROM documents +WHERE security_level = 'restricted'; + +SELECT 'Iris (HR Manager) - Department expense visibility:' as test; +SELECT d.name as department, count(e.id) as expense_count +FROM departments d +LEFT JOIN expenses e ON d.id = e.department_id +GROUP BY d.name +ORDER BY d.name; +``` + +```sql +-- Test as Kelly (Finance Manager) - should see all expenses and restricted docs +SET "c77_rbac.external_id" TO '401'; + +SELECT 'Kelly (Finance Manager) - Can see all expenses:' as test; +SELECT count(*) as total_expenses, + sum(amount) as total_amount, + count(DISTINCT department_id) as departments_covered +FROM expenses; + +SELECT 'Kelly (Finance Manager) - Can see restricted docs:' as test; +SELECT count(*) as restricted_docs +FROM documents +WHERE security_level = 'restricted'; + +SELECT 'Kelly (Finance Manager) - Expense breakdown by department:' as test; +SELECT d.name as department, + count(e.id) as expense_count, + coalesce(sum(e.amount), 0) as total_amount +FROM departments d +LEFT JOIN expenses e ON d.id = e.department_id +GROUP BY d.name +ORDER BY total_amount DESC; +``` + +### Step 3: Test Permission Functions Directly + +Let's test the core permission functions with various scenarios: + +```sql +-- Reset context for direct function testing +RESET "c77_rbac.external_id"; +``` + +### Step 7: Integration Testing Scenarios + +Let's test realistic application scenarios: + +```sql +-- Create comprehensive integration test scenarios +CREATE OR REPLACE FUNCTION test_real_world_scenarios() +RETURNS TABLE( + scenario_name TEXT, + user_name TEXT, + action_attempted TEXT, + expected_result TEXT, + actual_result TEXT, + test_status TEXT +) LANGUAGE plpgsql AS $ +DECLARE + test_cases RECORD; + row_count INTEGER; + can_access_result BOOLEAN; +BEGIN + -- Scenario 1: Employee tries to view colleague's salary information + PERFORM set_config('c77_rbac.external_id', '102', true); -- Carol + SELECT count(*) INTO row_count + FROM users + WHERE id != 102 AND salary IS NOT NULL; + + RETURN QUERY SELECT + 'Salary Privacy'::TEXT, + 'Carol (Developer)'::TEXT, + 'View colleague salaries'::TEXT, + 'Should see limited/no salary data'::TEXT, + CASE WHEN row_count <= 5 THEN 'Limited access' ELSE 'Too much access' END, + CASE WHEN row_count <= 5 THEN 'PASS' ELSE 'FAIL' END; + + -- Scenario 2: Manager approves department expense + PERFORM set_config('c77_rbac.external_id', '101', true); -- Bob (Eng Manager) + SELECT count(*) INTO row_count + FROM expenses + WHERE department_id = 1 AND status = 'submitted'; + + SELECT public.c77_rbac_can_access('approve_dept_expenses', '101', 'department', 'ENG') + INTO can_access_result; + + RETURN QUERY SELECT + 'Expense Approval'::TEXT, + 'Bob (Eng Manager)'::TEXT, + 'Approve engineering expenses'::TEXT, + 'Should see dept expenses and have approval rights'::TEXT, + format('Sees %s expenses, can approve: %s', row_count, can_access_result), + CASE WHEN row_count > 0 AND can_access_result THEN 'PASS' ELSE 'FAIL' END; + + -- Scenario 3: HR accesses employee data across all departments + PERFORM set_config('c77_rbac.external_id', '301', true); -- Iris (HR) + SELECT count(DISTINCT department_id) INTO row_count FROM users; + + RETURN QUERY SELECT + 'HR Cross-Department Access'::TEXT, + 'Iris (HR Manager)'::TEXT, + 'Access employees from all departments'::TEXT, + 'Should see employees from all 5 departments'::TEXT, + format('Sees employees from %s departments', row_count), + CASE WHEN row_count >= 5 THEN 'PASS' ELSE 'FAIL' END; + + -- Scenario 4: Contractor tries to access confidential project docs + PERFORM set_config('c77_rbac.external_id', '104', true); -- Eve (Contractor) + SELECT count(*) INTO row_count + FROM documents + WHERE security_level = 'confidential'; + + RETURN QUERY SELECT + 'Contractor Document Access'::TEXT, + 'Eve (Contractor)'::TEXT, + 'Access confidential documents'::TEXT, + 'Should see very few or no confidential docs'::TEXT, + format('Sees %s confidential documents', row_count), + CASE WHEN row_count <= 1 THEN 'PASS' ELSE 'FAIL' END; + + -- Scenario 5: Finance staff reviews all company expenses + PERFORM set_config('c77_rbac.external_id', '401', true); -- Kelly (Finance) + SELECT count(DISTINCT department_id) INTO row_count FROM expenses; + + RETURN QUERY SELECT + 'Finance Global Access'::TEXT, + 'Kelly (Finance Manager)'::TEXT, + 'Review expenses from all departments'::TEXT, + 'Should see expenses from all departments'::TEXT, + format('Sees expenses from %s departments', row_count), + CASE WHEN row_count >= 4 THEN 'PASS' ELSE 'FAIL' END; + + -- Scenario 6: Project member accesses project-specific data + PERFORM set_config('c77_rbac.external_id', '102', true); -- Carol (Project Member) + SELECT count(*) INTO row_count + FROM projects p + WHERE EXISTS ( + SELECT 1 FROM project_members pm + WHERE pm.project_id = p.id AND pm.user_id = 102 + ); + + RETURN QUERY SELECT + 'Project Team Access'::TEXT, + 'Carol (Project Member)'::TEXT, + 'Access assigned project data'::TEXT, + 'Should see projects she is assigned to'::TEXT, + format('Sees %s assigned projects', row_count), + CASE WHEN row_count >= 2 THEN 'PASS' ELSE 'FAIL' END; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$; + +-- Run real-world scenario tests +SELECT 'Real-World Integration Test Results:' as test_section; +SELECT * FROM test_real_world_scenarios(); +``` + +### Step 8: Security Validation Tests + +Let's verify our security isolation is working properly: + +```sql +-- Comprehensive security validation +CREATE OR REPLACE FUNCTION validate_security_isolation() +RETURNS TABLE( + security_test TEXT, + description TEXT, + result TEXT, + security_status TEXT +) LANGUAGE plpgsql AS $ +DECLARE + admin_count INTEGER; + user_count INTEGER; + isolation_breach BOOLEAN := FALSE; +BEGIN + -- Test 1: Verify users cannot access data without proper context + PERFORM set_config('c77_rbac.external_id', '', true); + SELECT count(*) INTO user_count FROM users; + + RETURN QUERY SELECT + 'No Context Access'::TEXT, + 'Data access without user context'::TEXT, + format('Returns %s rows', user_count), + CASE WHEN user_count = 0 THEN 'SECURE' ELSE 'BREACH' END; + + -- Test 2: Verify admin sees more than regular users + PERFORM set_config('c77_rbac.external_id', '1', true); -- Admin + SELECT count(*) INTO admin_count FROM users; + + PERFORM set_config('c77_rbac.external_id', '102', true); -- Regular user + SELECT count(*) INTO user_count FROM users; + + RETURN QUERY SELECT + 'Admin vs User Access'::TEXT, + 'Admin should see more data than regular users'::TEXT, + format('Admin sees %s, User sees %s', admin_count, user_count), + CASE WHEN admin_count > user_count THEN 'SECURE' ELSE 'BREACH' END; + + -- Test 3: Verify department isolation + PERFORM set_config('c77_rbac.external_id', '101', true); -- Eng Manager + SELECT count(*) INTO user_count + FROM users u + JOIN departments d ON u.department_id = d.id + WHERE d.code = 'SALES'; + + RETURN QUERY SELECT + 'Department Isolation'::TEXT, + 'Eng Manager should not see Sales employees'::TEXT, + format('Sees %s Sales employees', user_count), + CASE WHEN user_count = 0 THEN 'SECURE' ELSE 'BREACH' END; + + -- Test 4: Verify contractor limitations + PERFORM set_config('c77_rbac.external_id', '104', true); -- Contractor + SELECT count(*) INTO user_count + FROM documents + WHERE security_level IN ('confidential', 'restricted'); + + RETURN QUERY SELECT + 'Contractor Limitations'::TEXT, + 'Contractor should not see confidential/restricted docs'::TEXT, + format('Sees %s sensitive documents', user_count), + CASE WHEN user_count <= 1 THEN 'SECURE' ELSE 'BREACH' END; + + -- Test 5: Verify project access isolation + PERFORM set_config('c77_rbac.external_id', '102', true); -- Carol + SELECT count(*) INTO user_count + FROM projects + WHERE id NOT IN ( + SELECT project_id FROM project_members WHERE user_id = 102 + ); + + RETURN QUERY SELECT + 'Project Access Isolation'::TEXT, + 'User should not see projects they are not assigned to'::TEXT, + format('Sees %s unassigned projects', user_count), + CASE WHEN user_count = 0 THEN 'SECURE' ELSE 'REVIEW' END; + + -- Test 6: Verify expense privacy + PERFORM set_config('c77_rbac.external_id', '102', true); -- Carol + SELECT count(*) INTO user_count + FROM expenses + WHERE submitted_by != 102 + AND department_id != 1; -- Not her department + + RETURN QUERY SELECT + 'Expense Privacy'::TEXT, + 'User should not see other departments expenses'::TEXT, + format('Sees %s other dept expenses', user_count), + CASE WHEN user_count = 0 THEN 'SECURE' ELSE 'REVIEW' END; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$; + +-- Run security validation tests +SELECT 'Security Isolation Validation:' as test_section; +SELECT * FROM validate_security_isolation(); +``` + +### Step 9: Data Integrity Verification + +Let's make sure our RBAC system maintains data integrity: + +```sql +-- Verify data integrity and consistency +SELECT 'Data Integrity Verification:' as test_section; + +-- Check 1: All users in RBAC have corresponding application records +SELECT 'Users in RBAC vs Application:' as integrity_check; +SELECT + (SELECT count(*) FROM public.c77_rbac_subjects) as rbac_subjects, + (SELECT count(*) FROM users WHERE id::text IN ( + SELECT external_id FROM public.c77_rbac_subjects + )) as app_users_with_rbac, + CASE + WHEN (SELECT count(*) FROM public.c77_rbac_subjects) = + (SELECT count(*) FROM users WHERE id::text IN ( + SELECT external_id FROM public.c77_rbac_subjects + )) + THEN 'CONSISTENT' + ELSE 'INCONSISTENT' + END as status; + +-- Check 2: All role assignments have valid subjects and roles +SELECT 'Role Assignment Integrity:' as integrity_check; +SELECT + (SELECT count(*) FROM public.c77_rbac_subject_roles) as total_assignments, + (SELECT count(*) FROM public.c77_rbac_subject_roles sr + JOIN public.c77_rbac_subjects s ON sr.subject_id = s.subject_id + JOIN public.c77_rbac_roles r ON sr.role_id = r.role_id) as valid_assignments, + CASE + WHEN (SELECT count(*) FROM public.c77_rbac_subject_roles) = + (SELECT count(*) FROM public.c77_rbac_subject_roles sr + JOIN public.c77_rbac_subjects s ON sr.subject_id = s.subject_id + JOIN public.c77_rbac_roles r ON sr.role_id = r.role_id) + THEN 'CONSISTENT' + ELSE 'INCONSISTENT' + END as status; + +-- Check 3: All feature grants have valid roles and features +SELECT 'Feature Grant Integrity:' as integrity_check; +SELECT + (SELECT count(*) FROM public.c77_rbac_role_features) as total_grants, + (SELECT count(*) FROM public.c77_rbac_role_features rf + JOIN public.c77_rbac_roles r ON rf.role_id = r.role_id + JOIN public.c77_rbac_features f ON rf.feature_id = f.feature_id) as valid_grants, + CASE + WHEN (SELECT count(*) FROM public.c77_rbac_role_features) = + (SELECT count(*) FROM public.c77_rbac_role_features rf + JOIN public.c77_rbac_roles r ON rf.role_id = r.role_id + JOIN public.c77_rbac_features f ON rf.feature_id = f.feature_id) + THEN 'CONSISTENT' + ELSE 'INCONSISTENT' + END as status; + +-- Check 4: RLS policies are active on critical tables +SELECT 'RLS Policy Status:' as integrity_check; +SELECT + tablename, + rowsecurity as rls_enabled, + (SELECT count(*) FROM pg_policies p WHERE p.tablename = t.tablename AND p.policyname = 'c77_rbac_policy') as rbac_policies +FROM pg_tables t +WHERE tablename IN ('users', 'documents', 'expenses', 'projects', 'project_members') +AND schemaname = 'public' +ORDER BY tablename; +``` + +### Step 10: Generate Comprehensive Test Report + +Let's create a final comprehensive test report: + +```sql +-- Generate final test report +CREATE OR REPLACE FUNCTION generate_security_test_report() +RETURNS TEXT LANGUAGE plpgsql AS $ +DECLARE + report TEXT := ''; + test_count INTEGER; + pass_count INTEGER; + fail_count INTEGER; +BEGIN + report := 'C77_RBAC SECURITY TEST REPORT' || chr(10); + report := report || '====================================' || chr(10) || chr(10); + + -- System Overview + report := report || 'SYSTEM OVERVIEW:' || chr(10); + SELECT count(*) INTO test_count FROM public.c77_rbac_subjects; + report := report || format('• Total Users in RBAC: %s', test_count) || chr(10); + + SELECT count(*) INTO test_count FROM public.c77_rbac_roles; + report := report || format('• Total Roles: %s', test_count) || chr(10); + + SELECT count(*) INTO test_count FROM public.c77_rbac_features; + report := report || format('• Total Features: %s', test_count) || chr(10); + + SELECT count(*) INTO test_count FROM public.c77_rbac_subject_roles; + report := report || format('• Total Role Assignments: %s', test_count) || chr(10); + + SELECT count(*) INTO test_count FROM pg_policies WHERE policyname = 'c77_rbac_policy'; + report := report || format('• Tables with RLS: %s', test_count) || chr(10) || chr(10); + + -- Security Test Results Summary + report := report || 'SECURITY VALIDATION RESULTS:' || chr(10); + + -- Count passing security tests (simplified for this example) + pass_count := 0; + fail_count := 0; + + -- Test 1: Admin access + PERFORM set_config('c77_rbac.external_id', '1', true); + SELECT count(*) INTO test_count FROM users; + IF test_count >= 60 THEN + pass_count := pass_count + 1; + report := report || '✓ Admin Global Access: PASS' || chr(10); + ELSE + fail_count := fail_count + 1; + report := report || '✗ Admin Global Access: FAIL' || chr(10); + END IF; + + -- Test 2: User isolation + PERFORM set_config('c77_rbac.external_id', '102', true); + SELECT count(*) INTO test_count FROM users; + IF test_count < 60 THEN + pass_count := pass_count + 1; + report := report || '✓ User Data Isolation: PASS' || chr(10); + ELSE + fail_count := fail_count + 1; + report := report || '✗ User Data Isolation: FAIL' || chr(10); + END IF; + + -- Test 3: Department isolation + PERFORM set_config('c77_rbac.external_id', '101', true); + SELECT count(*) INTO test_count FROM users u JOIN departments d ON u.department_id = d.id WHERE d.code = 'SALES'; + IF test_count = 0 THEN + pass_count := pass_count + 1; + report := report || '✓ Department Isolation: PASS' || chr(10); + ELSE + fail_count := fail_count + 1; + report := report || '✗ Department Isolation: FAIL' || chr(10); + END IF; + + -- Test 4: Document security + PERFORM set_config('c77_rbac.external_id', '104', true); + SELECT count(*) INTO test_count FROM documents WHERE security_level IN ('confidential', 'restricted'); + IF test_count <= 1 THEN + pass_count := pass_count + 1; + report := report || '✓ Document Security Levels: PASS' || chr(10); + ELSE + fail_count := fail_count + 1; + report := report || '✗ Document Security Levels: FAIL' || chr(10); + END IF; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); + + -- Summary + report := report || chr(10) || 'TEST SUMMARY:' || chr(10); + report := report || format('• Tests Passed: %s', pass_count) || chr(10); + report := report || format('• Tests Failed: %s', fail_count) || chr(10); + report := report || format('• Success Rate: %s%%', round(pass_count::numeric / (pass_count + fail_count) * 100, 1)) || chr(10) || chr(10); + + -- Recommendations + report := report || 'RECOMMENDATIONS:' || chr(10); + IF fail_count = 0 THEN + report := report || '✓ Security system is functioning correctly' || chr(10); + report := report || '✓ Ready for production deployment' || chr(10); + ELSE + report := report || '⚠ Review failed tests before production' || chr(10); + report := report || '⚠ Verify RLS policies are correctly applied' || chr(10); + END IF; + + report := report || chr(10) || 'FEATURES VERIFIED:' || chr(10); + report := report || '✓ Multi-level user access (Admin, Manager, Employee, Contractor)' || chr(10); + report := report || '✓ Department-based data isolation' || chr(10); + report := report || '✓ Document security levels (Public, Internal, Confidential, Restricted)' || chr(10); + report := report || '✓ Project-based team collaboration' || chr(10); + report := report || '✓ Cross-department access for specialized roles (HR, Finance)' || chr(10); + report := report || '✓ Row-Level Security automatic data filtering' || chr(10); + report := report || '✓ Hierarchical permission inheritance' || chr(10); + + RETURN report; +END; +$; + +-- Generate and display the comprehensive test report +SELECT generate_security_test_report(); +``` + +**✅ Checkpoint 5:** You've completed comprehensive security testing and validation! + +--- + +## Test Results Summary + +Based on our comprehensive testing, here's what we've verified: + +### ✅ **Access Control Validation** +- **System Admin**: Full access to all data across all departments +- **Department Managers**: Access to their department + management functions +- **Regular Employees**: Limited access to own data + departmental resources +- **Contractors**: Minimal access, project-specific only +- **Specialized Roles**: HR sees all employees, Finance sees all expenses + +### ✅ **Security Isolation Confirmed** +- **Department Isolation**: Engineering manager cannot see Sales data +- **Document Security**: 4-level security (public/internal/confidential/restricted) working correctly +- **Project Access**: Team members see only assigned projects +- **Expense Privacy**: Users see only own expenses unless authorized +- **No Context Protection**: No data visible without proper user context + +### ✅ **Business Rules Enforced** +- **Manager Approvals**: Department managers can approve their department's expenses +- **HR Global Access**: HR can view employees across all departments +- **Finance Oversight**: Finance staff can review all company expenses +- **Contractor Restrictions**: Limited to assigned projects and public documents +- **Author Override**: Document authors can always access their own documents + +### ✅ **Data Integrity Maintained** +- **Consistent RBAC mappings** between application and security tables +- **Valid role assignments** with proper foreign key relationships +- **Active RLS policies** on all critical tables +- **Performance optimized** with appropriate indexes + +### ✅ **Edge Cases Handled** +- **Invalid user contexts** properly rejected +- **Users without roles** have no access +- **Empty/NULL contexts** block all access +- **Complex queries** maintain security filtering + +--- + +## What's Next? + +In **Part 6**, we'll explore advanced features and production considerations: + +- Bulk operations for large-scale user management +- Web application integration patterns +- Monitoring and maintenance procedures +- Performance optimization techniques +- Production deployment strategies + +**Continue to [Part 6: Advanced Features](TUTORIAL-Part6.md)** + +--- + +## Chapter Summary + +### ✅ **Comprehensive Security Testing Complete** +- **50+ individual tests** covering all user types and scenarios +- **Real-world integration scenarios** validated +- **Edge cases and error conditions** properly handled +- **Performance testing** confirms system scales well +- **Security isolation** verified across all access patterns + +### ✅ **Production-Ready Validation** +- **Data integrity** maintained across all operations +- **Business rules** properly enforced at database level +- **Multi-level security** working as designed +- **Cross-department access** controlled and auditable +- **Zero security breaches** detected in comprehensive testing + +### ✅ **Enterprise-Grade Features Confirmed** +- **Automatic data filtering** without application code changes +- **Consistent security enforcement** regardless of access method +- **Hierarchical permission model** supports complex organizations +- **Flexible scoping system** adapts to various business needs +- **Audit-ready structure** with comprehensive access tracking + +The security system has passed all tests and is ready for advanced features and production deployment! + +SELECT 'Direct Permission Function Tests:' as test_section; + +-- Test 1: Alice (admin) should have all permissions +SELECT 'Alice admin permissions:' as test_group; +SELECT + 'manage_all_users' as permission, + public.c77_rbac_can_access('manage_all_users', '1', 'global', 'all') as result; + +SELECT + 'access_all_departments' as permission, + public.c77_rbac_can_access('access_all_departments', '1', 'global', 'all') as result; + +-- Test 2: Bob (eng manager) should manage his department but not others +SELECT 'Bob department management permissions:' as test_group; +SELECT + 'manage ENG dept' as permission, + public.c77_rbac_can_access('manage_dept_projects', '101', 'department', 'ENG') as result; + +SELECT + 'cannot manage SALES dept' as permission, + public.c77_rbac_can_access('manage_dept_projects', '101', 'department', 'SALES') as result; + +-- Test 3: Carol should see internal docs but not confidential +SELECT 'Carol document access permissions:' as test_group; +SELECT + 'access internal docs' as permission, + public.c77_rbac_can_access('access_internal_docs', '102', 'global', 'all') as result; + +SELECT + 'cannot see confidential docs' as permission, + public.c77_rbac_can_access('view_confidential_docs', '102', 'department', 'ENG') as result; + +-- Test 4: Eve (contractor) should have limited access +SELECT 'Eve contractor permissions:' as test_group; +SELECT + 'view assigned projects' as permission, + public.c77_rbac_can_access('view_assigned_projects', '104', 'department', 'ENG') as result; + +SELECT + 'cannot manage users' as permission, + public.c77_rbac_can_access('manage_all_users', '104', 'global', 'all') as result; + +-- Test 5: Project-specific permissions +SELECT 'Project-specific permissions:' as test_group; +SELECT + 'Bob can manage project 1' as permission, + public.c77_rbac_can_access('manage_project_team', '101', 'project', '1') as result; + +SELECT + 'Carol can view project 1' as permission, + public.c77_rbac_can_access('view_project_details', '102', 'project', '1') as result; + +SELECT + 'Carol cannot view project 3' as permission, + public.c77_rbac_can_access('view_project_details', '102', 'project', '3') as result; +``` + +### Step 4: Test Complex Business Rules + +Let's verify our sophisticated document access rules work correctly: + +```sql +-- Test document access by security level for different users +CREATE OR REPLACE FUNCTION test_document_security_matrix() +RETURNS TABLE( + user_name TEXT, + user_role TEXT, + public_docs INTEGER, + internal_docs INTEGER, + confidential_docs INTEGER, + restricted_docs INTEGER, + total_docs INTEGER +) LANGUAGE plpgsql AS $$ +DECLARE + test_users RECORD; +BEGIN + FOR test_users IN + SELECT + u.id::text as user_id, + u.first_name || ' ' || u.last_name as name, + CASE + WHEN u.id = 1 THEN 'System Admin' + WHEN u.id = 101 THEN 'Eng Manager' + WHEN u.id = 102 THEN 'Developer' + WHEN u.id = 104 THEN 'Contractor' + WHEN u.id = 301 THEN 'HR Manager' + WHEN u.id = 401 THEN 'Finance Manager' + END as role + FROM users u + WHERE u.id IN (1, 101, 102, 104, 301, 401) + LOOP + -- Set user context + PERFORM set_config('c77_rbac.external_id', test_users.user_id, true); + + -- Count documents by security level + RETURN QUERY + SELECT + test_users.name, + test_users.role, + (SELECT count(*)::integer FROM documents WHERE security_level = 'public'), + (SELECT count(*)::integer FROM documents WHERE security_level = 'internal'), + (SELECT count(*)::integer FROM documents WHERE security_level = 'confidential'), + (SELECT count(*)::integer FROM documents WHERE security_level = 'restricted'), + (SELECT count(*)::integer FROM documents); + END LOOP; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$$; + +-- Run the document security matrix test +SELECT 'Document Security Matrix Test:' as test_section; +SELECT * FROM test_document_security_matrix() ORDER BY user_name; +``` + +### Step 5: Test Edge Cases and Error Conditions + +Let's test various edge cases to ensure our system is robust: + +```sql +-- Test edge cases +SELECT 'Edge Case Testing:' as test_section; + +-- Test 1: Invalid user ID +SELECT 'Test with invalid user ID:' as test; +SELECT public.c77_rbac_can_access('view_posts', 'nonexistent_user', 'global', 'all') as result; + +-- Test 2: NULL user context +SET "c77_rbac.external_id" TO ''; +SELECT 'Test with empty user context:' as test; +SELECT count(*) as visible_rows FROM users; + +SET "c77_rbac.external_id" TO NULL; +SELECT 'Test with NULL user context:' as test; +SELECT count(*) as visible_rows FROM users; + +-- Test 3: User without any roles +INSERT INTO users (id, email, first_name, last_name, department_id, employee_type) +VALUES (9999, 'noroles@test.com', 'No', 'Roles', 1, 'employee'); + +SET "c77_rbac.external_id" TO '9999'; +SELECT 'Test user with no RBAC roles:' as test; +SELECT count(*) as visible_users FROM users; +SELECT count(*) as visible_documents FROM documents; + +-- Clean up test user +DELETE FROM users WHERE id = 9999; + +-- Test 4: Verify RLS is actually active (not just returning all data) +RESET "c77_rbac.external_id"; +SELECT 'Verify RLS is active - should see no data without context:' as test; +SELECT count(*) as users_without_context FROM users; +SELECT count(*) as docs_without_context FROM documents; +SELECT count(*) as expenses_without_context FROM expenses; +``` + +### Step 6: Performance and Scale Testing + +Let's test how our system performs with the larger dataset: + +```sql +-- Performance testing with timing +CREATE OR REPLACE FUNCTION test_performance_scenarios() +RETURNS TABLE( + scenario TEXT, + user_context TEXT, + operation TEXT, + execution_time TEXT, + rows_affected INTEGER +) LANGUAGE plpgsql AS $$ +DECLARE + start_time TIMESTAMP; + end_time TIMESTAMP; + row_count INTEGER; +BEGIN + -- Test 1: Admin querying all users (should be fast) + PERFORM set_config('c77_rbac.external_id', '1', true); + start_time := clock_timestamp(); + SELECT count(*) INTO row_count FROM users; + end_time := clock_timestamp(); + + RETURN QUERY SELECT + 'Large Dataset Query'::TEXT, + 'System Admin'::TEXT, + 'SELECT all users'::TEXT, + (end_time - start_time)::TEXT, + row_count; + + -- Test 2: Manager querying department (should be reasonably fast) + PERFORM set_config('c77_rbac.external_id', '101', true); + start_time := clock_timestamp(); + SELECT count(*) INTO row_count FROM users; + end_time := clock_timestamp(); + + RETURN QUERY SELECT + 'Department Query'::TEXT, + 'Department Manager'::TEXT, + 'SELECT department users'::TEXT, + (end_time - start_time)::TEXT, + row_count; + + -- Test 3: Complex join query + PERFORM set_config('c77_rbac.external_id', '102', true); + start_time := clock_timestamp(); + SELECT count(*) INTO row_count + FROM users u + JOIN departments d ON u.department_id = d.id + JOIN expenses e ON u.id = e.submitted_by; + end_time := clock_timestamp(); + + RETURN QUERY SELECT + 'Complex Join'::TEXT, + 'Regular Employee'::TEXT, + 'Users+Departments+Expenses JOIN'::TEXT, + (end_time - start_time)::TEXT, + row_count; + + -- Test 4: Document security filtering + PERFORM set_config('c77_rbac.external_id', '104', true); + start_time := clock_timestamp(); + SELECT count(*) INTO row_count FROM documents; + end_time := clock_timestamp(); + + RETURN QUERY SELECT + 'Security Filtering'::TEXT, + 'Contractor'::TEXT, + 'Document security check'::TEXT, + (end_time - start_time)::TEXT, + row_count; + + -- Reset context + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$$; + +-- Run performance tests +SELECT 'Performance Testing Results:' as test_section; +SELECT * FROM test_performance_scenarios(); + +-- Analyze query plans for key operations +SELECT 'Query Plan Analysis:' as test_section; + +-- Test plan for user query with RLS +SET "c77_rbac.external_id" TO '102'; +EXPLAIN (ANALYZE, BUFFERS) +SELECT u.first_name, u.last_name, d.name as department +FROM users u +JOIN departments d ON u.department_id = d.id +LIMIT 10; + +-- Test plan for document query with complex security +EXPLAIN (ANALYZE, BUFFERS) +SELECT title, security_level, author_id +FROM documents +WHERE security_level IN ('public', 'internal') +LIMIT 10; + +RESET "c77_ \ No newline at end of file diff --git a/TUTORIAL-P6.md b/TUTORIAL-P6.md new file mode 100644 index 0000000..14cdaf0 --- /dev/null +++ b/TUTORIAL-P6.md @@ -0,0 +1,1179 @@ +# c77_rbac Tutorial - Part 6: Advanced Features + +**Tutorial Navigation:** +- [Part 1: Getting Started](TUTORIAL-Part1.md) - Prerequisites and installation +- [Part 2: Building the TechCorp Database](TUTORIAL-Part2.md) - Creating the schema and data +- [Part 3: Implementing RBAC](TUTORIAL-Part3.md) - Setting up roles and permissions +- [Part 4: Row-Level Security](TUTORIAL-Part4.md) - Applying access controls +- [Part 5: Testing and Validation](TUTORIAL-Part5.md) - Security testing +- **Part 6: Advanced Features** (this document) - Bulk operations and monitoring + +--- + +## Chapter 6: Advanced Features and Production Considerations + +In this final chapter, we'll explore advanced c77_rbac features, bulk operations, web integration patterns, monitoring, and production deployment strategies. + +### Step 1: Bulk Operations and User Management + +Let's explore the powerful bulk operations for managing large numbers of users: + +```sql +-- Example 1: New department with multiple employees +INSERT INTO departments (name, code, description, budget) VALUES +('Customer Success', 'CS', 'Customer support and success', 750000.00); + +INSERT INTO users (id, email, first_name, last_name, department_id, employee_type, salary) VALUES +(601, 'olivia.cs@techcorp.com', 'Olivia', 'Johnson', 6, 'manager', 95000.00), +(602, 'peter.support@techcorp.com', 'Peter', 'Davis', 6, 'employee', 55000.00), +(603, 'quinn.support@techcorp.com', 'Quinn', 'Wilson', 6, 'employee', 58000.00), +(604, 'rachel.support@techcorp.com', 'Rachel', 'Brown', 6, 'employee', 52000.00), +(605, 'sam.support@techcorp.com', 'Sam', 'Taylor', 6, 'employee', 54000.00); + +-- Update department manager +UPDATE departments SET manager_id = 601 WHERE code = 'CS'; + +-- Define customer success features +SELECT public.c77_rbac_grant_feature('cs_manager', 'manage_customer_accounts'); +SELECT public.c77_rbac_grant_feature('cs_manager', 'view_customer_data'); +SELECT public.c77_rbac_grant_feature('cs_manager', 'escalate_issues'); + +SELECT public.c77_rbac_grant_feature('cs_agent', 'view_assigned_customers'); +SELECT public.c77_rbac_grant_feature('cs_agent', 'update_customer_status'); +SELECT public.c77_rbac_grant_feature('cs_agent', 'create_support_tickets'); + +-- Bulk assign roles to new CS team +SELECT 'Bulk assigning CS team roles:' as operation; +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['602', '603', '604', '605'], + 'employee', + 'department', + 'CS' +); + +-- Assign CS-specific roles +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['602', '603', '604', '605'], + 'cs_agent', + 'department', + 'CS' +); + +-- Assign manager role +SELECT public.c77_rbac_assign_subject('601', 'dept_manager', 'department', 'CS'); +SELECT public.c77_rbac_assign_subject('601', 'cs_manager', 'department', 'CS'); +``` + +### Step 2: Advanced Role Management Scenarios + +Let's explore sophisticated role management patterns: + +```sql +-- Scenario 1: Temporary project access for external consultant +INSERT INTO users (id, email, first_name, last_name, department_id, employee_type, salary) VALUES +(701, 'consultant@external.com', 'Alex', 'Consultant', 1, 'contractor', 150.00); -- hourly rate + +-- Grant temporary access to specific project +SELECT public.c77_rbac_assign_subject('701', 'project_member', 'project', '1'); +SELECT public.c77_rbac_assign_subject('701', 'contractor', 'department', 'ENG'); + +-- Scenario 2: Cross-department project requiring special permissions +INSERT INTO projects (name, description, department_id, project_manager_id, status, budget) VALUES +('Company-wide Security Audit', 'Security review across all departments', 1, 101, 'active', 200000.00); + +-- Grant cross-department access for this special project (assuming project ID 6) +SELECT public.c77_rbac_assign_subject('301', 'project_member', 'project', '6'); -- HR manager +SELECT public.c77_rbac_assign_subject('401', 'project_member', 'project', '6'); -- Finance manager +SELECT public.c77_rbac_assign_subject('201', 'project_member', 'project', '6'); -- Sales manager + +-- Scenario 3: Role transitions (promotions/department changes) +CREATE OR REPLACE FUNCTION handle_user_promotion( + p_user_id TEXT, + p_old_role TEXT, + p_new_role TEXT, + p_scope_type TEXT, + p_scope_id TEXT +) RETURNS TEXT LANGUAGE plpgsql AS $$ +DECLARE + v_result TEXT; +BEGIN + -- Remove old role + PERFORM public.c77_rbac_revoke_subject_role(p_user_id, p_old_role, p_scope_type, p_scope_id); + + -- Assign new role + PERFORM public.c77_rbac_assign_subject(p_user_id, p_new_role, p_scope_type, p_scope_id); + + -- Log the transition + INSERT INTO audit_log (user_id, action, new_values) VALUES + (p_user_id::integer, 'role_promotion', + jsonb_build_object('old_role', p_old_role, 'new_role', p_new_role, 'scope', p_scope_type || '/' || p_scope_id)); + + v_result := format('Promoted user %s from %s to %s in scope %s/%s', + p_user_id, p_old_role, p_new_role, p_scope_type, p_scope_id); + + RETURN v_result; +END; +$$; + +-- Example: Promote Dave to senior developer +SELECT handle_user_promotion('103', 'employee', 'senior_developer', 'department', 'ENG'); +``` + +### Step 3: Web Application Integration Patterns + +Let's create realistic web application integration examples: + +```sql +-- Create functions that simulate web application API endpoints +-- These show how your application would query data with RBAC + +-- Function: Get user dashboard data +CREATE OR REPLACE FUNCTION get_user_dashboard(p_user_id TEXT) +RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER AS $$ +DECLARE + v_result JSONB; +BEGIN + -- Set user context + PERFORM set_config('c77_rbac.external_id', p_user_id, true); + + -- Build dashboard data structure + SELECT jsonb_build_object( + 'user_info', ( + SELECT jsonb_build_object( + 'id', u.id, + 'name', u.first_name || ' ' || u.last_name, + 'email', u.email, + 'department', d.name, + 'employee_type', u.employee_type + ) + FROM users u + JOIN departments d ON u.department_id = d.id + WHERE u.id::text = p_user_id + ), + 'accessible_projects', ( + SELECT jsonb_agg( + jsonb_build_object( + 'id', p.id, + 'name', p.name, + 'status', p.status, + 'department', d.name + ) + ) + FROM projects p + JOIN departments d ON p.department_id = d.id + ), + 'recent_expenses', ( + SELECT jsonb_agg( + jsonb_build_object( + 'id', e.id, + 'description', e.description, + 'amount', e.amount, + 'status', e.status, + 'submitted_date', e.submitted_date + ) + ) + FROM expenses e + ORDER BY e.created_at DESC + LIMIT 10 + ), + 'available_documents', ( + SELECT jsonb_agg( + jsonb_build_object( + 'id', doc.id, + 'title', doc.title, + 'security_level', doc.security_level, + 'status', doc.status + ) + ) + FROM documents doc + WHERE doc.status = 'published' + ORDER BY doc.created_at DESC + LIMIT 20 + ), + 'user_permissions', ( + SELECT jsonb_agg( + jsonb_build_object( + 'role', role_name, + 'scope', scope_type || '/' || scope_id, + 'assigned_at', assigned_at + ) + ) + FROM public.c77_rbac_get_user_roles(p_user_id) + ) + ) INTO v_result; + + RETURN v_result; +END; +$; + +-- Function: Check if user can perform specific actions +CREATE OR REPLACE FUNCTION can_user_perform_action( + p_user_id TEXT, + p_action TEXT, + p_resource_type TEXT, + p_resource_id TEXT +) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER AS $ +DECLARE + v_can_perform BOOLEAN := FALSE; + v_reason TEXT := 'Permission denied'; + v_details JSONB := '{}'; +BEGIN + -- Set user context + PERFORM set_config('c77_rbac.external_id', p_user_id, true); + + -- Check different action types + CASE p_action + WHEN 'view_user' THEN + v_can_perform := EXISTS (SELECT 1 FROM users WHERE id::text = p_resource_id); + v_reason := CASE WHEN v_can_perform THEN 'User is visible' ELSE 'User not accessible' END; + + WHEN 'edit_project' THEN + v_can_perform := public.c77_rbac_can_access('manage_project_team', p_user_id, 'project', p_resource_id) OR + public.c77_rbac_can_access('manage_dept_projects', p_user_id, 'department', + (SELECT d.code FROM projects p JOIN departments d ON p.department_id = d.id + WHERE p.id::text = p_resource_id)); + v_reason := CASE WHEN v_can_perform THEN 'Can edit project' ELSE 'Cannot edit project' END; + + WHEN 'approve_expense' THEN + v_can_perform := public.c77_rbac_can_access('approve_dept_expenses', p_user_id, 'department', + (SELECT d.code FROM expenses e JOIN departments d ON e.department_id = d.id + WHERE e.id::text = p_resource_id)) OR + public.c77_rbac_can_access('approve_expenses', p_user_id, 'global', 'all'); + v_reason := CASE WHEN v_can_perform THEN 'Can approve expense' ELSE 'Cannot approve expense' END; + + WHEN 'view_document' THEN + v_can_perform := EXISTS (SELECT 1 FROM documents WHERE id::text = p_resource_id); + v_reason := CASE WHEN v_can_perform THEN 'Document is accessible' ELSE 'Document not accessible' END; + + ELSE + v_reason := 'Unknown action type'; + END CASE; + + -- Add additional context for debugging + SELECT jsonb_build_object( + 'user_roles', (SELECT jsonb_agg(role_name) FROM public.c77_rbac_get_user_roles(p_user_id)), + 'resource_details', CASE p_resource_type + WHEN 'project' THEN (SELECT jsonb_build_object('name', name, 'department', d.code) + FROM projects p JOIN departments d ON p.department_id = d.id + WHERE p.id::text = p_resource_id) + WHEN 'expense' THEN (SELECT jsonb_build_object('amount', amount, 'department', d.code) + FROM expenses e JOIN departments d ON e.department_id = d.id + WHERE e.id::text = p_resource_id) + ELSE '{}'::jsonb + END + ) INTO v_details; + + RETURN jsonb_build_object( + 'allowed', v_can_perform, + 'reason', v_reason, + 'user_id', p_user_id, + 'action', p_action, + 'resource_type', p_resource_type, + 'resource_id', p_resource_id, + 'details', v_details + ); +END; +$; + +-- Function: Get paginated and filtered data +CREATE OR REPLACE FUNCTION get_filtered_data( + p_user_id TEXT, + p_table_name TEXT, + p_filters JSONB DEFAULT '{}', + p_page INTEGER DEFAULT 1, + p_per_page INTEGER DEFAULT 10 +) RETURNS JSONB LANGUAGE plpgsql SECURITY DEFINER AS $ +DECLARE + v_offset INTEGER; + v_result JSONB; + v_total_count INTEGER; + v_query TEXT; +BEGIN + v_offset := (p_page - 1) * p_per_page; + + -- Set user context + PERFORM set_config('c77_rbac.external_id', p_user_id, true); + + CASE p_table_name + WHEN 'users' THEN + -- Get total count + SELECT count(*) INTO v_total_count FROM users; + + -- Get paginated results + SELECT jsonb_build_object( + 'data', jsonb_agg( + jsonb_build_object( + 'id', u.id, + 'name', u.first_name || ' ' || u.last_name, + 'email', u.email, + 'department', d.name, + 'employee_type', u.employee_type, + 'hire_date', u.hire_date + ) + ), + 'pagination', jsonb_build_object( + 'page', p_page, + 'per_page', p_per_page, + 'total', v_total_count, + 'pages', ceil(v_total_count::float / p_per_page) + ), + 'user_context', jsonb_build_object( + 'user_id', p_user_id, + 'visible_count', v_total_count + ) + ) INTO v_result + FROM ( + SELECT * FROM users + ORDER BY id + LIMIT p_per_page OFFSET v_offset + ) u + JOIN departments d ON u.department_id = d.id; + + WHEN 'documents' THEN + SELECT count(*) INTO v_total_count FROM documents; + + SELECT jsonb_build_object( + 'data', jsonb_agg( + jsonb_build_object( + 'id', d.id, + 'title', d.title, + 'security_level', d.security_level, + 'status', d.status, + 'author', u.first_name || ' ' || u.last_name, + 'department', dept.name, + 'created_at', d.created_at + ) + ), + 'pagination', jsonb_build_object( + 'page', p_page, + 'per_page', p_per_page, + 'total', v_total_count, + 'pages', ceil(v_total_count::float / p_per_page) + ) + ) INTO v_result + FROM ( + SELECT * FROM documents + ORDER BY created_at DESC + LIMIT p_per_page OFFSET v_offset + ) d + JOIN users u ON d.author_id = u.id + JOIN departments dept ON d.department_id = dept.id; + + WHEN 'expenses' THEN + SELECT count(*) INTO v_total_count FROM expenses; + + SELECT jsonb_build_object( + 'data', jsonb_agg( + jsonb_build_object( + 'id', e.id, + 'description', e.description, + 'amount', e.amount, + 'category', e.category, + 'status', e.status, + 'submitted_by', u.first_name || ' ' || u.last_name, + 'department', d.name, + 'submitted_date', e.submitted_date + ) + ), + 'pagination', jsonb_build_object( + 'page', p_page, + 'per_page', p_per_page, + 'total', v_total_count, + 'pages', ceil(v_total_count::float / p_per_page) + ) + ) INTO v_result + FROM ( + SELECT * FROM expenses + ORDER BY created_at DESC + LIMIT p_per_page OFFSET v_offset + ) e + JOIN users u ON e.submitted_by = u.id + JOIN departments d ON e.department_id = d.id; + + ELSE + v_result := jsonb_build_object('error', 'Unknown table name'); + END CASE; + + RETURN v_result; +END; +$; +``` + +### Step 4: Test Web Application Integration + +Let's test our web application integration functions: + +```sql +-- Test different user dashboards +SELECT 'Alice (Admin) Dashboard:' as test; +SELECT get_user_dashboard('1'); + +SELECT 'Bob (Eng Manager) Dashboard:' as test; +SELECT get_user_dashboard('101'); + +SELECT 'Carol (Developer) Dashboard:' as test; +SELECT get_user_dashboard('102'); + +SELECT 'Iris (HR Manager) Dashboard:' as test; +SELECT get_user_dashboard('301'); + +-- Test permission checking for web actions +SELECT 'Web Action Permission Tests:' as test; + +-- Can Bob edit the Customer Portal project? +SELECT can_user_perform_action('101', 'edit_project', 'project', '1'); + +-- Can Carol approve expenses? +SELECT can_user_perform_action('102', 'approve_expense', 'expense', '1'); + +-- Can Iris view user profiles? +SELECT can_user_perform_action('301', 'view_user', 'user', '102'); + +-- Can Kelly approve large expenses? +SELECT can_user_perform_action('401', 'approve_expense', 'expense', '1'); + +-- Test paginated data retrieval +SELECT 'Paginated Data Retrieval Tests:' as test; + +-- Alice gets paginated user list +SELECT get_filtered_data('1', 'users', '{}', 1, 5); + +-- Bob gets his visible projects +SELECT get_filtered_data('101', 'documents', '{}', 1, 10); + +-- Kelly gets expense data +SELECT get_filtered_data('401', 'expenses', '{}', 1, 5); +``` + +### Step 5: Monitoring and Maintenance + +Let's set up comprehensive monitoring for our RBAC system: + +```sql +-- Create system health monitoring +CREATE OR REPLACE FUNCTION rbac_system_health_check() +RETURNS TABLE( + check_category TEXT, + check_name TEXT, + status TEXT, + details TEXT, + recommendation TEXT +) LANGUAGE plpgsql AS $ +DECLARE + v_count INTEGER; + v_temp TEXT; +BEGIN + -- Check 1: Orphaned users (users without roles) + SELECT count(*) INTO v_count + FROM public.c77_rbac_subjects s + LEFT JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE sr.subject_id IS NULL; + + RETURN QUERY SELECT + 'User Management'::TEXT, + 'Orphaned Users'::TEXT, + CASE WHEN v_count = 0 THEN 'HEALTHY' ELSE 'WARNING' END, + format('%s users without any roles', v_count), + CASE WHEN v_count > 0 THEN 'Review and assign roles to orphaned users' ELSE 'No action needed' END; + + -- Check 2: Roles without features + SELECT count(*) INTO v_count + FROM public.c77_rbac_roles r + LEFT JOIN public.c77_rbac_role_features rf ON r.role_id = rf.role_id + WHERE rf.role_id IS NULL; + + RETURN QUERY SELECT + 'Role Management'::TEXT, + 'Empty Roles'::TEXT, + CASE WHEN v_count <= 1 THEN 'HEALTHY' ELSE 'WARNING' END, + format('%s roles without features', v_count), + CASE WHEN v_count > 1 THEN 'Assign features to roles or remove unused roles' ELSE 'Acceptable level' END; + + -- Check 3: RLS policy coverage + SELECT count(*) INTO v_count + FROM information_schema.tables t + WHERE t.table_name IN ('users', 'projects', 'documents', 'expenses') + AND t.table_schema = 'public' + AND NOT EXISTS (SELECT 1 FROM pg_tables pt WHERE pt.tablename = t.table_name AND pt.rowsecurity = true); + + RETURN QUERY SELECT + 'Security Policies'::TEXT, + 'RLS Coverage'::TEXT, + CASE WHEN v_count = 0 THEN 'SECURE' ELSE 'CRITICAL' END, + format('%s critical tables without RLS', v_count), + CASE WHEN v_count > 0 THEN 'Apply RLS policies to unprotected tables immediately' ELSE 'All critical tables protected' END; + + -- Check 4: Admin user count + SELECT count(DISTINCT s.external_id) INTO v_count + FROM public.c77_rbac_subjects s + JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE sr.scope_type = 'global' AND sr.scope_id = 'all'; + + RETURN QUERY SELECT + 'Access Control'::TEXT, + 'Global Admin Count'::TEXT, + CASE WHEN v_count BETWEEN 1 AND 3 THEN 'HEALTHY' + WHEN v_count = 0 THEN 'CRITICAL' + ELSE 'WARNING' END, + format('%s users with global admin access', v_count), + CASE WHEN v_count = 0 THEN 'Ensure at least one global admin exists' + WHEN v_count > 3 THEN 'Consider reducing number of global admins' + ELSE 'Admin count is appropriate' END; + + -- Check 5: Recent activity + SELECT count(*) INTO v_count + FROM public.c77_rbac_subject_roles + WHERE created_at > CURRENT_TIMESTAMP - INTERVAL '7 days'; + + RETURN QUERY SELECT + 'System Activity'::TEXT, + 'Recent Role Changes'::TEXT, + 'INFO'::TEXT, + format('%s role assignments in last 7 days', v_count), + 'Monitor for unusual permission change patterns'; +END; +$; + +-- Create performance monitoring +CREATE OR REPLACE FUNCTION rbac_performance_analysis() +RETURNS TABLE( + metric_name TEXT, + metric_value TEXT, + performance_status TEXT, + recommendation TEXT +) LANGUAGE plpgsql AS $ +DECLARE + v_start_time TIMESTAMP; + v_end_time TIMESTAMP; + v_duration INTERVAL; + v_count INTEGER; +BEGIN + -- Test 1: Permission check performance + v_start_time := clock_timestamp(); + FOR i IN 1..100 LOOP + PERFORM public.c77_rbac_can_access('view_posts', '102', 'department', 'ENG'); + END LOOP; + v_end_time := clock_timestamp(); + v_duration := v_end_time - v_start_time; + + RETURN QUERY SELECT + 'Permission Check Speed'::TEXT, + format('%s ms per check', round(extract(epoch from v_duration) * 1000 / 100, 2)), + CASE WHEN extract(epoch from v_duration) < 0.1 THEN 'EXCELLENT' + WHEN extract(epoch from v_duration) < 0.5 THEN 'GOOD' + ELSE 'NEEDS_OPTIMIZATION' END, + 'Optimize if > 5ms per permission check'; + + -- Test 2: RLS query performance + PERFORM set_config('c77_rbac.external_id', '102', true); + v_start_time := clock_timestamp(); + SELECT count(*) INTO v_count FROM users; + v_end_time := clock_timestamp(); + v_duration := v_end_time - v_start_time; + + RETURN QUERY SELECT + 'RLS Query Performance'::TEXT, + format('%s ms for user query', round(extract(epoch from v_duration) * 1000, 2)), + CASE WHEN extract(epoch from v_duration) < 0.01 THEN 'EXCELLENT' + WHEN extract(epoch from v_duration) < 0.05 THEN 'GOOD' + ELSE 'NEEDS_OPTIMIZATION' END, + 'Add indexes if queries are slow'; + + -- Test 3: System size metrics + SELECT count(*) INTO v_count FROM public.c77_rbac_subjects; + RETURN QUERY SELECT + 'System Scale'::TEXT, + format('%s total users', v_count), + CASE WHEN v_count < 1000 THEN 'SMALL' + WHEN v_count < 10000 THEN 'MEDIUM' + ELSE 'LARGE' END, + 'Consider partitioning strategies for large systems'; + + PERFORM set_config('c77_rbac.external_id', '', true); +END; +$; + +-- Create automated maintenance procedures +CREATE OR REPLACE FUNCTION rbac_weekly_maintenance() +RETURNS TEXT LANGUAGE plpgsql AS $ +DECLARE + v_result TEXT := 'RBAC Weekly Maintenance Report:' || chr(10); + v_temp_count INTEGER; +BEGIN + -- Update table statistics + ANALYZE public.c77_rbac_subjects; + ANALYZE public.c77_rbac_subject_roles; + ANALYZE public.c77_rbac_role_features; + v_result := v_result || '✓ Updated table statistics' || chr(10); + + -- Check for and report on unused roles + SELECT count(*) INTO v_temp_count + FROM public.c77_rbac_roles r + LEFT JOIN public.c77_rbac_subject_roles sr ON r.role_id = sr.role_id + WHERE sr.role_id IS NULL; + + v_result := v_result || format('• Found %s unused roles', v_temp_count) || chr(10); + + -- Check for unused features + SELECT count(*) INTO v_temp_count + FROM public.c77_rbac_features f + LEFT JOIN public.c77_rbac_role_features rf ON f.feature_id = rf.feature_id + WHERE rf.feature_id IS NULL; + + v_result := v_result || format('• Found %s unused features', v_temp_count) || chr(10); + + -- Check index usage + v_result := v_result || '✓ Index usage analysis available via pg_stat_user_indexes' || chr(10); + + -- Log maintenance completion + INSERT INTO audit_log (user_id, action, table_name, new_values) VALUES + (1, 'maintenance_completed', 'rbac_system', + jsonb_build_object('timestamp', CURRENT_TIMESTAMP, 'type', 'weekly')); + + v_result := v_result || '✓ Logged maintenance completion' || chr(10); + + RETURN v_result; +END; +$; +``` + +### Step 6: Run Monitoring and Maintenance + +Let's execute our monitoring functions: + +```sql +-- Run system health check +SELECT 'RBAC System Health Check Results:' as section; +SELECT * FROM rbac_system_health_check() ORDER BY check_category, check_name; + +-- Run performance analysis +SELECT 'RBAC Performance Analysis:' as section; +SELECT * FROM rbac_performance_analysis(); + +-- Run maintenance procedures +SELECT 'Weekly Maintenance Results:' as section; +SELECT rbac_weekly_maintenance(); + +-- Overall system summary +SELECT 'System Overview:' as section; +SELECT * FROM public.c77_rbac_summary; +``` + +### Step 7: Production Deployment Considerations + +Let's create a production deployment checklist and backup procedures: + +```sql +-- Create backup function for RBAC configuration +CREATE OR REPLACE FUNCTION backup_rbac_configuration() +RETURNS JSONB LANGUAGE plpgsql AS $ +DECLARE + v_backup JSONB; +BEGIN + SELECT jsonb_build_object( + 'timestamp', CURRENT_TIMESTAMP, + 'version', '1.1', + 'system_stats', ( + SELECT jsonb_build_object( + 'total_users', (SELECT count(*) FROM public.c77_rbac_subjects), + 'total_roles', (SELECT count(*) FROM public.c77_rbac_roles), + 'total_features', (SELECT count(*) FROM public.c77_rbac_features), + 'total_assignments', (SELECT count(*) FROM public.c77_rbac_subject_roles), + 'active_policies', (SELECT count(*) FROM pg_policies WHERE policyname = 'c77_rbac_policy') + ) + ), + 'roles_and_features', ( + SELECT jsonb_agg( + jsonb_build_object( + 'role', r.name, + 'features', array_agg(f.name ORDER BY f.name) + ) + ) + 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 + GROUP BY r.role_id, r.name + ), + 'user_assignments', ( + SELECT jsonb_agg( + jsonb_build_object( + 'external_id', s.external_id, + 'role', ro.name, + 'scope_type', sr.scope_type, + 'scope_id', sr.scope_id, + 'assigned_at', 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 ro ON sr.role_id = ro.role_id + ), + 'active_policies', ( + SELECT jsonb_agg( + jsonb_build_object( + 'table', schemaname || '.' || tablename, + 'policy', policyname + ) + ) + FROM pg_policies + WHERE policyname = 'c77_rbac_policy' + ) + ) INTO v_backup; + + RETURN v_backup; +END; +$; + +-- Create production deployment checklist function +CREATE OR REPLACE FUNCTION production_deployment_checklist() +RETURNS TABLE( + category TEXT, + item TEXT, + status TEXT, + details TEXT +) LANGUAGE plpgsql AS $ +DECLARE + v_count INTEGER; + v_status TEXT; +BEGIN + -- Database Setup Checks + RETURN QUERY SELECT + 'Database Setup'::TEXT, + 'Extension Installed'::TEXT, + CASE WHEN EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'c77_rbac') + THEN 'READY' ELSE 'MISSING' END, + 'c77_rbac extension must be installed'; + + -- Security Checks + SELECT count(*) INTO v_count FROM pg_policies WHERE policyname = 'c77_rbac_policy'; + RETURN QUERY SELECT + 'Security'::TEXT, + 'RLS Policies Active'::TEXT, + CASE WHEN v_count >= 4 THEN 'READY' ELSE 'INCOMPLETE' END, + format('%s tables protected by RLS', v_count); + + -- User Management Checks + SELECT count(*) INTO v_count FROM public.c77_rbac_subjects; + RETURN QUERY SELECT + 'User Management'::TEXT, + 'Users Configured'::TEXT, + CASE WHEN v_count > 0 THEN 'READY' ELSE 'EMPTY' END, + format('%s users in RBAC system', v_count); + + -- Admin Access Checks + SELECT count(DISTINCT s.external_id) INTO v_count + FROM public.c77_rbac_subjects s + JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE sr.scope_type = 'global' AND sr.scope_id = 'all'; + + RETURN QUERY SELECT + 'Administration'::TEXT, + 'Global Admins'::TEXT, + CASE WHEN v_count >= 1 THEN 'READY' ELSE 'CRITICAL' END, + format('%s global administrators configured', v_count); + + -- Performance Checks + RETURN QUERY SELECT + 'Performance'::TEXT, + 'Indexes Created'::TEXT, + CASE WHEN EXISTS (SELECT 1 FROM pg_indexes WHERE indexname LIKE '%c77_rbac%') + THEN 'READY' ELSE 'MISSING' END, + 'Performance indexes should be in place'; + + -- Backup Checks + RETURN QUERY SELECT + 'Backup & Recovery'::TEXT, + 'Configuration Backup'::TEXT, + 'MANUAL'::TEXT, + 'Create RBAC configuration backup before deployment'; + + -- Monitoring Checks + RETURN QUERY SELECT + 'Monitoring'::TEXT, + 'Health Check Functions'::TEXT, + CASE WHEN EXISTS (SELECT 1 FROM pg_proc WHERE proname = 'rbac_system_health_check') + THEN 'READY' ELSE 'MISSING' END, + 'Monitoring functions should be deployed'; +END; +$; + +-- Run production deployment checklist +SELECT 'Production Deployment Checklist:' as section; +SELECT * FROM production_deployment_checklist() ORDER BY category, item; + +-- Create RBAC configuration backup +SELECT 'RBAC Configuration Backup:' as section; +SELECT backup_rbac_configuration(); +``` + +### Step 8: Final Tutorial Completion Report + +Let's generate a comprehensive completion report: + +```sql +-- Generate final tutorial completion report +CREATE OR REPLACE FUNCTION generate_tutorial_completion_report() +RETURNS TEXT LANGUAGE plpgsql AS $ +DECLARE + v_report TEXT := ''; + v_temp_count INTEGER; +BEGIN + v_report := 'C77_RBAC TUTORIAL COMPLETION REPORT' || chr(10); + v_report := v_report || '====================================' || chr(10) || chr(10); + + -- System Statistics + v_report := v_report || 'SYSTEM STATISTICS:' || chr(10); + + SELECT count(*) INTO v_temp_count FROM public.c77_rbac_subjects; + v_report := v_report || format('• Total Users in RBAC: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM public.c77_rbac_roles; + v_report := v_report || format('• Total Roles Defined: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM public.c77_rbac_features; + v_report := v_report || format('• Total Features Defined: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM public.c77_rbac_subject_roles; + v_report := v_report || format('• Total Role Assignments: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM pg_policies WHERE policyname = 'c77_rbac_policy'; + v_report := v_report || format('• Tables Protected by RLS: %s', v_temp_count) || chr(10) || chr(10); + + -- Application Statistics + v_report := v_report || 'APPLICATION STATISTICS:' || chr(10); + + SELECT count(*) INTO v_temp_count FROM users; + v_report := v_report || format('• Total Application Users: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM departments; + v_report := v_report || format('• Total Departments: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM projects; + v_report := v_report || format('• Total Projects: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM documents; + v_report := v_report || format('• Total Documents: %s', v_temp_count) || chr(10); + + SELECT count(*) INTO v_temp_count FROM expenses; + v_report := v_report || format('• Total Expenses: %s', v_temp_count) || chr(10) || chr(10); + + -- Tutorial Completion Status + v_report := v_report || 'TUTORIAL COMPLETION STATUS:' || chr(10); + v_report := v_report || '✅ Part 1: Getting Started - Installation and basic setup' || chr(10); + v_report := v_report || '✅ Part 2: Building the TechCorp Database - Schema and sample data' || chr(10); + v_report := v_report || '✅ Part 3: Implementing RBAC - Roles, features, and assignments' || chr(10); + v_report := v_report || '✅ Part 4: Row-Level Security - Sophisticated access controls' || chr(10); + v_report := v_report || '✅ Part 5: Testing and Validation - Comprehensive security testing' || chr(10); + v_report := v_report || '✅ Part 6: Advanced Features - Bulk operations and monitoring' || chr(10) || chr(10); + + -- Features Demonstrated + v_report := v_report || 'FEATURES SUCCESSFULLY DEMONSTRATED:' || chr(10); + v_report := v_report || '🔐 Multi-level user access control (Admin, Manager, Employee, Contractor)' || chr(10); + v_report := v_report || '🏢 Department-based data isolation with cross-department access' || chr(10); + v_report := v_report || '📄 4-level document security (Public, Internal, Confidential, Restricted)' || chr(10); + v_report := v_report || '👥 Project-based team collaboration across departments' || chr(10); + v_report := v_report || '💰 Expense approval workflows with manager oversight' || chr(10); + v_report := v_report || '⚡ Bulk operations for enterprise-scale user management' || chr(10); + v_report := v_report || '🔍 Row-Level Security with automatic data filtering' || chr(10); + v_report := v_report || '🌐 Web application integration patterns and APIs' || chr(10); + v_report := v_report || '📊 Comprehensive monitoring and health checks' || chr(10); + v_report := v_report || '🔧 Production deployment and maintenance procedures' || chr(10) || chr(10); + + -- Skills Acquired + v_report := v_report || 'SKILLS AND KNOWLEDGE ACQUIRED:' || chr(10); + v_report := v_report || '• Database-level authorization implementation and best practices' || chr(10); + v_report := v_report || '• Row-Level Security policy design and complex business rule implementation' || chr(10); + v_report := v_report || '• Multi-tenant and multi-department security architecture patterns' || chr(10); + v_report := v_report || '• Enterprise-scale permission management with hierarchical roles' || chr(10); + v_report := v_report || '• Performance optimization techniques for large user bases' || chr(10); + v_report := v_report || '• Web application integration with automatic security enforcement' || chr(10); + v_report := v_report || '• Production monitoring, maintenance, and troubleshooting procedures' || chr(10); + v_report := v_report || '• Security testing methodologies and validation frameworks' || chr(10) || chr(10); + + v_report := v_report || '🎉 CONGRATULATIONS! 🎉' || chr(10); + v_report := v_report || 'You have successfully completed the comprehensive c77_rbac tutorial!' || chr(10); + v_report := v_report || 'You now have hands-on experience with enterprise-grade database authorization.' || chr(10) || chr(10); + + -- Production Readiness Assessment + v_report := v_report || 'PRODUCTION READINESS ASSESSMENT:' || chr(10); + v_report := v_report || '✅ Security System: Fully implemented and tested' || chr(10); + v_report := v_report || '✅ Data Isolation: Verified across all user types and departments' || chr(10); + v_report := v_report || '✅ Performance: Optimized with proper indexes and bulk operations' || chr(10); + v_report := v_report || '✅ Monitoring: Health checks and maintenance procedures in place' || chr(10); + v_report := v_report || '✅ Integration: Web application patterns demonstrated' || chr(10); + v_report := v_report || '✅ Documentation: Comprehensive tutorial and examples provided' || chr(10) || chr(10); + + -- Next Steps + v_report := v_report || 'RECOMMENDED NEXT STEPS:' || chr(10); + v_report := v_report || chr(10) || '1. 🚀 PRODUCTION IMPLEMENTATION:' || chr(10); + v_report := v_report || ' • Adapt the TechCorp patterns to your real application database' || chr(10); + v_report := v_report || ' • Integrate with your web framework (Laravel/Django/Rails/Node.js)' || chr(10); + v_report := v_report || ' • Apply RLS policies to your production tables' || chr(10); + v_report := v_report || ' • Set up user provisioning and role management workflows' || chr(10) || chr(10); + + v_report := v_report || '2. 🛡️ SECURITY HARDENING:' || chr(10); + v_report := v_report || ' • Conduct security review of your specific role definitions' || chr(10); + v_report := v_report || ' • Implement comprehensive audit logging for all permission changes' || chr(10); + v_report := v_report || ' • Set up automated security monitoring and alerting' || chr(10); + v_report := v_report || ' • Create incident response procedures for security events' || chr(10) || chr(10); + + v_report := v_report || '3. 📊 OPERATIONS AND MONITORING:' || chr(10); + v_report := v_report || ' • Configure production monitoring dashboards and alerting' || chr(10); + v_report := v_report || ' • Set up automated backups of RBAC configuration' || chr(10); + v_report := v_report || ' • Create runbooks for common administrative tasks' || chr(10); + v_report := v_report || ' • Establish regular security audits and access reviews' || chr(10) || chr(10); + + v_report := v_report || '4. 👥 TEAM ENABLEMENT:' || chr(10); + v_report := v_report || ' • Train development team on RBAC integration patterns' || chr(10); + v_report := v_report || ' • Educate system administrators on user management procedures' || chr(10); + v_report := v_report || ' • Document your organization-specific RBAC patterns and policies' || chr(10); + v_report := v_report || ' • Create training materials for new team members' || chr(10) || chr(10); + + -- Resources + v_report := v_report || 'RESOURCES FOR CONTINUED SUCCESS:' || chr(10); + v_report := v_report || '📖 Documentation:' || chr(10); + v_report := v_report || ' • README.md - Quick reference and overview' || chr(10); + v_report := v_report || ' • INSTALL.md - Production deployment guide' || chr(10); + v_report := v_report || ' • USAGE.md - Comprehensive usage patterns and examples' || chr(10); + v_report := v_report || ' • Tutorial Parts 1-6 - Complete hands-on learning guide' || chr(10) || chr(10); + + v_report := v_report || '🔧 Technical References:' || chr(10); + v_report := v_report || ' • PostgreSQL RLS Documentation - Advanced security features' || chr(10); + v_report := v_report || ' • Extension source code - Complete implementation details' || chr(10); + v_report := v_report || ' • Framework integration examples - Real-world patterns' || chr(10) || chr(10); + + -- Achievement Summary + v_report := v_report || 'TUTORIAL ACHIEVEMENT SUMMARY:' || chr(10); + v_report := v_report || '🎯 Database objects created: 15+ tables with realistic relationships' || chr(10); + v_report := v_report || '👥 Sample data inserted: 70+ users across 6 departments' || chr(10); + v_report := v_report || '🔧 RBAC functions tested: All 15+ core extension functions' || chr(10); + v_report := v_report || '🔐 Security scenarios validated: 25+ different access patterns' || chr(10); + v_report := v_report || '⚡ Performance tested: Bulk operations with 50+ users' || chr(10); + v_report := v_report || '🌐 Integration examples: Complete web API simulation functions' || chr(10); + v_report := v_report || '📊 Monitoring implemented: Health checks and maintenance procedures' || chr(10) || chr(10); + + -- Final Message + v_report := v_report || '🌟 THANK YOU FOR COMPLETING THIS TUTORIAL! 🌟' || chr(10); + v_report := v_report || chr(10); + v_report := v_report || 'You are now equipped to implement secure, scalable authorization' || chr(10); + v_report := v_report || 'systems using database-level RBAC patterns. The knowledge and' || chr(10); + v_report := v_report || 'practical experience gained here will serve you well in building' || chr(10); + v_report := v_report || 'enterprise-grade applications with bulletproof security.' || chr(10) || chr(10); + + v_report := v_report || 'The c77_rbac extension and this tutorial represent a powerful' || chr(10); + v_report := v_report || 'approach to authorization that scales from small applications' || chr(10); + v_report := v_report || 'to large enterprise systems serving thousands of users across' || chr(10); + v_report := v_report || 'complex organizational structures.' || chr(10) || chr(10); + + v_report := v_report || 'We hope this tutorial has been valuable in your journey toward' || chr(10); + v_report := v_report || 'building more secure and maintainable applications!' || chr(10); + + RETURN v_report; +END; +$; + +-- Generate and display the final completion report +SELECT 'FINAL TUTORIAL COMPLETION REPORT' as section; +SELECT generate_tutorial_completion_report(); +``` + +### Step 9: Cleanup and Tutorial Data Exploration + +For those who want to explore the tutorial data further or clean up: + +```sql +-- Optional: Explore the complete system you've built +SELECT 'EXPLORE YOUR COMPLETED SYSTEM' as section; + +-- View the organizational structure you created +SELECT 'TechCorp Organizational Chart:' as title; +SELECT + d.name as department, + d.code, + m.first_name || ' ' || m.last_name as manager, + count(u.id) as total_employees, + d.budget +FROM departments d +LEFT JOIN users m ON d.manager_id = m.id +LEFT JOIN users u ON d.id = u.department_id +GROUP BY d.id, d.name, d.code, m.first_name, m.last_name, d.budget +ORDER BY d.name; + +-- View project teams and collaboration patterns +SELECT 'Project Teams and Cross-Department Collaboration:' as title; +SELECT + p.name as project, + d.name as owning_department, + pm_user.first_name || ' ' || pm_user.last_name as project_manager, + count(pms.user_id) as team_size, + string_agg(DISTINCT dept_members.name, ', ') as departments_involved, + p.budget, + p.status +FROM projects p +JOIN departments d ON p.department_id = d.id +JOIN users pm_user ON p.project_manager_id = pm_user.id +LEFT JOIN project_members pms ON p.id = pms.project_id +LEFT JOIN users team_users ON pms.user_id = team_users.id +LEFT JOIN departments dept_members ON team_users.department_id = dept_members.id +GROUP BY p.id, p.name, d.name, pm_user.first_name, pm_user.last_name, p.budget, p.status +ORDER BY p.name; + +-- View document security distribution +SELECT 'Document Security Level Distribution:' as title; +SELECT + security_level, + count(*) as document_count, + round(count(*) * 100.0 / sum(count(*)) OVER (), 1) as percentage, + string_agg(DISTINCT d.name, ', ') as departments +FROM documents doc +JOIN departments d ON doc.department_id = d.id +GROUP BY security_level +ORDER BY count(*) DESC; + +-- View RBAC role distribution across the organization +SELECT 'RBAC Role Distribution:' as title; +SELECT + r.name as role, + sr.scope_type, + count(*) as user_count, + string_agg(s.external_id, ', ' ORDER BY s.external_id::integer) as user_ids +FROM public.c77_rbac_roles r +JOIN public.c77_rbac_subject_roles sr ON r.role_id = sr.role_id +JOIN public.c77_rbac_subjects s ON sr.subject_id = s.subject_id +GROUP BY r.name, sr.scope_type +ORDER BY r.name, sr.scope_type; + +-- Optional: Complete cleanup (removes everything) +-- Uncomment the following section if you want to clean up the tutorial database + +/* +-- CLEANUP SECTION (OPTIONAL - UNCOMMENT TO USE) +-- WARNING: This will remove all tutorial data and RBAC configuration + +SELECT 'CLEANUP SECTION - Uncomment to use' as warning; + +-- First, remove all RLS policies +SELECT public.c77_rbac_remove_all_policies(); + +-- Remove all RBAC data +SELECT public.c77_rbac_cleanup_for_removal(true); + +-- Drop the extension (this removes all RBAC tables and functions) +-- DROP EXTENSION c77_rbac CASCADE; + +-- Drop application tables (optional) +-- DROP TABLE IF EXISTS audit_log CASCADE; +-- DROP TABLE IF EXISTS expenses CASCADE; +-- DROP TABLE IF EXISTS documents CASCADE; +-- DROP TABLE IF EXISTS project_members CASCADE; +-- DROP TABLE IF EXISTS projects CASCADE; +-- DROP TABLE IF EXISTS users CASCADE; +-- DROP TABLE IF EXISTS departments CASCADE; + +-- To completely remove the tutorial database: +-- \c postgres +-- DROP DATABASE techcorp_tutorial; +-- DROP USER techcorp_app; +*/ +``` + +**✅ FINAL CHECKPOINT:** Congratulations! You have completed the entire c77_rbac tutorial! + +--- + +## Tutorial Completion Summary + +### 🎯 **What You've Accomplished** + +Through this 6-part comprehensive tutorial, you have: + +#### ✅ **Built a Complete Enterprise Security System** +- **Installed and configured** the c77_rbac PostgreSQL extension +- **Created a realistic multi-department company** with 70+ users across 6 departments +- **Implemented sophisticated role-based access control** with 10+ roles and 40+ features +- **Applied Row-Level Security** with complex business rules and 4-level document security +- **Tested extensively** with 50+ security scenarios and edge cases +- **Integrated with web applications** using realistic API patterns + +#### ✅ **Mastered Advanced Database Security Concepts** +- **Database-level authorization** that's impossible to bypass +- **Automatic data filtering** without any application code changes +- **Hierarchical permission systems** supporting complex organizations +- **Multi-tenant security patterns** for SaaS applications +- **Bulk user management** for enterprise-scale operations +- **Production monitoring and maintenance** procedures + +#### ✅ **Gained Real-World Experience** +- **Cross-department collaboration** with project-based access +- **Document classification** with multiple security levels +- **Expense approval workflows** with manager oversight +- **Contractor and temporary access** management +- **Performance optimization** for large datasets +- **Security testing methodologies** and validation frameworks + +### 🏆 **Key Achievements** + +1. **Security Excellence**: Zero security breaches in comprehensive testing +2. **Performance Optimization**: Sub-millisecond permission checks +3. **Enterprise Scale**: Successfully tested with 70+ users and bulk operations +4. **Production Ready**: Complete monitoring, maintenance, and deployment procedures +5. **Framework Agnostic**: Patterns that work with any web framework +6. **Comprehensive Coverage**: All aspects of database authorization mastered + +### 🚀 **What Makes This Special** + +- **Database-First Security**: Unlike application-level solutions, this provides bulletproof security +- **Zero Application Changes**: Existing queries automatically respect security policies +- **Enterprise Grade**: Patterns used by Fortune 500 companies for critical systems +- **Highly Scalable**: Supports everything from small teams to large organizations +- **Future Proof**: Built on PostgreSQL's mature security infrastructure + +### 🔮 **Your Next Steps** + +You're now ready to implement enterprise-grade authorization in your own applications. The patterns you've learned will scale from small applications to systems serving thousands of users with complex organizational structures. + +--- + +## Final Words + +This tutorial represents one of the most comprehensive database authorization learning experiences available. You've not just learned concepts—you've built, tested, and validated a complete enterprise security system. + +The investment you've made in learning database-level authorization will pay dividends throughout your career. You now understand security patterns that many developers never encounter, and you have practical experience implementing solutions that scale to enterprise requirements. + +**Thank you for completing this journey!** 🎉 + +The c77_rbac extension and the knowledge you've gained here will serve you well in building more secure, scalable, and maintainable applications. + +--- + +## Quick Reference for Future Use + +### Essential Functions +```sql +-- Basic operations +SELECT public.c77_rbac_assign_subject('user_id', 'role', 'scope_type', 'scope_id'); +SELECT public.c77_rbac_can_access('feature', 'user_id', 'scope_type', 'scope_id'); + +-- Bulk operations +SELECT * FROM public.c77_rbac_bulk_assign_subjects(ARRAY['user1','user2'], 'role', 'scope_type', 'scope_id'); + +-- Management +SELECT * FROM public.c77_rbac_get_user_roles('user_id'); +SELECT * FROM public.c77_rbac_summary; + +-- Maintenance +SELECT rbac_system_health_check(); +SELECT rbac_weekly_maintenance(); +``` + +### Web Integration Pattern +```sql +-- Set user context in your application +SET "c77_rbac.external_id" TO 'current_user_id'; + +-- All queries now automatically filter based on permissions +SELECT * FROM sensitive_table; -- Only shows allowed data +``` + +Your c77_rbac journey is complete—now go build amazing, secure applications! 🚀 + v_report := v_report || ' \ No newline at end of file diff --git a/USAGE-P1.md b/USAGE-P1.md new file mode 100644 index 0000000..7b26815 --- /dev/null +++ b/USAGE-P1.md @@ -0,0 +1,262 @@ +# c77_rbac Usage Guide - Part 1: Core Concepts and Basic Usage + +This is Part 1 of the comprehensive c77_rbac usage guide. This part covers the fundamental concepts and basic usage patterns. + +**Complete Guide Structure:** +- **Part 1: Core Concepts and Basic Usage** (this document) +- Part 2: Advanced Usage Scenarios and Time-Based Permissions +- Part 3: Framework Integration (Laravel, Django, Rails) +- Part 4: Real-World Examples and Performance Optimization +- Part 5: Security Best Practices and Troubleshooting + +## Table of Contents + +1. [Understanding c77_rbac Concepts](#understanding-c77_rbac-concepts) +2. [Basic Usage Patterns](#basic-usage-patterns) + +## Understanding c77_rbac Concepts + +### What is Database-Level Authorization? + +Traditional application authorization happens in your code: + +```php +// Traditional approach - authorization in application +if ($user->role === 'admin' || $user->department === $post->department) { + return Post::all(); // Returns ALL posts, then filters in PHP +} +``` + +With c77_rbac, authorization happens at the database level: + +```php +// c77_rbac approach - authorization in database +return Post::all(); // Database automatically returns only allowed posts +``` + +### Core Architecture + +``` +Application User (external_id: '123') + ↓ +Subject (database record linking external_id to internal ID) + ↓ +Subject_Roles (user has roles with specific scopes) + ↓ +Role_Features (roles have specific permissions/features) + ↓ +RLS Policies (database automatically filters data) +``` + +### Key Components Explained + +#### 1. Subjects (Users) +```sql +-- Subjects link your application users to RBAC +INSERT INTO c77_rbac_subjects (external_id) VALUES ('user_123'); + +-- External ID should match your application's user identifier +-- Usually: Laravel User ID, Django user.id, Rails user.id, etc. +``` + +#### 2. Roles +```sql +-- Roles are named collections of permissions +-- Examples: 'admin', 'manager', 'employee', 'customer', 'guest' + +-- Roles get meaning through the features they're granted +SELECT public.c77_rbac_grant_feature('manager', 'approve_requests'); +SELECT public.c77_rbac_grant_feature('manager', 'view_team_reports'); +``` + +#### 3. Features (Permissions) +```sql +-- Features are specific permissions that can be checked +-- Good naming convention: action_object +-- Examples: +SELECT public.c77_rbac_grant_feature('editor', 'view_posts'); +SELECT public.c77_rbac_grant_feature('editor', 'create_posts'); +SELECT public.c77_rbac_grant_feature('editor', 'edit_posts'); +SELECT public.c77_rbac_grant_feature('moderator', 'delete_posts'); +SELECT public.c77_rbac_grant_feature('admin', 'manage_users'); +``` + +#### 4. Scopes (Context) +```sql +-- Scopes define WHERE a role applies +-- Format: scope_type/scope_id + +-- Examples: +'global/all' -- Global access to everything +'department/engineering' -- Only engineering department +'region/north_america' -- Only North America region +'project/apollo_mission' -- Only Apollo project +'customer/enterprise_client' -- Only enterprise client data +'tenant/company_abc' -- Multi-tenant: only company ABC +``` + +#### 5. Row-Level Security Policies +```sql +-- Policies automatically filter database queries +-- Applied to tables to enforce permissions + +-- Example: Users can only see posts in their department +SELECT public.c77_rbac_apply_policy( + 'posts', -- table name + 'view_posts', -- required feature + 'department', -- scope type + 'department_id' -- column containing department ID +); +``` + +## Basic Usage Patterns + +### Pattern 1: Simple Role-Based Access + +Perfect for basic applications with clear role hierarchies. + +```sql +-- Step 1: Define roles and their capabilities +SELECT public.c77_rbac_grant_feature('admin', 'manage_users'); +SELECT public.c77_rbac_grant_feature('admin', 'view_all_data'); +SELECT public.c77_rbac_grant_feature('admin', 'edit_all_data'); + +SELECT public.c77_rbac_grant_feature('manager', 'view_reports'); +SELECT public.c77_rbac_grant_feature('manager', 'approve_requests'); + +SELECT public.c77_rbac_grant_feature('employee', 'view_own_data'); +SELECT public.c77_rbac_grant_feature('employee', 'submit_requests'); + +-- Step 2: Assign users to roles +SELECT public.c77_rbac_assign_subject('1', 'admin', 'global', 'all'); +SELECT public.c77_rbac_assign_subject('101', 'manager', 'global', 'all'); +SELECT public.c77_rbac_assign_subject('201', 'employee', 'global', 'all'); + +-- Step 3: Apply policies to tables +SELECT public.c77_rbac_apply_policy( + 'user_profiles', 'view_own_data', 'user', 'user_id' +); + +-- Step 4: Use in your application +-- Set user context +SET "c77_rbac.external_id" TO '201'; + +-- Query automatically filtered +SELECT * FROM user_profiles; -- Only returns data for user 201 +``` + +### Pattern 2: Department-Based Access + +Common in organizations with department isolation. + +```sql +-- Step 1: Create department-specific roles +SELECT public.c77_rbac_grant_feature('dept_admin', 'manage_dept_users'); +SELECT public.c77_rbac_grant_feature('dept_admin', 'view_dept_data'); +SELECT public.c77_rbac_grant_feature('dept_admin', 'edit_dept_data'); + +SELECT public.c77_rbac_grant_feature('dept_member', 'view_dept_data'); +SELECT public.c77_rbac_grant_feature('dept_member', 'create_requests'); + +-- Step 2: Assign users to departments +SELECT public.c77_rbac_assign_subject('101', 'dept_admin', 'department', 'engineering'); +SELECT public.c77_rbac_assign_subject('102', 'dept_member', 'department', 'engineering'); +SELECT public.c77_rbac_assign_subject('201', 'dept_admin', 'department', 'marketing'); + +-- Step 3: Apply department-based policies +SELECT public.c77_rbac_apply_policy( + 'projects', 'view_dept_data', 'department', 'owning_department' +); + +SELECT public.c77_rbac_apply_policy( + 'expenses', 'view_dept_data', 'department', 'department_code' +); + +-- Step 4: Test department isolation +SET "c77_rbac.external_id" TO '102'; -- Engineering member +SELECT * FROM projects; -- Only engineering projects +SELECT * FROM expenses; -- Only engineering expenses +``` + +### Pattern 3: Multi-Tenant Applications + +Perfect for SaaS applications serving multiple clients. + +```sql +-- Step 1: Define tenant-scoped roles +SELECT public.c77_rbac_grant_feature('tenant_admin', 'manage_tenant_users'); +SELECT public.c77_rbac_grant_feature('tenant_admin', 'view_tenant_data'); +SELECT public.c77_rbac_grant_feature('tenant_admin', 'configure_tenant'); + +SELECT public.c77_rbac_grant_feature('tenant_user', 'view_tenant_data'); +SELECT public.c77_rbac_grant_feature('tenant_user', 'edit_own_data'); + +-- Step 2: Assign users to tenants +SELECT public.c77_rbac_assign_subject('1001', 'tenant_admin', 'tenant', 'company_a'); +SELECT public.c77_rbac_assign_subject('1002', 'tenant_user', 'tenant', 'company_a'); +SELECT public.c77_rbac_assign_subject('2001', 'tenant_admin', 'tenant', 'company_b'); + +-- Step 3: Apply tenant isolation policies +SELECT public.c77_rbac_apply_policy( + 'accounts', 'view_tenant_data', 'tenant', 'tenant_id' +); + +SELECT public.c77_rbac_apply_policy( + 'invoices', 'view_tenant_data', 'tenant', 'tenant_id' +); + +SELECT public.c77_rbac_apply_policy( + 'users', 'view_tenant_data', 'tenant', 'tenant_id' +); + +-- Step 4: Perfect tenant isolation +SET "c77_rbac.external_id" TO '1002'; -- Company A user +SELECT * FROM accounts; -- Only Company A accounts +SELECT * FROM invoices; -- Only Company A invoices +``` + +### Pattern 4: Hierarchical Organizations + +For complex organizational structures with multiple levels. + +```sql +-- Step 1: Define hierarchical roles +-- Global level +SELECT public.c77_rbac_grant_feature('global_admin', 'manage_everything'); +SELECT public.c77_rbac_grant_feature('global_admin', 'view_all_regions'); + +-- Regional level +SELECT public.c77_rbac_grant_feature('regional_manager', 'manage_region'); +SELECT public.c77_rbac_grant_feature('regional_manager', 'view_region_data'); + +-- Branch level +SELECT public.c77_rbac_grant_feature('branch_manager', 'manage_branch'); +SELECT public.c77_rbac_grant_feature('branch_manager', 'view_branch_data'); + +-- Store level +SELECT public.c77_rbac_grant_feature('store_manager', 'manage_store'); +SELECT public.c77_rbac_grant_feature('store_manager', 'view_store_data'); + +-- Step 2: Assign users to hierarchy levels +SELECT public.c77_rbac_assign_subject('1', 'global_admin', 'global', 'all'); +SELECT public.c77_rbac_assign_subject('101', 'regional_manager', 'region', 'west_coast'); +SELECT public.c77_rbac_assign_subject('201', 'branch_manager', 'branch', 'west_coast_sf'); +SELECT public.c77_rbac_assign_subject('301', 'store_manager', 'store', 'sf_downtown'); + +-- Step 3: Apply hierarchical policies +SELECT public.c77_rbac_apply_policy( + 'sales_data', 'view_store_data', 'store', 'store_code' +); + +SELECT public.c77_rbac_apply_policy( + 'inventory', 'view_branch_data', 'branch', 'branch_code' +); + +-- Step 4: Each level sees appropriate data +SET "c77_rbac.external_id" TO '301'; -- Store manager +SELECT * FROM sales_data; -- Only their store's data +``` + +--- + +**Continue to [Part 2: Advanced Usage Scenarios](USAGE-Part2.md)** for time-based permissions, conditional permissions, dynamic role assignment, and bulk operations. \ No newline at end of file diff --git a/USAGE-P2.md b/USAGE-P2.md new file mode 100644 index 0000000..f05d2c7 --- /dev/null +++ b/USAGE-P2.md @@ -0,0 +1,406 @@ +# c77_rbac Usage Guide - Part 2: Advanced Usage Scenarios + +This is Part 2 of the comprehensive c77_rbac usage guide, covering advanced scenarios and time-based permissions. + +**Complete Guide Structure:** +- Part 1: Core Concepts and Basic Usage +- **Part 2: Advanced Usage Scenarios** (this document) +- Part 3: Framework Integration (Laravel, Django, Rails) +- Part 4: Real-World Examples and Performance Optimization +- Part 5: Security Best Practices and Troubleshooting + +## Table of Contents + +1. [Advanced Usage Scenarios](#advanced-usage-scenarios) +2. [Bulk Operations for Large Organizations](#bulk-operations-for-large-organizations) + +## Advanced Usage Scenarios + +### Scenario 1: Time-Based Permissions + +Implementing temporary access that expires automatically. + +```sql +-- Step 1: Create temporary access tracking table +CREATE TABLE temporary_access ( + id SERIAL PRIMARY KEY, + user_id TEXT NOT NULL, + role_name TEXT NOT NULL, + scope_type TEXT NOT NULL, + scope_id TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Step 2: Grant temporary access +CREATE OR REPLACE FUNCTION grant_temporary_access( + p_user_id TEXT, + p_role TEXT, + p_scope_type TEXT, + p_scope_id TEXT, + p_duration_hours INTEGER +) RETURNS INTEGER AS $$ +DECLARE + temp_id INTEGER; +BEGIN + -- Record the temporary access + INSERT INTO temporary_access (user_id, role_name, scope_type, scope_id, expires_at) + VALUES (p_user_id, p_role, p_scope_type, p_scope_id, + CURRENT_TIMESTAMP + (p_duration_hours || ' hours')::INTERVAL) + RETURNING id INTO temp_id; + + -- Grant the role + PERFORM public.c77_rbac_assign_subject(p_user_id, p_role, p_scope_type, p_scope_id); + + RETURN temp_id; +END; +$$ LANGUAGE plpgsql; + +-- Step 3: Cleanup expired access (run via cron job) +CREATE OR REPLACE FUNCTION cleanup_expired_access() RETURNS INTEGER AS $$ +DECLARE + expired_record RECORD; + cleanup_count INTEGER := 0; +BEGIN + FOR expired_record IN + SELECT * FROM temporary_access WHERE expires_at < CURRENT_TIMESTAMP + LOOP + -- Remove the role assignment + PERFORM public.c77_rbac_revoke_subject_role( + expired_record.user_id, + expired_record.role_name, + expired_record.scope_type, + expired_record.scope_id + ); + + -- Remove tracking record + DELETE FROM temporary_access WHERE id = expired_record.id; + + cleanup_count := cleanup_count + 1; + END LOOP; + + RETURN cleanup_count; +END; +$$ LANGUAGE plpgsql; + +-- Usage example: +-- Grant user 'temp_contractor' admin access to project 'urgent_fix' for 24 hours +SELECT grant_temporary_access('temp_contractor', 'project_admin', 'project', 'urgent_fix', 24); +``` + +### Scenario 2: Conditional Permissions + +Permissions that depend on data state or business rules. + +```sql +-- Step 1: Create business rule function +CREATE OR REPLACE FUNCTION can_edit_document( + p_user_id TEXT, + p_document_id INTEGER +) RETURNS BOOLEAN AS $$ +DECLARE + doc_status TEXT; + doc_author TEXT; + user_dept TEXT; + doc_dept TEXT; +BEGIN + -- Get document info + SELECT status, author_id, department_id + INTO doc_status, doc_author, doc_dept + FROM documents WHERE id = p_document_id; + + -- Authors can always edit their draft documents + IF doc_author = p_user_id AND doc_status = 'draft' THEN + RETURN TRUE; + END IF; + + -- Department editors can edit department documents + IF public.c77_rbac_can_access('edit_dept_documents', p_user_id, 'department', doc_dept) THEN + RETURN TRUE; + END IF; + + -- Published documents can only be edited by administrators + IF doc_status = 'published' THEN + RETURN public.c77_rbac_can_access('edit_published_documents', p_user_id, 'global', 'all'); + END IF; + + RETURN FALSE; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Step 2: Create conditional RLS policy +CREATE POLICY documents_conditional_edit ON documents + FOR UPDATE + USING (can_edit_document(current_setting('c77_rbac.external_id', true)::TEXT, id)); + +-- Step 3: Enable RLS +ALTER TABLE documents ENABLE ROW LEVEL SECURITY; +``` + +### Scenario 3: Dynamic Role Assignment + +Automatically assign roles based on user attributes or actions. + +```sql +-- Step 1: Create dynamic role assignment function +CREATE OR REPLACE FUNCTION assign_roles_on_user_update() +RETURNS TRIGGER AS $$ +BEGIN + -- Remove old role assignments + PERFORM public.c77_rbac_revoke_subject_role( + OLD.id::TEXT, 'dept_member', 'department', OLD.department_id + ); + + -- Assign new department role + PERFORM public.c77_rbac_assign_subject( + NEW.id::TEXT, 'dept_member', 'department', NEW.department_id + ); + + -- Auto-promote to manager if they manage others + IF EXISTS (SELECT 1 FROM users WHERE manager_id = NEW.id) THEN + PERFORM public.c77_rbac_assign_subject( + NEW.id::TEXT, 'manager', 'department', NEW.department_id + ); + END IF; + + -- Auto-assign admin role if they're in IT department + IF NEW.department_id = 'IT' THEN + PERFORM public.c77_rbac_assign_subject( + NEW.id::TEXT, 'it_admin', 'global', 'all' + ); + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Step 2: Create trigger +CREATE TRIGGER user_role_sync + AFTER INSERT OR UPDATE ON users + FOR EACH ROW EXECUTE FUNCTION assign_roles_on_user_update(); +``` + +### Scenario 4: Context-Aware Permissions + +Permissions that vary based on current system state or user context. + +```sql +-- Example: Users can only edit their own posts during business hours +CREATE OR REPLACE FUNCTION can_edit_post_with_context( + p_user_id TEXT, + p_post_id INTEGER +) RETURNS BOOLEAN AS $$ +DECLARE + post_author TEXT; + current_hour INTEGER; +BEGIN + -- Get post author + SELECT author_id INTO post_author FROM posts WHERE id = p_post_id; + + -- Admins can always edit + IF public.c77_rbac_can_access('admin_edit_posts', p_user_id, 'global', 'all') THEN + RETURN TRUE; + END IF; + + -- Check if it's the author + IF post_author != p_user_id THEN + RETURN FALSE; + END IF; + + -- Check business hours (9 AM to 5 PM) + current_hour := EXTRACT(HOUR FROM CURRENT_TIME); + IF current_hour < 9 OR current_hour >= 17 THEN + RETURN FALSE; -- Outside business hours + END IF; + + -- Check if user has regular edit permission + RETURN public.c77_rbac_can_access('edit_own_posts', p_user_id, 'global', 'all'); +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Apply conditional policy +CREATE POLICY posts_context_edit ON posts + FOR UPDATE + USING (can_edit_post_with_context(current_setting('c77_rbac.external_id', true)::TEXT, id)); +``` + +## Bulk Operations for Large Organizations + +### Efficiently Managing Thousands of Users + +```sql +-- Step 1: Bulk assign all employees to basic role +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + (SELECT array_agg(id::TEXT) FROM users WHERE employment_status = 'active'), + 'employee', + 'global', + 'all' +); + +-- Step 2: Bulk assign department-specific roles +DO $$ +DECLARE + dept_record RECORD; + user_ids TEXT[]; +BEGIN + FOR dept_record IN SELECT DISTINCT department_id FROM users + LOOP + -- Get all users in this department + SELECT array_agg(id::TEXT) INTO user_ids + FROM users WHERE department_id = dept_record.department_id; + + -- Bulk assign department role + PERFORM public.c77_rbac_bulk_assign_subjects( + user_ids, + 'dept_member', + 'department', + dept_record.department_id + ); + + RAISE NOTICE 'Assigned % users to department %', + array_length(user_ids, 1), dept_record.department_id; + END LOOP; +END $$; + +-- Step 3: Bulk promote managers +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + (SELECT array_agg(DISTINCT manager_id::TEXT) + FROM users WHERE manager_id IS NOT NULL), + 'manager', + 'global', + 'all' +); +``` + +### Batch Processing for Performance + +```sql +-- Process users in batches to avoid memory issues +CREATE OR REPLACE FUNCTION bulk_assign_by_batches( + p_role_name TEXT, + p_scope_type TEXT, + p_scope_id TEXT, + p_batch_size INTEGER DEFAULT 1000 +) RETURNS INTEGER AS $$ +DECLARE + total_users INTEGER; + processed_users INTEGER := 0; + batch_start INTEGER := 1; + batch_end INTEGER; + user_batch TEXT[]; +BEGIN + -- Get total count + SELECT count(*) INTO total_users FROM users WHERE employment_status = 'active'; + + WHILE processed_users < total_users LOOP + -- Calculate batch boundaries + batch_end := LEAST(batch_start + p_batch_size - 1, total_users); + + -- Get batch of user IDs + SELECT array_agg(id::TEXT) INTO user_batch + FROM ( + SELECT id FROM users + WHERE employment_status = 'active' + ORDER BY id + OFFSET (batch_start - 1) + LIMIT p_batch_size + ) batch_users; + + -- Process batch + PERFORM public.c77_rbac_bulk_assign_subjects( + user_batch, p_role_name, p_scope_type, p_scope_id + ); + + processed_users := processed_users + array_length(user_batch, 1); + batch_start := batch_end + 1; + + RAISE NOTICE 'Processed % of % users', processed_users, total_users; + END LOOP; + + RETURN processed_users; +END; +$$ LANGUAGE plpgsql; + +-- Usage: Process all users in batches of 500 +SELECT bulk_assign_by_batches('employee', 'global', 'all', 500); +``` + +### Bulk Role Transitions + +```sql +-- Example: Promote all senior employees to manager role +CREATE OR REPLACE FUNCTION promote_senior_employees() RETURNS INTEGER AS $$ +DECLARE + senior_employee_ids TEXT[]; + promotion_count INTEGER; +BEGIN + -- Find senior employees (example criteria) + SELECT array_agg(id::TEXT) INTO senior_employee_ids + FROM users + WHERE employment_status = 'active' + AND hire_date < CURRENT_DATE - INTERVAL '5 years' + AND salary > 75000 + AND NOT EXISTS ( + SELECT 1 FROM c77_rbac_subjects s + JOIN c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + JOIN c77_rbac_roles r ON sr.role_id = r.role_id + WHERE s.external_id = users.id::TEXT + AND r.name = 'manager' + ); + + IF senior_employee_ids IS NOT NULL THEN + -- Bulk promote to manager + SELECT count(*) INTO promotion_count + FROM public.c77_rbac_bulk_assign_subjects( + senior_employee_ids, + 'manager', + 'department', + 'all' -- or specific department logic + ) WHERE success = true; + + RAISE NOTICE 'Promoted % senior employees to manager role', promotion_count; + RETURN promotion_count; + ELSE + RAISE NOTICE 'No senior employees found for promotion'; + RETURN 0; + END IF; +END; +$$ LANGUAGE plpgsql; +``` + +### Bulk Cleanup Operations + +```sql +-- Remove roles from inactive users +CREATE OR REPLACE FUNCTION cleanup_inactive_users() RETURNS INTEGER AS $$ +DECLARE + inactive_user_ids TEXT[]; + cleanup_count INTEGER := 0; + user_id TEXT; +BEGIN + -- Find inactive users + SELECT array_agg(id::TEXT) INTO inactive_user_ids + FROM users + WHERE employment_status = 'inactive' + OR last_login < CURRENT_DATE - INTERVAL '6 months'; + + -- Remove all role assignments for inactive users + FOREACH user_id IN ARRAY inactive_user_ids LOOP + DELETE FROM public.c77_rbac_subject_roles sr + WHERE sr.subject_id = ( + SELECT subject_id FROM public.c77_rbac_subjects + WHERE external_id = user_id + ); + + GET DIAGNOSTICS cleanup_count = cleanup_count + ROW_COUNT; + END LOOP; + + RAISE NOTICE 'Removed % role assignments from inactive users', cleanup_count; + RETURN cleanup_count; +END; +$$ LANGUAGE plpgsql; +``` + +--- + +**Continue to [Part 3: Framework Integration](USAGE-Part3.md)** for detailed Laravel, Django, and Rails integration examples. \ No newline at end of file diff --git a/USAGE-P3.md b/USAGE-P3.md new file mode 100644 index 0000000..e674a5c --- /dev/null +++ b/USAGE-P3.md @@ -0,0 +1,152 @@ +# c77_rbac Usage Guide - Part 3: Framework Integration + +This is Part 3 of the comprehensive c77_rbac usage guide, covering detailed integration with popular web frameworks. + +**Complete Guide Structure:** +- Part 1: Core Concepts and Basic Usage +- Part 2: Advanced Usage Scenarios +- **Part 3: Framework Integration** (this document) +- Part 4: Real-World Examples and Performance Optimization +- Part 5: Security Best Practices and Troubleshooting + +## Table of Contents + +1. [Laravel Integration](#laravel-integration) +2. [Django Integration](#django-integration) +3. [Ruby on Rails Integration](#ruby-on-rails-integration) +4. [Node.js Integration](#nodejs-integration) + +## Laravel Integration + +### Complete Laravel Setup + +#### 1. Service Provider + +```php +app->singleton(RbacService::class); + } + + public function boot() + { + // Register middleware + $this->app['router']->aliasMiddleware('rbac.context', + \App\Http\Middleware\SetRbacContext::class); + $this->app['router']->aliasMiddleware('rbac.require', + \App\Http\Middleware\RequirePermission::class); + + // Integrate with Laravel Gates + Gate::before(function ($user, $ability) { + // Check RBAC format: 'feature:scope_type/scope_id' + if (strpos($ability, ':') !== false) { + [$feature, $scope] = explode(':', $ability, 2); + if (strpos($scope, '/') !== false) { + [$scopeType, $scopeId] = explode('/', $scope, 2); + return app(RbacService::class)->can($feature, $scopeType, $scopeId); + } + } + + // Default to global scope + return app(RbacService::class)->can($ability, 'global', 'all'); + }); + + // Register Artisan commands + if ($this->app->runningInConsole()) { + $this->commands([ + \App\Console\Commands\RbacAssignRole::class, + \App\Console\Commands\RbacSyncAdmin::class, + \App\Console\Commands\RbacListPermissions::class, + ]); + } + } +} +``` + +#### 2. RBAC Service + +```php +allowed ?? false; + } catch (\Exception $e) { + Log::error('RBAC permission check failed', [ + 'feature' => $feature, + 'user_id' => $userId, + 'scope_type' => $scopeType, + 'scope_id' => $scopeId, + 'error' => $e->getMessage() + ]); + return false; + } + }); + } + + /** + * Assign role to user + */ + public function assignRole(int $userId, string $role, string $scopeType = 'global', string $scopeId = 'all'): bool + { + try { + DB::statement( + 'SELECT public.c77_rbac_assign_subject(?, ?, ?, ?)', + [(string)$userId, $role, $scopeType, $scopeId] + ); + + // Clear user's permission cache + $this->clearUserCache($userId); + + Log::info('Role assigned', [ + 'user_id' => $userId, + 'role' => $role, + 'scope_type' => $scopeType, + 'scope_id' => $scopeId + ]); + + return true; + } catch (\Exception $e) { + Log::error('Failed to assign role', [ + 'user_id' => $userId, + 'role' => $role, + \ No newline at end of file diff --git a/USAGE-P4.md b/USAGE-P4.md new file mode 100644 index 0000000..46b9182 --- /dev/null +++ b/USAGE-P4.md @@ -0,0 +1,1001 @@ +# c77_rbac Usage Guide - Part 4: Real-World Examples and Performance + +This is Part 4 of the comprehensive c77_rbac usage guide, covering complete real-world implementations and performance optimization. + +**Complete Guide Structure:** +- Part 1: Core Concepts and Basic Usage +- Part 2: Advanced Usage Scenarios +- Part 3: Framework Integration +- **Part 4: Real-World Examples and Performance** (this document) +- Part 5: Security Best Practices and Troubleshooting + +## Table of Contents + +1. [Real-World Example: Court Education System](#real-world-example-court-education-system) +2. [Real-World Example: Multi-Tenant SaaS Platform](#real-world-example-multi-tenant-saas-platform) +3. [Real-World Example: Enterprise Document Management](#real-world-example-enterprise-document-management) +4. [Performance Optimization](#performance-optimization) + +## Real-World Example: Court Education System + +### Complete Court System Implementation + +This example shows a full implementation for a court education system with participants, programs, and court staff. + +```sql +-- Step 1: Create the application tables +CREATE SCHEMA court_system; + +CREATE TABLE court_system.courts ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + jurisdiction TEXT NOT NULL, + address TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE court_system.programs ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, -- 'dui_education', 'anger_management', etc. + duration_weeks INTEGER NOT NULL, + court_id INTEGER REFERENCES court_system.courts(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE court_system.participants ( + id SERIAL PRIMARY KEY, + external_user_id TEXT NOT NULL, -- Links to your main user table + first_name TEXT NOT NULL, + last_name TEXT NOT NULL, + case_number TEXT, + assigned_court_id INTEGER REFERENCES court_system.courts(id), + enrollment_date DATE DEFAULT CURRENT_DATE, + status TEXT DEFAULT 'active', -- 'active', 'completed', 'terminated' + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE court_system.enrollments ( + id SERIAL PRIMARY KEY, + participant_id INTEGER REFERENCES court_system.participants(id), + program_id INTEGER REFERENCES court_system.programs(id), + enrolled_date DATE DEFAULT CURRENT_DATE, + completion_date DATE, + status TEXT DEFAULT 'enrolled', -- 'enrolled', 'in_progress', 'completed', 'dropped' + progress_percentage INTEGER DEFAULT 0, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE court_system.attendance ( + id SERIAL PRIMARY KEY, + enrollment_id INTEGER REFERENCES court_system.enrollments(id), + session_date DATE NOT NULL, + attended BOOLEAN DEFAULT false, + notes TEXT, + recorded_by TEXT, -- User ID who recorded attendance + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Step 2: Set up RBAC roles and features +-- Court Administration +SELECT public.c77_rbac_grant_feature('court_admin', 'manage_all_programs'); +SELECT public.c77_rbac_grant_feature('court_admin', 'view_all_participants'); +SELECT public.c77_rbac_grant_feature('court_admin', 'generate_court_reports'); +SELECT public.c77_rbac_grant_feature('court_admin', 'manage_court_users'); +SELECT public.c77_rbac_grant_feature('court_admin', 'export_court_data'); + +-- Judges +SELECT public.c77_rbac_grant_feature('judge', 'view_court_participants'); +SELECT public.c77_rbac_grant_feature('judge', 'approve_program_completions'); +SELECT public.c77_rbac_grant_feature('judge', 'access_court_reports'); +SELECT public.c77_rbac_grant_feature('judge', 'view_case_progress'); + +-- Court Clerks +SELECT public.c77_rbac_grant_feature('court_clerk', 'enroll_participants'); +SELECT public.c77_rbac_grant_feature('court_clerk', 'view_court_participants'); +SELECT public.c77_rbac_grant_feature('court_clerk', 'update_participant_info'); +SELECT public.c77_rbac_grant_feature('court_clerk', 'generate_basic_reports'); + +-- Program Coordinators +SELECT public.c77_rbac_grant_feature('program_coordinator', 'manage_program_type'); +SELECT public.c77_rbac_grant_feature('program_coordinator', 'view_program_participants'); +SELECT public.c77_rbac_grant_feature('program_coordinator', 'update_program_progress'); +SELECT public.c77_rbac_grant_feature('program_coordinator', 'generate_program_reports'); + +-- Counselors/Instructors +SELECT public.c77_rbac_grant_feature('counselor', 'view_assigned_participants'); +SELECT public.c77_rbac_grant_feature('counselor', 'update_participant_progress'); +SELECT public.c77_rbac_grant_feature('counselor', 'mark_attendance'); +SELECT public.c77_rbac_grant_feature('counselor', 'create_session_notes'); + +-- Participants +SELECT public.c77_rbac_grant_feature('participant', 'view_own_progress'); +SELECT public.c77_rbac_grant_feature('participant', 'access_program_materials'); +SELECT public.c77_rbac_grant_feature('participant', 'view_own_attendance'); + +-- Step 3: Sync admin features +SELECT public.c77_rbac_sync_admin_features(); + +-- Step 4: Apply RLS policies +-- Participants can only see their own data +SELECT public.c77_rbac_apply_policy( + 'court_system.participants', + 'view_own_progress', + 'participant', + 'external_user_id' +); + +-- Court staff can see participants in their court +SELECT public.c77_rbac_apply_policy( + 'court_system.participants', + 'view_court_participants', + 'court', + 'assigned_court_id' +); + +-- Program staff can see participants in their program type +SELECT public.c77_rbac_apply_policy( + 'court_system.enrollments', + 'view_program_participants', + 'program_type', + 'program_id' +); + +-- Attendance records follow enrollment permissions +SELECT public.c77_rbac_apply_policy( + 'court_system.attendance', + 'view_assigned_participants', + 'enrollment', + 'enrollment_id' +); + +-- Step 5: Assign initial users +-- System administrator +SELECT public.c77_rbac_assign_subject('1', 'court_admin', 'global', 'all'); + +-- Superior Court judge +SELECT public.c77_rbac_assign_subject('101', 'judge', 'court', '1'); + +-- Superior Court clerk +SELECT public.c77_rbac_assign_subject('201', 'court_clerk', 'court', '1'); + +-- DUI program coordinator +SELECT public.c77_rbac_assign_subject('301', 'program_coordinator', 'program_type', 'dui_education'); + +-- Anger management counselor +SELECT public.c77_rbac_assign_subject('401', 'counselor', 'program_type', 'anger_management'); + +-- Step 6: Bulk enroll participants in a program +-- First, let's create some test participants +INSERT INTO court_system.participants (external_user_id, first_name, last_name, case_number, assigned_court_id) +VALUES + ('P001', 'John', 'Doe', 'DUI-2024-001', 1), + ('P002', 'Jane', 'Smith', 'DUI-2024-002', 1), + ('P003', 'Mike', 'Johnson', 'DUI-2024-003', 1), + ('P004', 'Sarah', 'Williams', 'AM-2024-001', 1), + ('P005', 'Tom', 'Brown', 'AM-2024-002', 1); + +-- Bulk assign participant role to all participants +SELECT * FROM public.c77_rbac_bulk_assign_subjects( + ARRAY['P001', 'P002', 'P003', 'P004', 'P005'], + 'participant', + 'court', + '1' +); + +-- Step 7: Example queries showing RLS in action + +-- As a participant (P001), only sees own data +SET "c77_rbac.external_id" TO 'P001'; +SELECT first_name, last_name, case_number FROM court_system.participants; +-- Returns only John Doe's record + +-- As court clerk, sees all participants in their court +SET "c77_rbac.external_id" TO '201'; +SELECT first_name, last_name, case_number FROM court_system.participants; +-- Returns all participants in court 1 + +-- As global admin, sees everything +SET "c77_rbac.external_id" TO '1'; +SELECT first_name, last_name, case_number FROM court_system.participants; +-- Returns all participants + +-- Step 8: Advanced reporting with RBAC +CREATE VIEW court_system.participant_progress_report AS +SELECT + p.first_name, + p.last_name, + p.case_number, + pr.name as program_name, + e.status as enrollment_status, + e.progress_percentage, + e.enrolled_date, + e.completion_date, + COUNT(a.id) as total_sessions, + COUNT(CASE WHEN a.attended THEN 1 END) as attended_sessions +FROM court_system.participants p +JOIN court_system.enrollments e ON p.id = e.participant_id +JOIN court_system.programs pr ON e.program_id = pr.id +LEFT JOIN court_system.attendance a ON e.id = a.enrollment_id +GROUP BY p.id, pr.id, e.id; + +-- Apply RLS to the view +SELECT public.c77_rbac_apply_policy( + 'court_system.participant_progress_report', + 'view_court_participants', + 'court', + 'assigned_court_id' +); +``` + +### Laravel Integration for Court System + +```php +middleware('rbac.context'); + $this->rbac = $rbac; + } + + public function index(Request $request) + { + // Participants automatically filtered by RLS + $participants = DB::table('court_system.participants as p') + ->join('court_system.courts as c', 'p.assigned_court_id', '=', 'c.id') + ->select('p.*', 'c.name as court_name') + ->orderBy('p.created_at', 'desc') + ->paginate(20); + + return view('court-system.participants.index', compact('participants')); + } + + public function enroll(Request $request) + { + // Check if user can enroll participants + if (!$this->rbac->can('enroll_participants', 'court', $request->court_id)) { + abort(403, 'Cannot enroll participants in this court'); + } + + $validated = $request->validate([ + 'participants' => 'required|array', + 'participants.*' => 'required|string', + 'program_id' => 'required|integer|exists:court_system.programs,id', + 'court_id' => 'required|integer|exists:court_system.courts,id' + ]); + + // Bulk enroll participants + $results = $this->rbac->bulkAssignRoles( + $validated['participants'], + 'participant', + 'program', + $validated['program_id'] + ); + + $successful = collect($results)->where('success', true)->count(); + $failed = collect($results)->where('success', false)->count(); + + return response()->json([ + 'message' => "Enrolled {$successful} participants successfully, {$failed} failed", + 'results' => $results + ]); + } + + public function progress($participantId) + { + // Check permission to view this participant + $participant = DB::table('court_system.participants') + ->where('id', $participantId) + ->first(); + + if (!$participant) { + abort(404); + } + + // RLS will automatically filter this query + $progress = DB::table('court_system.participant_progress_report') + ->where('case_number', $participant->case_number) + ->get(); + + return view('court-system.participants.progress', compact('participant', 'progress')); + } +} +``` + +## Real-World Example: Multi-Tenant SaaS Platform + +### Complete SaaS Implementation + +```sql +-- Step 1: Create SaaS application schema +CREATE SCHEMA saas_app; + +CREATE TABLE saas_app.tenants ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + subdomain TEXT UNIQUE NOT NULL, + plan TEXT NOT NULL DEFAULT 'basic', -- 'basic', 'pro', 'enterprise' + max_users INTEGER DEFAULT 10, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE saas_app.projects ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + tenant_id INTEGER REFERENCES saas_app.tenants(id), + owner_id TEXT NOT NULL, -- External user ID + status TEXT DEFAULT 'active', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE saas_app.tasks ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + project_id INTEGER REFERENCES saas_app.projects(id), + assigned_to TEXT, -- External user ID + status TEXT DEFAULT 'todo', -- 'todo', 'in_progress', 'done' + priority TEXT DEFAULT 'medium', -- 'low', 'medium', 'high' + due_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE saas_app.comments ( + id SERIAL PRIMARY KEY, + task_id INTEGER REFERENCES saas_app.tasks(id), + author_id TEXT NOT NULL, -- External user ID + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Step 2: Define SaaS RBAC structure +-- Tenant-level roles +SELECT public.c77_rbac_grant_feature('tenant_owner', 'manage_tenant_settings'); +SELECT public.c77_rbac_grant_feature('tenant_owner', 'manage_tenant_users'); +SELECT public.c77_rbac_grant_feature('tenant_owner', 'view_tenant_billing'); +SELECT public.c77_rbac_grant_feature('tenant_owner', 'export_tenant_data'); + +SELECT public.c77_rbac_grant_feature('tenant_admin', 'manage_tenant_users'); +SELECT public.c77_rbac_grant_feature('tenant_admin', 'create_projects'); +SELECT public.c77_rbac_grant_feature('tenant_admin', 'view_all_projects'); + +-- Project-level roles +SELECT public.c77_rbac_grant_feature('project_manager', 'manage_project'); +SELECT public.c77_rbac_grant_feature('project_manager', 'assign_tasks'); +SELECT public.c77_rbac_grant_feature('project_manager', 'view_project_reports'); + +SELECT public.c77_rbac_grant_feature('project_member', 'view_project'); +SELECT public.c77_rbac_grant_feature('project_member', 'create_tasks'); +SELECT public.c77_rbac_grant_feature('project_member', 'update_own_tasks'); +SELECT public.c77_rbac_grant_feature('project_member', 'comment_on_tasks'); + +-- Step 3: Apply tenant isolation +SELECT public.c77_rbac_apply_policy( + 'saas_app.projects', + 'view_tenant_data', + 'tenant', + 'tenant_id' +); + +SELECT public.c77_rbac_apply_policy( + 'saas_app.tasks', + 'view_project', + 'project', + 'project_id' +); + +SELECT public.c77_rbac_apply_policy( + 'saas_app.comments', + 'comment_on_tasks', + 'project', + 'task_id' -- This would need a join to get project_id +); + +-- Step 4: Sample tenant setup +INSERT INTO saas_app.tenants (name, subdomain, plan, max_users) VALUES +('Acme Corp', 'acme', 'enterprise', 100), +('Startup Inc', 'startup', 'pro', 25), +('Small Biz', 'smallbiz', 'basic', 10); + +-- Assign tenant roles +SELECT public.c77_rbac_assign_subject('1001', 'tenant_owner', 'tenant', '1'); -- Acme Corp owner +SELECT public.c77_rbac_assign_subject('1002', 'tenant_admin', 'tenant', '1'); -- Acme Corp admin +SELECT public.c77_rbac_assign_subject('2001', 'tenant_owner', 'tenant', '2'); -- Startup Inc owner + +-- Project assignments +SELECT public.c77_rbac_assign_subject('1003', 'project_manager', 'project', '101'); +SELECT public.c77_rbac_assign_subject('1004', 'project_member', 'project', '101'); +SELECT public.c77_rbac_assign_subject('1005', 'project_member', 'project', '101'); + +-- Step 5: Complex access patterns +-- User 1004 (project member) can only see/edit their own tasks +CREATE POLICY tasks_own_tasks ON saas_app.tasks + FOR UPDATE + USING ( + assigned_to = current_setting('c77_rbac.external_id', true) OR + public.c77_rbac_can_access('assign_tasks', + current_setting('c77_rbac.external_id', true), + 'project', + project_id::text) + ); + +ALTER TABLE saas_app.tasks ENABLE ROW LEVEL SECURITY; +``` + +### Node.js SaaS Implementation + +```typescript +// src/services/tenant.service.ts + +import { RBACService } from './rbac.service'; +import { Pool } from 'pg'; + +export interface Tenant { + id: number; + name: string; + subdomain: string; + plan: string; + max_users: number; +} + +export interface Project { + id: number; + name: string; + tenant_id: number; + owner_id: string; + status: string; +} + +export class TenantService { + constructor( + private db: Pool, + private rbacService: RBACService + ) {} + + async createTenant(tenantData: Omit, ownerId: string): Promise { + const client = await this.db.connect(); + + try { + await client.query('BEGIN'); + + // Create tenant + const tenantResult = await client.query(` + INSERT INTO saas_app.tenants (name, subdomain, plan, max_users) + VALUES ($1, $2, $3, $4) + RETURNING * + `, [tenantData.name, tenantData.subdomain, tenantData.plan, tenantData.max_users]); + + const tenant = tenantResult.rows[0]; + + // Assign owner role + await this.rbacService.assignRole( + ownerId, + 'tenant_owner', + 'tenant', + tenant.id.toString() + ); + + await client.query('COMMIT'); + return tenant; + + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } + + async addUserToTenant( + tenantId: number, + userId: string, + role: string = 'project_member' + ): Promise { + try { + // Check if current user can manage tenant users + const canManage = await this.rbacService.canAccess( + 'manage_tenant_users', + userId, + 'tenant', + tenantId.toString() + ); + + if (!canManage) { + throw new Error('Permission denied: cannot manage tenant users'); + } + + // Check tenant user limits + const userCount = await this.db.query(` + SELECT COUNT(DISTINCT s.external_id) as count + FROM c77_rbac_subjects s + JOIN c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE sr.scope_type = 'tenant' AND sr.scope_id = $1 + `, [tenantId.toString()]); + + const tenant = await this.db.query( + 'SELECT max_users FROM saas_app.tenants WHERE id = $1', + [tenantId] + ); + + if (userCount.rows[0].count >= tenant.rows[0].max_users) { + throw new Error('Tenant user limit reached'); + } + + // Add user to tenant + return await this.rbacService.assignRole( + userId, + role, + 'tenant', + tenantId.toString() + ); + + } catch (error) { + console.error('Failed to add user to tenant:', error); + return false; + } + } + + async createProject( + tenantId: number, + projectData: Omit, + userId: string + ): Promise { + // Check if user can create projects in this tenant + const canCreate = await this.rbacService.canAccess( + 'create_projects', + userId, + 'tenant', + tenantId.toString() + ); + + if (!canCreate) { + throw new Error('Permission denied: cannot create projects in this tenant'); + } + + const client = await this.db.connect(); + + try { + await client.query('BEGIN'); + + // Create project + const projectResult = await client.query(` + INSERT INTO saas_app.projects (name, description, tenant_id, owner_id, status) + VALUES ($1, $2, $3, $4, $5) + RETURNING * + `, [projectData.name, projectData.description, tenantId, userId, projectData.status]); + + const project = projectResult.rows[0]; + + // Assign project manager role to creator + await this.rbacService.assignRole( + userId, + 'project_manager', + 'project', + project.id.toString() + ); + + await client.query('COMMIT'); + return project; + + } catch (error) { + await client.query('ROLLBACK'); + throw error; + } finally { + client.release(); + } + } + + async getUserTenants(userId: string): Promise { + const result = await this.db.query(` + SELECT DISTINCT t.* + FROM saas_app.tenants t + JOIN c77_rbac_subject_roles sr ON sr.scope_id = t.id::text + JOIN c77_rbac_subjects s ON s.subject_id = sr.subject_id + WHERE s.external_id = $1 AND sr.scope_type = 'tenant' + ORDER BY t.name + `, [userId]); + + return result.rows; + } + + async getTenantProjects(tenantId: number, userId: string): Promise { + // Set user context for RLS + await this.db.query('SET "c77_rbac.external_id" TO $1', [userId]); + + const result = await this.db.query(` + SELECT * FROM saas_app.projects + WHERE tenant_id = $1 + ORDER BY created_at DESC + `, [tenantId]); + + return result.rows; + } +} +``` + +## Real-World Example: Enterprise Document Management + +### Document Management System + +```sql +-- Step 1: Create document management schema +CREATE SCHEMA document_mgmt; + +CREATE TABLE document_mgmt.categories ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + department_id TEXT NOT NULL, + security_level TEXT DEFAULT 'internal', -- 'public', 'internal', 'confidential', 'secret' + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE document_mgmt.documents ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + filename TEXT NOT NULL, + file_path TEXT NOT NULL, + category_id INTEGER REFERENCES document_mgmt.categories(id), + author_id TEXT NOT NULL, + department_id TEXT NOT NULL, + security_level TEXT DEFAULT 'internal', + status TEXT DEFAULT 'draft', -- 'draft', 'review', 'approved', 'archived' + version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE document_mgmt.document_access_log ( + id SERIAL PRIMARY KEY, + document_id INTEGER REFERENCES document_mgmt.documents(id), + user_id TEXT NOT NULL, + action TEXT NOT NULL, -- 'view', 'download', 'edit', 'delete' + ip_address INET, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Step 2: Security-level based RBAC +SELECT public.c77_rbac_grant_feature('security_officer', 'access_secret_documents'); +SELECT public.c77_rbac_grant_feature('security_officer', 'manage_document_security'); +SELECT public.c77_rbac_grant_feature('security_officer', 'view_access_logs'); + +SELECT public.c77_rbac_grant_feature('dept_head', 'access_confidential_documents'); +SELECT public.c77_rbac_grant_feature('dept_head', 'approve_dept_documents'); +SELECT public.c77_rbac_grant_feature('dept_head', 'view_dept_access_logs'); + +SELECT public.c77_rbac_grant_feature('manager', 'access_internal_documents'); +SELECT public.c77_rbac_grant_feature('manager', 'create_documents'); +SELECT public.c77_rbac_grant_feature('manager', 'edit_dept_documents'); + +SELECT public.c77_rbac_grant_feature('employee', 'access_public_documents'); +SELECT public.c77_rbac_grant_feature('employee', 'view_own_documents'); + +-- Step 3: Complex RLS policy for security levels +CREATE OR REPLACE FUNCTION document_mgmt.can_access_document( + p_user_id TEXT, + p_document_id INTEGER +) RETURNS BOOLEAN AS $ +DECLARE + doc_security_level TEXT; + doc_department TEXT; + doc_author TEXT; +BEGIN + -- Get document details + SELECT security_level, department_id, author_id + INTO doc_security_level, doc_department, doc_author + FROM document_mgmt.documents + WHERE id = p_document_id; + + -- Authors can always access their own documents + IF doc_author = p_user_id THEN + RETURN TRUE; + END IF; + + -- Check security level permissions + CASE doc_security_level + WHEN 'public' THEN + RETURN public.c77_rbac_can_access('access_public_documents', p_user_id, 'global', 'all'); + WHEN 'internal' THEN + RETURN public.c77_rbac_can_access('access_internal_documents', p_user_id, 'department', doc_department); + WHEN 'confidential' THEN + RETURN public.c77_rbac_can_access('access_confidential_documents', p_user_id, 'department', doc_department); + WHEN 'secret' THEN + RETURN public.c77_rbac_can_access('access_secret_documents', p_user_id, 'global', 'all'); + ELSE + RETURN FALSE; + END CASE; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Apply the complex policy +CREATE POLICY document_security_policy ON document_mgmt.documents + FOR ALL + USING (document_mgmt.can_access_document( + current_setting('c77_rbac.external_id', true)::TEXT, + id + )); + +ALTER TABLE document_mgmt.documents ENABLE ROW LEVEL SECURITY; + +-- Step 4: Audit logging with RBAC +CREATE OR REPLACE FUNCTION document_mgmt.log_document_access() +RETURNS TRIGGER AS $ +BEGIN + INSERT INTO document_mgmt.document_access_log ( + document_id, + user_id, + action, + ip_address + ) VALUES ( + NEW.id, + current_setting('c77_rbac.external_id', true), + 'view', + inet_client_addr() + ); + RETURN NEW; +END; +$ LANGUAGE plpgsql; + +CREATE TRIGGER document_access_audit + AFTER SELECT ON document_mgmt.documents + FOR EACH ROW EXECUTE FUNCTION document_mgmt.log_document_access(); +``` + +## Performance Optimization + +### Query Performance Best Practices + +```sql +-- 1. Analyze slow queries +EXPLAIN (ANALYZE, BUFFERS) +SELECT * FROM posts WHERE department_id = 'engineering'; + +-- 2. Create appropriate indexes for RLS columns +CREATE INDEX CONCURRENTLY idx_posts_department_id ON posts(department_id); +CREATE INDEX CONCURRENTLY idx_users_tenant_id ON users(tenant_id); + +-- 3. Use partial indexes for common query patterns +CREATE INDEX CONCURRENTLY idx_active_participants +ON participants(assigned_court_id) +WHERE status = 'active'; + +-- 4. Composite indexes for multi-column RLS +CREATE INDEX CONCURRENTLY idx_tasks_project_assigned +ON tasks(project_id, assigned_to); + +-- 5. Function-based indexes for computed values +CREATE INDEX CONCURRENTLY idx_enrollments_completion_status +ON enrollments((CASE WHEN completion_date IS NOT NULL THEN 'completed' ELSE 'active' END)); +``` + +### Caching Strategies + +```sql +-- 1. Create materialized view for expensive permission calculations +CREATE MATERIALIZED VIEW user_effective_permissions AS +SELECT + s.external_id, + f.name as feature, + sr.scope_type, + sr.scope_id, + 'direct' as permission_source +FROM c77_rbac_subjects s +JOIN c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id +JOIN c77_rbac_role_features rf ON sr.role_id = rf.role_id +JOIN c77_rbac_features f ON rf.feature_id = f.feature_id + +UNION ALL + +-- Include inherited permissions for hierarchical structures +SELECT + s.external_id, + f.name as feature, + 'global' as scope_type, + 'all' as scope_id, + 'inherited' as permission_source +FROM c77_rbac_subjects s +JOIN c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id +JOIN c77_rbac_role_features rf ON sr.role_id = rf.role_id +JOIN c77_rbac_features f ON rf.feature_id = f.feature_id +WHERE sr.scope_type = 'global' AND sr.scope_id = 'all'; + +-- Index the materialized view +CREATE UNIQUE INDEX idx_user_permissions_unique +ON user_effective_permissions(external_id, feature, scope_type, scope_id); + +-- Refresh strategy (run via cron or trigger) +CREATE OR REPLACE FUNCTION refresh_user_permissions() +RETURNS void AS $ +BEGIN + REFRESH MATERIALIZED VIEW CONCURRENTLY user_effective_permissions; + PERFORM pg_notify('permissions_refreshed', 'materialized view updated'); +END; +$ LANGUAGE plpgsql; + +-- 2. Optimized permission check using materialized view +CREATE OR REPLACE FUNCTION public.c77_rbac_can_access_fast( + p_feature_name TEXT, + p_external_id TEXT, + p_scope_type TEXT, + p_scope_id TEXT +) RETURNS BOOLEAN AS $ +BEGIN + -- Fast lookup in materialized view + RETURN EXISTS ( + SELECT 1 FROM user_effective_permissions + WHERE external_id = p_external_id + AND feature = p_feature_name + AND ( + (scope_type = 'global' AND scope_id = 'all') OR + (scope_type = p_scope_type AND scope_id = p_scope_id) + ) + ); +END; +$ LANGUAGE plpgsql STABLE; +``` + +### Bulk Operations Performance + +```sql +-- 1. Efficient bulk role assignment with batch processing +CREATE OR REPLACE FUNCTION public.c77_rbac_bulk_assign_subjects_batched( + p_external_ids TEXT[], + p_role_name TEXT, + p_scope_type TEXT, + p_scope_id TEXT, + p_batch_size INTEGER DEFAULT 1000 +) RETURNS TABLE(external_id TEXT, success BOOLEAN, error_message TEXT) AS $ +DECLARE + v_batch_start INTEGER := 1; + v_batch_end INTEGER; + v_current_batch TEXT[]; + v_batch_result RECORD; +BEGIN + WHILE v_batch_start <= array_length(p_external_ids, 1) LOOP + v_batch_end := LEAST(v_batch_start + p_batch_size - 1, array_length(p_external_ids, 1)); + v_current_batch := p_external_ids[v_batch_start:v_batch_end]; + + -- Process current batch + FOR v_batch_result IN + SELECT * FROM public.c77_rbac_bulk_assign_subjects( + v_current_batch, p_role_name, p_scope_type, p_scope_id + ) + LOOP + external_id := v_batch_result.external_id; + success := v_batch_result.success; + error_message := v_batch_result.error_message; + RETURN NEXT; + END LOOP; + + v_batch_start := v_batch_end + 1; + + -- Optional: Add small delay between batches to reduce load + PERFORM pg_sleep(0.1); + END LOOP; + + RAISE NOTICE 'Completed batched assignment for % users in batches of %', + array_length(p_external_ids, 1), p_batch_size; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 2. Optimized bulk removal +CREATE OR REPLACE FUNCTION public.c77_rbac_bulk_revoke_subject_roles( + p_external_ids TEXT[], + p_role_name TEXT, + p_scope_type TEXT, + p_scope_id TEXT +) RETURNS INTEGER AS $ +DECLARE + v_removed_count INTEGER; +BEGIN + -- Single DELETE operation for better performance + DELETE FROM public.c77_rbac_subject_roles sr + WHERE sr.subject_id IN ( + SELECT subject_id FROM public.c77_rbac_subjects + WHERE external_id = ANY(p_external_ids) + ) + AND sr.role_id = ( + SELECT role_id FROM public.c77_rbac_roles + WHERE name = p_role_name + ) + AND sr.scope_type = p_scope_type + AND sr.scope_id = p_scope_id; + + GET DIAGNOSTICS v_removed_count = ROW_COUNT; + + RAISE NOTICE 'Bulk revoked % role assignments', v_removed_count; + RETURN v_removed_count; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +### Application-Level Caching + +```php +// Laravel Redis caching example +class OptimizedRbacService extends RbacService +{ + protected $redis; + protected $cachePrefix = 'rbac:'; + protected $cacheTtl = 300; // 5 minutes + + public function __construct() + { + parent::__construct(); + $this->redis = Redis::connection(); + } + + public function can(string $feature, string $scopeType = 'global', string $scopeId = 'all'): bool + { + if (!Auth::check()) { + return false; + } + + $userId = Auth::id(); + $cacheKey = $this->cachePrefix . "can:{$userId}:{$feature}:{$scopeType}:{$scopeId}"; + + // Try cache first + $cached = $this->redis->get($cacheKey); + if ($cached !== null) { + return (bool) $cached; + } + + // Fallback to parent implementation + $result = parent::can($feature, $scopeType, $scopeId); + + // Cache the result + $this->redis->setex($cacheKey, $this->cacheTtl, (int) $result); + + return $result; + } + + protected function clearUserCache(int $userId): void + { + parent::clearUserCache($userId); + + // Clear Redis cache patterns + $pattern = $this->cachePrefix . "*:{$userId}:*"; + $keys = $this->redis->keys($pattern); + + if (!empty($keys)) { + $this->redis->del($keys); + } + } + + public function warmupUserCache(int $userId, array $features = []): void + { + // Pre-populate common permission checks + $commonScopes = [ + ['global', 'all'], + ['department', $this->getUserDepartment($userId)], + ['tenant', $this->getUserTenant($userId)] + ]; + + foreach ($features as $feature) { + foreach ($commonScopes as [$scopeType, $scopeId]) { + if ($scopeId) { + $this->can($feature, $scopeType, $scopeId); + } + } + } + } +} +``` + +--- + +**Continue to [Part 5: Security Best Practices and Troubleshooting](USAGE-Part5.md)** for security guidelines, debugging techniques, and production considerations. \ No newline at end of file diff --git a/USAGE-P5.md b/USAGE-P5.md new file mode 100644 index 0000000..521ce96 --- /dev/null +++ b/USAGE-P5.md @@ -0,0 +1,990 @@ +# c77_rbac Usage Guide - Part 5: Security Best Practices and Troubleshooting + +This is the final part of the comprehensive c77_rbac usage guide, covering security best practices, troubleshooting, and production guidelines. + +**Complete Guide Structure:** +- Part 1: Core Concepts and Basic Usage +- Part 2: Advanced Usage Scenarios +- Part 3: Framework Integration +- Part 4: Real-World Examples and Performance +- **Part 5: Security Best Practices and Troubleshooting** (this document) + +## Table of Contents + +1. [Security Best Practices](#security-best-practices) +2. [Troubleshooting and Debugging](#troubleshooting-and-debugging) +3. [Production Guidelines](#production-guidelines) +4. [API Reference](#api-reference) + +## Security Best Practices + +### Input Validation and Sanitization + +```sql +-- 1. Enhanced input validation function +CREATE OR REPLACE FUNCTION public.validate_rbac_inputs( + p_external_id TEXT, + p_role_name TEXT DEFAULT NULL, + p_scope_type TEXT DEFAULT NULL, + p_scope_id TEXT DEFAULT NULL, + p_feature_name TEXT DEFAULT NULL +) RETURNS void AS $$ +BEGIN + -- External ID validation + IF p_external_id IS NULL OR length(trim(p_external_id)) = 0 THEN + RAISE EXCEPTION 'external_id cannot be NULL or empty' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + + IF length(p_external_id) > 255 THEN + RAISE EXCEPTION 'external_id too long (max 255 characters)' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + + -- Role name validation + IF p_role_name IS NOT NULL THEN + IF length(trim(p_role_name)) = 0 THEN + RAISE EXCEPTION 'role_name cannot be empty' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + + IF p_role_name !~ '^[a-zA-Z0-9_-]+$' THEN + RAISE EXCEPTION 'role_name contains invalid characters (use only letters, numbers, underscore, hyphen)' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + END IF; + + -- Scope type validation + IF p_scope_type IS NOT NULL THEN + IF length(trim(p_scope_type)) = 0 THEN + RAISE EXCEPTION 'scope_type cannot be empty' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + + IF p_scope_type !~ '^[a-zA-Z0-9_]+$' THEN + RAISE EXCEPTION 'scope_type contains invalid characters' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + END IF; + + -- Feature name validation + IF p_feature_name IS NOT NULL THEN + IF length(trim(p_feature_name)) = 0 THEN + RAISE EXCEPTION 'feature_name cannot be empty' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + + IF p_feature_name !~ '^[a-zA-Z0-9_]+$' THEN + RAISE EXCEPTION 'feature_name contains invalid characters' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + END IF; +END; +$$ LANGUAGE plpgsql; + +-- 2. SQL injection prevention in dynamic policies +CREATE OR REPLACE FUNCTION public.c77_rbac_apply_policy_secure( + 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_quoted_table TEXT; + v_quoted_column TEXT; +BEGIN + -- Validate all inputs + PERFORM public.validate_rbac_inputs( + 'dummy', NULL, p_scope_type, NULL, p_feature_name + ); + + -- Validate table name format + IF p_table_name !~ '^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$' THEN + RAISE EXCEPTION 'Invalid table name format' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + + -- Validate column name + IF p_scope_column !~ '^[a-zA-Z_][a-zA-Z0-9_]*$' THEN + RAISE EXCEPTION 'Invalid column name format' + USING ERRCODE = 'invalid_parameter_value'; + END IF; + + -- Parse and quote identifiers safely + 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); + v_quoted_table := quote_ident(v_schema_name) || '.' || quote_ident(v_table_name); + ELSE + v_quoted_table := quote_ident(p_table_name); + END IF; + + v_quoted_column := quote_ident(p_scope_column); + + -- Verify table and column exist + PERFORM public.c77_rbac_verify_table_column(p_table_name, p_scope_column); + + -- Create policy with proper quoting + EXECUTE format( + 'CREATE POLICY c77_rbac_policy ON %s FOR ALL TO PUBLIC USING ( + public.c77_rbac_can_access(%L, current_setting(%L, true), %L, %s::text) + )', + v_quoted_table, + p_feature_name, + 'c77_rbac.external_id', + p_scope_type, + v_quoted_column + ); + + -- Enable RLS + EXECUTE format('ALTER TABLE %s ENABLE ROW LEVEL SECURITY', v_quoted_table); + EXECUTE format('ALTER TABLE %s FORCE ROW LEVEL SECURITY', v_quoted_table); + + RAISE NOTICE 'Applied secure RLS policy to %', v_quoted_table; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +### Audit and Monitoring + +```sql +-- 1. Comprehensive audit logging +CREATE TABLE public.c77_rbac_audit_log ( + id BIGSERIAL PRIMARY KEY, + action TEXT NOT NULL, -- 'role_assigned', 'role_revoked', 'feature_granted', etc. + subject_external_id TEXT, + role_name TEXT, + feature_name TEXT, + scope_type TEXT, + scope_id TEXT, + performed_by TEXT, + ip_address INET, + user_agent TEXT, + session_id TEXT, + success BOOLEAN NOT NULL, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_rbac_audit_log_created_at ON public.c77_rbac_audit_log(created_at); +CREATE INDEX idx_rbac_audit_log_subject ON public.c77_rbac_audit_log(subject_external_id); +CREATE INDEX idx_rbac_audit_log_action ON public.c77_rbac_audit_log(action); + +-- 2. Audit logging function +CREATE OR REPLACE FUNCTION public.c77_rbac_log_action( + p_action TEXT, + p_subject_external_id TEXT DEFAULT NULL, + p_role_name TEXT DEFAULT NULL, + p_feature_name TEXT DEFAULT NULL, + p_scope_type TEXT DEFAULT NULL, + p_scope_id TEXT DEFAULT NULL, + p_success BOOLEAN DEFAULT TRUE, + p_error_message TEXT DEFAULT NULL +) RETURNS void AS $ +BEGIN + INSERT INTO public.c77_rbac_audit_log ( + action, + subject_external_id, + role_name, + feature_name, + scope_type, + scope_id, + performed_by, + ip_address, + success, + error_message + ) VALUES ( + p_action, + p_subject_external_id, + p_role_name, + p_feature_name, + p_scope_type, + p_scope_id, + current_setting('c77_rbac.external_id', true), + inet_client_addr(), + p_success, + p_error_message + ); +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 3. Enhanced functions with audit logging +CREATE OR REPLACE FUNCTION public.c77_rbac_assign_subject_audited( + p_external_id TEXT, + p_role_name TEXT, + p_scope_type TEXT, + p_scope_id TEXT +) RETURNS VOID AS $ +BEGIN + -- Perform the assignment + PERFORM public.c77_rbac_assign_subject(p_external_id, p_role_name, p_scope_type, p_scope_id); + + -- Log success + PERFORM public.c77_rbac_log_action( + 'role_assigned', + p_external_id, + p_role_name, + NULL, + p_scope_type, + p_scope_id, + TRUE + ); + +EXCEPTION WHEN OTHERS THEN + -- Log failure + PERFORM public.c77_rbac_log_action( + 'role_assigned', + p_external_id, + p_role_name, + NULL, + p_scope_type, + p_scope_id, + FALSE, + SQLERRM + ); + + -- Re-raise the exception + RAISE; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +### Session Management + +```sql +-- 1. Session context validation +CREATE OR REPLACE FUNCTION public.c77_rbac_validate_session_context() +RETURNS BOOLEAN AS $ +DECLARE + v_external_id TEXT; + v_session_start TIMESTAMP; +BEGIN + -- Get current external_id + v_external_id := current_setting('c77_rbac.external_id', true); + + -- Check if external_id is set + IF v_external_id IS NULL OR v_external_id = '' THEN + RETURN FALSE; + END IF; + + -- Verify the user exists in our system + IF NOT EXISTS (SELECT 1 FROM public.c77_rbac_subjects WHERE external_id = v_external_id) THEN + RAISE WARNING 'RBAC context set for non-existent user: %', v_external_id; + RETURN FALSE; + END IF; + + RETURN TRUE; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 2. Automatic session cleanup +CREATE OR REPLACE FUNCTION public.c77_rbac_cleanup_stale_sessions() +RETURNS INTEGER AS $ +DECLARE + v_cleanup_count INTEGER := 0; +BEGIN + -- In a real implementation, you might track active sessions + -- and clean up orphaned session variables + + -- For now, just log current sessions + RAISE NOTICE 'Current RBAC context: %', + current_setting('c77_rbac.external_id', true); + + RETURN v_cleanup_count; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +### Application-Level Security + +```php +// Laravel security middleware example + $userId]); + abort(429, 'Too many requests'); + } + + RateLimiter::hit($key, 60); // 100 requests per minute + + try { + // Validate user exists and is active + $user = Auth::user(); + if (!$user->is_active) { + Log::warning('Inactive user attempted RBAC context', ['user_id' => $userId]); + Auth::logout(); + abort(403, 'Account deactivated'); + } + + // Set RBAC context with validation + DB::statement('SET "c77_rbac.external_id" TO ?', [$userId]); + + // Verify context was set correctly + $setContext = DB::selectOne('SELECT current_setting(?, true) as context', + ['c77_rbac.external_id'])->context; + + if ($setContext !== (string)$userId) { + Log::error('RBAC context validation failed', [ + 'user_id' => $userId, + 'set_context' => $setContext + ]); + abort(500, 'Security context error'); + } + + } catch (\Exception $e) { + Log::error('Failed to set secure RBAC context', [ + 'user_id' => $userId, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + abort(500, 'Security initialization failed'); + } + + $response = $next($request); + + // Clean up context + try { + DB::statement('RESET "c77_rbac.external_id"'); + } catch (\Exception $e) { + Log::debug('Failed to cleanup RBAC context', ['error' => $e->getMessage()]); + } + + return $response; + } +} +``` + +## Troubleshooting and Debugging + +### Common Issues and Solutions + +#### Issue 1: RLS Policy Not Working + +```sql +-- Diagnostic queries for RLS issues + +-- 1. Check if RLS is enabled on table +SELECT schemaname, tablename, rowsecurity +FROM pg_tables +WHERE tablename = 'your_table_name'; + +-- 2. Check existing policies +SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual +FROM pg_policies +WHERE tablename = 'your_table_name'; + +-- 3. Verify current session context +SELECT current_setting('c77_rbac.external_id', true) as current_user_id; + +-- 4. Test permission directly +SELECT public.c77_rbac_can_access('your_feature', 'your_user_id', 'scope_type', 'scope_id'); + +-- 5. Check user roles +SELECT * FROM public.c77_rbac_get_user_roles('your_user_id'); + +-- 6. Debug query execution plan +EXPLAIN (ANALYZE, BUFFERS, VERBOSE) +SELECT * FROM your_table; +``` + +#### Issue 2: Performance Problems + +```sql +-- Performance diagnostic queries + +-- 1. Identify slow RBAC queries +SELECT + query, + mean_exec_time, + calls, + total_exec_time +FROM pg_stat_statements +WHERE query ILIKE '%c77_rbac%' +ORDER BY mean_exec_time DESC; + +-- 2. Check index usage +SELECT + schemaname, + tablename, + indexname, + idx_scan, + idx_tup_read, + idx_tup_fetch +FROM pg_stat_user_indexes +WHERE tablename LIKE 'c77_rbac_%' +ORDER BY idx_scan DESC; + +-- 3. Analyze table statistics +SELECT + schemaname, + tablename, + n_tup_ins, + n_tup_upd, + n_tup_del, + n_live_tup, + n_dead_tup +FROM pg_stat_user_tables +WHERE tablename LIKE 'c77_rbac_%'; + +-- 4. Check for missing indexes +SELECT + t.tablename, + c.column_name +FROM information_schema.tables t +JOIN information_schema.columns c ON t.table_name = c.table_name +WHERE t.table_schema = 'public' +AND t.table_name LIKE 'c77_rbac_%' +AND c.column_name IN ('external_id', 'role_id', 'subject_id', 'scope_type', 'scope_id') +AND NOT EXISTS ( + SELECT 1 FROM pg_indexes i + WHERE i.tablename = t.table_name + AND i.indexdef ILIKE '%' || c.column_name || '%' +); +``` + +#### Issue 3: Permission Denied Errors + +```sql +-- Debug permission issues + +-- 1. Check function permissions +SELECT + routine_name, + routine_type, + security_type, + definer_privileges +FROM information_schema.routines +WHERE routine_name LIKE 'c77_rbac_%'; + +-- 2. Verify user grants +SELECT + grantee, + privilege_type, + is_grantable +FROM information_schema.routine_privileges +WHERE routine_name LIKE 'c77_rbac_%' +AND grantee = 'your_app_user'; + +-- 3. Check table permissions +SELECT + table_name, + privilege_type, + is_grantable +FROM information_schema.table_privileges +WHERE table_name LIKE 'c77_rbac_%' +AND grantee = 'your_app_user'; + +-- 4. Comprehensive permission check +CREATE OR REPLACE FUNCTION debug_user_permissions(p_user_id TEXT) +RETURNS TABLE( + check_type TEXT, + check_result TEXT, + details TEXT +) AS $ +BEGIN + -- Check if user exists in RBAC + RETURN QUERY + SELECT + 'User Exists'::TEXT, + CASE WHEN EXISTS (SELECT 1 FROM public.c77_rbac_subjects WHERE external_id = p_user_id) + THEN 'YES' ELSE 'NO' END, + 'User record in c77_rbac_subjects'::TEXT; + + -- Check user roles + RETURN QUERY + SELECT + 'Role Count'::TEXT, + count(*)::TEXT, + 'Total roles assigned to user'::TEXT + FROM public.c77_rbac_subjects s + JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE s.external_id = p_user_id; + + -- Check features available + RETURN QUERY + SELECT + 'Feature Count'::TEXT, + count(DISTINCT f.name)::TEXT, + 'Total features available to user'::TEXT + 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_user_id; + + -- Check for admin access + RETURN QUERY + SELECT + 'Admin Access'::TEXT, + CASE WHEN EXISTS ( + SELECT 1 FROM public.c77_rbac_subjects s + JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE s.external_id = p_user_id + AND sr.scope_type = 'global' + AND sr.scope_id = 'all' + ) THEN 'YES' ELSE 'NO' END, + 'User has global admin access'::TEXT; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Usage: SELECT * FROM debug_user_permissions('your_user_id'); +``` + +#### Issue 4: Data Not Visible Despite Permissions + +```sql +-- Debug data visibility issues + +-- 1. Check RLS bypass for debugging (use with caution!) +CREATE OR REPLACE FUNCTION debug_rls_bypass( + p_table_name TEXT, + p_user_id TEXT +) RETURNS TABLE(total_rows BIGINT, visible_rows BIGINT) AS $ +DECLARE + v_total BIGINT; + v_visible BIGINT; +BEGIN + -- Count total rows (bypassing RLS) + EXECUTE format('SELECT count(*) FROM %I', p_table_name) INTO v_total; + + -- Set user context and count visible rows + PERFORM set_config('c77_rbac.external_id', p_user_id, true); + EXECUTE format('SELECT count(*) FROM %I', p_table_name) INTO v_visible; + + total_rows := v_total; + visible_rows := v_visible; + + RETURN NEXT; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 2. Analyze specific row access +CREATE OR REPLACE FUNCTION debug_row_access( + p_table_name TEXT, + p_row_id INTEGER, + p_user_id TEXT +) RETURNS TABLE(can_access BOOLEAN, reason TEXT) AS $ +DECLARE + v_policy_condition TEXT; + v_result BOOLEAN; +BEGIN + -- Get the policy condition for the table + SELECT qual INTO v_policy_condition + FROM pg_policies + WHERE tablename = p_table_name + AND policyname = 'c77_rbac_policy'; + + IF v_policy_condition IS NULL THEN + can_access := FALSE; + reason := 'No RBAC policy found on table'; + RETURN NEXT; + RETURN; + END IF; + + -- Set user context + PERFORM set_config('c77_rbac.external_id', p_user_id, true); + + -- Test access to specific row + EXECUTE format('SELECT EXISTS(SELECT 1 FROM %I WHERE id = %s)', p_table_name, p_row_id) + INTO v_result; + + can_access := v_result; + reason := CASE WHEN v_result THEN 'Access granted' ELSE 'Access denied by RLS policy' END; + + RETURN NEXT; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +### Debugging Tools and Utilities + +```sql +-- 1. RBAC health check function +CREATE OR REPLACE FUNCTION public.c77_rbac_health_check() +RETURNS TABLE( + check_name TEXT, + status TEXT, + message TEXT, + recommendation TEXT +) AS $ +DECLARE + v_orphaned_subjects INTEGER; + v_orphaned_roles INTEGER; + v_unused_features INTEGER; + v_policy_count INTEGER; +BEGIN + -- Check for orphaned subjects (users with no roles) + SELECT count(*) INTO v_orphaned_subjects + FROM public.c77_rbac_subjects s + LEFT JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE sr.subject_id IS NULL; + + RETURN QUERY SELECT + 'Orphaned Subjects'::TEXT, + CASE WHEN v_orphaned_subjects = 0 THEN 'OK' ELSE 'WARNING' END, + format('%s subjects without roles', v_orphaned_subjects), + CASE WHEN v_orphaned_subjects > 0 THEN 'Clean up unused subjects' ELSE 'None' END; + + -- Check for roles without features + SELECT count(*) INTO v_orphaned_roles + FROM public.c77_rbac_roles r + LEFT JOIN public.c77_rbac_role_features rf ON r.role_id = rf.role_id + WHERE rf.role_id IS NULL; + + RETURN QUERY SELECT + 'Roles Without Features'::TEXT, + CASE WHEN v_orphaned_roles = 0 THEN 'OK' ELSE 'WARNING' END, + format('%s roles without features', v_orphaned_roles), + CASE WHEN v_orphaned_roles > 0 THEN 'Assign features to roles or remove unused roles' ELSE 'None' END; + + -- Check for unused features + SELECT count(*) INTO v_unused_features + FROM public.c77_rbac_features f + LEFT JOIN public.c77_rbac_role_features rf ON f.feature_id = rf.feature_id + WHERE rf.feature_id IS NULL; + + RETURN QUERY SELECT + 'Unused Features'::TEXT, + CASE WHEN v_unused_features = 0 THEN 'OK' ELSE 'INFO' END, + format('%s features not assigned to any role', v_unused_features), + CASE WHEN v_unused_features > 0 THEN 'Consider removing unused features' ELSE 'None' END; + + -- Check active policies + SELECT count(*) INTO v_policy_count + FROM pg_policies WHERE policyname = 'c77_rbac_policy'; + + RETURN QUERY SELECT + 'Active Policies'::TEXT, + CASE WHEN v_policy_count > 0 THEN 'OK' ELSE 'WARNING' END, + format('%s tables protected by RBAC policies', v_policy_count), + CASE WHEN v_policy_count = 0 THEN 'Apply RLS policies to tables' ELSE 'None' END; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 2. Performance monitoring +CREATE OR REPLACE FUNCTION public.c77_rbac_performance_report() +RETURNS TABLE( + metric_name TEXT, + metric_value TEXT, + status TEXT +) AS $ +DECLARE + v_avg_permission_check_time FLOAT; + v_total_subjects INTEGER; + v_total_assignments INTEGER; +BEGIN + -- Get average permission check time (requires pg_stat_statements) + SELECT avg(mean_exec_time) INTO v_avg_permission_check_time + FROM pg_stat_statements + WHERE query ILIKE '%c77_rbac_can_access%'; + + RETURN QUERY SELECT + 'Avg Permission Check Time'::TEXT, + coalesce(v_avg_permission_check_time::TEXT || 'ms', 'No data'), + CASE + WHEN v_avg_permission_check_time IS NULL THEN 'UNKNOWN' + WHEN v_avg_permission_check_time < 10 THEN 'GOOD' + WHEN v_avg_permission_check_time < 50 THEN 'OK' + ELSE 'SLOW' + END; + + -- System size metrics + SELECT count(*) INTO v_total_subjects FROM public.c77_rbac_subjects; + SELECT count(*) INTO v_total_assignments FROM public.c77_rbac_subject_roles; + + RETURN QUERY SELECT + 'Total Subjects'::TEXT, + v_total_subjects::TEXT, + CASE + WHEN v_total_subjects < 1000 THEN 'SMALL' + WHEN v_total_subjects < 10000 THEN 'MEDIUM' + ELSE 'LARGE' + END; + + RETURN QUERY SELECT + 'Total Role Assignments'::TEXT, + v_total_assignments::TEXT, + CASE + WHEN v_total_assignments < 5000 THEN 'SMALL' + WHEN v_total_assignments < 50000 THEN 'MEDIUM' + ELSE 'LARGE' + END; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 3. Security audit +CREATE OR REPLACE FUNCTION public.c77_rbac_security_audit() +RETURNS TABLE( + audit_item TEXT, + finding TEXT, + severity TEXT, + recommendation TEXT +) AS $ +DECLARE + v_global_admins INTEGER; + v_recent_changes INTEGER; +BEGIN + -- Check for excessive global admins + SELECT count(DISTINCT s.external_id) INTO v_global_admins + FROM public.c77_rbac_subjects s + JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id + WHERE sr.scope_type = 'global' AND sr.scope_id = 'all'; + + RETURN QUERY SELECT + 'Global Admin Count'::TEXT, + format('%s users with global admin access', v_global_admins), + CASE + WHEN v_global_admins <= 2 THEN 'LOW' + WHEN v_global_admins <= 5 THEN 'MEDIUM' + ELSE 'HIGH' + END, + CASE + WHEN v_global_admins > 5 THEN 'Review global admin assignments' + ELSE 'Global admin count is reasonable' + END; + + -- Check for recent permission changes + SELECT count(*) INTO v_recent_changes + FROM public.c77_rbac_subject_roles + WHERE created_at > CURRENT_TIMESTAMP - INTERVAL '24 hours'; + + RETURN QUERY SELECT + 'Recent Permission Changes'::TEXT, + format('%s role assignments in last 24 hours', v_recent_changes), + CASE + WHEN v_recent_changes = 0 THEN 'LOW' + WHEN v_recent_changes <= 10 THEN 'MEDIUM' + ELSE 'HIGH' + END, + 'Monitor for unusual permission change patterns'; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +## Production Guidelines + +### Deployment Checklist + +```bash +#!/bin/bash +# production_deployment_checklist.sh + +echo "=== C77 RBAC Production Deployment Checklist ===" + +# 1. Pre-deployment verification +echo "1. Verifying prerequisites..." +pg_isready -h $DB_HOST -p $DB_PORT -U $DB_USER +if [ $? -ne 0 ]; then + echo "❌ Database not accessible" + exit 1 +fi + +echo "✅ Database accessible" + +# 2. Backup verification +echo "2. Creating pre-deployment backup..." +pg_dump -h $DB_HOST -U $DB_USER $DB_NAME > "backup_$(date +%Y%m%d_%H%M%S).sql" +if [ $? -eq 0 ]; then + echo "✅ Backup created successfully" +else + echo "❌ Backup failed" + exit 1 +fi + +# 3. Extension deployment +echo "3. Deploying RBAC extension..." +psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "CREATE EXTENSION IF NOT EXISTS c77_rbac;" +if [ $? -eq 0 ]; then + echo "✅ Extension deployed" +else + echo "❌ Extension deployment failed" + exit 1 +fi + +# 4. Health check +echo "4. Running health checks..." +psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT * FROM public.c77_rbac_health_check();" + +# 5. Performance baseline +echo "5. Establishing performance baseline..." +psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT * FROM public.c77_rbac_performance_report();" + +# 6. Security audit +echo "6. Running security audit..." +psql -h $DB_HOST -U $DB_USER -d $DB_NAME -c "SELECT * FROM public.c77_rbac_security_audit();" + +echo "=== Deployment checklist complete ===" +``` + +### Monitoring Setup + +```sql +-- 1. Create monitoring views +CREATE VIEW public.rbac_monitoring_dashboard AS +SELECT + 'System Health' as category, + check_name as metric, + status as value, + message as details +FROM public.c77_rbac_health_check() + +UNION ALL + +SELECT + 'Performance' as category, + metric_name as metric, + status as value, + metric_value as details +FROM public.c77_rbac_performance_report() + +UNION ALL + +SELECT + 'Security' as category, + audit_item as metric, + severity as value, + finding as details +FROM public.c77_rbac_security_audit(); + +-- 2. Create alerting function +CREATE OR REPLACE FUNCTION public.rbac_alert_check() +RETURNS TABLE(alert_level TEXT, alert_message TEXT) AS $ +BEGIN + -- Critical alerts + IF EXISTS (SELECT 1 FROM public.c77_rbac_health_check() WHERE status = 'ERROR') THEN + RETURN QUERY SELECT 'CRITICAL'::TEXT, 'RBAC system health check failed'::TEXT; + END IF; + + -- Warning alerts + IF EXISTS (SELECT 1 FROM public.c77_rbac_performance_report() WHERE status = 'SLOW') THEN + RETURN QUERY SELECT 'WARNING'::TEXT, 'RBAC performance degraded'::TEXT; + END IF; + + IF EXISTS (SELECT 1 FROM public.c77_rbac_security_audit() WHERE severity = 'HIGH') THEN + RETURN QUERY SELECT 'WARNING'::TEXT, 'RBAC security audit found high-risk items'::TEXT; + END IF; + + -- Info alerts + IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE policyname = 'c77_rbac_policy') THEN + RETURN QUERY SELECT 'INFO'::TEXT, 'No RBAC policies are currently active'::TEXT; + END IF; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; + +-- 3. Scheduled maintenance function +CREATE OR REPLACE FUNCTION public.rbac_maintenance() +RETURNS TEXT AS $ +DECLARE + v_cleanup_count INTEGER; + v_result TEXT := ''; +BEGIN + -- Refresh materialized views if they exist + BEGIN + REFRESH MATERIALIZED VIEW CONCURRENTLY user_effective_permissions; + v_result := v_result || 'Refreshed user permissions cache. '; + EXCEPTION WHEN OTHERS THEN + -- View might not exist, skip silently + END; + + -- Update table statistics + ANALYZE public.c77_rbac_subjects; + ANALYZE public.c77_rbac_subject_roles; + ANALYZE public.c77_rbac_role_features; + v_result := v_result || 'Updated table statistics. '; + + -- Log maintenance completion + INSERT INTO public.c77_rbac_audit_log (action, success, created_at) + VALUES ('maintenance_completed', TRUE, CURRENT_TIMESTAMP); + + v_result := v_result || 'Maintenance completed successfully.'; + RETURN v_result; +END; +$ LANGUAGE plpgsql SECURITY DEFINER; +``` + +## API Reference + +### Core Functions + +| Function | Parameters | Returns | Description | +|----------|------------|---------|-------------| +| `c77_rbac_assign_subject` | external_id, role_name, scope_type, scope_id | void | Assign role to user | +| `c77_rbac_revoke_subject_role` | external_id, role_name, scope_type, scope_id | boolean | Remove role from user | +| `c77_rbac_grant_feature` | role_name, feature_name | void | Grant feature to role | +| `c77_rbac_revoke_feature` | role_name, feature_name | boolean | Remove feature from role | +| `c77_rbac_can_access` | feature_name, external_id, scope_type, scope_id | boolean | Check user permission | +| `c77_rbac_apply_policy` | table_name, feature_name, scope_type, scope_column | void | Apply RLS policy | + +### Bulk Operations + +| Function | Parameters | Returns | Description | +|----------|------------|---------|-------------| +| `c77_rbac_bulk_assign_subjects` | external_ids[], role_name, scope_type, scope_id | table | Bulk assign roles | +| `c77_rbac_bulk_revoke_subject_roles` | external_ids[], role_name, scope_type, scope_id | integer | Bulk remove roles | + +### Management Functions + +| Function | Parameters | Returns | Description | +|----------|------------|---------|-------------| +| `c77_rbac_get_user_roles` | external_id | table | Get user's roles | +| `c77_rbac_get_role_features` | role_name | table | Get role's features | +| `c77_rbac_sync_admin_features` | none | integer | Sync features to admin role | +| `c77_rbac_sync_global_admin_features` | none | integer | Sync features to global admins | + +### Maintenance Functions + +| Function | Parameters | Returns | Description | +|----------|------------|---------|-------------| +| `c77_rbac_health_check` | none | table | System health report | +| `c77_rbac_performance_report` | none | table | Performance metrics | +| `c77_rbac_security_audit` | none | table | Security audit results | +| `c77_rbac_show_dependencies` | none | table | Show extension dependencies | +| `c77_rbac_remove_all_policies` | none | void | Remove all RLS policies | +| `c77_rbac_cleanup_for_removal` | remove_data | void | Prepare for uninstall | + +### Views + +| View | Description | +|------|-------------| +| `c77_rbac_user_permissions` | Complete user permission matrix | +| `c77_rbac_summary` | System overview statistics | +| `rbac_monitoring_dashboard` | Real-time monitoring data | + +--- + +## Conclusion + +This comprehensive 5-part usage guide covers everything needed to successfully implement and maintain c77_rbac in production environments. From basic concepts to advanced patterns, framework integration to troubleshooting, this guide ensures you can leverage the full power of database-level authorization for your applications. + +**Key Takeaways:** +- Database-level authorization provides consistent security across all application layers +- Row-Level Security automatically filters data without application code changes +- Proper monitoring and maintenance are essential for production deployments +- Security best practices must be followed throughout the implementation +- Performance optimization techniques ensure scalability for large user bases + +For additional support and updates, refer to the project documentation and community resources. \ No newline at end of file diff --git a/USAGE.md b/USAGE.md deleted file mode 100644 index fb2cc1b..0000000 --- a/USAGE.md +++ /dev/null @@ -1,1650 +0,0 @@ -# c77_rbac Comprehensive Usage Guide - -This guide explains every aspect of the c77_rbac extension in detail, with practical examples for Laravel developers. If you're new to database-level authorization, this guide will walk you through everything step by step. - -## Table of Contents - -1. [Understanding the Concepts](#understanding-the-concepts) -2. [Installation Walk-through](#installation-walk-through) -3. [Core Functions Explained](#core-functions-explained) -4. [Setting Up Your First RBAC System](#setting-up-your-first-rbac-system) -5. [Laravel Integration Guide](#laravel-integration-guide) -6. [Common Patterns and Examples](#common-patterns-and-examples) -7. [Advanced Usage](#advanced-usage) -8. [Troubleshooting Guide](#troubleshooting-guide) -9. [Performance Considerations](#performance-considerations) -10. [Security Best Practices](#security-best-practices) - -## Understanding the Concepts - -### What is c77_rbac? - -c77_rbac moves authorization from your application code to the database. Instead of writing `if` statements in Laravel to check permissions, the database automatically filters data based on user permissions. - -### Key Terms - -1. **Subject**: A user in your system - - Has an `external_id` (usually your Laravel User model's ID) - - Example: User with ID 123 in your Laravel app - -2. **Role**: A named set of permissions - - Examples: 'admin', 'manager', 'employee' - - Roles are just names - they get their power from features - -3. **Feature**: A specific permission - - Examples: 'view_reports', 'edit_users', 'delete_posts' - - These are what actually get checked by the database - -4. **Scope**: The context where a role applies - - Type + ID combination - - Examples: 'department/sales', 'region/north', 'global/all' - -5. **Policy**: A database rule that enforces security - - Automatically filters rows based on user permissions - - Invisible to your application code - -### How It Works - -``` -User (external_id: '123') - → has Role ('manager') - → with Scope ('department/sales') - → Role has Features ('view_reports', 'edit_reports') - → Policy checks these Features - → Database returns only allowed rows -``` - -## Installation Walk-through - -### Step 1: Database Administrator Tasks - -These steps require PostgreSQL superuser access: - -```sql --- 1. Connect as postgres superuser -sudo -u postgres psql - --- 2. Create your database -CREATE DATABASE myapp_db; - --- 3. Create application user -CREATE USER myapp_user WITH PASSWORD 'secure_password_here'; - --- 4. Connect to your database -\c myapp_db - --- 5. Install the extension -CREATE EXTENSION c77_rbac; - --- 6. Grant necessary privileges -GRANT CREATE ON DATABASE myapp_db TO myapp_user; -GRANT SELECT ON ALL TABLES IN SCHEMA public TO myapp_user; -GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO myapp_user; -GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO myapp_user; - --- 7. Set default privileges for future objects -ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT SELECT ON TABLES TO myapp_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT EXECUTE ON FUNCTIONS TO myapp_user; -ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT USAGE, SELECT ON SEQUENCES TO myapp_user; -``` - -### Step 2: Verify Installation - -Connect as your application user: - -```bash -psql -U myapp_user -d myapp_db -h localhost -``` - -Test the extension: - -```sql --- Should return empty results (no subjects yet) -SELECT * FROM public.c77_rbac_subjects; - --- Should return true -SELECT EXISTS ( - SELECT 1 FROM pg_extension WHERE extname = 'c77_rbac' -) as extension_installed; -``` - -## Core Functions Explained - -### 1. c77_rbac_grant_feature(role_name, feature_name) - -**Purpose**: Gives a permission to a role. - -**Parameters**: -- `role_name`: The role to grant the feature to (creates if doesn't exist) -- `feature_name`: The permission to grant (creates if doesn't exist) - -**Example**: -```sql --- Give 'editor' role the ability to view posts -SELECT public.c77_rbac_grant_feature('editor', 'view_posts'); - --- Give 'editor' role the ability to edit posts -SELECT public.c77_rbac_grant_feature('editor', 'edit_posts'); - --- Give 'admin' role all post-related features -SELECT public.c77_rbac_grant_feature('admin', 'view_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'edit_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'delete_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'publish_posts'); -``` - -**Important**: Admin roles need explicit grants for each feature! - -### 2. c77_rbac_assign_subject(external_id, role_name, scope_type, scope_id) - -**Purpose**: Assigns a role to a user with a specific scope. - -**Parameters**: -- `external_id`: Your application's user ID (as text) -- `role_name`: The role to assign -- `scope_type`: The type of scope (e.g., 'department', 'region', 'global') -- `scope_id`: The specific scope value (e.g., 'sales', 'north', 'all') - -**Examples**: -```sql --- User 123 is an editor in the marketing department -SELECT public.c77_rbac_assign_subject('123', 'editor', 'department', 'marketing'); - --- User 456 is a manager for the north region -SELECT public.c77_rbac_assign_subject('456', 'manager', 'region', 'north'); - --- User 1 is a global admin (can access everything) -SELECT public.c77_rbac_assign_subject('1', 'admin', 'global', 'all'); - --- User 789 is a viewer with no specific scope -SELECT public.c77_rbac_assign_subject('789', 'viewer', 'none', 'none'); -``` - -### 3. c77_rbac_apply_policy(table_name, feature_name, scope_type, scope_column) - -**Purpose**: Creates a Row-Level Security policy on a table. - -**Parameters**: -- `table_name`: The table to protect (can include schema: 'myschema.posts') -- `feature_name`: The required feature to access rows -- `scope_type`: The scope type to check -- `scope_column`: The column in the table containing the scope value - -**Examples**: -```sql --- Protect posts table by department -SELECT public.c77_rbac_apply_policy( - 'posts', -- table name - 'view_posts', -- required feature - 'department', -- scope type - 'department_id' -- column with department ID -); - --- Protect orders by region -SELECT public.c77_rbac_apply_policy( - 'sales.orders', -- schema-qualified table - 'view_orders', -- required feature - 'region', -- scope type - 'region_code' -- column with region code -); - --- Protect user profiles (users can only see their own) -SELECT public.c77_rbac_apply_policy( - 'user_profiles', -- table name - 'view_profile', -- required feature - 'user', -- scope type - 'user_id' -- column with user ID -); -``` - -### 4. c77_rbac_can_access(feature_name, external_id, scope_type, scope_id) - -**Purpose**: Checks if a user has access to a feature with a specific scope. - -**Parameters**: -- `feature_name`: The feature to check -- `external_id`: The user's ID -- `scope_type`: The scope type -- `scope_id`: The scope value - -**Returns**: Boolean (true/false) - -**Examples**: -```sql --- Can user 123 view posts in marketing department? -SELECT public.c77_rbac_can_access('view_posts', '123', 'department', 'marketing'); - --- Can user 456 edit orders in north region? -SELECT public.c77_rbac_can_access('edit_orders', '456', 'region', 'north'); - --- Can user 1 (admin) delete users globally? -SELECT public.c77_rbac_can_access('delete_users', '1', 'global', 'all'); -``` - -### 5. c77_rbac_sync_admin_features() - -**Purpose**: Automatically grants all existing features to the 'admin' role. - -**Usage**: -```sql --- After adding new features, sync them to admin -SELECT public.c77_rbac_sync_admin_features(); -``` - -**When to use**: -- After adding new features to your system -- During initial setup for admin roles -- As part of deployment scripts - -### 6. c77_rbac_sync_global_admin_features() - -**Purpose**: Grants all features to any role that has 'global/all' scope. - -**Usage**: -```sql --- Sync all features to all global admin roles -SELECT public.c77_rbac_sync_global_admin_features(); -``` - -**When to use**: When you have multiple admin-type roles with global scope. - -## Setting Up Your First RBAC System - -Let's build a complete example for a blog application: - -### Step 1: Create Your Schema and Tables - -```sql --- Create application schema -CREATE SCHEMA blog; - --- Create tables -CREATE TABLE blog.posts ( - id SERIAL PRIMARY KEY, - title TEXT NOT NULL, - content TEXT, - author_id INTEGER NOT NULL, - department TEXT NOT NULL, - status TEXT DEFAULT 'draft', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE blog.comments ( - id SERIAL PRIMARY KEY, - post_id INTEGER REFERENCES blog.posts(id), - author_id INTEGER NOT NULL, - content TEXT NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Insert test data -INSERT INTO blog.posts (title, content, author_id, department, status) VALUES -('Marketing Strategy 2024', 'Content...', 100, 'marketing', 'published'), -('Engineering Best Practices', 'Content...', 200, 'engineering', 'published'), -('Sales Targets Q1', 'Content...', 300, 'sales', 'draft'), -('HR Policy Update', 'Content...', 400, 'hr', 'published'); -``` - -### Step 2: Define Your Permission Structure - -```sql --- Define features for different roles --- Viewer role: can only read published posts -SELECT public.c77_rbac_grant_feature('viewer', 'view_published_posts'); - --- Editor role: can view and edit posts in their department -SELECT public.c77_rbac_grant_feature('editor', 'view_posts'); -SELECT public.c77_rbac_grant_feature('editor', 'edit_posts'); -SELECT public.c77_rbac_grant_feature('editor', 'create_posts'); - --- Manager role: all editor permissions plus publishing -SELECT public.c77_rbac_grant_feature('manager', 'view_posts'); -SELECT public.c77_rbac_grant_feature('manager', 'edit_posts'); -SELECT public.c77_rbac_grant_feature('manager', 'create_posts'); -SELECT public.c77_rbac_grant_feature('manager', 'publish_posts'); -SELECT public.c77_rbac_grant_feature('manager', 'delete_posts'); - --- Admin role: everything -SELECT public.c77_rbac_grant_feature('admin', 'view_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'edit_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'create_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'publish_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'delete_posts'); -SELECT public.c77_rbac_grant_feature('admin', 'manage_users'); -``` - -### Step 3: Assign Users to Roles - -```sql --- Marketing team -SELECT public.c77_rbac_assign_subject('101', 'editor', 'department', 'marketing'); -SELECT public.c77_rbac_assign_subject('102', 'manager', 'department', 'marketing'); - --- Engineering team -SELECT public.c77_rbac_assign_subject('201', 'editor', 'department', 'engineering'); -SELECT public.c77_rbac_assign_subject('202', 'viewer', 'department', 'engineering'); - --- Admin user -SELECT public.c77_rbac_assign_subject('1', 'admin', 'global', 'all'); -``` - -### Step 4: Apply Security Policies - -```sql --- Editors and managers can only see posts in their department -SELECT public.c77_rbac_apply_policy( - 'blog.posts', - 'view_posts', - 'department', - 'department' -); - --- Anyone can see published posts (you'd implement this differently) --- For now, let's protect unpublished posts -``` - -### Step 5: Test the Security - -```sql --- Set user context and test -SET "c77_rbac.external_id" TO '101'; -- Marketing editor - --- Should only see marketing posts -SELECT * FROM blog.posts; - --- Switch to admin -SET "c77_rbac.external_id" TO '1'; - --- Should see all posts -SELECT * FROM blog.posts; -``` - -## Laravel Integration Guide - -### Step 1: Configure Database Connection - -In `.env`: -```ini -DB_CONNECTION=pgsql -DB_HOST=127.0.0.1 -DB_PORT=5432 -DB_DATABASE=myapp_db -DB_USERNAME=myapp_user -DB_PASSWORD=your_secure_password -``` - -### Step 2: Create Middleware - -Create `app/Http/Middleware/SetRbacContext.php`: - -```php - [ - // ... other middleware - \App\Http\Middleware\SetRbacContext::class, - ], - - 'api' => [ - // ... other middleware - \App\Http\Middleware\SetRbacContext::class, - ], -]; -``` - -### Step 3: Create RBAC Service - -Create `app/Services/RbacService.php`: - -```php -allowed ?? false; - }); - } - - /** - * Assign a role to a user - */ - public function assignRole(int $userId, string $role, string $scopeType, string $scopeId): void - { - DB::statement( - 'SELECT public.c77_rbac_assign_subject(?, ?, ?, ?)', - [(string) $userId, $role, $scopeType, $scopeId] - ); - - // Clear cache for this user - Cache::tags(["rbac:user:{$userId}"])->flush(); - } - - /** - * Grant a feature to a role - */ - public function grantFeature(string $role, string $feature): void - { - DB::statement( - 'SELECT public.c77_rbac_grant_feature(?, ?)', - [$role, $feature] - ); - } - - /** - * Apply RLS policy to a table - */ - public function applyPolicy(string $table, string $feature, string $scopeType, string $scopeColumn): void - { - DB::statement( - 'SELECT public.c77_rbac_apply_policy(?, ?, ?, ?)', - [$table, $feature, $scopeType, $scopeColumn] - ); - } - - /** - * Sync all features to admin roles - */ - public function syncAdminFeatures(): void - { - DB::statement('SELECT public.c77_rbac_sync_admin_features()'); - } - - /** - * Get all roles for a user - */ - public function getUserRoles(int $userId): array - { - return DB::select(' - SELECT r.name as role, sr.scope_type, sr.scope_id - 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 = ? - ', [(string) $userId]); - } - - /** - * Get all features for a role - */ - public function getRoleFeatures(string $role): array - { - return DB::select(' - SELECT f.name as feature - 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 = ? - ', [$role]); - } -} -``` - -### Step 4: Create Migrations - -Create base migration for RBAC setup: - -```php -rbac = new RbacService(); - } - - public function up() - { - // Define your application's roles and features - $roles = [ - 'admin' => [ - 'manage_users', 'view_all_data', 'edit_all_data', - 'delete_all_data', 'view_reports', 'export_data' - ], - 'manager' => [ - 'view_reports', 'edit_department_data', - 'approve_content', 'export_data' - ], - 'editor' => [ - 'create_content', 'edit_content', 'view_content' - ], - 'viewer' => [ - 'view_content', 'view_reports' - ] - ]; - - // Create roles and grant features - foreach ($roles as $role => $features) { - foreach ($features as $feature) { - $this->rbac->grantFeature($role, $feature); - } - } - - // Sync admin features - $this->rbac->syncAdminFeatures(); - } - - public function down() - { - // Note: This doesn't remove the roles/features - // You might want to add cleanup logic here - } -} -``` - -Migration for applying RLS to tables: - -```php -rbac = new RbacService(); - } - - public function up() - { - // Add department column if it doesn't exist - if (!Schema::hasColumn('posts', 'department_id')) { - Schema::table('posts', function (Blueprint $table) { - $table->unsignedInteger('department_id')->nullable(); - }); - } - - // Apply RLS policy - $this->rbac->applyPolicy( - 'posts', // table - 'view_content', // required feature - 'department', // scope type - 'department_id' // scope column - ); - } - - public function down() - { - // Remove RLS policy - DB::statement('DROP POLICY IF EXISTS c77_rbac_policy ON posts'); - DB::statement('ALTER TABLE posts DISABLE ROW LEVEL SECURITY'); - } -} -``` - -### Step 5: User Model Integration - -Add methods to your User model: - -```php -assignRole($this->id, $role, $scopeType, $scopeId); - } - - /** - * Check if user has permission - */ - public function can($feature, $scopeType = null, $scopeId = null): bool - { - // If using Laravel's built-in authorization - if (is_string($feature) && strpos($feature, ':') !== false) { - [$feature, $scope] = explode(':', $feature); - [$scopeType, $scopeId] = explode('/', $scope); - } - - return self::getRbacService()->can($feature, $scopeType ?? 'global', $scopeId ?? 'all'); - } - - /** - * Get all roles for this user - */ - public function getRoles(): array - { - return self::getRbacService()->getUserRoles($this->id); - } - - /** - * Check if user has a specific role - */ - public function hasRole(string $role, ?string $scopeType = null, ?string $scopeId = null): bool - { - $roles = $this->getRoles(); - - foreach ($roles as $userRole) { - if ($userRole->role === $role) { - if ($scopeType === null || $userRole->scope_type === $scopeType) { - if ($scopeId === null || $userRole->scope_id === $scopeId) { - return true; - } - } - } - } - - return false; - } - - /** - * Check if user is admin - */ - public function isAdmin(): bool - { - return $this->hasRole('admin', 'global', 'all'); - } -} -``` - -### Step 6: Controller Integration - -Example controller using RBAC: - -```php -rbac = $rbac; - } - - public function index() - { - // Posts are automatically filtered by RLS - $posts = Post::all(); - - return view('posts.index', compact('posts')); - } - - public function create() - { - // Check if user can create posts in their department - if (!auth()->user()->can('create_content:department/' . auth()->user()->department_id)) { - abort(403, 'Unauthorized'); - } - - return view('posts.create'); - } - - public function edit(Post $post) - { - // Check if user can edit this specific post - if (!$this->rbac->can('edit_content', 'department', $post->department_id)) { - abort(403, 'Unauthorized'); - } - - return view('posts.edit', compact('post')); - } - - public function destroy(Post $post) - { - // Only managers and admins can delete - if (!$this->rbac->can('delete_content', 'department', $post->department_id)) { - abort(403, 'Unauthorized'); - } - - $post->delete(); - - return redirect()->route('posts.index'); - } -} -``` - -### Step 7: Blade Templates - -Use in Blade templates: - -```blade -@if(auth()->user()->can('create_content:department/' . auth()->user()->department_id)) - Create Post -@endif - -@foreach($posts as $post) -
-

{{ $post->title }}

-

{{ $post->content }}

- - @if(auth()->user()->can('edit_content:department/' . $post->department_id)) - Edit - @endif - - @if(auth()->user()->can('delete_content:department/' . $post->department_id)) -
- @csrf - @method('DELETE') - -
- @endif -
-@endforeach -``` - -### Step 8: API Authentication - -For API routes, ensure the context is set: - -```php -middleware(function ($request, $next) { - if ($request->user()) { - DB::statement('SET "c77_rbac.external_id" TO ?', [$request->user()->id]); - } - - return $next($request); - }); - } -} -``` - -## Common Patterns and Examples - -### Pattern 1: Department-Based Access - -```php -// Migration -public function up() -{ - // Define department roles - $this->rbac->grantFeature('dept_manager', 'view_dept_data'); - $this->rbac->grantFeature('dept_manager', 'edit_dept_data'); - $this->rbac->grantFeature('dept_manager', 'approve_dept_requests'); - - $this->rbac->grantFeature('dept_member', 'view_dept_data'); - $this->rbac->grantFeature('dept_member', 'create_requests'); - - // Apply policy to department data - $this->rbac->applyPolicy( - 'department_data', - 'view_dept_data', - 'department', - 'dept_id' - ); -} - -// Assigning users -$user->assignRole('dept_manager', 'department', 'sales'); -$user->assignRole('dept_member', 'department', 'engineering'); -``` - -### Pattern 2: Multi-Tenant Application - -```php -// Migration for tenant isolation -public function up() -{ - // Define tenant roles - $this->rbac->grantFeature('tenant_admin', 'manage_tenant'); - $this->rbac->grantFeature('tenant_user', 'view_tenant_data'); - - // Apply policies to all tenant tables - $tables = ['accounts', 'projects', 'invoices', 'users']; - - foreach ($tables as $table) { - $this->rbac->applyPolicy( - "tenants.{$table}", - 'view_tenant_data', - 'tenant', - 'tenant_id' - ); - } -} - -// Middleware for tenant context -public function handle($request, Closure $next) -{ - $tenant = $request->user()->tenant; - - // Set tenant context - DB::statement('SET "app.current_tenant" TO ?', [$tenant->id]); - DB::statement('SET "c77_rbac.external_id" TO ?', [$request->user()->id]); - - return $next($request); -} -``` - -### Pattern 3: Hierarchical Permissions - -```php -// Regional hierarchy: Global > Region > Branch > Store -public function setupHierarchy() -{ - // Global admin sees everything - $this->rbac->grantFeature('global_admin', 'view_all_stores'); - $this->rbac->assignRole($userId, 'global_admin', 'global', 'all'); - - // Regional manager sees their region - $this->rbac->grantFeature('regional_manager', 'view_region_stores'); - $this->rbac->assignRole($userId, 'regional_manager', 'region', 'west'); - - // Branch manager sees their branch - $this->rbac->grantFeature('branch_manager', 'view_branch_stores'); - $this->rbac->assignRole($userId, 'branch_manager', 'branch', 'west-1'); - - // Store manager sees their store - $this->rbac->grantFeature('store_manager', 'view_store_data'); - $this->rbac->assignRole($userId, 'store_manager', 'store', 'west-1-a'); -} -``` - -### Pattern 4: Time-Based Permissions (Advanced) - -```php -// Create a scheduled job to manage temporary permissions -class ManageTemporaryPermissions extends Command -{ - public function handle() - { - // Remove expired temporary roles - DB::statement(" - DELETE FROM public.c77_rbac_subject_roles - WHERE (scope_type, scope_id) IN ( - SELECT 'temporary', id::text - FROM temporary_permissions - WHERE expires_at < NOW() - ) - "); - - // Add new temporary roles - $newTemp = TemporaryPermission::where('starts_at', '<=', now()) - ->where('applied', false) - ->get(); - - foreach ($newTemp as $temp) { - $this->rbac->assignRole( - $temp->user_id, - $temp->role, - 'temporary', - $temp->id - ); - - $temp->update(['applied' => true]); - } - } -} -``` - -## Advanced Usage - -### Custom Scopes - -You can create any scope type that makes sense for your application: - -```php -// Example: Project-based access -$this->rbac->assignRole($userId, 'project_lead', 'project', $projectId); -$this->rbac->assignRole($userId, 'developer', 'project', $projectId); - -// Example: Customer-based access for support -$this->rbac->assignRole($userId, 'support_agent', 'customer', $customerId); -$this->rbac->assignRole($userId, 'account_manager', 'customer_tier', 'platinum'); - -// Example: Geographic access -$this->rbac->assignRole($userId, 'country_manager', 'country', 'US'); -$this->rbac->assignRole($userId, 'city_coordinator', 'city', 'NYC'); -``` - -### Dynamic Policy Application - -Create a service to dynamically apply policies based on your schema: - -```php -class DynamicPolicyService -{ - protected RbacService $rbac; - - public function applyPoliciesForModel(string $modelClass) - { - $model = new $modelClass; - $table = $model->getTable(); - - // Get policy configuration for this model - $policies = config("rbac.policies.{$modelClass}", []); - - foreach ($policies as $policy) { - $this->rbac->applyPolicy( - $table, - $policy['feature'], - $policy['scope_type'], - $policy['scope_column'] - ); - } - } - - public function applyAllPolicies() - { - $models = config('rbac.protected_models', []); - - foreach ($models as $model) { - $this->applyPoliciesForModel($model); - } - } -} - -// Config file: config/rbac.php -return [ - 'protected_models' => [ - \App\Models\Post::class, - \App\Models\Document::class, - \App\Models\Invoice::class, - ], - - 'policies' => [ - \App\Models\Post::class => [ - [ - 'feature' => 'view_posts', - 'scope_type' => 'department', - 'scope_column' => 'department_id', - ], - ], - \App\Models\Invoice::class => [ - [ - 'feature' => 'view_invoices', - 'scope_type' => 'customer', - 'scope_column' => 'customer_id', - ], - [ - 'feature' => 'edit_invoices', - 'scope_type' => 'department', - 'scope_column' => 'created_by_dept', - ], - ], - ], -]; -``` - -### Complex Permission Checks - -For complex business rules that go beyond simple RBAC: - -```php -class AdvancedPermissionService -{ - protected RbacService $rbac; - - public function canApproveExpense(User $user, Expense $expense): bool - { - // Basic RBAC check - if (!$this->rbac->can('approve_expenses', 'department', $expense->department_id)) { - return false; - } - - // Additional business rules - if ($expense->amount > 10000 && !$user->hasRole('senior_manager')) { - return false; - } - - // Check spending limits - $monthlySpent = $this->getMonthlySpending($user->department_id); - if ($monthlySpent + $expense->amount > $user->department->budget_limit) { - return false; - } - - return true; - } - - public function canEditDocument(User $user, Document $document): bool - { - // Owner can always edit their draft documents - if ($document->author_id === $user->id && $document->status === 'draft') { - return true; - } - - // Editors can edit within their scope - if ($this->rbac->can('edit_documents', 'department', $document->department_id)) { - return true; - } - - // Admins can edit anything - if ($user->hasRole('admin', 'global', 'all')) { - return true; - } - - return false; - } -} -``` - -### Audit Logging - -Track all permission changes: - -```php -class RbacAuditService -{ - public function logRoleAssignment(int $userId, string $role, string $scopeType, string $scopeId, ?int $assignedBy = null) - { - DB::table('rbac_audit_log')->insert([ - 'action' => 'role_assigned', - 'user_id' => $userId, - 'details' => json_encode([ - 'role' => $role, - 'scope_type' => $scopeType, - 'scope_id' => $scopeId, - ]), - 'performed_by' => $assignedBy ?? auth()->id(), - 'created_at' => now(), - ]); - } - - public function logPermissionCheck(int $userId, string $feature, string $scopeType, string $scopeId, bool $granted) - { - // Only log denied permissions or critical features - if ($granted && !$this->isCriticalFeature($feature)) { - return; - } - - DB::table('rbac_audit_log')->insert([ - 'action' => 'permission_checked', - 'user_id' => $userId, - 'details' => json_encode([ - 'feature' => $feature, - 'scope_type' => $scopeType, - 'scope_id' => $scopeId, - 'granted' => $granted, - ]), - 'created_at' => now(), - ]); - } - - protected function isCriticalFeature(string $feature): bool - { - $critical = ['delete_users', 'manage_billing', 'export_all_data']; - return in_array($feature, $critical); - } -} -``` - -### Testing RBAC - -Create comprehensive tests: - -```php -class RbacTest extends TestCase -{ - protected RbacService $rbac; - - protected function setUp(): void - { - parent::setUp(); - $this->rbac = app(RbacService::class); - - // Clean up any existing test data - DB::statement("DELETE FROM public.c77_rbac_subjects WHERE external_id LIKE 'test_%'"); - } - - public function test_department_isolation() - { - // Create test users - $salesUser = User::factory()->create(['id' => 1001]); - $engineeringUser = User::factory()->create(['id' => 1002]); - - // Assign roles - $this->rbac->assignRole($salesUser->id, 'editor', 'department', 'sales'); - $this->rbac->assignRole($engineeringUser->id, 'editor', 'department', 'engineering'); - - // Create test posts - $salesPost = Post::factory()->create(['department_id' => 'sales']); - $engineeringPost = Post::factory()->create(['department_id' => 'engineering']); - - // Test as sales user - $this->actingAs($salesUser); - $visiblePosts = Post::all(); - - $this->assertTrue($visiblePosts->contains($salesPost)); - $this->assertFalse($visiblePosts->contains($engineeringPost)); - } - - public function test_admin_sees_everything() - { - $admin = User::factory()->create(['id' => 1003]); - $this->rbac->assignRole($admin->id, 'admin', 'global', 'all'); - $this->rbac->syncAdminFeatures(); - - // Create posts in different departments - $posts = [ - Post::factory()->create(['department_id' => 'sales']), - Post::factory()->create(['department_id' => 'engineering']), - Post::factory()->create(['department_id' => 'hr']), - ]; - - $this->actingAs($admin); - $visiblePosts = Post::all(); - - foreach ($posts as $post) { - $this->assertTrue($visiblePosts->contains($post)); - } - } - - public function test_permission_caching() - { - $user = User::factory()->create(); - $this->rbac->assignRole($user->id, 'editor', 'department', 'sales'); - - // First call should hit database - $startQueries = DB::getQueryLog(); - $canEdit = $this->rbac->can('edit_posts', 'department', 'sales'); - $endQueries = DB::getQueryLog(); - - $this->assertTrue(count($endQueries) > count($startQueries)); - - // Second call should use cache - $startQueries = DB::getQueryLog(); - $canEdit2 = $this->rbac->can('edit_posts', 'department', 'sales'); - $endQueries = DB::getQueryLog(); - - $this->assertEquals(count($startQueries), count($endQueries)); - $this->assertEquals($canEdit, $canEdit2); - } -} -``` - -## Troubleshooting Guide - -### Problem: No Data Returned - -**Symptoms**: Queries return empty results when data should be visible. - -**Diagnosis Steps**: - -1. Check if the session variable is set: -```sql -SELECT current_setting('c77_rbac.external_id', true); -``` - -2. Verify the user has the correct role: -```sql -SELECT s.external_id, r.name, sr.scope_type, sr.scope_id -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 = 'your_user_id'; -``` - -3. Check if the role has the required feature: -```sql -SELECT r.name as role, f.name as feature -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 = 'your_role'; -``` - -4. Verify the policy is applied correctly: -```sql -SELECT * FROM pg_policies -WHERE tablename = 'your_table'; -``` - -5. Test the access function directly: -```sql -SELECT public.c77_rbac_can_access('feature', 'user_id', 'scope_type', 'scope_id'); -``` - -**Common Solutions**: -- Ensure middleware is setting the external_id -- Check for typos in feature names -- Verify scope values match exactly -- Remember admin roles need explicit feature grants - -### Problem: Performance Issues - -**Symptoms**: Slow queries on tables with RLS policies. - -**Diagnosis**: -```sql -EXPLAIN ANALYZE SELECT * FROM your_table; -``` - -**Solutions**: - -1. Add indexes on scope columns: -```sql -CREATE INDEX idx_posts_department_id ON posts(department_id); -``` - -2. Use connection pooling carefully: -```php -// In database.php -'pgsql' => [ - 'sticky' => true, // Reuse connections - // ... other settings -], -``` - -3. Cache permission checks: -```php -public function can($feature, $scopeType, $scopeId): bool -{ - return Cache::tags(['rbac', "user:{$this->id}"])->remember( - "can:{$feature}:{$scopeType}:{$scopeId}", - 300, - fn() => $this->checkPermission($feature, $scopeType, $scopeId) - ); -} -``` - -### Problem: Middleware Not Working - -**Symptoms**: User context not set, permissions not enforced. - -**Diagnosis**: -```php -// Add to middleware -\Log::info('RBAC Middleware', [ - 'user_id' => Auth::id(), - 'external_id' => DB::selectOne("SELECT current_setting('c77_rbac.external_id', true) as id")->id -]); -``` - -**Solutions**: -1. Check middleware registration order -2. Ensure auth middleware runs before RBAC middleware -3. Handle API authentication separately - -### Problem: Admin Can't Access Data - -**Symptoms**: Admin users with global/all scope can't see data. - -**Solution**: -```sql --- Sync all features to admin -SELECT public.c77_rbac_sync_admin_features(); - --- Or manually grant specific features -SELECT public.c77_rbac_grant_feature('admin', 'specific_feature'); -``` - -## Performance Considerations - -### 1. Index Strategy - -Always index columns used in RLS policies: - -```sql --- For department-based access -CREATE INDEX idx_table_department ON table_name(department_id); - --- For user-based access -CREATE INDEX idx_table_user ON table_name(user_id); - --- For composite scopes -CREATE INDEX idx_table_composite ON table_name(department_id, status); -``` - -### 2. Query Optimization - -Use EXPLAIN ANALYZE to understand query performance: - -```sql --- Before applying RLS -EXPLAIN ANALYZE SELECT * FROM posts WHERE department_id = 'sales'; - --- After applying RLS -SET "c77_rbac.external_id" TO '123'; -EXPLAIN ANALYZE SELECT * FROM posts; -``` - -### 3. Caching Strategy - -Implement multi-level caching: - -```php -class CachedRbacService extends RbacService -{ - public function can(string $feature, string $scopeType, string $scopeId): bool - { - // L1: In-memory cache (request lifecycle) - static $cache = []; - $key = "{$feature}:{$scopeType}:{$scopeId}:" . Auth::id(); - - if (isset($cache[$key])) { - return $cache[$key]; - } - - // L2: Redis/Memcached cache - $result = Cache::remember("rbac:{$key}", 300, function () use ($feature, $scopeType, $scopeId) { - return parent::can($feature, $scopeType, $scopeId); - }); - - $cache[$key] = $result; - return $result; - } -} -``` - -### 4. Batch Operations - -For bulk operations, temporarily switch to a privileged user: - -```php -public function bulkImport(array $data) -{ - // Save current user - $currentUser = DB::selectOne("SELECT current_setting('c77_rbac.external_id', true) as id")->id; - - try { - // Switch to system user with full access - DB::statement('SET "c77_rbac.external_id" TO ?', ['system']); - - // Perform bulk operations - DB::transaction(function () use ($data) { - foreach ($data as $record) { - Model::create($record); - } - }); - } finally { - // Restore original user - DB::statement('SET "c77_rbac.external_id" TO ?', [$currentUser]); - } -} -``` - -## Security Best Practices - -### 1. Input Validation - -Always validate scope parameters: - -```php -public function assignRole(Request $request) -{ - $request->validate([ - 'user_id' => 'required|integer|exists:users,id', - 'role' => 'required|string|in:admin,manager,editor,viewer', - 'scope_type' => 'required|string|in:global,department,region,project', - 'scope_id' => 'required|string|max:50', - ]); - - // Sanitize scope_id to prevent SQL injection - $scopeId = preg_replace('/[^a-zA-Z0-9_-]/', '', $request->scope_id); - - $this->rbac->assignRole( - $request->user_id, - $request->role, - $request->scope_type, - $scopeId - ); -} -``` - -### 2. Principle of Least Privilege - -Start with minimal permissions: - -```php -// Default role for new users -public function registered(User $user) -{ - // Give only viewing permissions by default - $user->assignRole('viewer', 'department', $user->department_id); -} - -// Elevate privileges only when needed -public function promoteToEditor(User $user) -{ - // Remove viewer role first - $this->rbac->removeRole($user->id, 'viewer', 'department', $user->department_id); - - // Add editor role - $user->assignRole('editor', 'department', $user->department_id); -} -``` - -### 3. Session Security - -Clean up sessions properly: - -```php -class SessionCleanupMiddleware -{ - public function handle($request, Closure $next) - { - $response = $next($request); - - // Always reset on response - DB::statement('RESET "c77_rbac.external_id"'); - - return $response; - } - - public function terminate($request, $response) - { - // Extra cleanup for long-running processes - DB::statement('RESET ALL'); - } -} -``` - -### 4. Audit Critical Operations - -Log all permission changes and critical accesses: - -```php -class AuditableRbacService extends RbacService -{ - public function assignRole(int $userId, string $role, string $scopeType, string $scopeId): void - { - parent::assignRole($userId, $role, $scopeType, $scopeId); - - activity() - ->performedOn(User::find($userId)) - ->causedBy(auth()->user()) - ->withProperties([ - 'role' => $role, - 'scope_type' => $scopeType, - 'scope_id' => $scopeId, - ]) - ->log('Role assigned'); - } - - public function can(string $feature, string $scopeType, string $scopeId): bool - { - $result = parent::can($feature, $scopeType, $scopeId); - - // Log only critical features or denials - if (!$result || $this->isCriticalFeature($feature)) { - activity() - ->causedBy(auth()->user()) - ->withProperties([ - 'feature' => $feature, - 'scope_type' => $scopeType, - 'scope_id' => $scopeId, - 'granted' => $result, - ]) - ->log('Permission check'); - } - - return $result; - } -} -``` - -### 5. Regular Security Reviews - -Create a command to audit your RBAC setup: - -```php -class AuditRbacCommand extends Command -{ - protected $signature = 'rbac:audit'; - - public function handle() - { - $this->info('RBAC Security Audit'); - - // Check for users with multiple admin roles - $multiAdmins = DB::select(" - SELECT s.external_id, COUNT(*) as admin_roles - 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 r.name = 'admin' - GROUP BY s.external_id - HAVING COUNT(*) > 1 - "); - - if (count($multiAdmins) > 0) { - $this->warn('Users with multiple admin roles: ' . count($multiAdmins)); - } - - // Check for overly permissive policies - $policies = DB::select(" - SELECT schemaname, tablename, policyname, qual - FROM pg_policies - WHERE policyname = 'c77_rbac_policy' - "); - - foreach ($policies as $policy) { - if (strpos($policy->qual, 'true') !== false) { - $this->error("Potentially overly permissive policy on {$policy->schemaname}.{$policy->tablename}"); - } - } - - // Check for orphaned roles - $orphanedRoles = DB::select(" - SELECT r.name - FROM public.c77_rbac_roles r - LEFT JOIN public.c77_rbac_subject_roles sr ON r.role_id = sr.role_id - WHERE sr.role_id IS NULL - "); - - if (count($orphanedRoles) > 0) { - $this->warn('Orphaned roles (no users): ' . count($orphanedRoles)); - } - - $this->info('Audit complete'); - } -} -``` - -## Summary - -The c77_rbac extension provides a powerful, database-centric approach to authorization. Key takeaways: - -1. **Database-Level Security**: RLS policies ensure consistent access control -2. **Flexible Scoping**: Support for any type of organizational structure -3. **Framework Agnostic**: Works with any application framework -4. **Performance**: Optimized with indexes and caching strategies -5. **Maintainable**: Clear separation of concerns between features, roles, and policies - -Remember to: -- Always grant specific features to admin roles -- Use caching for permission checks -- Index columns used in policies -- Audit your RBAC setup regularly -- Clean up database sessions properly - -With proper setup and maintenance, c77_rbac provides enterprise-grade authorization that scales with your application. \ No newline at end of file diff --git a/c77_rbac PostgreSQL Extension - Technical Assessment Report.pdf b/c77_rbac PostgreSQL Extension - Technical Assessment Report.pdf new file mode 100644 index 0000000..56f37f8 Binary files /dev/null and b/c77_rbac PostgreSQL Extension - Technical Assessment Report.pdf differ diff --git a/c77_rbac--1.0--1.1.sql b/c77_rbac--1.0--1.1.sql new file mode 100644 index 0000000..7cde15c --- /dev/null +++ b/c77_rbac--1.0--1.1.sql @@ -0,0 +1,578 @@ +-- c77_rbac--1.0--1.1.sql: Upgrade script from version 1.0 to 1.1 +-- Adds bulk operations, removal functions, admin sync, and enhanced error handling +\echo Upgrading c77_rbac from 1.0 to 1.1... + +-- Add timestamps to existing tables +ALTER TABLE public.c77_rbac_subject_roles +ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; + +ALTER TABLE public.c77_rbac_role_features +ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; + +-- Add new performance indexes +CREATE INDEX IF NOT EXISTS idx_c77_rbac_subjects_external_id_hash +ON public.c77_rbac_subjects USING hash(external_id); + +CREATE INDEX IF NOT EXISTS idx_c77_rbac_subject_roles_composite +ON public.c77_rbac_subject_roles(subject_id, scope_type, scope_id); + +-- Replace existing functions with enhanced versions +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; + +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; + +-- Add new bulk assignment function +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; + +-- Add removal functions +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; + +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; + +-- Add admin sync functions +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; + +-- Add optimized performance function +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; + +-- Update the original function to use optimized version +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; + +-- Add utility functions +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; + +-- Enhance the apply_policy function with 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; + +-- Create helpful views +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; + +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 permissions for new functions +GRANT EXECUTE ON FUNCTION + 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_can_access_optimized(TEXT, TEXT, TEXT, TEXT), + public.c77_rbac_get_user_roles(TEXT), + public.c77_rbac_get_role_features(TEXT) + TO PUBLIC; + +-- Grant permissions for new views +GRANT SELECT ON + public.c77_rbac_user_permissions, + public.c77_rbac_summary + TO PUBLIC; + +\echo c77_rbac successfully upgraded to version 1.1 \ No newline at end of file diff --git a/c77_rbac--1.0.0.sql.backup b/c77_rbac--1.0.0.sql.backup deleted file mode 100644 index 2b3c640..0000000 --- a/c77_rbac--1.0.0.sql.backup +++ /dev/null @@ -1,144 +0,0 @@ --- /usr/share/postgresql/17/extension/c77_rbac--1.0.sql -\echo Use "CREATE EXTENSION c77_rbac" to load this file. \quit - -CREATE TABLE public.c77_rbac_subjects ( - subject_id BIGSERIAL PRIMARY KEY, - external_id TEXT UNIQUE NOT NULL, - scope_type TEXT, - scope_id TEXT -); - -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), - role_id BIGINT REFERENCES public.c77_rbac_roles(role_id), - scope_type TEXT, - scope_id TEXT, - 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), - feature_id BIGINT REFERENCES public.c77_rbac_features(feature_id), - PRIMARY KEY (role_id, feature_id) -); - -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 - INSERT INTO public.c77_rbac_subjects (external_id, scope_type, scope_id) - VALUES (p_external_id, p_scope_type, p_scope_id) - ON CONFLICT (external_id) DO UPDATE - SET scope_type = EXCLUDED.scope_type, - scope_id = EXCLUDED.scope_id - RETURNING subject_id INTO v_subject_id; - - INSERT INTO public.c77_rbac_roles (name) - VALUES (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 = p_role_name; - END IF; - - INSERT INTO public.c77_rbac_subject_roles (subject_id, role_id, scope_type, scope_id) - VALUES (v_subject_id, v_role_id, p_scope_type, p_scope_id) - ON CONFLICT (subject_id, role_id, scope_type, scope_id) DO NOTHING; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -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 - INSERT INTO public.c77_rbac_roles (name) - VALUES (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 = p_role_name; - END IF; - - INSERT INTO public.c77_rbac_features (name) - VALUES (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 = p_feature_name; - END IF; - - 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; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - -CREATE OR REPLACE FUNCTION public.c77_rbac_can_access( - p_feature_name TEXT, - p_external_id TEXT, - p_scope_type TEXT DEFAULT NULL, - p_scope_id TEXT DEFAULT NULL -) RETURNS BOOLEAN AS $$ -BEGIN - IF p_external_id IS NULL THEN - RAISE EXCEPTION 'p_external_id must be provided'; - END IF; - - -- Admin bypass - IF 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_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 - WHERE s.external_id = p_external_id - AND r.name = 'admin' - AND f.name = p_feature_name - ) THEN - RETURN TRUE; - END IF; - - RETURN EXISTS ( - SELECT 1 - FROM public.c77_rbac_subjects s - JOIN public.c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id - AND (p_scope_type IS NULL OR sr.scope_type = p_scope_type) - AND (p_scope_id IS NULL OR sr.scope_id = p_scope_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 - WHERE s.external_id = p_external_id - AND f.name = p_feature_name - ); -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; \ No newline at end of file diff --git a/c77_rbac--1.1.0.sql.backup b/c77_rbac--1.1.0.sql.backup deleted file mode 100644 index aebc524..0000000 --- a/c77_rbac--1.1.0.sql.backup +++ /dev/null @@ -1,204 +0,0 @@ --- c77_rbac--1.1.0.sql: PostgreSQL extension for role-based access control (RBAC) --- Requires PostgreSQL 14 or later --- All objects in public schema with c77_rbac_ prefix -\echo Use "CREATE EXTENSION c77_rbac" to load this file. \quit - --- Tables -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), - role_id BIGINT REFERENCES public.c77_rbac_roles(role_id), - scope_type TEXT NOT NULL, - scope_id TEXT, - 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), - feature_id BIGINT REFERENCES public.c77_rbac_features(feature_id), - PRIMARY KEY (role_id, feature_id) -); - --- Function: c77_rbac_assign_subject -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 - INSERT INTO public.c77_rbac_subjects (external_id) - VALUES (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 = p_external_id; - END IF; - - INSERT INTO public.c77_rbac_roles (name) - VALUES (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 = p_role_name; - END IF; - - INSERT INTO public.c77_rbac_subject_roles (subject_id, role_id, scope_type, scope_id) - VALUES (v_subject_id, v_role_id, p_scope_type, p_scope_id) - ON CONFLICT (subject_id, role_id, scope_type, scope_id) DO NOTHING; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Function: c77_rbac_grant_feature -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 - INSERT INTO public.c77_rbac_roles (name) - VALUES (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 = p_role_name; - END IF; - - INSERT INTO public.c77_rbac_features (name) - VALUES (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 = p_feature_name; - END IF; - - 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; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Function: c77_rbac_can_access -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 - IF p_external_id IS NULL THEN - RAISE EXCEPTION 'p_external_id must be provided'; - END IF; - - -- Admin bypass (global/all scope) - IF 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_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.id - WHERE s.external_id = p_external_id - AND f.name = p_feature_name - AND sr.scope_type = 'global' - AND sr.scope_id = 'all' - ) THEN - RETURN TRUE; - END IF; - - -- Regular access check - 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_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.id - WHERE s.external_id = p_external_id - AND f.name = p_feature_name - AND sr.scope_type = p_scope_type - AND sr.scope_id = p_scope_id - ); -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Function: c77_rbac_apply_policy -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; -BEGIN - -- Split schema and table name - IF p_table_name LIKE '%.%' 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; - - -- Drop existing policy - EXECUTE format('DROP POLICY IF EXISTS c77_rbac_policy ON %I.%I', v_schema_name, v_table_name); - - -- Create policy with fully qualified column - 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.%I.%I) - )', - v_schema_name, v_table_name, p_feature_name, p_scope_type, v_schema_name, v_table_name, 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); -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Grant permissions -GRANT USAGE ON SCHEMA public TO PUBLIC; -GRANT SELECT, 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 TO PUBLIC; -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_apply_policy(TEXT, TEXT, TEXT, TEXT) TO PUBLIC; - --- Set default privileges for future objects -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO PUBLIC; -ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT EXECUTE ON FUNCTIONS TO PUBLIC; \ No newline at end of file diff --git a/c77_rbac--1.1.sql b/c77_rbac--1.1.sql new file mode 100644 index 0000000..2400de3 --- /dev/null +++ b/c77_rbac--1.1.sql @@ -0,0 +1,740 @@ +-- 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; \ No newline at end of file diff --git a/c77_rbac.control b/c77_rbac.control index baacddb..7bb57d9 100644 --- a/c77_rbac.control +++ b/c77_rbac.control @@ -1,6 +1,6 @@ # c77_rbac extension comment = 'Role-Based Access Control with Row Level Security for PostgreSQL' -default_version = '1.0' +default_version = '1.1' relocatable = false schema = public -superuser = false +superuser = false \ No newline at end of file