alumni_lookup

Permissions Matrix

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.


Table of Contents

  1. Executive Summary
  2. Current Permission Audit
  3. Feature Permissions Matrix
  4. Future Role Definitions
  5. Migration Path

Executive Summary

Phase 1-3 Complete (November 30, 2025): Role-based access control is now fully enforced.

The application uses:

The 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:


Current Permission Audit

Authorization Methods (Phase 3 Complete)

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

Controllers Using 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

Controllers Using 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

Controllers Using 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)

Admin Notification (Non-Guard Usage)

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

Database Schema Fields

Field Type Location Purpose
admin boolean users table Admin flag (single source of truth)

Feature Permissions Matrix

Legend

Symbol Meaning
Full access (enforced)
👁️ View only
No access (enforced)
🔜 Future: should have access
Future: no access planned

Internal Site (lookup.bualum.co)

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

Settings Namespace (Admin-Only)

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

User Management (Internal Portal)

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

API Endpoints (Internal Portal)

Feature Controller Anonymous Staff Admin
Alumni Typeahead Api::AlumniController
Majors Dropdown Api::MajorsController
Affinities Dropdown Api::AffinitiesController
Activity Descriptions Api::ActivityDescriptionsController

External Site (champions.bualum.co)

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 🔜

Future Role Definitions

The application uses a two-portal architecture with separate role hierarchies:

Lookup Portal (lookup.bualum.co) - Internal Staff

Admin

Description: Full system access including configuration, user management, and data imports. Higher-level operational role.

Capabilities:

Guard Pattern: ensure_admin! or require_admin

Staff

Description: Daily operational access for alumni office staff. Standard internal user role.

Capabilities:

NOT Permitted:

Guard Pattern: ensure_staff! or require_staff


Champion Portal (champions.bualum.co) - External Alumni

City Leadership Council (CLC)

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:

Champion

Description: 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:


Migration Path

Phase 1: Unify Admin Checks on Internal Portal

  1. Audit all access_level == 1 checks
  2. Add admin? method that checks both fields for backwards compatibility
  3. Migrate controllers to use consistent admin? method
  4. Document deprecation of access_level for admin checks

Phase 2: Add Staff Role to Internal Portal

  1. Add role enum to User model: admin, staff
  2. Create role-checking helper methods (admin?, staff?)
  3. Update controllers with role-specific guards
  4. Test role-based access thoroughly

Phase 3: Build Champion Portal Foundation

  1. Implement champion authentication (separate from internal staff)
  2. Create Champion model or extend User with champion role
  3. Build champion-facing features in Champions:: namespace
  4. Test isolation from internal features

Phase 4: Add CLC Role to Champion Portal

  1. Add CLC role capability to champion authentication
  2. Add regional scoping fields (region, city)
  3. Build CLC-specific views with regional filtering
  4. Create Champions::Regional* controllers for CLC features
  5. Test CLC access boundaries

Hybrid Authorization Model: Roles + Permission Flags

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

Why Hybrid?

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

Design Principles

  1. Roles define baseline access — Admin gets everything Staff gets, plus settings/imports
  2. Permission flags are exceptions — Only add flags when a feature genuinely needs to be granted independent of role
  3. Keep flags minimal — Avoid creating a permission for every feature; most access is role-based
  4. Admins bypass permission checks — Admin role automatically grants all permissions

Permission Flag Use Cases

Add a permission flag when:

Do NOT add a permission flag when:

Current Permission Flags

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

Implementation Pattern

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

Adding New Permission Flags

When a new cross-cutting feature is identified:

  1. Confirm it crosses role boundaries — If it’s cleanly Staff-only or Admin-only, use roles
  2. Add migration with boolean flag defaulting to false
  3. Add helper method on User that includes admin bypass
  4. Create/update Pundit policy with permission check
  5. Update this document with the new flag
  6. Update admin UI to allow toggling the permission