Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Intelligence View
⚡ tsecurity.de Intelligence

Role-Based Access Control in CodeIgniter 4 — A Practical Guide

Difficulty: Intermediate | Read time: 14 min | Framework: CodeIgniter 4

Why RBAC Matters in Real Projects

Every project I've built beyond a simple blog has needed access control. Not just "is the user logged in?" — but "can this user do this specific thing?"

Without a proper RBAC system you end up with scattered if ($user->is_admin) checks everywhere, and one missed check means a security hole.

In this tutorial I'll show you how I built a clean, reusable RBAC system for an internal CRM — covering roles, permissions, middleware, and view-level gating.

What we're building: A role and permission system where each user has a role (Admin, Manager, Staff), each role has specific permissions, and access is enforced at the route, controller, and view level.

Database Structure

Three tables — users, roles, and permissions.

CREATE TABLE `tblroles` (
  `id`          INT AUTO_INCREMENT PRIMARY KEY,
  `name`        VARCHAR(100) NOT NULL UNIQUE,
  `description` VARCHAR(255),
  `created_at`  DATETIME DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE `tblpermissions` (
  `id`          INT AUTO_INCREMENT PRIMARY KEY,
  `name`        VARCHAR(100) NOT NULL UNIQUE,
  `module`      VARCHAR(100),
  `description` VARCHAR(255)
);

CREATE TABLE `tblrole_permissions` (
  `role_id`       INT NOT NULL,
  `permission_id` INT NOT NULL,
  PRIMARY KEY (`role_id`, `permission_id`),
  FOREIGN KEY (`role_id`)       REFERENCES `tblroles`(`id`) ON DELETE CASCADE,
  FOREIGN KEY (`permission_id`) REFERENCES `tblpermissions`(`id`) ON DELETE CASCADE
);

-- Add role_id to your staff/users table
ALTER TABLE `tblstaff` ADD COLUMN `role_id` INT NULL REFERENCES `tblroles`(`id`);

Seed Some Default Data

-- Roles
INSERT INTO `tblroles` (`name`, `description`) VALUES
('Admin',   'Full access to everything'),
('Manager', 'Can manage staff and view reports'),
('Staff',   'Limited access to assigned tasks only');

-- Permissions
INSERT INTO `tblpermissions` (`name`, `module`) VALUES
('view_dashboard',   'dashboard'),
('manage_users',     'users'),
('view_reports',     'reports'),
('export_reports',   'reports'),
('manage_tickets',   'tickets'),
('view_tickets',     'tickets'),
('manage_leads',     'leads'),
('view_leads',       'leads');

-- Assign all permissions to Admin (role_id = 1)
INSERT INTO `tblrole_permissions` (`role_id`, `permission_id`)
SELECT 1, id FROM `tblpermissions`;

-- Manager gets view + manage reports + view leads
INSERT INTO `tblrole_permissions` (`role_id`, `permission_id`)
SELECT 2, id FROM `tblpermissions`
WHERE `name` IN ('view_dashboard','view_reports','export_reports','view_leads','view_tickets');

-- Staff gets minimal access
INSERT INTO `tblrole_permissions` (`role_id`, `permission_id`)
SELECT 3, id FROM `tblpermissions`
WHERE `name` IN ('view_dashboard','view_tickets','view_leads');

The Permission Helper

This is the core of the system — a single function you'll call everywhere.

<?php
// app/Helpers/permission_helper.php

/**
 * Check if the currently logged-in user has a specific permission.
 */
function has_permission(string $permission): bool
{
    $session     = session();
    $permissions = $session->get('permissions') ?? [];

    return in_array($permission, $permissions);
}

/**
 * Check if the user has ANY of the given permissions.
 */
function has_any_permission(array $permissions): bool
{
    foreach ($permissions as $permission) {
        if (has_permission($permission)) {
            return true;
        }
    }
    return false;
}

/**
 * Abort with 403 if user doesn't have permission.
 */
function require_permission(string $permission): void
{
    if (!has_permission($permission)) {
        throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
    }
}

Load it in app/Config/Autoload.php:

public $helpers = ['url', 'form', 'permission'];

Load Permissions Into Session at Login

After the user logs in, load their permissions into the session so every check is fast (no DB query per request).

<?php
// app/Controllers/Auth.php (your login controller)

private function loadUserPermissions(int $staffId): void
{
    $db = \Config\Database::connect();

    $permissions = $db->table('tblrole_permissions rp')
        ->select('p.name')
        ->join('tblpermissions p', 'p.id = rp.permission_id')
        ->join('tblstaff s',       's.role_id = rp.role_id')
        ->where('s.staffid', $staffId)
        ->get()
        ->getResultArray();

    $permissionNames = array_column($permissions, 'name');

    session()->set([
        'staff_id'    => $staffId,
        'permissions' => $permissionNames,
        'role'        => $this->getUserRole($staffId),
    ]);
}

private function getUserRole(int $staffId): string
{
    $db = \Config\Database::connect();

    $row = $db->table('tblstaff s')
        ->select('r.name')
        ->join('tblroles r', 'r.id = s.role_id')
        ->where('s.staffid', $staffId)
        ->get()->getRowArray();

    return $row['name'] ?? 'Staff';
}

Call $this->loadUserPermissions($staffId) right after successful login verification.

The RBAC Middleware (Filter)

Create a filter that protects entire route groups.

<?php
// app/Filters/RBACFilter.php
namespace App\Filters;

use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\FilterInterface;

class RBACFilter implements FilterInterface
{
    public function before(RequestInterface $request, $arguments = null)
    {
        // Not logged in at all
        if (!session()->get('staff_id')) {
            return redirect()->to(base_url('login'));
        }

        // No permission argument passed — just check login
        if (empty($arguments)) {
            return;
        }

        // Check each required permission
        $userPermissions = session()->get('permissions') ?? [];

        foreach ($arguments as $required) {
            if (!in_array($required, $userPermissions)) {
                // Redirect to 403 page
                return redirect()->to(base_url('errors/permission_denied'));
            }
        }
    }

    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
    {
        // Nothing needed after
    }
}

Register it in app/Config/Filters.php:

public array $aliases = [
    'csrf'     => CSRF::class,
    'toolbar'  => DebugToolbar::class,
    'honeypot' => Honeypot::class,
    'rbac'     => \App\Filters\RBACFilter::class,  // <-- add this
];

Protect Routes With the Filter

// app/Config/Routes.php

// Anyone logged in
$routes->group('dashboard', ['filter' => 'rbac'], function ($routes) {
    $routes->get('/', 'Dashboard::index');
});

// Only users with manage_users permission
$routes->group('users', ['filter' => 'rbac:manage_users'], function ($routes) {
    $routes->get('/',        'Users::index');
    $routes->get('create',   'Users::create');
    $routes->post('store',   'Users::store');
    $routes->get('edit/(:num)',   'Users::edit/$1');
    $routes->post('update/(:num)', 'Users::update/$1');
    $routes->post('delete/(:num)', 'Users::delete/$1');
});

// Reports — needs view_reports
$routes->group('reports', ['filter' => 'rbac:view_reports'], function ($routes) {
    $routes->get('/',       'Reports::index');
    $routes->get('export',  'Reports::export', ['filter' => 'rbac:export_reports']);
});

// Tickets
$routes->group('tickets', ['filter' => 'rbac:view_tickets'], function ($routes) {
    $routes->get('/', 'Tickets::index');
    $routes->get('manage', 'Tickets::manage', ['filter' => 'rbac:manage_tickets']);
});

Controller-Level Check

For finer control inside a controller method:

<?php
// app/Controllers/Reports.php
namespace App\Controllers;

class Reports extends BaseController
{
    public function export(): \CodeIgniter\HTTP\ResponseInterface
    {
        // Extra check at controller level
        if (!has_permission('export_reports')) {
            return redirect()->to(base_url('errors/permission_denied'));
        }

        // ... export logic
    }

    public function delete(int $id): \CodeIgniter\HTTP\ResponseInterface
    {
        // Use the helper shortcut — throws 404 if no permission
        require_permission('manage_reports');

        // ... delete logic
    }
}

View-Level Gating

Hide buttons and menu items the user can't access:

<!-- Sidebar navigation — show only what the user can access -->
<ul class="sidebar-menu">

    <li>
        <a href="<?= base_url('dashboard') ?>">
            <i class="fas fa-home"></i> Dashboard
        </a>
    </li>

    <?php if (has_permission('manage_users')): ?>
    <li>
        <a href="<?= base_url('users') ?>">
            <i class="fas fa-users"></i> Users
        </a>
    </li>
    <?php endif; ?>

    <?php if (has_permission('view_reports')): ?>
    <li>
        <a href="<?= base_url('reports') ?>">
            <i class="fas fa-chart-bar"></i> Reports
        </a>
    </li>
    <?php endif; ?>

    <?php if (has_permission('view_tickets')): ?>
    <li>
        <a href="<?= base_url('tickets') ?>">
            <i class="fas fa-ticket-alt"></i> Tickets
        </a>
    </li>
    <?php endif; ?>

</ul>

<!-- Hide export button from users without permission -->
<?php if (has_permission('export_reports')): ?>
<a href="<?= base_url('reports/export') ?>" class="btn btn-success">
    <i class="fas fa-download"></i> Export CSV
</a>
<?php endif; ?>

The 403 Permission Denied Page

Create a clean error page instead of crashing:

<?php
// app/Controllers/Errors.php
namespace App\Controllers;

class Errors extends BaseController
{
    public function permissionDenied(): string
    {
        return view('errors/permission_denied');
    }
}
<!-- app/Views/errors/permission_denied.php -->
<div class="container text-center py-5">
    <div class="mb-4">
        <i class="fas fa-lock fa-4x text-danger"></i>
    </div>
    <h2 class="fw-bold">Access Denied</h2>
    <p class="text-muted">
        You don't have permission to view this page.<br>
        Contact your administrator if you think this is a mistake.
    </p>
    <a href="<?= base_url('dashboard') ?>" class="btn btn-primary mt-3">
        Go to Dashboard
    </a>
</div>

Add the route:

$routes->get('errors/permission_denied', 'Errors::permissionDenied');

Common Pitfalls

⚠️ 1. Checking permissions on every request with a DB query

Don't query the DB on every request to check permissions — it kills performance. Load permissions into the session at login and check from there. That's what we did above with session()->get('permissions').

⚠️ 2. Forgetting to refresh permissions after role change

If an admin changes a user's role, that user's session still has old permissions until they log out. Fix this by forcing a session refresh when role changes:

// In your Users controller — after updating role
if ($userId == session()->get('staff_id')) {
    $this->loadUserPermissions($userId); // Refresh own session
}

⚠️ 3. Hiding buttons is NOT enough security

Never rely on just hiding buttons in the view. Always check permissions in the controller or filter too. A user can manually type a URL and bypass view-level checks.

⚠️ 4. Route filter not applying

If your filter isn't working, make sure the filter is registered in Filters.php AND CodeIgniter's filters are enabled. Check app/Config/Filters.php for the $globals or $methods array too.

What to Build Next

  • ABAC (Attribute-Based Access Control) — go beyond roles, add conditions like "manager can only see their own team's data"
  • Role management UI — let admins create/edit roles and assign permissions from a dashboard
  • Permission audit log — track who accessed what and when
  • API route protection — extend the filter to check Bearer tokens with permissions

Conclusion

A solid RBAC system isn't hard to build in CodeIgniter 4 — it just needs to be done right from the start. Load permissions into the session, use a filter for route groups, and gate at the view level for UI polish.

Your controllers stay clean, your routes are self-documenting, and you never have another scattered is_admin check again.

Follow me for more CodeIgniter production tutorials — 3 new articles every week. 🙌

Senior PHP Developer · 12+ years building production systems on CodeIgniter, Laravel & WordPress

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Role-Based Access Control in CodeIgniter 4 — A Practical Guide

Thematisch verwandte Begriffe: RoleBased, Access, Control, CodeIgniter · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-61591 | djust provides Phoenix LiveView-style reactive server-side rendering for…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Radar › Alle Kategorien
Alle aus Alle Kategorien 2.659.488
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Rechts: Artikel Ziehen Links: RSS
Hoch: nächster Artikel Runter: zurück / schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Rechts: Original Links: RSS-Ansicht
↗ Original-Quelle
Social Reaktionen Stimme abgeben (+5 Karma)
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick