Chapter 10: The Roles & Permissions System (RBAC)

1. Introduction: Role-Based Access Control (RBAC)

A critical part of any non-trivial application is controlling what users are allowed to see and do. We use a far more powerful and scalable solution than simple checks. A Role-Based Access Control (RBAC), where we can assign permissions to roles, and roles to users.

In this system, we do not assign permissions directly to users. Instead:

  1. We assign Permissions (e.g., 'admin.view') to Roles (e.g., "Editor").
  2. We assign Roles to Users.

This system allows us to manage access for thousands of users by simply changing a few roles.


2. The Database Schema (In Detail)

We built the RBAC system on a foundation of interconnected tables. We designed this structure to be robust and flexible; it supports two key advanced features: multi-tenancy and hierarchical roles. To distinguish security data from business data, we prefix all RBAC tables with rbac_.

Multi-Tenancy with Organisations

The rbac_organisations table is the key to our multi-tenancy model. It allows us to scope users, roles, and permissions to a specific organisation, which means a user who is an "Administrator" in one organisation will have no special privileges in another.

CREATE TABLE rbac_organisations (
    orgId INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);

Hierarchical Roles

The parentRoleId column in the rbac_roles table enables role inheritance, allowing us to create a hierarchy in which a "child" role automatically inherits all the permissions of its "parent." For example, an "Editor" role could inherit all the permissions of a "Contributor" role, plus its own additional permissions.

CREATE TABLE rbac_roles (
    roleId INT AUTO_INCREMENT PRIMARY KEY,
    orgId INT NOT NULL,
    name VARCHAR(255) NOT NULL,
    parentRoleId INT NULL,
    FOREIGN KEY (orgId) REFERENCES rbac_organisations(orgId),
    FOREIGN KEY (parentRoleId) REFERENCES rbac_roles(roleId)
);

Permissions and Joins

The rbac_permissions table defines the specific actions a user can take (e.g., 'forms.edit'). We link these together using join tables:

  • rbac_user2roles: Links a User to a Role.
  • rbac_role2permissions: Links a Role to a Permission.

3. The "Fail-Secure" Logic (The Enforcer)

The PermissionEnforcerTrait enforces a Default Deny policy, which ensures that if a developer forgets to configure a route with a permission, the system automatically blocks access.

  1. Public Check: If the route is marked 'public', allow immediately.
  2. Safety Catch: If the route has NULL permission (our table default), throw a 500 Error.
  3. Guest Check: If the user is a Guest, the system throws a 401 Error ('Unauthorised'). The controller catches this exception and presents the login form with registration links.
  4. Database Check: The user must be logged in at this point, so we perform a live check using the repository.

4. The PHP Implementation (Repository Pattern)

With the database schema established, we turn to the PHP code that will interact with it. To keep our data access logic clean, we use the Repository Pattern. Following the Dependency Inversion Principle, by using a Contract, or "interface", not the concrete Repository Class.

The PermissionRepository

The PermissionRepository contains methods for managing permissions. Its most crucial method gets all permissions for a user, accounting for inherited permissions from parent roles. Note how we use a specific Stored Procedure (rbac_checkUserHasPermission) for efficiency.

namespace Avmoz\infrastructure\persistence;

use Avmoz\domain\permission\PermissionRepositoryInterface;

readonly class PermissionRepository implements PermissionRepositoryInterface
{
    public function __construct(private DatabaseInterface $db) {}

    /**
     * Checks if a user has a specific permission via any of their roles.
     * Uses an optimised Stored Procedure for performance.
     */
    public function userHasPermission(int $userId, string $permissionKey): bool
    {
        // Calls 'rbac_checkUserHasPermission' which joins the rbac_ tables
        // and returns 1 or 0.
        $result = $this->db->spa('rbac_checkUserHasPermission', [$userId, $permissionKey]);
        
        return $result && $result['hasPermission'];
    }
}

The RoleRepository

The RoleRepository handles all logic related to roles, such as fetching the roles assigned to a specific user. It hydrates raw database rows into clean Data Transfer Objects( RoleDTO ).


5. The Admin Interface: The Matrix Editor

To easily manage this complexity, we have built a Role-Permission Matrix.

This grid view allows an administrator to see every permission assignment at a glance. It uses the RolePermissionMatrixModel to fetch data in a single efficient batch and save changes via AJAX.

Conclusion: Secure, Scalable Access Control

By combining a flexible database schema with the Repository Pattern's clean separation, we have built a Role-Based Access Control system that is powerful, secure, and easy to maintain. The database provides a single source of truth for all permissions, and the controller provides a single, consistent point of enforcement.

Shopping summary

Item

SubTotal: £

Empty Basket

this is a hidden panel

Completed in milliseconds

Full Script completed in 9 milliseconds

This is the top Panel

This is the bottom Panel