# RBAC Audit Report

Date: 2026-05-08

## Status

The platform already uses `spatie/laravel-permission`, and the standard Spatie tables exist through `database/migrations/2026_01_08_191302_create_permission_tables.php`.

The previous RBAC state was partial and inconsistent:

- `User` uses Spatie `HasRoles`, but the legacy `users.role` column is still used in middleware, policies, menus, and redirects.
- Role names were inconsistent: `super_admin` and `super-admin`, plus `teacher` and `instructor`.
- Permissions were spread across many seeders with different naming conventions, for example `view payments`, `finance_approve`, `admin.courses.view`, and newer dotted permissions.
- Several admin routes were protected by broad `EnsureUserIsAdmin` checks rather than action-level permissions.
- Some Livewire components had direct authorization, but coverage was uneven.

Overall status after this pass: **partially remediated**. The core RBAC source of truth, security management pages, and critical payment approval policy are now in place. A broader second pass should continue converting older admin routes from coarse admin checks to exact permissions.

## Evidence

### Existing RBAC package

- `composer.json` includes `spatie/laravel-permission`.
- `app/Models/User.php` uses `Spatie\Permission\Traits\HasRoles`.
- `bootstrap/app.php` registers Spatie middleware aliases: `role`, `permission`, and `role_or_permission`.

### Current route protection

- `/portal/*` is routed through `auth`, `student_portal`, and `profile_completed`.
- `/teacher/*` and `/instructor/*` are routed through `auth` and `InstructorPortalMiddleware`.
- `EnsureUserIsAdmin` now keeps legacy admin protection, but it passes through routes that declare explicit `can:*` middleware so the exact permission can decide access.
- Many high-value `/admin/*` routes now use exact permissions instead of broad admin-only checks.
- New security routes are protected by exact permissions:
  - `/admin/security/roles` requires `roles.view`.
  - `/admin/security/roles/{role}/edit` requires `roles.update`.
  - `/admin/security/users` requires `users.view`.
  - `/admin/security/audit` requires `roles.view`.
- Follow-up routes were moved from role-only admin/finance access to exact permissions:
  - `/admin/registrations` requires `enrollments.view`.
  - `/admin/registrations/pipeline` requires `enrollments.view`.
  - `/admin/registrations/create` requires `enrollments.create`.
  - `/admin/registrations/{id}/edit` requires `enrollments.update`.
  - `/admin/payments/review` requires `payments.view`.
  - `/admin/finance/pre-enrollment-payments` requires `payments.view`.
- Operational admin routes now include exact permission middleware:
  - `/admin/courses*` requires `courses.view/create/update`.
  - `/admin/cohorts*` requires `cohorts.view/create/update` or session/billing permissions.
  - `/admin/students` requires `admin.students.view`.
  - `/admin/teachers*` requires teacher management permissions.
  - `/admin/cms*`, `/admin/blog*`, and `/admin/faq*` require CMS permissions.
  - `/admin/certificates*` requires certificate permissions.
  - `/admin/messaging/*` requires messaging permissions.

### Policies found

Policies already exist for:

- Enrollment
- Payment
- Session
- AttendanceSession
- Material
- Certificate
- User
- Event and event registrations
- Payout
- PreEnrollment
- Registration

Important gap before this fix:

- `PaymentPolicy::approve()` did not recognize the new `payments.approve` permission. Support users could not be selectively granted payment approval without legacy finance permissions.
- `EnrollmentPolicy` only handled student-owner viewing and did not provide admin/support-level `viewAny`, `update`, or `cancel`.
- `UserPolicy` used legacy permissions only and did not protect super admin accounts.

## Fixes Applied

### Core permission catalog

Added `app/Support/Rbac/PermissionCatalog.php` as a single source of truth for:

- Core roles: `super_admin`, `admin`, `teacher`, `support`, `student`.
- Legacy aliases: `super-admin` and `instructor`.
- Grouped permissions for UI.
- Default permissions per role.
- Legacy permissions kept for backward compatibility.

### Seeder

Rebuilt `database/seeders/RolesAndPermissionsSeeder.php` to:

- Create all permissions idempotently.
- Create all core roles.
- Sync default permissions per role.
- Preserve legacy aliases.
- Preserve finance and instructor-manager legacy roles.

`DatabaseSeeder` now calls `RolesAndPermissionsSeeder` first so other permission seeders have a stable base.

### Role and user helpers

Updated `app/Models/User.php`:

- `isAdmin()` now checks both legacy `users.role` and Spatie roles.
- Added `isSuperAdmin()`.
- Updated `scopeRole()` to include Spatie role assignments, not only the legacy column.

### Policies

Updated:

- `app/Policies/PaymentPolicy.php`
- `app/Policies/EnrollmentPolicy.php`
- `app/Policies/UserPolicy.php`

Key behavior:

- `payments.approve` and `payments.reject` are now real permissions.
- Support can view and update enrollments, but cannot approve payments unless explicitly granted `payments.approve`.
- Support can access registration/payment follow-up pages through `enrollments.view` and `payments.view`, not through broad admin access.
- Super admin protection is enforced in `UserPolicy`.

### Admin security UI

Added:

- `/admin/security/roles`
- `/admin/security/roles/{role}/edit`
- `/admin/security/users`
- `/admin/security/audit`

Files:

- `app/Livewire/Admin/Security/RolesPage.php`
- `app/Livewire/Admin/Security/RoleEditPage.php`
- `app/Livewire/Admin/Security/UsersPage.php`
- `app/Livewire/Admin/Security/AuditPage.php`
- `resources/views/livewire/admin/security/*`

The admin sidebar now shows the RBAC section only if the user has `roles.view`.

### Permission-first admin access

Custom operators can now access only the areas they have explicit permission for, without being promoted to `admin`.

Example:

- A user with only `courses.view` can open `/admin/courses`.
- The same user cannot open `/admin/cohorts`.
- A user with only `messaging.outbox.view` can open `/admin/messaging/outbox`.
- The same user cannot manage messaging templates.

### i18n

Added:

- `lang/ar/security.php`
- `lang/fr/security.php`

## Remaining Gaps

These are known and should be handled in the next RBAC hardening pass:

- Many admin feature routes still rely on broad `EnsureUserIsAdmin`.
- Some low-risk admin/help/governance routes still rely on broad `EnsureUserIsAdmin`.
- Some components still mix legacy permission names and dotted permission names.
- Teacher and student IDOR coverage exists in key areas, but every new query should be reviewed for authenticated-user scoping.
- API event endpoints are public by design, but should receive a separate abuse/rate-limit review.
- There is no dedicated `CohortPolicy` yet; session/cohort access is partially handled through existing session and attendance assignment services.
- Security audit page currently reads existing audit logs. If audit logging is disabled by environment config, it will show an empty state.

## Recommended Next Steps

1. Convert high-risk admin routes from broad admin middleware to exact permissions:
   - payments
   - finance
   - enrollments
   - certificates
   - messaging
2. Add `CohortPolicy` and route model authorization for cohort detail/action pages.
3. Add feature tests for every high-risk admin action.
4. Gradually migrate UI checks from legacy names to the new dotted permission matrix.
5. Keep legacy aliases until production users are migrated, then remove them in a planned cleanup.
