RBAC Authorization

Casbin-powered Access Control

Home | SoftLixx Creates

System Overview

Casbin-powered RBAC with branch scoping

AlKareem ERP uses Casbin via the laravel-authz package for fine-grained access control. The system supports role inheritance, branch-scoped permissions, and time-based access rules.

Role-Based

Users inherit permissions from their assigned roles with hierarchical support.

Branch Scoped

Permissions can be scoped to specific branches for multi-branch operations.

Time-Based

Permissions can have start and end times for temporary access grants.

RBAC Model Definition

The Casbin model is defined in config/lauthz-rbac-model.conf. It specifies the request format, policy structure, role definitions, and matching rules.

config/lauthz-rbac-model.conf

[request_definition]
r = sub, obj, act, dom, now

[policy_definition]
p = sub, obj, act, dom, start_time, end_time

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && (r.act == p.act || p.act == "*") && (r.dom == p.dom || p.dom == "*")

Request Definition

  • sub — Subject (user/role)
  • obj — Object (module)
  • act — Action (VIEW, CREATE, etc.)
  • dom — Domain (branch)
  • now — Current timestamp

Policy Definition

  • sub — Role identifier
  • obj — Module permission code
  • act — Allowed action
  • dom — Branch scope (* = all)
  • start/end_time — Validity period

canAccess Helper Function

The canAccess() helper function is the primary way to check permissions. It resolves the user's roles, considers branch context, and validates time-based constraints.

app/Helpers/CasbinHelper.php

function canAccess(string $module, string|array $action): bool
{
    $user = Auth::user();
    if (!$user) return false;

    // Build subject identifiers
    $userSub = "user:{$user->id}";
    $roleSub = Enforcer::getRolesForUser($userSub)[0] ?? null;
    
    // Build object identifier
    $obj = "module:{$module}";
    
    // Get branch context
    $branch = session("active_branch")["id"] ?? null;
    $branch = $branch ? "branch:{$branch}" : null;

    // Current timestamp for time-based checks
    $now = now()->toDateTimeString();

    // Custom matcher with time validation
    $matcher = 'g(r.sub, p.sub) && r.obj == p.obj && ...';
    
    return Enforcer::enforceWithMatcher(
        $matcher, 
        $userSub, 
        $obj, 
        $action, 
        $branch, 
        $now
    );
}

Single Action Check

if (canAccess('TRANSFERS', 'CREATE')) {
    // User can create transfers
}

Multiple Actions Check

if (canAccess('TRANSFERS', ['VIEW', 'CREATE'])) {
    // User has ANY of the actions
}

Route Middleware

Routes are protected using the casbin middleware. This middleware automatically checks permissions before allowing access to the route.

Route Protection Examples

use App\Livewire\Inventory\Form\Transfers\{Index, Create, Edit, Show};

Route::middleware(['auth', 'verified'])->group(function () {
    
    // List view - requires VIEW permission
    Route::get('/transfers', Index::class)
        ->middleware('casbin:TRANSFERS,VIEW');
    
    // Create form - requires CREATE permission
    Route::get('/transfers/create', Create::class)
        ->middleware('casbin:TRANSFERS,CREATE');
    
    // Edit form - requires UPDATE permission
    Route::get('/transfers/{id}/edit', Edit::class)
        ->middleware('casbin:TRANSFERS,UPDATE');
    
    // Detail view - requires VIEW permission
    Route::get('/transfers/{id}', Show::class)
        ->middleware('casbin:TRANSFERS,VIEW');
        
});

Middleware Format

The middleware format is casbin:MODULE,ACTION where MODULE is the permission code (UPPERCASE) and ACTION is the operation (VIEW, CREATE, UPDATE, DELETE, APPROVE).

Blade Directives

Use the @canAccess directive in Blade templates to conditionally render UI elements based on user permissions.

Menu Items Example

<!-- Single permission check -->
@canAccess('TRANSFERS', 'VIEW')
    <x-mary-menu-item title="All Transfers" link="/transfers" />
@endcanAccess

<!-- Multiple permissions - show if user has ANY of these -->
@canAccess('TRANSFERS', ['VIEW', 'CREATE', 'UPDATE', 'DELETE'])
    <x-mary-menu-sub title="Stock Transfers">
        
        @canAccess('TRANSFERS', 'VIEW')
            <x-mary-menu-item title="All Transfers" link="/transfers" />
        @endcanAccess
        
        @canAccess('TRANSFERS', 'CREATE')
            <x-mary-menu-item title="Create Transfer" link="/transfers/create" />
        @endcanAccess
        
    </x-mary-menu-sub>
@endcanAccess

Action Buttons Example

<!-- Conditional action buttons in a table -->
<div class="flex gap-2">
    @canAccess('TRANSFERS', 'UPDATE')
        <x-mary-button 
            icon="pencil" 
            link="/transfers/{{ $transfer->id }}/edit" 
            class="btn-sm" 
        />
    @endcanAccess
    
    @canAccess('TRANSFERS', 'DELETE')
        <x-mary-button 
            icon="trash" 
            wire:click="delete({{ $transfer->id }})" 
            class="btn-sm btn-error" 
        />
    @endcanAccess
</div>

Permission Seeding

Permissions must be defined in seeders to be available in the system. This involves two critical files: PermissionSeeder for defining permissions and UserSeeder for mapping them to roles.

database/seeders/Core/PermissionSeeder.php

$permissions = [
    [
        'permission_name' => 'Transfer Management',
        'permission_code' => 'TRANSFERS',  // ← UPPERCASE!
        'description' => 'Manage inter-warehouse stock transfers',
        'is_active' => true,
    ],
    [
        'permission_name' => 'Purchase Invoice Management',
        'permission_code' => 'PURCHASE_INVOICES',
        'description' => 'Manage purchase invoices',
        'is_active' => true,
    ],
    // ... more permissions
];

database/seeders/Core/UserSeeder.php

$policies = [
    'SUPER_ADMIN' => [
        'TRANSFERS' => ['VIEW', 'CREATE', 'UPDATE', 'DELETE', 'APPROVE'],
        'PURCHASE_INVOICES' => ['VIEW', 'CREATE', 'UPDATE', 'DELETE'],
        'VOUCHERS' => ['VIEW', 'CREATE', 'UPDATE', 'DELETE'],
        // ... full access
    ],
    'ADMIN' => [
        'TRANSFERS' => ['VIEW', 'CREATE', 'UPDATE'],  // No DELETE
        'PURCHASE_INVOICES' => ['VIEW', 'CREATE'],
    ],
    'WAREHOUSE_STAFF' => [
        'TRANSFERS' => ['VIEW'],  // View only
    ],
];

Critical: Run Seeders After Changes

After updating seeders, you must run them for changes to take effect:

php artisan db:seed --class=Core\\PermissionSeeder
php artisan db:seed --class=Core\\UserSeeder

Naming Conventions

Type Convention Example
Permission Codes UPPER_SNAKE_CASE TRANSFERS, PURCHASE_INVOICES
Actions UPPERCASE VIEW, CREATE, UPDATE, DELETE, APPROVE
Role Names UPPER_SNAKE_CASE SUPER_ADMIN, WAREHOUSE_STAFF
User Subject user:{id} user:1, user:42
Module Object module:{code} module:TRANSFERS
Branch Domain branch:{id} branch:1, branch:* (all)

Common Pitfall

Using lowercase permission codes like transfers instead of TRANSFERS will cause authorization to fail silently. Always use UPPERCASE for permission codes and actions.