惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
宝玉的分享
宝玉的分享
月光博客
月光博客
小众软件
小众软件
Jina AI
Jina AI
WordPress大学
WordPress大学
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 【当耐特】
博客园 - 三生石上(FineUI控件)
博客园 - 司徒正美
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
G
Google Developers Blog
M
MIT News - Artificial intelligence
N
Netflix TechBlog - Medium
云风的 BLOG
云风的 BLOG
MyScale Blog
MyScale Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Role-Based Access Control in CodeIgniter 4 — A Practical ...
sunakshi Thakur · 2026-06-27 · via DEV Community

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