Authorization
The starter implements a role-based access control (RBAC) system built on Spatie Laravel Permission with extensive enhancements — role types, permission metadata, auditable role changes, and API role segregation.
How It Works
Section titled “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
Section titled “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
Section titled “When to Use Each Type”- System Managed — Reserved for roles the starter ships with. Their definitions (name, permissions) are managed by seeders and cannot be edited or deleted through the admin panel. However, 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
Section titled “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
Section titled “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
Section titled “What It Is”When a role is assignment-locked, it cannot be attached to or detached from users through Filament. The role is exclusively managed through programmatic processes (SSO provisioning, third-party API integrations, scheduled commands).
Distinction from System Managed
Section titled “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
Section titled “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
Section titled “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
Section titled “Programmatic Assignment”The audited methods (assignRoleWithAudit, removeRoleWithAudit) work normally 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 is trusted and can assign or remove locked roles freely.
Default Locked Roles
Section titled “Default Locked Roles”- Northwestern User — assignment-locked (managed by SSO provisioning)
- Super Administrator — intentionally not assignment-locked for onboarding
Emergency Detachment
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Audited Role Changes”The starter makes Spatie’s built-in assignRole() and removeRole() methods private on the User model, forcing all role changes to go through audited methods instead.
assignRoleWithAudit()
Section titled “assignRoleWithAudit()”$user->assignRoleWithAudit( roles: $role, origin: RoleModificationOrigin::UiAction, context: ['reason' => 'Promoted to team lead'],);removeRoleWithAudit()
Section titled “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
Section titled “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 always allowed — the lock is not enforced at the domain layer.
Audited Permission Sync
Section titled “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
Section titled “Role-Specific Permission Checks”The TracksPermissionSources trait on the User model enables fine-grained checks when you need to know 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
Section titled “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 |