1650 lines
45 KiB
Markdown
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. |