# ## Status **Approved** --- ## Story **As a** system administrator, **I want** to create and manage training institutions with multiple admin users (super and regular admins), **so that** private training institutions can manage their cohorts independently. --- ## Acceptance Criteria 1. Database schema for institutions and admin roles exists 2. Super admins can create institutions and invite other admins 3. Regular admins can manage cohorts within their institution 4. Admins cannot access other institutions' data 5. Role-based permissions are enforced at API and UI levels --- ## Tasks / Subtasks ### Database Setup (AC: #1) - WINSTON'S ARCHITECTURAL REVISION #### **Critical: 4-Layer Data Isolation Foundation** - [ ] **Migration 1: Add institution_id to account_access** - [ ] Add `institution_id` reference to `account_accesses` table (nullable initially) - [ ] Add foreign key constraint: `fk_account_accesses_to_institutions` - [ ] Backfill existing data: Link users to their institution via account - [ ] Make non-nullable: `change_column_null :account_access, :institution_id, false` - [ ] Add unique index: `[user_id, institution_id]` (prevents duplicate roles) - [ ] **Migration 2: Update institutions table** - [ ] Add fields: account_id, name, registration_number, address, contact_email, contact_phone, super_admin_id, settings - [ ] Add indexes: account_id (unique), registration_number (unique within account), super_admin_id - [ ] Add foreign key: super_admin_id → users.id - [ ] **Migration 3: Create cohort_admin_invitations table** - [ ] Add fields: email, hashed_token, token_preview, role, institution_id, sent_at, expires_at, used_at, created_by_id - [ ] Add indexes: institution_id, email, expires_at - [ ] **Important:** Store SHA-256 hashed tokens, never plaintext - [ ] **Migration 4: Update account_access roles** - [ ] Add cohort_admin and cohort_super_admin to role enum - [ ] Add index on role for performance - [ ] **Rollback Strategy (CRITICAL)** - [ ] Test rollback on production-like data - [ ] Document step-by-step rollback procedure - [ ] Verify no data loss during rollback ### Models (AC: #1, #2, #3, #4) - WINSTON'S 4-LAYER ISOLATION #### **Layer 1: Institution Model (Foundation)** - [ ] Create `app/models/institution.rb` - [ ] Add belongs_to :account - [ ] Add belongs_to :super_admin (class_name: 'User') - [ ] Add has_many :cohorts - [ ] Add has_many :sponsors - [ ] Add has_many :account_accesses (NEW - critical for isolation) - [ ] Add validations (name, registration_number, uniqueness within account) - [ ] **CRITICAL SCOPE:** `scope :for_user, ->(user) { where(id: user.institutions.select(:id)) }` - [ ] **CRITICAL SCOPE:** `scope :managed_by, ->(user) { where(super_admin_id: user.id) }` - [ ] **CRITICAL METHOD:** `def accessible_by?(user) account_accesses.exists?(user_id: user.id) end` #### **Layer 2: User Model Extension** - [ ] Extend `app/models/user.rb` (add to existing) - [ ] Add has_many :account_accesses, dependent: :destroy - [ ] Add has_many :institutions, through: :account_accesses - [ ] Add has_many :managed_institutions, class_name: 'Institution', foreign_key: 'super_admin_id' - [ ] **CRITICAL METHOD:** `def can_access_institution?(institution) institutions.exists?(institution.id) || managed_institutions.exists?(institution.id) end` - [ ] **CRITICAL METHOD:** `def cohort_super_admin? account_accesses.exists?(role: 'cohort_super_admin') end` - [ ] **CRITICAL METHOD:** `def cohort_admin? account_accesses.exists?(role: 'cohort_admin') end` #### **Layer 3: Account Access Model (Security Core)** - [ ] Extend `app/models/account_access.rb` - [ ] Add belongs_to :institution (NEW - critical for isolation) - [ ] Add cohort_admin and cohort_super_admin to role enum - [ ] **CRITICAL VALIDATION:** `validates :user_id, uniqueness: { scope: :institution_id }` - [ ] Add scopes: `cohort_admins`, `cohort_super_admins`, `for_institution(institution)` #### **Layer 4: Invitation Token Model (Cryptographic Security)** - [ ] Create `app/models/cohort_admin_invitation.rb` - [ ] Add belongs_to :institution - [ ] Add belongs_to :created_by, class_name: 'User' - [ ] **CRITICAL:** Store `hashed_token` (SHA-256), never plaintext - [ ] Add `token_preview` for debugging (first 8 chars + '...') - [ ] **CRITICAL METHODS:** - `def generate_token` - uses SecureRandom.urlsafe_base64(64) - `def valid_token?(raw_token)` - validates hash + Redis single-use - `def expired?` - checks expires_at - `def used?` - checks used_at - [ ] Validations: email format, role inclusion, expires_at presence - [ ] Scopes: `active`, `used`, `cleanup_expired` - [ ] **Rate limiting:** Max 5 pending invitations per email #### **Layer 5: Security Event Model (Audit Trail)** - [ ] Create `app/models/security_event.rb` - [ ] Add user_id, event_type, ip_address, details (jsonb) - [ ] Add indexes: user_id, event_type, created_at - [ ] **CRITICAL METHOD:** `def self.log(event_type, user, details = {})` ### API Controllers (AC: #2, #3, #4, #5) - WINSTON'S 4-LAYER SECURITY #### **Layer 1: API Base Controller Security Extensions** - [ ] Extend `app/controllers/api/api_base_controller.rb` - [ ] **CRITICAL:** Add `verify_institution_access` before_action ```ruby def verify_institution_access return true unless params[:institution_id].present? institution = Institution.find_by(id: params[:institution_id]) unless institution && current_user.can_access_institution?(institution) log_security_event(:unauthorized_institution_access) render json: { error: 'Access denied to this institution' }, status: :forbidden return false end true end ``` - [ ] **CRITICAL:** Add `verify_institution_role(required_role)` method - [ ] **CRITICAL:** Add `log_security_event(event_type, details)` method - [ ] Add security event logging for all authorization failures #### **Layer 2: Institutions Controller** - [ ] Create `app/controllers/api/v1/institutions_controller.rb` - [ ] Add `before_action :verify_institution_access` (except index/create) - [ ] **Authorization:** - `create` - requires cohort_super_admin role on account - `update/destroy` - requires cohort_super_admin role + ownership - `index/show` - requires any institution access - [ ] Implement actions with 4-layer isolation: ```ruby def index @institutions = Institution.for_user(current_user) # Layer 1 scope end def show @institution = Institution.for_user(current_user).find(params[:id]) # Layer 1 + 2 end ``` - [ ] Add strong parameters with validation - [ ] Add error handling with security event logging #### **Layer 3: Admin Invitations Controller** - [ ] Create `app/controllers/api/v1/admin/invitations_controller.rb` - [ ] **CRITICAL:** Rate limiting - max 5 pending invitations per email - [ ] **CRITICAL:** Redis-backed single-use token enforcement - [ ] Actions: - `create` - cohort_super_admin only, email validation, rate limit - `index` - scoped to institution, cohort_admin+ access - `revoke` - cohort_super_admin only, mark used_at - [ ] **CRITICAL:** Use `InvitationService` for business logic - [ ] Email delivery via `CohortAdminInvitationJob` (async) #### **Layer 4: Invitation Acceptance Controller** - [ ] Create `app/controllers/api/v1/admin/invitation_acceptance_controller.rb` - [ ] **CRITICAL:** Token validation with Redis single-use check - [ ] Email verification (token only valid for matching email) - [ ] Create `AccountAccess` record on successful acceptance - [ ] One-time use only - immediate invalidation #### **Layer 5: Security Event Controller (Monitoring)** - [ ] Create `app/controllers/api/v1/admin/security_events_controller.rb` - [ ] cohort_super_admin only access - [ ] Filter by user, institution, event_type, date range - [ ] Pagination support - [ ] Export capability for audit purposes ### Web Controllers (AC: #2, #3) - [ ] Create `app/controllers/cohorts/admin_controller.rb` - [ ] Add authentication and role checks - [ ] Implement institution list, new, create, show actions - [ ] Add invite form handling - [ ] Add routes to `config/routes.rb` - [ ] Add API routes under `/api/v1/cohorts/` - [ ] Add web routes under `/cohorts/admin/` ### Frontend Components (AC: #2, #3) - [ ] Create `app/javascript/cohorts/admin/InstitutionWizard.vue` - [ ] Build form with all institution fields - [ ] Add validation and error handling - [ ] Connect to API client - [ ] Add success/error notifications - [ ] Create `app/javascript/cohorts/admin/AdminInviteModal.vue` - [ ] Build invite form with role selection - [ ] Add email validation - [ ] Connect to invitation API - [ ] Create `app/javascript/cohorts/admin/InstitutionList.vue` - [ ] Display institutions table - [ ] Add navigation to institution details - [ ] Update `app/javascript/api/client.js` - [ ] Add institution API methods - [ ] Add invitation API methods ### Jobs & Mailers (AC: #2) - WINSTON'S ASYNCHRONOUS SECURITY #### **Invitation Job Architecture** - [ ] **Create `app/services/invitation_service.rb`** ```ruby class InvitationService def self.create_invitation(institution, email, role, created_by) # Rate limiting check pending = CohortAdminInvitation.where(email: email, institution: institution, used_at: nil) .where('expires_at > ?', Time.current) .count raise RateLimit::LimitApproached if pending >= 5 invitation = CohortAdminInvitation.create!( institution: institution, email: email, role: role, created_by: created_by ) # Async email delivery CohortAdminInvitationJob.perform_async(invitation.id) invitation end def self.accept_invitation(raw_token, accepting_user) invitation = CohortAdminInvitation.active.find_by(token_preview: raw_token[0..8] + '...') return nil unless invitation return nil unless invitation.email == accepting_user.email return nil unless invitation.valid_token?(raw_token) # Atomic transaction AccountAccess.create!( account: invitation.institution.account, user: accepting_user, institution: invitation.institution, role: invitation.role ) invitation end end ``` - [ ] **Create `app/jobs/cohort_admin_invitation_job.rb`** - [ ] `include Sidekiq::Job` - [ ] `sidekiq_options queue: :mailers, retry: 5` - [ ] Check invitation validity before sending - [ ] Handle email delivery failures gracefully - [ ] Log security event on successful delivery #### **Mailer Architecture** - [ ] **Create `app/mailers/cohort_mailer.rb`** - [ ] Inherits from `ApplicationMailer` - [ ] **CRITICAL:** Never include raw token in logs - [ ] **CRITICAL:** Use HTTPS invitation URL - [ ] **CRITICAL:** Token only in email, not in URL params - [ ] Methods: - `admin_invitation(invitation)` - sends invitation email - `super_admin_demoted(user, institution)` - security notification - [ ] **Email Templates** (`app/views/cohort_mailer/`) - [ ] `admin_invitation.html.erb` - HTML version - [ ] `admin_invitation.text.erb` - Plain text version - [ ] **Security:** Include institution name, role, expiration - [ ] **CTA:** Direct link to acceptance flow - [ ] **Warning:** "Do not forward" disclaimer #### **Additional Jobs** - [ ] **Create `app/jobs/security_alert_job.rb`** - [ ] Send immediate alerts for critical events - [ ] Queue: `critical_security` - [ ] Retry: 3 attempts max - [ ] **Create `app/jobs/invitation_cleanup_job.rb`** - [ ] Daily job to remove expired invitations - [ ] Queue: `default` - [ ] Retain for 1 week after expiration for audit ### Authorization & Security (AC: #4, #5) - WINSTON'S DEFENSE-IN-DEPTH #### **4-Layer Security Architecture (MANDATORY)** **Layer 1: Database-Level Security** - [ ] **Foreign Key Constraints:** All relationships must have FK constraints - [ ] **Unique Indexes:** `[user_id, institution_id]` on account_access - [ ] **Scoped Queries:** ALL database queries use `Institution.for_user(user)` - [ ] **Non-nullable:** institution_id on account_access after backfill - [ ] **Test:** Verify SQL injection attempts fail **Layer 2: Model-Level Security** - [ ] **Update `lib/ability.rb` (Cancancan)** ```ruby # Add to Ability.initialize if user.cohort_super_admin? can :manage, Institution, id: user.managed_institutions.select(:id) can :manage, Cohort, institution_id: user.managed_institutions.select(:id) can :manage, Sponsor, institution_id: user.managed_institutions.select(:id) can :manage, CohortAdminInvitation, institution_id: user.managed_institutions.select(:id) end if user.cohort_admin? can :read, Institution, id: user.institutions.select(:id) can :manage, Cohort, institution_id: user.institutions.select(:id) can :read, Sponsor, institution_id: user.institutions.select(:id) end ``` - [ ] **User Model Methods:** Add `can_access_institution?`, `cohort_super_admin?`, `cohort_admin?` - [ ] **Institution Model Methods:** Add `accessible_by?(user)` verification - [ ] **Account Access Validation:** `validates :user_id, uniqueness: { scope: :institution_id }` **Layer 3: Controller-Level Security** - [ ] **API Base Controller Extensions:** - [ ] `verify_institution_access` - checks institution_id parameter - [ ] `verify_institution_role(role)` - checks role + institution - [ ] `log_security_event(type, details)` - audit logging - [ ] **All Controllers:** Add `before_action :verify_institution_access` where appropriate - [ ] **Strong Parameters:** All controllers use strong params with validation - [ ] **Error Handling:** 403 Forbidden for unauthorized access, log all attempts **Layer 4: UI-Level Security** - [ ] **Vue Route Guards:** Prevent manual URL navigation to unauthorized paths - [ ] **API Client Validation:** Validate institution_id before sending requests - [ ] **Context Storage:** Store institution context in Vuex, validate server-side - [ ] **Role-Based UI:** Show/hide elements based on `cohort_super_admin?` / `cohort_admin?` #### **Token Security Architecture (CRITICAL)** **Cryptographic Token System:** - [ ] **Token Generation:** `SecureRandom.urlsafe_base64(64)` - 512 bits entropy - [ ] **Storage:** SHA-256 hash stored in database, never plaintext - [ ] **Preview:** Store first 8 chars + '...' for debugging only - [ ] **Single-Use:** Redis-backed enforcement with atomic operations - [ ] **Expiration:** 24-hour default, configurable per invitation - [ ] **Rate Limiting:** Max 5 pending invitations per email per institution **Redis Enforcement:** - [ ] **Setup:** Configure Redis connection in `config/initializers/redis.rb` - [ ] **Atomic Operations:** Use `SET key value NX EX 86400` for single-use - [ ] **Race Condition Prevention:** Concurrent requests handled correctly - [ ] **TTL Management:** Automatic cleanup after expiration **Invitation Flow Security:** - [ ] **Email Verification:** Token only valid for matching email address - [ ] **Domain Validation:** Optional email domain verification for institutions - [ ] **Acceptance Controller:** One-time use, immediate invalidation - [ ] **AccountAccess Creation:** Atomic transaction, rollback on failure #### **Security Event Logging & Monitoring** **Security Event Model:** - [ ] **Create `app/models/security_event.rb`** ```ruby class SecurityEvent < ApplicationRecord belongs_to :user, optional: true validates :event_type, presence: true validates :ip_address, presence: true def self.log(event_type, user = nil, details = {}) create!( user: user, event_type: event_type, ip_address: details[:ip_address] || '0.0.0.0', details: details.except(:ip_address) ) end end ``` **Event Types to Log:** - [ ] `:unauthorized_institution_access` - Cross-institution access attempt - [ ] `:insufficient_privileges` - Role-based access denied - [ ] `:token_validation_failure` - Invalid/expired token attempt - [ ] `:rate_limit_exceeded` - Too many invitation attempts - [ ] `:invitation_accepted` - Successful admin invitation acceptance - [ ] `:super_admin_demoted` - Role change that invalidates tokens **Monitoring & Alerting:** - [ ] **Alert Thresholds:** - >5 unauthorized access attempts/hour → Security alert - >20 token validation failures/hour → Potential attack - Any super_admin demotion → Immediate notification - [ ] **Dashboard:** Security events view for cohort_super_admin - [ ] **Export:** CSV/PDF export for audit compliance #### **Integration with Existing DocuSeal Security** **Authentication Compatibility:** - [ ] **Devise + JWT:** No changes to existing auth system - [ ] **2FA Support:** Existing 2FA continues to work - [ ] **API Tokens:** Existing access tokens unaffected - [ ] **Session Management:** No changes to session handling **Authorization Compatibility:** - [ ] **Existing Abilities:** All existing CanCanCan abilities preserved - [ ] **Additive Only:** New roles added to existing enum - [ ] **Account Isolation:** Existing account-level isolation maintained - [ ] **Template/Submission Access:** Existing permissions unchanged **Data Isolation Compatibility:** - [ ] **Account-Level:** Existing `account_id` isolation preserved - [ ] **Institution-Level:** New `institution_id` isolation within accounts - [ ] **Combined Scopes:** Queries must satisfy both levels - [ ] **Backward Compatibility:** Existing data remains accessible #### **Security Testing Requirements** **Unit Tests:** - [ ] **Model Scopes:** `Institution.for_user(user)` returns correct records - [ ] **User Methods:** `can_access_institution?`, role checks work correctly - [ ] **Token Security:** Hash generation, validation, single-use enforcement - [ ] **Rate Limiting:** Max 5 invitations per email enforced - [ ] **Validation:** Uniqueness constraints prevent duplicates **Request Tests:** - [ ] **Cross-Institution Access:** All endpoints tested with wrong institution_id - [ ] **Role-Based Access:** Each role tested for allowed/denied actions - [ ] **Token Security:** Reuse, expiration, wrong email scenarios - [ ] **Security Events:** All violations logged correctly - [ ] **Rate Limiting:** 429 responses when limit exceeded **Integration Tests:** - [ ] **Complete Invitation Flow:** Create → Email → Accept → Access - [ ] **Concurrent Access:** Multiple users from different institutions - [ ] **Race Conditions:** Token validation under load - [ ] **Rollback Scenarios:** Migration rollback preserves security **Security Audit:** - [ ] **Penetration Testing:** Attempt cross-institution data access - [ ] **Token Analysis:** Verify cryptographic strength - [ ] **Redis Security:** Verify atomic operations prevent race conditions - [ ] **OWASP Check:** Review for common vulnerabilities #### **Deployment Security Checklist** **Pre-Production:** - [ ] **Security Audit:** Third-party review of token system - [ ] **Penetration Test:** Attempt to breach data isolation - [ ] **Performance Test:** Verify security doesn't degrade performance >10% - [ ] **Rollback Test:** Verify security events persist after rollback **Production Monitoring:** - [ ] **Security Dashboard:** Real-time event monitoring - [ ] **Alert System:** Immediate notification of violations - [ ] **Audit Trail:** All events retained for compliance - [ ] **Incident Response:** Documented procedure for security breaches **Emergency Procedures:** - [ ] **Token Revocation:** Ability to invalidate all pending invitations - [ ] **Access Lockdown:** Emergency role removal capability - [ ] **Data Breach Protocol:** Steps if isolation is compromised - [ ] **Recovery Plan:** Restore security without data loss ### Integration Verification - WINSTON'S COMPREHENSIVE TESTING #### **IV1: Existing DocuSeal Authentication Compatibility** - [ ] **Test:** Existing user login flows work unchanged - [ ] **Test:** JWT tokens work for legacy endpoints - [ ] **Test:** 2FA continues to function normally - [ ] **Test:** API access tokens unaffected - [ ] **Test:** Session management unchanged #### **IV2: Role System Compatibility** - [ ] **Test:** Existing DocuSeal roles (member, admin) unaffected - [ ] **Test:** New roles (cohort_admin, cohort_super_admin) are additive only - [ ] **Test:** No conflicts between old and new role enums - [ ] **Test:** Existing template/submission access unchanged - [ ] **Test:** Account-level isolation preserved #### **IV3: Performance Impact** - [ ] **Benchmark:** Existing user operations before/after - [ ] **Target:** <10% performance degradation on existing flows - [ ] **Test:** Query performance with 1000+ institutions - [ ] **Test:** Concurrent user load (100+ simultaneous) - [ ] **Test:** Database query optimization (EXPLAIN ANALYZE) #### **IV4: New Architecture Security (WINSTON'S MANDATORY)** - [ ] **Test:** 4-layer isolation with malicious inputs - [ ] Attempt cross-institution access via altered institution_id - [ ] Test SQL injection attempts on scoped queries - [ ] Verify 403 responses for all unauthorized attempts - [ ] **Test:** Redis token enforcement under concurrent load - [ ] 50 concurrent token validation attempts - [ ] Race condition prevention verified - [ ] Single-use enforcement works correctly - [ ] **Test:** Security event logging captures all violations - [ ] All 6 event types logged correctly - [ ] IP address captured accurately - [ ] Details JSON contains relevant information - [ ] **Test:** Rate limiting prevents invitation spam - [ ] 6th invitation attempt returns 429 - [ ] Counter resets correctly - [ ] Per-email limit enforced per institution - [ ] **Test:** Token security scenarios - [ ] Token reuse attempts fail - [ ] Expired tokens rejected - [ ] Wrong email address rejected - [ ] Concurrent same-token validation handled correctly #### **IV5: Integration with Existing Features** - [ ] **Test:** Template sharing works with new institutions - [ ] **Test:** Submission workflows integrate correctly - [ ] **Test:** Webhook delivery unaffected - [ ] **Test:** Email notifications work for new roles - [ ] **Test:** Export functionality includes new data ### Testing - [ ] **Model Tests:** `spec/models/institution_spec.rb` - [ ] Validations - [ ] Associations - [ ] Scopes (for_user) - [ ] Data isolation - [ ] **Request Tests:** `spec/requests/api/v1/institutions_spec.rb` - [ ] Authentication requirements - [ ] Authorization (super_admin only) - [ ] Success/failure scenarios - [ ] Error responses - [ ] **System Tests:** `spec/system/institution_management_spec.rb` - [ ] Institution creation flow - [ ] Admin invitation flow - [ ] Data isolation verification - [ ] Role-based access control --- ## Dev Notes ### Overview This is the **foundation story** for the entire 3-portal cohort management system. All subsequent stories depend on the institution and admin management infrastructure created here. ### Key Integration Points **Existing DocuSeal Systems to Reference:** - `app/models/user.rb` - Authentication patterns (Devise + 2FA) - `app/models/account.rb` - Multi-tenancy structure - `app/models/account_access.rb` - Role-based permissions (extend this) - `app/controllers/api/api_base_controller.rb` - API authentication - `lib/ability.rb` - Cancancan authorization rules - `app/javascript/template_builder/` - Form builder integration patterns **Critical Files for Implementation:** - `config/routes.rb` - Add cohort management routes - `app/javascript/api/client.js` - Add API methods - `app/views/mailers/` - Email templates location ### Winston's Architecture Requirements (MANDATORY) #### **4-Layer Data Isolation Foundation** This is the **cornerstone** of the entire architecture. Without this, the story cannot proceed safely. **Layer 1: Database Constraints** - `account_access.institution_id` with FK constraint - Unique index `[user_id, institution_id]` - Non-nullable after backfill **Layer 2: Model Scopes** - `Institution.for_user(user)` - used in ALL queries - `User.can_access_institution?(institution)` - verification method - `Institution.accessible_by?(user)` - security check **Layer 3: Controller Authorization** - `verify_institution_access` before_action - `verify_institution_role(role)` for role checks - `log_security_event` for audit trail **Layer 4: UI Validation** - Vue route guards - API client pre-validation - Context-based access control #### **Cryptographic Token Security** - **Generation:** `SecureRandom.urlsafe_base64(64)` (512 bits) - **Storage:** SHA-256 hash only, never plaintext - **Enforcement:** Redis-backed single-use with atomic operations - **Expiration:** 24-hour default, strict validation #### **Security Event Logging** - **Model:** `SecurityEvent` with user, event_type, ip_address, details - **Events:** 6 types covering all security violations - **Monitoring:** Real-time alerts for critical thresholds - **Audit:** Export capability for compliance #### **Integration Compatibility** - **Additive Only:** No changes to existing DocuSeal schemas - **Authentication:** Devise + JWT unchanged - **Authorization:** Existing CanCanCan abilities preserved - **Performance:** <10% degradation on existing operations #### **Implementation Sequence (CRITICAL)** 1. ✅ **Database migrations** (institution_id on account_access) 2. ✅ **Model layer** (4-layer isolation foundation) 3. ✅ **Security layer** (token system + event logging) 4. ✅ **Controller layer** (authorization + validation) 5. ✅ **UI layer** (route guards + context management) 6. ✅ **Testing** (comprehensive security tests) 7. ✅ **Features** (invitation flow, CRUD operations) **⚠️ DO NOT implement features before security foundation is complete and tested.** ### Technical Constraints (from Architecture) **Must Follow:** - Ruby 3.4.2, Rails 7.x exact versions - Vue 3.3.2 with Composition API (`