Updated: January 14, 2026
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. Permission Flags Added (January 14, 2026): Evolved from 3-role hierarchy to 2 primary roles + supplemental permissions.
The application uses a hybrid authorization model:
staff, admin (enum) — baseline access levelscan_portal_admin, can_support_respond (boolean flags) — layered on staff role| Method | Returns True When |
|---|---|
admin? |
User has admin role OR legacy admin boolean |
portal_admin? |
User is admin OR has can_portal_admin flag |
support_responder? |
User is admin OR has can_support_respond flag |
staff? |
User has any role (staff, admin) |
| Callback | Access Level |
|---|---|
ensure_admin! |
Admin only |
ensure_portal_admin! |
Admin OR staff with can_portal_admin |
ensure_staff! |
Any authenticated staff user |
Current State (January 2026):
Lookup Portal (alumnilookup.com) - Internal Staff:
Champion Portal (alumnichampions.com) - External Alumni:
All controllers use standardized authorization callbacks from ApplicationController:
# app/controllers/application_controller.rb
def ensure_admin!
unless current_user&.admin?
redirect_to login_redirect_path, alert: 'You are not authorized to access this page.'
end
end
def ensure_portal_admin!
unless current_user&.portal_admin?
redirect_to login_redirect_path, alert: 'You are not authorized to access this page.'
end
end
def ensure_staff!
unless current_user&.staff?
redirect_to login_redirect_path, alert: 'You are not authorized to access this page.'
end
end
# app/models/user.rb
# Primary roles: staff (default), admin
enum :role, { staff: 'staff', portal_admin: 'portal_admin', admin: 'admin' }, default: :staff
# Superuser check
def admin?
role == 'admin' || read_attribute(:admin) == true
end
# Champion Program management (admin OR staff with flag)
def portal_admin?
admin? || can_portal_admin
end
# Support thread notifications (admin OR staff with flag)
def support_responder?
admin? || can_support_respond
end
# Any authenticated internal user
def staff?
role.in?(%w[staff portal_admin admin])
end
ensure_staff! (Staff-Accessible)| File | Protected Behavior | Role Access |
|---|---|---|
AlumniController |
Alumni search, view, update | Staff + Portal Admin + Admin |
AlumniAffinitiesController |
Manage alumni affinities | Staff + Portal Admin + Admin |
BatchSearchController |
Batch name search | Staff + Portal Admin + Admin |
StatisticsController |
View degree statistics | Staff + Portal Admin + Admin |
EngagementStatsController |
View engagement stats, clear cache | Staff + Portal Admin + Admin |
EngagementActivitiesController |
View engagement activities | Staff + Portal Admin + Admin |
ChampionSignupsController |
View/edit signups | Staff + Portal Admin + Admin |
ensure_portal_admin! (Champion Program Management)| File | Protected Behavior | Role Access |
|---|---|---|
ChampionSignupsController |
Delete signups, merge duplicates | Portal Admin + Admin |
| (Future) Champion Verification | Verify champions, link BUIDs | Portal Admin + Admin |
| (Future) Champion Metrics | View champion activity metrics | Portal Admin + 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 |
|---|---|---|---|---|
Settings::UsersController |
authorize_user_management! |
current_user.admin? |
User list, create, delete | Admin-only |
Settings::UsersController |
authorize_profile_access! |
current_user == @user \|\| current_user.admin? |
Edit any profile | Admin-only (others: own profile only) |
Settings::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 | Settings::UsersController#index |
❌ | ❌ | ✅ |
| Create User | Settings::UsersController#create |
❌ | ❌ | ✅ |
| Delete User | Settings::UsersController#destroy |
❌ | ❌ | ✅ |
| Edit Own Profile | Settings::UsersController#edit |
❌ | ✅ | ✅ |
| Edit Any Profile | Settings::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