Revision 1.0.0 c77_rbac_laravel
This commit is contained in:
parent
181c245b62
commit
39976f1500
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
# MIT License
|
||||
|
||||
Copyright (c) 2025 c77_rbac_laravel Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
217
README.md
Normal file
217
README.md
Normal file
@ -0,0 +1,217 @@
|
||||
# c77_rbac_laravel
|
||||
|
||||
A PostgreSQL extension that provides Laravel integration for the `c77_rbac` Role-Based Access Control system. This extension simplifies the use of `c77_rbac` in Laravel applications by providing Laravel-specific functions and utilities.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- PostgreSQL 13 or later (tested on 17)
|
||||
- `c77_rbac` extension must be installed
|
||||
- Laravel 9.0 or later
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Install the Extension
|
||||
|
||||
Copy the extension files to your PostgreSQL extension directory:
|
||||
|
||||
```bash
|
||||
sudo cp c77_rbac_laravel--1.0.sql c77_rbac_laravel.control /usr/share/postgresql/17/extension/
|
||||
```
|
||||
|
||||
### Step 2: Create/Connect to Your Database
|
||||
|
||||
Ensure you have a database with the `c77_rbac` extension already installed:
|
||||
|
||||
```sql
|
||||
-- Connect to your database
|
||||
\c your_database_name
|
||||
|
||||
-- Verify c77_rbac is installed
|
||||
SELECT * FROM pg_extension WHERE extname = 'c77_rbac';
|
||||
```
|
||||
|
||||
### Step 3: Enable the Laravel Extension
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION c77_rbac_laravel;
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
The extension provides two key functions:
|
||||
|
||||
1. **c77_rbac_laravel_auth_id()**
|
||||
- Retrieves the current Laravel user ID from the session variable
|
||||
- Used in RLS policies to identify the current user
|
||||
- Returns NULL if the session variable is not set
|
||||
|
||||
2. **c77_rbac_laravel_assign_user()**
|
||||
- Assigns a Laravel user to a role with optional scope
|
||||
- Automatically converts Laravel's integer IDs to the text format used by `c77_rbac`
|
||||
|
||||
## Integration with Laravel
|
||||
|
||||
### Middleware Setup
|
||||
|
||||
Create a middleware to set the current user's ID as the `c77_rbac.external_id` session variable:
|
||||
|
||||
```php
|
||||
<?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()]);
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Register this middleware in your `app/Http/Kernel.php`:
|
||||
|
||||
```php
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
// Other middleware...
|
||||
\App\Http\Middleware\SetRbacExternalId::class,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Setting Up RBAC in Migrations
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SetupRbacRoles extends Migration
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
// Assign an admin user
|
||||
DB::statement("SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)", [
|
||||
1, // User ID
|
||||
'admin',
|
||||
NULL,
|
||||
NULL
|
||||
]);
|
||||
|
||||
// Grant features to the admin role
|
||||
DB::statement("SELECT public.c77_rbac_grant_feature(?, ?)", [
|
||||
'admin', 'view_dashboard'
|
||||
]);
|
||||
|
||||
// Assign a regular user to a scoped role
|
||||
DB::statement("SELECT public.c77_rbac_laravel_assign_user(?, ?, ?, ?)", [
|
||||
2, // User ID
|
||||
'editor',
|
||||
'department',
|
||||
'marketing'
|
||||
]);
|
||||
|
||||
// Grant features to the editor role
|
||||
DB::statement("SELECT public.c77_rbac_grant_feature(?, ?)", [
|
||||
'editor', 'edit_content'
|
||||
]);
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// Cleanup code if needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Creating RLS Policies
|
||||
|
||||
Apply RLS policies that use the Laravel user ID:
|
||||
|
||||
```php
|
||||
DB::statement("
|
||||
CREATE POLICY department_access_policy ON documents
|
||||
FOR ALL
|
||||
TO PUBLIC
|
||||
USING (public.c77_rbac_can_access('edit_content', public.c77_rbac_laravel_auth_id(), 'department', department))
|
||||
");
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Checking Access in Queries
|
||||
|
||||
Once the middleware is active, your Laravel queries will automatically respect RLS policies:
|
||||
|
||||
```php
|
||||
// This query will only return rows the user has access to based on RLS
|
||||
$documents = DB::table('documents')->get();
|
||||
```
|
||||
|
||||
### Manual Access Checks
|
||||
|
||||
You can also perform manual access checks in your code:
|
||||
|
||||
```php
|
||||
$userId = Auth::id();
|
||||
$canAccess = DB::selectOne("
|
||||
SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS result
|
||||
", ['edit_content', $userId, 'department', 'marketing'])->result;
|
||||
|
||||
if ($canAccess) {
|
||||
// Allow the action
|
||||
} else {
|
||||
// Deny the action
|
||||
}
|
||||
```
|
||||
|
||||
## Connection Pooling Considerations
|
||||
|
||||
If you're using connection pooling (e.g., PgBouncer), be aware that session variables like `c77_rbac.external_id` persist for the duration of the database connection. For session pooling to work correctly:
|
||||
|
||||
1. Ensure your pooling is configured in "Session" mode, not "Transaction" mode
|
||||
2. Set the external ID at the beginning of each request via middleware
|
||||
3. Consider using a cleanup middleware for long-lived connections
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No Rows Returned
|
||||
|
||||
If your queries return no rows when you expect data:
|
||||
|
||||
1. Verify the middleware is setting the external ID:
|
||||
```php
|
||||
$externalId = DB::selectOne("SELECT current_setting('c77_rbac.external_id', true) AS id")->id;
|
||||
```
|
||||
|
||||
2. Check if your user has the required role and feature:
|
||||
```php
|
||||
$hasAccess = DB::selectOne("
|
||||
SELECT public.c77_rbac_can_access(?, ?, ?, ?) AS result
|
||||
", ['required_feature', Auth::id(), 'scope_type', 'scope_id'])->result;
|
||||
```
|
||||
|
||||
### Database Errors
|
||||
|
||||
For "undefined_object" errors related to `c77_rbac.external_id`, ensure your middleware is running and setting the variable correctly.
|
||||
|
||||
## License
|
||||
|
||||
MIT License. See `LICENSE` file for details.
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [c77_rbac](https://github.com/yourusername/c77_rbac) - The core RBAC extension for PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
For more detailed information on the underlying RBAC system, please refer to the documentation for the `c77_rbac` extension.
|
675
USEAGE.md
Normal file
675
USEAGE.md
Normal file
@ -0,0 +1,675 @@
|
||||
# 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
|
||||
|
||||
1. [Extension Functions](#extension-functions)
|
||||
2. [Laravel Integration Setup](#laravel-integration-setup)
|
||||
3. [Common Usage Patterns](#common-usage-patterns)
|
||||
4. [Advanced Usage](#advanced-usage)
|
||||
5. [Best Practices](#best-practices)
|
||||
6. [Troubleshooting](#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.
|
||||
|
||||
```sql
|
||||
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**:
|
||||
```sql
|
||||
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.
|
||||
|
||||
```sql
|
||||
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 assign
|
||||
- `p_scope_type`: Scope category (can be NULL)
|
||||
- `p_scope_id`: Scope value (can be NULL)
|
||||
- **Example**:
|
||||
```sql
|
||||
-- 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
|
||||
<?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`:
|
||||
|
||||
```php
|
||||
protected $middlewareGroups = [
|
||||
'web' => [
|
||||
// Other middleware...
|
||||
\App\Http\Middleware\SetRbacExternalId::class,
|
||||
],
|
||||
];
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
|
||||
Create migrations to set up your RBAC rules:
|
||||
|
||||
```php
|
||||
<?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
|
||||
<?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
|
||||
<?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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
// 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:
|
||||
|
||||
```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:
|
||||
|
||||
```php
|
||||
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:
|
||||
|
||||
```php
|
||||
// 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
|
||||
<?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:
|
||||
|
||||
```bash
|
||||
php artisan rbac:assign-role 1 editor --scope-type=department --scope-id=marketing
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Connection Management
|
||||
|
||||
1. **Connection Pooling**: If using connection pooling (like PgBouncer), always reset the `c77_rbac.external_id` variable at the end of each request:
|
||||
|
||||
```php
|
||||
// In a middleware or terminating callback
|
||||
public function terminate($request, $response)
|
||||
{
|
||||
DB::statement('RESET "c77_rbac.external_id"');
|
||||
}
|
||||
```
|
||||
|
||||
2. **Long-running Jobs**: For queued jobs, set the external ID at the beginning of the job:
|
||||
|
||||
```php
|
||||
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
|
||||
|
||||
1. **Middleware Priority**: Ensure your RBAC middleware runs early in the request cycle to protect all database queries.
|
||||
|
||||
2. **Authorization Wrapper**: Create a service for all authorization checks:
|
||||
|
||||
```php
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Policy Integration**: Integrate with Laravel's policy system:
|
||||
|
||||
```php
|
||||
// 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
|
||||
|
||||
1. **Eager Loading**: When loading related models that will be filtered by RLS, use eager loading with care:
|
||||
|
||||
```php
|
||||
// This will load ALL departments first, then filter
|
||||
$user->load('departments');
|
||||
|
||||
// Better approach for large datasets
|
||||
$departments = Department::all(); // Already filtered by RLS
|
||||
```
|
||||
|
||||
2. **Cache Role Assignments**: Cache role assignments for frequently accessed permissions:
|
||||
|
||||
```php
|
||||
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
|
||||
|
||||
1. **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**:
|
||||
```php
|
||||
// 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;
|
||||
```
|
||||
|
||||
2. **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**:
|
||||
```sql
|
||||
-- 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';
|
||||
```
|
||||
|
||||
3. **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:
|
||||
```php
|
||||
// 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);
|
||||
}
|
||||
```
|
||||
|
||||
4. **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:
|
||||
```sql
|
||||
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:
|
||||
|
||||
```php
|
||||
// In a debug route or controller
|
||||
DB::listen(function ($query) {
|
||||
\Log::debug($query->sql, [
|
||||
'bindings' => $query->bindings,
|
||||
'time' => $query->time
|
||||
]);
|
||||
});
|
||||
```
|
21
c77_rbac_laravel--1.0.0.sql
Normal file
21
c77_rbac_laravel--1.0.0.sql
Normal file
@ -0,0 +1,21 @@
|
||||
-- /usr/share/postgresql/17/extension/c77_rbac_laravel--1.0.sql
|
||||
\echo Use "CREATE EXTENSION c77_rbac_laravel" to load this file. \quit
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.c77_rbac_laravel_auth_id() RETURNS TEXT AS $$
|
||||
BEGIN
|
||||
RETURN current_setting('c77_rbac.external_id', true);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.c77_rbac_laravel_assign_user(
|
||||
p_laravel_id BIGINT,
|
||||
p_role_name TEXT,
|
||||
p_scope_type TEXT,
|
||||
p_scope_id TEXT
|
||||
) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
PERFORM public.c77_rbac_assign_subject(p_laravel_id::TEXT, p_role_name, p_scope_type, p_scope_id);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
6
c77_rbac_laravel.control
Normal file
6
c77_rbac_laravel.control
Normal file
@ -0,0 +1,6 @@
|
||||
# /usr/share/postgresql/17/extension/c77_rbac_laravel.control
|
||||
comment = 'Laravel integration for c77_rbac'
|
||||
default_version = '1.0'
|
||||
module_pathname = '$libdir/c77_rbac_laravel'
|
||||
relocatable = true
|
||||
requires = 'c77_rbac'
|
Loading…
x
Reference in New Issue
Block a user