{{ $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)) + + @endif +diff --git a/README.md b/README.md index a085b79..7777d1a 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,250 @@ # c77_rbac PostgreSQL Extension -The `c77_rbac` extension provides role-based access control (RBAC) for PostgreSQL, securing tables with row-level security (RLS) for multi-campus applications. All objects are in the `public` schema with `c77_rbac_` prefixes (e.g., `c77_rbac_subjects`, `c77_rbac_apply_policy`) to coexist with Laravel and third-party packages. Application tables use custom schemas (e.g., `myapp.orders`). +A PostgreSQL extension that provides Role-Based Access Control (RBAC) with Row-Level Security (RLS) for enterprise applications. This extension pushes authorization logic to the database layer, ensuring consistent security across all application frameworks and direct database access. ## Features -- Scoped role assignments (e.g., campus-specific access). -- Admin access via `global/all` scope for any user ID. -- RLS policies via `c77_rbac_apply_policy`. -- Compatible with PostgreSQL 14+ and Laravel. - -## Installation - -1. Ensure PostgreSQL 14 or later is installed. - -2. Place `c77_rbac.control` and `c77_rbac--1.1.0.sql.backup` in `/usr/share/postgresql/17/extension/`. - -3. Run as a superuser: - - ```sql - CREATE EXTENSION c77_rbac SCHEMA public; - ``` - -## Usage - -See `USAGE.md` for beginner-friendly instructions on securing tables and assigning roles. +- **Database-Centric Authorization**: Authorization rules enforced at the database level +- **Row-Level Security**: Fine-grained access control on individual rows +- **Scope-Based Permissions**: Support for department, region, or any custom scope +- **Global Admin Support**: Special `global/all` scope for administrative access +- **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 ## Requirements -- PostgreSQL 14 or later. -- Superuser access for installation. +- PostgreSQL 14 or later +- Superuser access for initial installation + +## Installation + +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/ + ``` + +2. **Install the extension (requires superuser):** + ```sql + -- Connect as superuser + CREATE DATABASE your_db; + CREATE USER app_user WITH PASSWORD 'secure_password'; + + -- Install extension + \c your_db + CREATE EXTENSION c77_rbac; + + -- Grant necessary privileges to application user + GRANT CREATE ON DATABASE your_db TO app_user; + GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_user; + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO app_user; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user; + ``` + +## Core Concepts + +### 1. Subjects (Users) +- Identified by an `external_id` (typically your application's user ID) +- Can have multiple roles with different scopes + +### 2. Roles +- Named collections of features (permissions) +- Can be scoped to specific contexts (department, region, etc.) + +### 3. Features +- Specific permissions that can be checked in policies +- Examples: `view_reports`, `edit_users`, `delete_records` + +### 4. Scopes +- Context for role assignments +- Examples: `department/engineering`, `region/north`, `global/all` + +## Basic Usage + +### 1. Define Features and Roles +```sql +-- Define features (permissions) +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'); +``` + +### 2. Assign Users to Roles +```sql +-- Assign user to manager role for engineering department +SELECT public.c77_rbac_assign_subject('123', 'manager', 'department', 'engineering'); + +-- Assign admin with global access +SELECT public.c77_rbac_assign_subject('1', 'admin', 'global', 'all'); +``` + +### 3. Apply Row-Level Security +```sql +-- Apply RLS policy to a table +SELECT public.c77_rbac_apply_policy( + 'myschema.reports', -- table name (can be schema-qualified) + 'view_reports', -- required feature + 'department', -- scope type + 'department_id' -- column containing scope value +); +``` + +### 4. Set User Context +```sql +-- Set the current user for RLS checks +SET "c77_rbac.external_id" TO '123'; + +-- Now queries automatically filter based on permissions +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: + +```sql +-- Sync all features to admin role +SELECT public.c77_rbac_sync_admin_features(); + +-- Or sync to all roles with global/all scope +SELECT public.c77_rbac_sync_global_admin_features(); +``` + +## Integration Examples + +### Laravel Integration +```php +// Middleware to set user context +public function handle($request, Closure $next) +{ + if (Auth::check()) { + DB::statement('SET "c77_rbac.external_id" TO ?', [Auth::id()]); + } + return $next($request); +} + +// Check permissions +$canView = DB::selectOne(" + SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS allowed +", ['view_reports', Auth::id(), 'department', 'engineering'])->allowed; +``` + +### Schema-Aware Usage +```sql +-- Works with any schema +CREATE SCHEMA finance; +CREATE TABLE finance.accounts (...); + +-- Apply RLS with schema qualification +SELECT public.c77_rbac_apply_policy( + 'finance.accounts', + 'view_finance', + 'department', + 'dept_id' +); +``` + +## 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 + +### 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 + +### 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 + +## Uninstallation + +1. **Check dependencies:** + ```sql + SELECT * FROM public.c77_rbac_show_dependencies(); + ``` + +2. **Remove policies and optionally data:** + ```sql + -- Just remove policies + SELECT public.c77_rbac_remove_all_policies(); + + -- Or remove policies and all RBAC data + SELECT public.c77_rbac_cleanup_for_removal(true); + ``` + +3. **Drop the extension:** + ```sql + DROP EXTENSION c77_rbac CASCADE; + ``` + +## Best Practices + +1. **Feature Naming Convention:** + - Use prefixes: `view_*`, `edit_*`, `delete_*`, `manage_*` + - 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 + - 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 + +4. **Security:** + - Always use parameterized queries + - Reset session variables in connection pools + - Audit role assignments regularly + +## 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 + +### Performance Issues +1. Verify indexes exist on RBAC tables +2. Check query plans with EXPLAIN ANALYZE +3. Consider caching permission checks in your application + +## Contributing + +This extension is designed to be framework-agnostic. When contributing: +- Keep the core extension simple and focused +- Add framework-specific features to companion extensions +- Include tests for new functionality +- Update documentation for new features ## License -MIT License \ No newline at end of file +MIT License - See LICENSE file for details + +## Support + +- Create an issue for bugs or feature requests +- Check existing issues before creating new ones +- Include PostgreSQL version and reproduction steps for bugs + +--- + +For framework-specific extensions, see: +- [c77_rbac_laravel](https://github.com/yourusername/c77_rbac_laravel) - Laravel integration \ No newline at end of file diff --git a/USAGE.md b/USAGE.md index b1d4c15..fb2cc1b 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1,199 +1,1650 @@ -# c77_rbac Usage Guide +# c77_rbac Comprehensive Usage Guide -This guide helps beginner developers use the `c77_rbac` PostgreSQL extension to secure database tables with role-based access control (RBAC). All tables and functions are in the `public` schema with `c77_rbac_` prefixes (e.g., `c77_rbac_subjects`, `c77_rbac_apply_policy`) to avoid conflicts with Laravel or other packages. Your application tables should use custom schemas (e.g., `myapp.orders`). +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. -## What is c77_rbac? +## Table of Contents -`c77_rbac` uses row-level security (RLS) to control table access. For example, a Chicago manager sees only Chicago orders, while an admin sees all orders. Admin rights use a `global/all` scope, so any user ID (e.g., `'2'`, `'999'`) can be an admin. +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) -## Prerequisites +## Understanding the Concepts -- PostgreSQL 14 or later. -- A superuser (e.g., `homestead`) for installation. -- A database (e.g., `c77_rbac_test`). -- Basic SQL knowledge. +### What is c77_rbac? -## Step 1: Install the Extension +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. -1. **Log in as the superuser**: +### Key Terms - ```bash - psql -h 192.168.49.115 -p 5432 -U homestead -d c77_rbac_test - ``` +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. **Create the extension**: +2. **Role**: A named set of permissions + - Examples: 'admin', 'manager', 'employee' + - Roles are just names - they get their power from features - ```sql - CREATE EXTENSION c77_rbac SCHEMA public; - ``` +3. **Feature**: A specific permission + - Examples: 'view_reports', 'edit_users', 'delete_posts' + - These are what actually get checked by the database - This sets up `c77_rbac_` tables and functions in `public`. +4. **Scope**: The context where a role applies + - Type + ID combination + - Examples: 'department/sales', 'region/north', 'global/all' -3. **Exit**: +5. **Policy**: A database rule that enforces security + - Automatically filters rows based on user permissions + - Invisible to your application code - ```sql - \q - ``` +### How It Works -## Step 2: Set Up Your Application Schema +``` +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 +``` -Use a custom schema (e.g., `myapp`) for your tables. +## Installation Walk-through -1. **Log in as your application user** (e.g., `app_user`): +### Step 1: Database Administrator Tasks - ```bash - psql -h 192.168.49.115 -p 5432 -U app_user -d c77_rbac_test - ``` +These steps require PostgreSQL superuser access: -2. **Create the myapp schema**: +```sql +-- 1. Connect as postgres superuser +sudo -u postgres psql - ```sql - CREATE SCHEMA myapp; - ``` +-- 2. Create your database +CREATE DATABASE myapp_db; -3. **Create a test table**: +-- 3. Create application user +CREATE USER myapp_user WITH PASSWORD 'secure_password_here'; - ```sql - CREATE TABLE myapp.orders ( - id SERIAL PRIMARY KEY, - campus TEXT NOT NULL, - amount NUMERIC - ); - ``` +-- 4. Connect to your database +\c myapp_db -4. **Insert test data**: +-- 5. Install the extension +CREATE EXTENSION c77_rbac; - ```sql - INSERT INTO myapp.orders (campus, amount) VALUES ('chicago', 500), ('miami', 1500); - ``` +-- 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; -## Step 3: Apply Row-Level Security (RLS) +-- 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; +``` -1. **Apply an RLS policy**: +### Step 2: Verify Installation - ```sql - SELECT public.c77_rbac_apply_policy('myapp.orders', 'view_sales_page', 'campus', 'campus'); - ``` +Connect as your application user: - - `myapp.orders`: Table to secure. - - `view_sales_page`: Required permission. - - `campus`: Scope type. - - `campus`: Column for scope (e.g., `chicago`). +```bash +psql -U myapp_user -d myapp_db -h localhost +``` - A `NOTICE: policy "c77_rbac_policy" ... does not exist` is normal for new tables. +Test the extension: -2. **Check the policy**: +```sql +-- Should return empty results (no subjects yet) +SELECT * FROM public.c77_rbac_subjects; - ```sql - \dp myapp.orders - ``` +-- Should return true +SELECT EXISTS ( + SELECT 1 FROM pg_extension WHERE extname = 'c77_rbac' +) as extension_installed; +``` - Expect `c77_rbac_policy` with: +## Core Functions Explained - ``` - c77_rbac_can_access('view_sales_page'::text, current_setting('c77_rbac.external_id'::text, true), 'campus'::text, campus) - ``` +### 1. c77_rbac_grant_feature(role_name, feature_name) -## Step 4: Assign Roles to Users +**Purpose**: Gives a permission to a role. -Users have an `external_id` (e.g., `'1'`, `'2'`). Admin rights use `global/all`. +**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) -1. **Assign a sales manager role** (Chicago): +**Example**: +```sql +-- Give 'editor' role the ability to view posts +SELECT public.c77_rbac_grant_feature('editor', 'view_posts'); - ```sql - SELECT public.c77_rbac_assign_subject('1', 'sales_manager', 'campus', 'chicago'); - SELECT public.c77_rbac_grant_feature('sales_manager', 'view_sales_page'); - ``` +-- Give 'editor' role the ability to edit posts +SELECT public.c77_rbac_grant_feature('editor', 'edit_posts'); -2. **Assign an admin role** (all data): +-- 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'); +``` - ```sql - SELECT public.c77_rbac_assign_subject('999', 'admin', 'global', 'all'); - SELECT public.c77_rbac_grant_feature('admin', 'view_sales_page'); - ``` +**Important**: Admin roles need explicit grants for each feature! - For another admin (e.g., `'2'`): +### 2. c77_rbac_assign_subject(external_id, role_name, scope_type, scope_id) - ```sql - SELECT public.c77_rbac_assign_subject('2', 'admin', 'global', 'all'); - SELECT public.c77_rbac_grant_feature('admin', 'view_sales_page'); - ``` +**Purpose**: Assigns a role to a user with a specific scope. -## Step 5: Test Access +**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') -1. **Chicago manager**: +**Examples**: +```sql +-- User 123 is an editor in the marketing department +SELECT public.c77_rbac_assign_subject('123', 'editor', 'department', 'marketing'); - ```sql - SET "c77_rbac.external_id" TO '1'; - SELECT * FROM myapp.orders; - ``` +-- User 456 is a manager for the north region +SELECT public.c77_rbac_assign_subject('456', 'manager', 'region', 'north'); - **Expected**: +-- User 1 is a global admin (can access everything) +SELECT public.c77_rbac_assign_subject('1', 'admin', 'global', 'all'); - ``` - id | campus | amount - ----+---------+-------- - 1 | chicago | 500 - ``` +-- User 789 is a viewer with no specific scope +SELECT public.c77_rbac_assign_subject('789', 'viewer', 'none', 'none'); +``` -2. **Admin** (e.g., `'2'`): +### 3. c77_rbac_apply_policy(table_name, feature_name, scope_type, scope_column) - ```sql - SET "c77_rbac.external_id" TO '2'; - SELECT * FROM myapp.orders; - ``` +**Purpose**: Creates a Row-Level Security policy on a table. - **Expected**: +**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 - ``` - id | campus | amount - ----+---------+-------- - 1 | chicago | 500 - 2 | miami | 1500 - ``` +**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 +); -3. **Unauthorized user**: +-- 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 +); - ```sql - SET "c77_rbac.external_id" TO 'unknown'; - SELECT * FROM myapp.orders; - ``` +-- 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 +); +``` - **Expected**: +### 4. c77_rbac_can_access(feature_name, external_id, scope_type, scope_id) - ``` - id | campus | amount - ----+--------+-------- - ``` +**Purpose**: Checks if a user has access to a feature with a specific scope. -## Step 6: Use in Your Application +**Parameters**: +- `feature_name`: The feature to check +- `external_id`: The user's ID +- `scope_type`: The scope type +- `scope_id`: The scope value -For Laravel: +**Returns**: Boolean (true/false) -1. **Set user ID**: +**Examples**: +```sql +-- Can user 123 view posts in marketing department? +SELECT public.c77_rbac_can_access('view_posts', '123', 'department', 'marketing'); - ```php - DB::statement("SET c77_rbac.external_id TO '1'"); - ``` +-- Can user 456 edit orders in north region? +SELECT public.c77_rbac_can_access('edit_orders', '456', 'region', 'north'); -2. **Query**: +-- Can user 1 (admin) delete users globally? +SELECT public.c77_rbac_can_access('delete_users', '1', 'global', 'all'); +``` - ```sql - SELECT * FROM myapp.orders; - ``` +### 5. c77_rbac_sync_admin_features() -## Troubleshooting +**Purpose**: Automatically grants all existing features to the 'admin' role. -- **No rows**: Check role (`c77_rbac_assign_subject`) and feature (`c77_rbac_grant_feature`). -- **Policy missing**: Verify `\dp myapp.orders`. Re-run `c77_rbac_apply_policy`. -- **NOTICE messages**: Normal for new tables. -- **Display quirk**: `\dp` may show `campus` instead of `myapp.orders.campus`. This is cosmetic. +**Usage**: +```sql +-- After adding new features, sync them to admin +SELECT public.c77_rbac_sync_admin_features(); +``` -## Notes +**When to use**: +- After adding new features to your system +- During initial setup for admin roles +- As part of deployment scripts -- Use `schema.table` (e.g., `myapp.orders`) with `c77_rbac_apply_policy`. -- `public` is for `c77_rbac_`, Laravel, and third-party packages. Use `myapp` for app tables. -- `c77_rbac_` tables are accessible to all database users. Manage roles responsibly. -- This covers `c77_rbac` only, not `c77_rbac_laravel`. +### 6. c77_rbac_sync_global_admin_features() -For help, ask your database administrator or the `c77_rbac` community. \ No newline at end of file +**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->content }}
+ + @if(auth()->user()->can('edit_content:department/' . $post->department_id)) + Edit + @endif + + @if(auth()->user()->can('delete_content:department/' . $post->department_id)) + + @endif +