Core Concepts

How SystemRoles Works

On this page

This page explains the ideas behind SystemRoles. Read it once before you design your roles.

The five building blocks#

Block Table One-line summary
Role system_roles A named job, such as Admin or Manager
Permission system_roles_in A named right, switched on or off for each role
User role user_roles The one role a user has
Controller access system_classes_in Whether a role may use a controller
Route access system_routes_in Whether a role may use a route

Menus (system_menus*) are a sixth, experimental block. See Menus.

Names and guard names#

Roles and permissions each have two names:

  • a name, shown to people (Super Admin, Publish articles);
  • a guard name, used in code (super_admin, article.publish).

Both must be unique: role names across all roles, and permission names within one role. roleHas() accepts either the guard name or the name, so you can check roleHas($roleId, 'article.publish').

The term "guard name" here is SystemRoles' own. It is not related to Laravel authentication guards (web, api).

The permission matrix#

A permission is not one row shared by all roles. Each role has its own row for every permission, with a switch:

in_role Meaning
1 On: the role has the permission
0 Off: the role does not have it
2 Reserved, not used by the package

Example with three roles and two permissions, six rows in system_roles_in:

role article.publish article.delete
Admin 1 1
Editor 1 0
User 0 0

The package keeps the matrix complete:

  • When you add a permission to one role, every other role gets the same permission, switched off.
  • When you create a role, it gets every existing permission, switched off (switched on for the super admin).
  • When you open a role's Manage screen, any missing permissions are filled in, switched off.

So you never have to wonder whether a row is missing. Missing and off mean the same thing.

One role per user#

Each user has one row in user_roles. Its user_role column holds the system_roles.role_id.

  • Assigning a new role updates that row. It never adds a second one.
  • Every assignment writes a row to user_roles_history: who got which role, and which admin gave it (role_admin).
  • A user with no row has role id 0, which has no permissions.

If your application needs a user to hold several roles at once, model the combination as its own role (for example "Editor + Reviewer").

The super admin#

One role is the super admin. Its id is set by super_admin_role_id (default 1).

The super admin:

  • passes every permission, controller and route check, whatever the matrix says;
  • is the only role allowed into /systemroles/admin.

Keep the number of super admin users small. Use an Admin role, with permissions switched on, for day-to-day administrators.

Who enforces what#

SystemRoles stores the rules and gives you helpers to read them. It enforces them in one place only:

Area Enforced by the package?
/systemroles/admin/* Yes, by the super_admin middleware
Your application's routes and controllers No. You add checks with the helpers

This is deliberate. Each app protects its routes differently (middleware, policies, controller checks, Blade). The package gives you the data and the helpers, and you choose where to call them. The Protecting Routes with Middleware page has ready-to-use middleware.

Three levels of control#

You can use any combination of these:

  1. Permissions (most common). Check a named right: roleHas($roleId, 'invoice.approve').
  2. Controller access. Allow or deny a role on a whole controller: checkSystemClassesInit($roleId, InvoiceController::class).
  3. Route access. Allow or deny a role on a single route, using the system_routes_in table.

Permissions are the most flexible and are the recommended starting point. Controller and route access are useful when you want an administrator to lock whole areas without code changes.

A request, step by step#

With the middleware from this documentation in place:

  1. The user requests POST /articles/5/publish.
  2. auth confirms they are logged in.
  3. permission:article.publish calls SystemRolesService::roleIdForUser($userId) and gets, for example, 3 (Editor).
  4. It calls SystemRolesService::roleHas(3, 'article.publish'), which looks for a row in system_roles_in with role_id = 3, in_guard_name or in_name = article.publish, and in_role = '1'.
  5. Found: the controller runs. Not found: 403.