Skip to content

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.

  1. Permissions define granular actions a user can perform (view users, edit roles, manage API tokens, etc.)

  2. Roles are named collections of permissions that are assigned to users

  3. Role types categorize roles and control their behavior (system-managed roles are read-only, API roles are restricted to API users)

  4. Policies check permissions on specific actions using standard Laravel authorization


Every role belongs to a role type that determines how it can be managed and who it can be assigned to.

TypePurposeUI EditableAssignable To
System ManagedCritical roles managed by code (Super Administrator, Northwestern User)No — read-only in FilamentAll users (assignable by ManageAll users)
Application AdminAdministrative roles with elevated privilegesYesSSO and local users
Application RoleStandard user roles for day-to-day accessYesSSO and local users
API IntegrationRoles for programmatic API consumersYesAPI users only
  • 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 ManageAll can 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.

The starter ships with two system-managed roles, created by RoleSeeder:

RolePermissionsAssignment LockedAssigned When
Super AdministratorAll permissionsNoStakeholderSeeder runs for NetIDs in SUPER_ADMIN_NETIDS
Northwestern UserNone by defaultYesAutomatically 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:

app/Domains/Auth/Seeders/RoleSeeder.php
[
'name' => SystemRole::NorthwesternUser,
'role_type_id' => $systemManagedRoleId,
'assignment_locked' => true,
'permissions' => [
// Add default permissions for all Northwestern users here
],
],

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.

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).

These are two independent concepts:

CombinationCan Edit DefinitionCan Assign via UI
NeitherYesYes
System Managed onlyNoYes
Assignment Locked onlyYesNo
BothNoNo
  • 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.
  • 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

Set assignment_locked => true in the role seeder:

app/Domains/Auth/Seeders/RoleSeeder.php
Role::updateOrCreate(
['name' => 'My External Role'],
[
'role_type_id' => $roleTypeId,
'assignment_locked' => true,
],
);

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.

  • Northwestern User — assignment-locked (managed by SSO provisioning)
  • Super Administrator — intentionally not assignment-locked for onboarding

For emergency situations, use the role:force-detach Artisan command, which uses the SYSTEM origin to bypass the lock while preserving the audit trail:

Terminal window
php artisan role:force-detach username "Northwestern User" --reason="Emergency access fix"

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.

PermissionDescriptionSystem ManagedAPI Relevant
ManageAllBypasses all authorization checksYes
AccessAdministrationPanelAccess to the Filament admin panelYes
ManageImpersonationImpersonate other usersYes
ViewUsersView user profiles and listingsYes
CreateUsersCreate new user accounts
EditUsersEdit existing user accounts
ViewRolesView role definitions and assignments
EditRolesCreate and edit role definitions
DeleteRolesPermanently delete rolesYes
AssignRolesAssign and remove roles from users
ManageApiUsersCreate and manage API users and tokens
ViewAuditLogsView audit trail entriesYes
ViewLoginRecordsView login analyticsYes
ViewSupportTicketsView support ticket submissionsYes

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.

  1. Add a case to SystemPermission

    app/Domains/Auth/Enums/SystemPermission.php
    case ViewReports = 'view-reports';
  2. Update the metadata methods

    Three match blocks 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 this
    default => 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.

  3. Add a description

    public function description(): string
    {
    return match ($this) {
    // ...existing cases...
    self::ViewReports => 'View reporting dashboards and export report data.',
    };
    }
  4. Run the seeder

    Terminal window
    php artisan db:seed --class=PermissionSeeder

    The seeder creates new permissions and updates metadata on existing ones (idempotent).


Users listed in SUPER_ADMIN_NETIDS receive the Super Administrator role (which includes ManageAll) via StakeholderSeeder:

.env
SUPER_ADMIN_NETIDS=abc123,xyz789,def456

The ManageAll permission triggers a Gate::before() hook that short-circuits all authorization. Every $this->authorize(), @can, and Gate::allows() call returns true.


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.

$user->assignRoleWithAudit(
roles: $role,
origin: RoleModificationOrigin::UiAction,
context: ['reason' => 'Promoted to team lead'],
);
$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

The RoleModificationOrigin tracks why a role change occurred:

OriginManualUsed When
UiActionYesAdmin assigns/removes a role in Filament
SsoProvisioningNoNorthwestern User role assigned on first SSO login
NetIdStatusChangeNoRoles stripped when a NetID is deactivated via EventHub
RemovedByDeletionNoRoles removed from users when a role is deleted
SystemNoProgrammatic 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.

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.


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.

MethodReturnsDescription
hasPermissionFromRole($permission, $role)boolCheck 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

ClassPurpose
SystemPermissionSingle source of truth for all permissions (cases, metadata, seeder)
RoleTypeEnumDefines role type categories and their behavioral rules
SystemRoleNames for the two system-managed roles
RoleModificationOriginTracks why role changes occurred, classifies manual vs. programmatic
AuditsRolesTrait providing assignRoleWithAudit() / removeRoleWithAudit()
AuditsPermissionsTrait providing syncPermissionsWithAudit()
TracksPermissionSourcesTrait for querying which roles grant which permissions
RoleSeederCreates system roles with their permissions
PermissionSeederSyncs SystemPermission cases to the database