Authorization
The starter implements a role-based access control (RBAC) system built on Spatie Laravel Permission with added role types, permission metadata, auditable role changes, and API role segregation.
How It Works
-
Permissions define granular actions a user can perform (view users, edit roles, manage API tokens, etc.)
-
Roles are named collections of permissions that are assigned to users
-
Role types categorize roles and control their behavior (system-managed roles are read-only, API roles are restricted to API users)
-
Policies check permissions on specific actions using standard Laravel authorization
Role Types
Every role belongs to a role type that determines how it can be managed and who it can be assigned to.
| Type | Purpose | UI Editable | Assignable To |
|---|---|---|---|
| System Managed | Critical roles managed by code (Super Administrator, Northwestern User) | No (read-only in Filament) | All users (assignable by ManageAll users) |
| Application Admin | Administrative roles with elevated privileges | Yes | SSO and local users |
| Application Role | Standard user roles for day-to-day access | Yes | SSO and local users |
| API Integration | Roles for programmatic API consumers | Yes | API users only |
When to Use Each Type
- System Managed - Reserved for roles the starter ships with. Their definitions (name, permissions) are seeder-managed and cannot be edited or deleted through the admin panel. Users with
ManageAllcan assign and remove system-managed roles from users via the UI (unless the role is also assignment-locked). - Application Admin - For roles that grant administrative capabilities within your application (e.g., “Department Manager”, “Content Editor”). Created and managed by administrators in Filament.
- Application Role - For standard user roles that grant specific feature access (e.g., “Report Viewer”, “Data Entry”). The most common type you’ll create.
- API Integration - For API consumer roles. These can only be assigned to API users and only display API-relevant permissions in the role form.
Default Roles
The starter ships with two system-managed roles, created by RoleSeeder:
| Role | Permissions | Assignment Locked | Assigned When |
|---|---|---|---|
| Super Administrator | All permissions | No | StakeholderSeeder runs for NetIDs in SUPER_ADMIN_NETIDS |
| Northwestern User | None by default | Yes | Automatically on first SSO login |
The Northwestern User role serves as a baseline. It confirms the user has authenticated via SSO but grants no application-specific permissions. Add permissions to this role in RoleSeeder if your application needs default access for all Northwestern users:
[ 'name' => SystemRole::NorthwesternUser, 'role_type_id' => $systemManagedRoleId, 'assignment_locked' => true, 'permissions' => [ // Add default permissions for all Northwestern users here ],],Assignment-Locked Roles
Assignment-locked roles have a per-role assignment_locked flag that prevents manual assignment or removal through the UI, regardless of permission level, including Super Administrators with ManageAll.
What It Is
When a role is assignment-locked, it cannot be attached to or detached from users through Filament. The role is managed through programmatic processes (SSO provisioning, third-party API integrations, scheduled commands).
Distinction from System Managed
These are two independent concepts:
| Combination | Can Edit Definition | Can Assign via UI |
|---|---|---|
| Neither | Yes | Yes |
| System Managed only | No | Yes |
| Assignment Locked only | Yes | No |
| Both | No | No |
- System Managed controls a role’s definition. Its name, permissions, and type are seeder-synced and cannot be edited in the UI.
- Assignment Locked controls a role’s assignment. It cannot be attached to or detached from users in the UI.
Use Cases
- Roles driven by external APIs where the application is not the source of truth
- Roles assigned during SSO provisioning that should never be manually changed
- Roles managed by scheduled sync commands from external systems
How to Enable
Set assignment_locked => true in the role seeder:
Role::updateOrCreate( ['name' => 'My External Role'], [ 'role_type_id' => $roleTypeId, 'assignment_locked' => true, ],);Programmatic Assignment
The audited methods (assignRoleWithAudit, removeRoleWithAudit) work for all origins. The lock is enforced at the UI layer: Filament hides attach/detach actions and filters locked roles from assignment dropdowns. Programmatic code can assign or remove locked roles freely.
Default Locked Roles
- Northwestern User - assignment-locked (managed by SSO provisioning)
- Super Administrator - intentionally not assignment-locked for onboarding
Emergency Detachment
For emergency situations, use the role:force-detach Artisan command, which uses the SYSTEM origin to bypass the lock while preserving the audit trail:
php artisan role:force-detach username "Northwestern User" --reason="Emergency access fix"Permissions
Permissions are defined as cases in SystemPermission, the single source of truth for all permissions in the application. The PermissionSeeder reads this enum to create and update permission records in the database.
Default Permissions
| Permission | Description | System Managed | API Relevant |
|---|---|---|---|
ManageAll | Bypasses all authorization checks | Yes | — |
AccessAdministrationPanel | Access to the Filament admin panel | Yes | — |
ManageImpersonation | Impersonate other users | Yes | — |
ViewUsers | View user profiles and listings | — | Yes |
CreateUsers | Create new user accounts | — | — |
EditUsers | Edit existing user accounts | — | — |
ViewRoles | View role definitions and assignments | — | — |
EditRoles | Create and edit role definitions | — | — |
DeleteRoles | Permanently delete roles | Yes | — |
AssignRoles | Assign and remove roles from users | — | — |
ManageApiUsers | Create and manage API users and tokens | — | — |
ViewAuditLogs | View audit trail entries | Yes | — |
ViewLoginRecords | View login analytics | Yes | — |
ViewSupportTickets | View support ticket submissions | Yes | — |
System Managed permissions are security-sensitive and can only be included in system-managed roles or by users with ManageAll. They appear in a separate “Sensitive Permissions” section in the role form.
API Relevant permissions are the only ones shown when editing an API Integration role.
Adding a New Permission
-
Add a case to
SystemPermissionapp/Domains/Auth/Enums/SystemPermission.php case ViewReports = 'view-reports'; -
Update the metadata methods
Three
matchblocks need the new case:// Is this permission too sensitive for non-super-admins to assign?public function isSystemManaged(): bool{return match ($this) {// ...existing cases...self::ViewReports => false,default => false,};}// Should this permission appear in API Integration role forms?public function isApiRelevant(): bool{return match ($this) {// ...existing cases...self::ViewReports => true, // if API consumers need thisdefault => false,};}// System-wide or limited to owned resources?public function scope(): PermissionScope{return match ($this) {// ...existing cases...default => PermissionScope::SystemWide,};}The
getLabel()method auto-generates a label from the value (e.g.,'view-reports'→"View Reports"). Add an explicit case only if the auto-generated label isn’t right. -
Add a description
public function description(): string{return match ($this) {// ...existing cases...self::ViewReports => 'View reporting dashboards and export report data.',};} -
Run the seeder
Terminal window php artisan db:seed --class=PermissionSeederThe seeder creates new permissions and updates metadata on existing ones (idempotent).
Super Administrators
Users listed in SUPER_ADMIN_NETIDS receive the Super Administrator role (which includes ManageAll) via StakeholderSeeder:
SUPER_ADMIN_NETIDS=abc123,xyz789,def456The ManageAll permission triggers a Gate::before() hook that short-circuits all authorization. Every $this->authorize(), @can, and Gate::allows() call returns true.
Audited Role Changes
The starter makes Spatie’s assignRole(), removeRole(), and syncRoles() methods private on the User model, forcing all role changes through audited methods.
assignRoleWithAudit()
$user->assignRoleWithAudit( roles: $role, origin: RoleModificationOrigin::UiAction, context: ['reason' => 'Promoted to team lead'],);removeRoleWithAudit()
$user->removeRoleWithAudit( roles: $role, origin: RoleModificationOrigin::NetIdStatusChange, context: ['netid_action' => 'deactivate'],);Both methods create an audit entry capturing:
- Before state - all roles the user had before the change
- After state - all roles the user has after the change
- Origin - why the change happened
- Context - optional additional metadata
Origin Types
The RoleModificationOrigin tracks why a role change occurred:
| Origin | Manual | Used When |
|---|---|---|
UiAction | Yes | Admin assigns/removes a role in Filament |
SsoProvisioning | No | Northwestern User role assigned on first SSO login |
NetIdStatusChange | No | Roles stripped when a NetID is deactivated via EventHub |
RemovedByDeletion | No | Roles removed from users when a role is deleted |
System | No | Programmatic changes (seeders, Artisan commands) |
The Filament UI prevents manual assignment and removal for assignment-locked roles. Programmatic origins are allowed; the lock is not enforced at the domain layer.
Audited Permission Sync
When editing a role’s permissions in Filament, syncPermissionsWithAudit() creates an audit entry with before/after snapshots of the permission set. An audit entry is only created when the permissions actually change.
Role-Specific Permission Checks
The TracksPermissionSources trait on the User model lets you check not just if a user has a permission, but which role grants it.
| Method | Returns | Description |
|---|---|---|
hasPermissionFromRole($permission, $role) | bool | Check if a specific role grants a permission to the user |
getRolesWithPermission($permission) | Collection<Role> | Get all of the user’s roles that grant a permission |
getPermissionsFromRole($role) | Collection<SystemPermission> | Get all permissions the user has from a specific role |
Key Classes
| Class | Purpose |
|---|---|
SystemPermission | Single source of truth for all permissions (cases, metadata, seeder) |
RoleTypeEnum | Defines role type categories and their behavioral rules |
SystemRole | Names for the two system-managed roles |
RoleModificationOrigin | Tracks why role changes occurred, classifies manual vs. programmatic |
AuditsRoles | Trait providing assignRoleWithAudit() / removeRoleWithAudit() |
AuditsPermissions | Trait providing syncPermissionsWithAudit() |
TracksPermissionSources | Trait for querying which roles grant which permissions |
RoleSeeder | Creates system roles with their permissions |
PermissionSeeder | Syncs SystemPermission cases to the database |