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_ |
A named job, such as Admin or Manager |
| Permission | system_ |
A named right, switched on or off for each role |
| User role | user_ |
The one role a user has |
| Controller access | system_ |
Whether a role may use a controller |
| Route access | system_ |
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. |
article. |
|---|---|---|
| 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? |
|---|---|
/ |
Yes, by the super_ 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:
- Permissions (most common). Check a named right:
roleHas($roleId, 'invoice.approve'). - Controller access. Allow or deny a role on a whole controller:
checkSystemClassesInit($roleId, InvoiceController::class). - Route access. Allow or deny a role on a single route, using the
system_routes_intable.
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:
- The user requests
POST /articles/5/publish. authconfirms they are logged in.permission:article.publishcallsSystemRolesService::roleIdForUser($userId)and gets, for example,3(Editor).- It calls
SystemRolesService::roleHas(3, 'article.publish'), which looks for a row insystem_roles_inwithrole_id = 3,in_guard_nameorin_name=article.publish, andin_role = '1'. - Found: the controller runs. Not found:
403.