Updated: November 30, 2025
Purpose: Document current authorization patterns, map existing permissions to future role categories, and provide a blueprint for role-based access control implementation.
Phase 1-3 Complete (November 30, 2025): Role-based access control is now fully enforced.
The application uses:
admin? method on User model - Single source of truth for admin statusstaff? method on User model - Returns true for staff and admin rolesensure_admin! method in ApplicationController - Standard callback for admin-only controllersensure_staff! method in ApplicationController - Standard callback for staff-accessible controllersThe role enum (staff, admin) is the primary authorization mechanism.
Current State (2 Roles Enforced):**
Lookup Portal (lookup.bualum.co) - Internal Staff:
Target State (Future - 2 Portals, 4 Roles):**
Lookup Portal (lookup.bualum.co) - Internal Staff:
Champion Portal (champions.bualum.co) - External Alumni:
All controllers now use standardized authorization callbacks from ApplicationController:
# app/controllers/application_controller.rb
def ensure_admin!
unless current_user&.admin?
redirect_to root_path, alert: 'You are not authorized to access this page.'
end
end
def ensure_staff!
unless current_user&.staff?
redirect_to root_path, alert: 'You are not authorized to access this page.'
end
end
ensure_staff! (Staff-Accessible)| File | Protected Behavior | Role Access |
|---|---|---|
AlumniController |
Alumni search, view, update | Staff + Admin |
AlumniAffinitiesController |
Manage alumni affinities | Staff + Admin |
BatchSearchController |
Batch name search | Staff + Admin |
StatisticsController |
View degree statistics | Staff + Admin |
EngagementStatsController |
View engagement stats (clear_cache: admin only) | Staff + Admin |
EngagementActivitiesController |
View engagement activities | Staff + Admin |
ChampionSignupsController |
Manage signups (delete/merge: admin only) | Staff + Admin |
ensure_admin! (Admin-Only)| File | Protected Behavior | Role Access |
|---|---|---|
PeopleController |
Alumni data import | Admin-only |
Settings::SettingsController |
Settings index | Admin-only |
Settings::MajorsController |
CRUD majors | Admin-only |
Settings::CollegesController |
CRUD colleges | Admin-only |
Settings::AffinitiesController |
CRUD affinities | Admin-only |
Settings::AlumniController |
Import alumni/degrees | Admin-only |
Settings::EngagementActivitiesController |
Import engagement data | Admin-only |
admin? Directly| File | Method/Callback | Guard Type | Protected Behavior | Future Role Mapping |
|---|---|---|---|---|
UsersController |
authorize_admin! |
current_user.admin? |
User list, create, delete | Admin-only |
UsersController |
authorize_profile_access! |
current_user == @user \|\| current_user.admin? |
Edit any profile | Admin-only (others: own profile only) |
UsersController#update |
inline check | current_user.admin? |
Redirect destination | Admin-only |
_navbar.html.erb |
conditional | current_user&.admin? |
Show “Settings” link | Admin-only |
users/_form.html.erb |
conditional | current_user&.admin? |
Show admin checkbox | Admin-only |
users/index.html.erb |
conditional | user.admin? |
Display admin badge | Admin-only (view) |
| File | Usage | Context | Notes |
|---|---|---|---|
ChampionSignupMailer |
admin_notification |
Email method name | Sends to configured admin email list |
Champions::ChampionSignupsController |
ChampionSignupMailer.admin_notification |
Mailer call | Notifies staff of new signups |
champion_signup.rb (config) |
champion_signup_admin_emails |
Environment config | List of staff recipients |
| Field | Type | Location | Purpose |
|---|---|---|---|
admin |
boolean | users table |
Admin flag (single source of truth) |
| Symbol | Meaning |
|---|---|
| ✅ | Full access (enforced) |
| 👁️ | View only |
| ❌ | No access (enforced) |
| 🔜 | Future: should have access |
| ⬜ | Future: no access planned |
| Feature | Controller | Anonymous | Staff | Admin |
|---|---|---|---|---|
| Alumni Search | AlumniController |
❌ | ✅ | ✅ |
| Alumni Profile View | AlumniController#show |
❌ | ✅ | ✅ |
| Alumni Photo Upload | AlumniController#update |
❌ | ✅ | ✅ |
| Alumni CSV Export | AlumniController#export_csv |
❌ | ✅ | ✅ |
| Batch Search | BatchSearchController |
❌ | ✅ | ✅ |
| Engagement Stats - Overview | EngagementStatsController |
❌ | ✅ | ✅ |
| Engagement Stats - Analytics | EngagementStatsController |
❌ | ✅ | ✅ |
| Engagement Stats - Breakdown | EngagementStatsController |
❌ | ✅ | ✅ |
| Engagement Stats - Demographics | EngagementStatsController |
❌ | ✅ | ✅ |
| Engagement Stats - Matrix | EngagementStatsController |
❌ | ✅ | ✅ |
| Engagement Stats - Top Alumni | EngagementStatsController |
❌ | ✅ | ✅ |
| Engagement Stats - Activity Pairs | EngagementStatsController |
❌ | ✅ | ✅ |
| Engagement Stats - Clear Cache | EngagementStatsController |
❌ | ❌ | ✅ |
| Top Engaged Alumni | TopEngagedAlumniController |
❌ | ✅ | ✅ |
| Champion Signups List | ChampionSignupsController |
❌ | ✅ | ✅ |
| Champion Signup Edit | ChampionSignupsController |
❌ | ✅ | ✅ |
| Champion Delete | ChampionSignupsController#destroy |
❌ | ❌ | ✅ |
| Champion Merge Duplicates | ChampionSignupsController |
❌ | ❌ | ✅ |
| Degree Statistics | StatisticsController |
❌ | ✅ | ✅ |
| Engagement Activities List | EngagementActivitiesController |
❌ | ✅ | ✅ |
| Engagement Activity Import | EngagementActivitiesController |
❌ | ❌ | ✅ |
| Alumni Affinities CRUD | AlumniAffinitiesController |
❌ | ✅ | ✅ |
| People Import | PeopleController |
❌ | ❌ | ✅ |
| Feature | Controller | Anonymous | Staff | Admin |
|---|---|---|---|---|
| Settings Index | Settings::SettingsController |
❌ | ❌ | ✅ |
| Majors CRUD | Settings::MajorsController |
❌ | ❌ | ✅ |
| Majors CSV Import | Settings::MajorsController |
❌ | ❌ | ✅ |
| Colleges CRUD | Settings::CollegesController |
❌ | ❌ | ✅ |
| Colleges CSV Import | Settings::CollegesController |
❌ | ❌ | ✅ |
| Affinities CRUD | Settings::AffinitiesController |
❌ | ❌ | ✅ |
| Affinities CSV Import | Settings::AffinitiesController |
❌ | ❌ | ✅ |
| Alumni Import | Settings::AlumniController |
❌ | ❌ | ✅ |
| Degrees Import | Settings::AlumniController |
❌ | ❌ | ✅ |
| Orphaned Alumni | Settings::AlumniController |
❌ | ❌ | ✅ |
| Engagement Activities Import | Settings::EngagementActivitiesController |
❌ | ❌ | ✅ |
| Feature | Controller | Anonymous | Staff | Admin |
|---|---|---|---|---|
| User List | UsersController#index |
❌ | ❌ | ✅ |
| Create User | UsersController#create |
❌ | ❌ | ✅ |
| Delete User | UsersController#destroy |
❌ | ❌ | ✅ |
| Edit Own Profile | UsersController#edit |
❌ | ✅ | ✅ |
| Edit Any Profile | UsersController#edit |
❌ | ❌ | ✅ |
| Feature | Controller | Anonymous | Staff | Admin |
|---|---|---|---|---|
| Alumni Typeahead | Api::AlumniController |
❌ | ✅ | ✅ |
| Majors Dropdown | Api::MajorsController |
❌ | ✅ | ✅ |
| Affinities Dropdown | Api::AffinitiesController |
❌ | ✅ | ✅ |
| Activity Descriptions | Api::ActivityDescriptionsController |
❌ | ✅ | ✅ |
| Feature | Controller | Anonymous | Champion (Future) | CLC (Future) |
|---|---|---|---|---|
| Champion Signup Form | Champions::ChampionSignupsController |
✅ | ✅ | ✅ |
| Signup Confirmation | Champions::ChampionSignupsController |
✅ | ✅ | ✅ |
| (Future) Champion Dashboard | Champions::DashboardController |
⬜ | 🔜 | 🔜 |
| (Future) Champion Profile | Champions::ProfileController |
⬜ | 🔜 | 🔜 |
| (Future) Event RSVP | Champions::EventsController |
⬜ | 🔜 | 🔜 |
| (Future) Regional Champion List | Champions::RegionalController |
⬜ | ⬜ | 🔜 |
| (Future) Regional Engagement Stats | Champions::RegionalController |
⬜ | ⬜ | 🔜 |
| (Future) Champion Management | Champions::ManagementController |
⬜ | ⬜ | 🔜 |
The application uses a two-portal architecture with separate role hierarchies:
Description: Full system access including configuration, user management, and data imports. Higher-level operational role.
Capabilities:
Guard Pattern: ensure_admin! or require_admin
Description: Daily operational access for alumni office staff. Standard internal user role.
Capabilities:
NOT Permitted:
Guard Pattern: ensure_staff! or require_staff
Description: Regional alumni leaders who need elevated access within the Champion Portal. CLC is to Champion as Admin is to Staff - the “regional admin” version of Champion.
Capabilities:
NOT Permitted:
Guard Pattern: require_city_leader! or ensure_clc!
Special Considerations:
Champions:: namespaceregion or city field for geographic scopingDescription: Authenticated alumni who have completed the champion signup process. Standard external user role.
Capabilities:
NOT Permitted:
Guard Pattern: require_champion! (separate authentication system)
Special Considerations:
Champions:: namespaceaccess_level == 1 checksadmin? method that checks both fields for backwards compatibilityadmin? methodaccess_level for admin checksrole enum to User model: admin, staffadmin?, staff?)Champion model or extend User with champion roleChampions:: namespaceregion, city)Champions::Regional* controllers for CLC featuresThe application uses a hybrid authorization approach that combines role-based access with feature-specific permission flags. This provides the right balance between simplicity and flexibility.
| Approach | When to Use | Example |
|---|---|---|
| Roles | Broad access categories where most users fit cleanly | Admin vs Staff vs Champion |
| Permission Flags | Features that cross role boundaries or need one-off access | Event check-in for volunteers |
Add a permission flag when:
Do NOT add a permission flag when:
| Flag | Purpose | Who Gets It |
|---|---|---|
can_event_checkin |
Event check-in, search registrants, view stats | Volunteers, Staff, Admin |
can_event_manage |
Create/edit events, import CSV, export data | Staff, Admin |
Note: Event permissions are planned for future Event Check-in Integration. See docs/planning/event-checkin-integration/.
User model:
class User < ApplicationRecord
# Role (baseline access)
enum :role, { staff: 0, admin: 1 }
# Permission flags (cross-cutting features)
# t.boolean :can_event_checkin, default: false
# t.boolean :can_event_manage, default: false
def can_event_checkin?
admin? || read_attribute(:can_event_checkin)
end
def can_event_manage?
admin? || read_attribute(:can_event_manage)
end
end
Pundit policy:
class EventPolicy < ApplicationPolicy
def checkin?
user.can_event_checkin?
end
def manage?
user.can_event_manage?
end
end
Controller:
class EventsController < ApplicationController
def check_in
authorize @event, :checkin?
# ...
end
def edit
authorize @event, :manage?
# ...
end
end
When a new cross-cutting feature is identified:
false