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 identifierobj— Module permission codeact— Allowed actiondom— 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.