18 KiB
c77_rbac_laravel Usage Guide
This guide provides detailed instructions for using the c77_rbac_laravel
PostgreSQL extension, which integrates the c77_rbac
Role-Based Access Control system with Laravel applications. The guide assumes you have already installed both the c77_rbac
and c77_rbac_laravel
extensions.
Table of Contents
- Extension Functions
- Laravel Integration Setup
- Common Usage Patterns
- Advanced Usage
- Best Practices
- Troubleshooting
Extension Functions
The c77_rbac_laravel
extension provides two key functions that bridge Laravel's authentication system with PostgreSQL's RBAC implementation:
c77_rbac_laravel_auth_id()
Retrieves the current Laravel user ID from the session variable.
SELECT public.c77_rbac_laravel_auth_id();
- Returns: TEXT value of the current user's ID, or NULL if not set
- Use Case: Used within RLS policies to identify the current user
- Example:
CREATE POLICY user_data_policy ON user_data FOR ALL TO PUBLIC USING (user_id::text = public.c77_rbac_laravel_auth_id());
c77_rbac_laravel_assign_user()
Assigns a Laravel user to a role with optional scope.
SELECT public.c77_rbac_laravel_assign_user(
p_laravel_id BIGINT, -- Laravel user ID (integer)
p_role_name TEXT, -- Role name to assign
p_scope_type TEXT, -- Optional scope type
p_scope_id TEXT -- Optional scope ID
);
- Parameters:
p_laravel_id
: Laravel's integer user ID (will be converted to TEXT)p_role_name
: Name of the role to assignp_scope_type
: Scope category (can be NULL)p_scope_id
: Scope value (can be NULL)
- Example:
-- Assign user ID 1 to the 'editor' role for the 'marketing' department SELECT public.c77_rbac_laravel_assign_user(1, 'editor', 'department', 'marketing'); -- Assign user ID 2 to the global 'admin' role SELECT public.c77_rbac_laravel_assign_user(2, 'admin', NULL, NULL);
Laravel Integration Setup
Setting Up the Middleware
Create a middleware to set the c77_rbac.external_id
session variable for each request:
<?php
// app/Http/Middleware/SetRbacExternalId.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
class SetRbacExternalId
{
public function handle($request, Closure $next)
{
if (Auth::check()) {
DB::statement('SET "c77_rbac.external_id" TO ?', [Auth::id()]);
} else {
// Optional: Reset the external_id if no user is logged in
DB::statement('RESET "c77_rbac.external_id"');
}
return $next($request);
}
}
Register this middleware in your app/Http/Kernel.php
:
protected $middlewareGroups = [
'web' => [
// Other middleware...
\App\Http\Middleware\SetRbacExternalId::class,
],
];
Database Migrations
Create migrations to set up your RBAC rules:
<?php
// database/migrations/2025_04_12_000001_setup_rbac_roles.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
class SetupRbacRoles extends Migration
{
public function up()
{
// Create roles
$roles = ['admin', 'editor', 'viewer'];
foreach ($roles as $role) {
DB::statement("
INSERT INTO public.c77_rbac_roles (name)
VALUES (?) ON CONFLICT (name) DO NOTHING
", [$role]);
}
// Define features
$features = [
'view_dashboard', 'edit_content', 'publish_content',
'manage_users', 'view_reports'
];
foreach ($features as $feature) {
DB::statement("
INSERT INTO public.c77_rbac_features (name)
VALUES (?) ON CONFLICT (name) DO NOTHING
", [$feature]);
}
// Grant features to roles
$roleFeatures = [
'admin' => ['view_dashboard', 'edit_content', 'publish_content', 'manage_users', 'view_reports'],
'editor' => ['view_dashboard', 'edit_content', 'publish_content', 'view_reports'],
'viewer' => ['view_dashboard', 'view_reports']
];
foreach ($roleFeatures as $role => $features) {
foreach ($features as $feature) {
DB::statement("
SELECT public.c77_rbac_grant_feature(?, ?)
", [$role, $feature]);
}
}
}
public function down()
{
// Clean up if needed
DB::statement("TRUNCATE public.c77_rbac_role_features CASCADE");
}
}
<?php
// database/migrations/2025_04_12_000002_assign_initial_users.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use App\Models\User;
class AssignInitialUsers extends Migration
{
public function up()
{
// Find admin user(s)
$adminUsers = User::where('is_admin', true)->get();
foreach ($adminUsers as $admin) {
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$admin->id, 'admin', NULL, NULL]);
}
// Assign department editors
$editors = User::where('role', 'editor')->get();
foreach ($editors as $editor) {
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$editor->id, 'editor', 'department', $editor->department]);
}
// Assign viewers
$viewers = User::where('role', 'viewer')->get();
foreach ($viewers as $viewer) {
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$viewer->id, 'viewer', 'department', $viewer->department]);
}
}
public function down()
{
// Clean up if needed
DB::statement("TRUNCATE public.c77_rbac_subject_roles CASCADE");
}
}
Applying RLS to Tables
Create migrations to enable RLS on your tables:
<?php
// database/migrations/2025_04_12_000003_enable_rls_policies.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class EnableRlsPolicies extends Migration
{
public function up()
{
// Enable RLS on content table
DB::statement("ALTER TABLE content ENABLE ROW LEVEL SECURITY");
// Create policy for content access
DB::statement("
CREATE POLICY content_access_policy ON content
FOR ALL
TO PUBLIC
USING (
public.c77_rbac_can_access(
'edit_content',
public.c77_rbac_laravel_auth_id(),
'department',
department
)
)
");
// Enable RLS on users table
DB::statement("ALTER TABLE users ENABLE ROW LEVEL SECURITY");
// Create policy for user management
DB::statement("
CREATE POLICY user_management_policy ON users
FOR ALL
TO PUBLIC
USING (
public.c77_rbac_can_access(
'manage_users',
public.c77_rbac_laravel_auth_id(),
NULL,
NULL
)
)
");
}
public function down()
{
// Drop policies
DB::statement("DROP POLICY IF EXISTS content_access_policy ON content");
DB::statement("DROP POLICY IF EXISTS user_management_policy ON users");
// Disable RLS
DB::statement("ALTER TABLE content DISABLE ROW LEVEL SECURITY");
DB::statement("ALTER TABLE users DISABLE ROW LEVEL SECURITY");
}
}
Common Usage Patterns
User Registration
When registering new users, assign them appropriate roles:
public function register(Request $request)
{
// Validate request and create user
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'department' => $request->department,
]);
// Assign default role with department scope
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$user->id, 'viewer', 'department', $user->department]);
// Continue with registration process...
}
Role Management
Create a controller for managing user roles:
public function assignRole(User $user, Request $request)
{
$request->validate([
'role' => 'required|string|exists:c77_rbac_roles,name',
'scope_type' => 'nullable|string',
'scope_id' => 'nullable|string',
]);
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [
$user->id,
$request->role,
$request->scope_type,
$request->scope_id
]);
return back()->with('success', 'Role assigned successfully');
}
Checking Permissions in Controllers
Check if a user has a specific permission:
public function edit(Content $content)
{
$canEdit = DB::selectOne("
SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS result
", ['edit_content', Auth::id(), 'department', $content->department])->result;
if (!$canEdit) {
abort(403, 'Unauthorized action.');
}
return view('content.edit', compact('content'));
}
Blade Directives
Create custom Blade directives for permission checks:
// In AppServiceProvider.php
public function boot()
{
Blade::directive('can', function ($expression) {
list($feature, $scopeType, $scopeId) = explode(',', $expression);
return "<?php if(DB::selectOne(\"
SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS result
\", [{$feature}, auth()->id(), {$scopeType}, {$scopeId}])->result): ?>";
});
Blade::directive('endcan', function () {
return "<?php endif; ?>";
});
}
Usage in Blade:
@can('edit_content', 'department', $content->department)
<a href="{{ route('content.edit', $content) }}" class="btn btn-primary">Edit</a>
@endcan
Advanced Usage
Batch User Assignments
For batch operations, use a transaction to improve performance:
DB::transaction(function () use ($users, $role, $scopeType, $scopeId) {
foreach ($users as $user) {
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$user->id, $role, $scopeType, $scopeId]);
}
});
Dynamic Scope Resolution
Create a helper for dynamic scope resolution:
// In a helper file or service
function resolveScope($model)
{
switch (get_class($model)) {
case 'App\Models\Department':
return ['department', $model->id];
case 'App\Models\Project':
return ['project', $model->id];
case 'App\Models\Team':
return ['team', $model->id];
default:
return [null, null];
}
}
// Usage
list($scopeType, $scopeId) = resolveScope($department);
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$user->id, 'manager', $scopeType, $scopeId]);
Command-Line Role Management
Create an Artisan command for role management:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class AssignUserRole extends Command
{
protected $signature = 'rbac:assign-role {user} {role} {--scope-type=} {--scope-id=}';
protected $description = 'Assign a role to a user with optional scope';
public function handle()
{
$user = User::findOrFail($this->argument('user'));
$role = $this->argument('role');
$scopeType = $this->option('scope-type');
$scopeId = $this->option('scope-id');
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$user->id, $role, $scopeType, $scopeId]);
$this->info("Role '$role' assigned to user #{$user->id}");
return 0;
}
}
Usage:
php artisan rbac:assign-role 1 editor --scope-type=department --scope-id=marketing
Best Practices
Connection Management
- Connection Pooling: If using connection pooling (like PgBouncer), always reset the
c77_rbac.external_id
variable at the end of each request:
// In a middleware or terminating callback
public function terminate($request, $response)
{
DB::statement('RESET "c77_rbac.external_id"');
}
- Long-running Jobs: For queued jobs, set the external ID at the beginning of the job:
public function handle()
{
// Set the RBAC external ID for this job
DB::statement('SET "c77_rbac.external_id" TO ?', [$this->user_id]);
// Job logic...
// Reset when done
DB::statement('RESET "c77_rbac.external_id"');
}
Security
-
Middleware Priority: Ensure your RBAC middleware runs early in the request cycle to protect all database queries.
-
Authorization Wrapper: Create a service for all authorization checks:
// app/Services/RbacService.php
class RbacService
{
public function can($feature, $scopeType = null, $scopeId = null)
{
$userId = auth()->id();
if (!$userId) return false;
return DB::selectOne("
SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS result
", [$feature, $userId, $scopeType, $scopeId])->result;
}
public function assignRole($userId, $role, $scopeType = null, $scopeId = null)
{
DB::statement("
SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)
", [$userId, $role, $scopeType, $scopeId]);
}
}
- Policy Integration: Integrate with Laravel's policy system:
// app/Policies/ContentPolicy.php
class ContentPolicy
{
protected $rbac;
public function __construct(RbacService $rbac)
{
$this->rbac = $rbac;
}
public function update(User $user, Content $content)
{
return $this->rbac->can('edit_content', 'department', $content->department);
}
}
Performance
- Eager Loading: When loading related models that will be filtered by RLS, use eager loading with care:
// This will load ALL departments first, then filter
$user->load('departments');
// Better approach for large datasets
$departments = Department::all(); // Already filtered by RLS
- Cache Role Assignments: Cache role assignments for frequently accessed permissions:
public function can($feature, $scopeType = null, $scopeId = null)
{
$cacheKey = "rbac:{$feature}:{$scopeType}:{$scopeId}:" . auth()->id();
return Cache::remember($cacheKey, now()->addMinutes(10), function () use ($feature, $scopeType, $scopeId) {
return DB::selectOne("
SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS result
", [$feature, auth()->id(), $scopeType, $scopeId])->result;
});
}
Troubleshooting
Common Issues
-
No Data Returned
Symptom: Queries return empty results when you expect data.
Possible Causes:
- External ID not set
- User doesn't have required permissions
- RLS policy incorrectly configured
Solution:
// Check if external ID is set $externalId = DB::selectOne("SELECT current_setting('c77_rbac.external_id', true) AS id")->id; // Check direct permissions $hasPermission = DB::selectOne(" SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS result ", ['required_feature', auth()->id(), 'scope_type', 'scope_id'])->result; // Verify RLS policy $policyExists = DB::selectOne(" SELECT EXISTS ( SELECT 1 FROM pg_policies WHERE tablename = 'your_table' AND policyname = 'your_policy' ) AS exists ")->exists;
-
Permission Denied Errors
Symptom: PostgreSQL returns permission denied errors.
Possible Causes:
- Database user doesn't have execute permission on functions
- RLS preventing access to tables
Solution:
-- Grant execute permission GRANT EXECUTE ON FUNCTION public.c77_rbac_laravel_auth_id() TO app_user; GRANT EXECUTE ON FUNCTION public.c77_rbac_laravel_assign_user(BIGINT, TEXT, TEXT, TEXT) TO app_user; -- Check for explicit denials in RLS SELECT tablename, policyname, cmd, qual FROM pg_policies WHERE tablename = 'your_table';
-
Middleware Not Working
Symptom: RBAC checks fail despite user being logged in.
Possible Causes:
- Middleware not registered or running
- Auth system not initialized when middleware runs
Solution:
- Check middleware registration in Kernel.php
- Add debug code to verify middleware execution:
// In your middleware public function handle($request, Closure $next) { \Log::debug('RBAC Middleware - User: ' . (Auth::check() ? Auth::id() : 'Guest')); if (Auth::check()) { DB::statement('SET "c77_rbac.external_id" TO ?', [Auth::id()]); \Log::debug('Set c77_rbac.external_id to ' . Auth::id()); } return $next($request); }
-
Role Assignment Issues
Symptom: User assignments aren't taking effect.
Possible Causes:
- Transaction issues
- Scopes not matching exactly
Solution:
- Ensure transactions are committed
- Check for exact scope matches:
SELECT * FROM c77_rbac_subject_roles WHERE subject_id = ( SELECT subject_id FROM c77_rbac_subjects WHERE external_id = '1' );
For persistent issues, you can enable PostgreSQL query logging to see exactly what's happening:
// In a debug route or controller
DB::listen(function ($query) {
\Log::debug($query->sql, [
'bindings' => $query->bindings,
'time' => $query->time
]);
});