c77_rbac/USAGE.md

1650 lines
45 KiB
Markdown

# 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
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class SetRbacContext
{
public function handle($request, Closure $next)
{
if (Auth::check()) {
// Set the RBAC context for the current user
DB::statement('SET "c77_rbac.external_id" TO ?', [Auth::id()]);
} else {
// Clear the context for guests
DB::statement('RESET "c77_rbac.external_id"');
}
return $next($request);
}
public function terminate($request, $response)
{
// Clean up after the request
DB::statement('RESET "c77_rbac.external_id"');
}
}
```
Register in `app/Http/Kernel.php`:
```php
protected $middlewareGroups = [
'web' => [
// ... 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
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class RbacService
{
/**
* Check if current user has access to a feature
*/
public function can(string $feature, string $scopeType, string $scopeId): bool
{
if (!Auth::check()) {
return false;
}
// Cache permission checks for performance
$cacheKey = "rbac:{$feature}:{$scopeType}:{$scopeId}:" . Auth::id();
return Cache::remember($cacheKey, 300, function () use ($feature, $scopeType, $scopeId) {
$result = DB::selectOne(
'SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS allowed',
[$feature, Auth::id(), $scopeType, $scopeId]
);
return $result->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
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use App\Services\RbacService;
class SetupRbacRoles extends Migration
{
protected RbacService $rbac;
public function __construct()
{
$this->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
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use App\Services\RbacService;
class ApplyRlsToPosts extends Migration
{
protected RbacService $rbac;
public function __construct()
{
$this->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
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Services\RbacService;
class User extends Authenticatable
{
protected static ?RbacService $rbacService = null;
protected static function getRbacService(): RbacService
{
if (!self::$rbacService) {
self::$rbacService = app(RbacService::class);
}
return self::$rbacService;
}
/**
* Assign a role to this user
*/
public function assignRole(string $role, string $scopeType = 'global', string $scopeId = 'all'): void
{
self::getRbacService()->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
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Services\RbacService;
use Illuminate\Http\Request;
class PostController extends Controller
{
protected RbacService $rbac;
public function __construct(RbacService $rbac)
{
$this->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))
<a href="{{ route('posts.create') }}" class="btn btn-primary">Create Post</a>
@endif
@foreach($posts as $post)
<div class="post">
<h2>{{ $post->title }}</h2>
<p>{{ $post->content }}</p>
@if(auth()->user()->can('edit_content:department/' . $post->department_id))
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endif
@if(auth()->user()->can('delete_content:department/' . $post->department_id))
<form action="{{ route('posts.destroy', $post) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
@endif
</div>
@endforeach
```
### Step 8: API Authentication
For API routes, ensure the context is set:
```php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ApiController extends Controller
{
public function __construct()
{
$this->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.