Skip to content

RBAC Hardening & Expansion#60

Merged
Systemsaholic merged 30 commits intomainfrom
feature/rbac-hardening
Mar 20, 2026
Merged

RBAC Hardening & Expansion#60
Systemsaholic merged 30 commits intomainfrom
feature/rbac-hardening

Conversation

@Systemsaholic
Copy link
Copy Markdown
Owner

@Systemsaholic Systemsaholic commented Mar 20, 2026

Summary

  • Unified @AdminOnly() decorator — consolidated 3 inconsistent admin guard patterns across 10 controllers
  • Public endpoint audit — fixed class-level @Public() on AppController, gated debug-sentry behind admin
  • Platform settings guard — locked Stripe/settings endpoints to admin
  • Contact access filtering — linked-data endpoints (trips, bookings, payments) gated by ownership for agents, "Limited View" badge + "Request Access" button in UI
  • Dashboard agent filteringgetStats() scoped to agent's trips/contacts, revenue suppressed for non-admins
  • Role display rename — "User" → "Agent" across admin UI
  • Security audit logging — new security_audit_logs table + SecurityAuditService + exception filter for login_failed/access_denied/role_changed events
  • Contact share request workflow — new contact_share_requests table, create/approve/deny flow with notifications, 30-day expiry
  • Impersonation system — new impersonation_sessions table, ImpersonationGuard with session lock + full context swap, 30min sessions (4hr max), frontend banner with countdown, audit trail + agent notification

Database Migrations

  • 20260320120000_create_security_audit_logs (idx 162)
  • 20260320120100_create_contact_share_requests (idx 163)
  • 20260320120200_create_impersonation_sessions (idx 164)

Test plan

  • Verified on tf-demo preview environment
  • Impersonation tested in browser: start → banner shown → admin features blocked → agent-scoped data → exit → admin restored
  • Role display shows "Agent" instead of "User"
  • Zero new TypeScript errors (only pre-existing)
  • All old guard patterns removed (grep verified)
  • Sentry issue [Sentry] PostgresError: relation "impersonation_sessions" does not exist #58 fixed (graceful table-missing handling)
  • Manual QA: contact share request flow (request → owner notification → approve/deny)

Spec

docs/superpowers/specs/2026-03-20-rbac-hardening-design.md

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added contact access request workflow—users can now request access to contacts from owners
    • Added admin impersonation capability with session management and expiration controls
    • Added "Limited View" badge for contacts with restricted access
  • Bug Fixes

    • Improved email authentication failure handling with clearer error messages and credential update prompts
    • Fixed contact data filtering to properly restrict sensitive information for users without full access
  • Enhancements

    • Enhanced security audit logging for tracking administrative actions and access control events

Systemsaholic and others added 30 commits March 20, 2026 09:35
Covers role permissions, admin decorator standardization, contact
access filtering, share request workflow, impersonation system,
auth audit logging, and report/export agent isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Impersonation: add header-to-session validation, write policy,
  admin endpoint blocking, max 4hr/8 extension limits
- Permission matrix: add contact deletion, tasks, email, calendar, notes
- Public endpoint audit: enumerate all @public endpoints individually
- AdminOnly: document all 3 existing patterns (not just 2)
- Share requests: add agency scope enforcement + 30-day expiration
- Audit events: separate security.* namespace from entity audit.*
- Role display: include client_portal in display map
- Dashboard: note existing filtering, scope as verification

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical fixes:
- Impersonation: use Interceptor (not middleware) for correct execution order
- Impersonation: add @BypassImpersonation() for control endpoints to prevent deadlock

High fixes:
- Public endpoint audit: correct route paths, add missing endpoints
- Contact RBAC: gate contact-linked data (trips/bookings/payments/notes)
- Notes/calendar: note ownership check requirements
- Dashboard: explicitly fix GET /dashboard/stats agency-wide leak
- Platform settings: enumerate specific Stripe/settings endpoints
- Audit logging: new security_audit_logs table, separate from entity audit

Medium fixes:
- Share requests: explicitly reuse ContactSharesService/ContactAccessService
- Notifications: add contact.share_* event handlers + notification types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Impersonation: use Guard (not Interceptor) registered between
   JwtAuthGuard and RolesGuard — correct NestJS execution order
   so role swap happens before admin checks
2. Platform settings: correct route paths to actual /agencies/:agencyId/*

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Unify all references to ImpersonationGuard (was still saying Interceptor in one place)
- Fix method names to match actual codebase: create(), canAccessSensitiveData(), send()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When IMAP credentials become invalid, the system now:
- Detects auth failures via ImapFlow error properties and sets
  lastSyncError='IMAP_AUTH_FAILED' on the account
- Excludes auth-failed accounts from background sync dispatch
- Returns HTTP 502 with machine-readable code to the frontend
- Shows inline auth error banner in inbox with link to update credentials
- Clears error state when user updates credentials, re-enabling sync
- Handles auth failures consistently across all 5 IMAP entry points
- Maps sentinel to friendly text in profile tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Consolidates role metadata + AdminRoleGuard into a single reusable
decorator. JwtAuthGuard is global so it is intentionally excluded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces three inconsistent admin guard patterns across 10 controllers:

- Pattern 1 (UseGuards(AdminGuard)): commission, users, api-credentials,
  trips, reference-data, aerodatabox controllers
- Pattern 2 (@roles('admin') + RolesGuard): suppliers, loyalty-programs,
  enrichment controllers
- Pattern 3 (UseGuards(JwtAuthGuard, AdminRoleGuard)): automation controller

JwtAuthGuard is global so it was redundant in Pattern 3.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ROLE_DISPLAY_NAMES constant and getRoleDisplayName() helper.
Updates users-table getRoleBadge() to use the helper so the 'user'
role now displays as 'Agent' and 'client_portal' displays as 'Client'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dmin

CRITICAL FIX: @public() was applied at class-level on AppController,
making ALL endpoints public including debug-sentry. This meant a method-level
@adminonly() could not override it because JwtAuthGuard short-circuits on
@public() before AdminRoleGuard can run, leaving request.user undefined.

Fix:
- Remove class-level @public()
- Add method-level @public() to getHealth() and getInfo()
- Add @adminonly() to debugSentry()
- Remove production env check — admin auth is sufficient

Trip share token write endpoints all have @Throttle() — verified OK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 5 platform settings / Stripe Connect methods now require admin role:
getAgencySettings, updateAgencySettings, startOnboarding,
getAccountStatus, getDashboardLink.

Existing agencyId !== auth.agencyId cross-agency checks are preserved —
they remain necessary to prevent cross-agency access even among admins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(Task 9)

Creates the security_audit_logs table with indexes on event, agency_id,
user_id, and created_at for efficient querying. Registers migration at
idx 162 in the Drizzle journal. Exports the securityAuditLogs schema
from the database package index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or agents

- GET /contacts/:id/trips: filter to agent-accessible trips for non-owners without full access
- GET /contacts/:id/bookings: same ownership filter using b.trip.id
- GET /contacts/:id/payment-transactions: block entirely for non-owners without full access
- Add _accessLevel: 'basic' | 'full' to ContactResponseDto for frontend badge support
- Update applyAccessControl() and applyAccessControlToMany() to include _accessLevel in responses
- notes.controller.ts verifyEntityAccess() only checks agencyId for contact-linked notes (documented gap, not fixed here as it only gates who can create notes, not reads sensitive data)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tration (Task 10)

Introduces SecurityAuditModule with a service that listens for all
security.* events via EventEmitter2 wildcard and persists them to
security_audit_logs. Errors are caught and logged rather than thrown
so a failed audit write never disrupts the primary request path.
Module is registered in AppModule imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…agents

- Create ContactShareRequestButton component: POST /contacts/:id/share-requests, shows "Request Sent" state after success
- Add useUser() and isAdmin check to contact detail page
- Show amber "Limited View" badge + Request Access button in contact header when _accessLevel === 'basic' and user is not admin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ship

- dashboard.controller.ts: pass full auth (not just agencyId) to getStats()
- dashboard.service.ts: getStats() now accepts AuthContext
  - Agents: trips scoped to accessible trip IDs via tripAccessService
  - Agents: contacts scoped to owned contacts (ownerId = auth.userId)
  - Revenue left agency-scoped (payment_transactions has no direct trip_id column)
  - Early return { 0, 0, 0, 0 } when agent has no accessible trips
  - getOverview() already correctly uses tripAccessService — unchanged

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…events

- Create SecurityExceptionFilter catching 401/403 and emitting security events via EventEmitter2
- Register as APP_FILTER in AppModule (after SentryGlobalFilter)
- Inject EventEmitter2 into UsersService and emit security.role_changed when role changes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add migration, Drizzle schema, schema index export, and journal registration
for the contact_share_requests table (idx 163).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ogic

Create ResolveShareRequestDto and ContactShareRequestsService.
Service handles: stale request expiry (30d), duplicate prevention,
approve/deny workflow that calls ContactSharesService.create(), and
emits contact.share_* + security.share_* events.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…actsModule

Register controller (POST /:id/share-requests, GET /share-requests/pending,
PATCH /share-requests/:id) and ContactShareRequestsService in ContactsModule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'contact_share' to NotificationCategory union, NOTIFICATION_CATEGORY_VALUES,
  CategoryPreferences, and default channels map (platform only)
- Add handlers for contact.share_requested (notify owner),
  contact.share_approved (notify requester), contact.share_denied (notify requester)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration idx 164 adds table for tracking admin impersonation sessions
with indexes for active session lookup, target user, and agency filtering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Global guard inserted between JwtAuthGuard and RolesGuard
- Session lock: admins with active sessions must include X-Impersonate-User-Id header
- Bypass decorator exempts impersonation control endpoints
- Builds full AuthContext from target user (not spread from admin)
- Stores originalAdmin on request for audit attribution

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- startSession: validates target (not admin, same agency, active), creates 30min session
- extendSession: extends by 30min, enforces 4hr max total duration
- endSession: ends session, emits security + notification events
- getStatus: returns active session info with target user name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Endpoints:
- POST /admin/impersonate/:userId — start session (@adminonly)
- POST /admin/impersonate/extend — extend by 30min (@BypassImpersonation)
- DELETE /admin/impersonate — end session (@BypassImpersonation)
- GET /admin/impersonate/status — check active session (@BypassImpersonation)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Notifies the target agent when an admin ends an impersonation session,
including admin name and session duration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create useImpersonation hook: status check, 30s polling when active,
  start/extend/end methods with localStorage key management
- Create ImpersonationBanner: fixed amber overlay with countdown timer,
  Extend and Exit buttons with toast feedback
- Mount ImpersonationBanner inside AuthProvider in providers.tsx
- Inject X-Impersonate-User-Id header in api.ts getAuthHeaders()
- Add Impersonate action to UsersTable (admin-only, non-admin targets)
- Wire impersonation into UsersSettingsPage via onImpersonate prop

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… leak

1. use-impersonation.ts: Clear localStorage 'impersonate-user-id' when
   checkStatus() returns active:false or errors. Prevents 401s on all
   requests after natural session expiry.

2. dashboard.service.ts: Suppress totalRevenue for non-admin agents in
   legacy getStats() endpoint. Revenue had no trip-scoping (no trip_id
   on payment_transactions). Agents use getOverview() KPIs instead.

Fixes both blocking issues from Codex post-implementation review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ImpersonationGuard queries the table on every guarded request. During
deploys, Railway can start the new code before migrations complete,
causing ~245 PostgresError "relation does not exist" errors.

Fix: cache a table-existence check. If the table doesn't exist yet,
skip impersonation checks silently (session lock) or reject explicitly
(active impersonation header). Cache resets on app restart after
migration runs.

Fixes #58

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rlap

1. Redirect to /dashboard instead of reloading the current page (users
   tab) when starting impersonation — agents can't access admin pages.
2. Add spacer div so the fixed banner pushes content down instead of
   overlapping the sticky navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
tailfire-client Ready Ready Preview, Comment Mar 20, 2026 6:49pm
tailfire-ota Ready Ready Preview, Comment Mar 20, 2026 6:49pm

Request Review

@Systemsaholic Systemsaholic merged commit e24b6cd into main Mar 20, 2026
3 of 4 checks passed
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd626d66-9ce2-43e3-aaad-16ff9044cf10

📥 Commits

Reviewing files that changed from the base of the PR and between 0cbe1f4 and 279c7d2.

📒 Files selected for processing (61)
  • apps/admin/src/app/contacts/[id]/_components/contact-share-request-button.tsx
  • apps/admin/src/app/contacts/[id]/page.tsx
  • apps/admin/src/app/emails/inbox/page.tsx
  • apps/admin/src/app/profile/_components/email-tab.tsx
  • apps/admin/src/app/providers.tsx
  • apps/admin/src/app/settings/users/_components/users-table.tsx
  • apps/admin/src/app/settings/users/page.tsx
  • apps/admin/src/components/impersonation/impersonation-banner.tsx
  • apps/admin/src/hooks/use-emails.ts
  • apps/admin/src/hooks/use-impersonation.ts
  • apps/admin/src/lib/api.ts
  • apps/admin/src/lib/constants/roles.ts
  • apps/api/src/api-credentials/api-credentials.controller.ts
  • apps/api/src/app.controller.ts
  • apps/api/src/app.module.ts
  • apps/api/src/auth/decorators/admin-only.decorator.ts
  • apps/api/src/auth/decorators/bypass-impersonation.decorator.ts
  • apps/api/src/auth/guards/impersonation.guard.ts
  • apps/api/src/automation/admin/automation.controller.ts
  • apps/api/src/contacts/contact-access.service.ts
  • apps/api/src/contacts/contact-share-requests.controller.ts
  • apps/api/src/contacts/contact-share-requests.service.ts
  • apps/api/src/contacts/contacts.controller.ts
  • apps/api/src/contacts/contacts.module.ts
  • apps/api/src/contacts/dto/contact-share-request.dto.ts
  • apps/api/src/dashboard/dashboard.controller.ts
  • apps/api/src/dashboard/dashboard.service.ts
  • apps/api/src/email-accounts/email-accounts.service.ts
  • apps/api/src/email-accounts/email-sync.processor.ts
  • apps/api/src/email-accounts/imap-sync.service.ts
  • apps/api/src/enrichment/enrichment.controller.ts
  • apps/api/src/external-apis/providers/aerodatabox/aerodatabox.controller.ts
  • apps/api/src/financials/commission/commission.controller.ts
  • apps/api/src/financials/stripe-connect.controller.ts
  • apps/api/src/impersonation/impersonation.controller.ts
  • apps/api/src/impersonation/impersonation.module.ts
  • apps/api/src/impersonation/impersonation.service.ts
  • apps/api/src/loyalty-programs/loyalty-programs.controller.ts
  • apps/api/src/main.ts
  • apps/api/src/notifications/listeners/notification-events.listener.ts
  • apps/api/src/notifications/notification.service.ts
  • apps/api/src/notifications/notification.types.ts
  • apps/api/src/reference-data/reference-data.controller.ts
  • apps/api/src/security-audit/security-audit.module.ts
  • apps/api/src/security-audit/security-audit.service.ts
  • apps/api/src/security-audit/security-audit.types.ts
  • apps/api/src/security-audit/security-exception.filter.ts
  • apps/api/src/suppliers/suppliers.controller.ts
  • apps/api/src/trips/trips.controller.ts
  • apps/api/src/users/users.controller.ts
  • apps/api/src/users/users.service.ts
  • docs/superpowers/specs/2026-03-20-rbac-hardening-design.md
  • packages/database/src/migrations/20260320120000_create_security_audit_logs.sql
  • packages/database/src/migrations/20260320120100_create_contact_share_requests.sql
  • packages/database/src/migrations/20260320120200_create_impersonation_sessions.sql
  • packages/database/src/migrations/meta/_journal.json
  • packages/database/src/schema/contact-share-requests.schema.ts
  • packages/database/src/schema/impersonation-sessions.schema.ts
  • packages/database/src/schema/index.ts
  • packages/database/src/schema/security-audit-logs.schema.ts
  • packages/shared-types/src/api/contacts.types.ts

📝 Walkthrough

Walkthrough

This PR implements a comprehensive RBAC hardening and expansion system, introducing: a standardized @AdminOnly() decorator for admin authorization across the API, a new impersonation system allowing admins to assume user contexts, a contact share request workflow for agent-to-agent contact access, and security audit logging for security events. Additionally, email authentication failures are now handled more gracefully with specific error codes and UI notifications.

Changes

Cohort / File(s) Summary
Impersonation Frontend
apps/admin/src/components/impersonation/impersonation-banner.tsx, apps/admin/src/hooks/use-impersonation.ts
New client-side impersonation system: banner component displays active session status with extend/exit controls and countdown timer; hook manages session state via API calls, localStorage persistence, 30s polling, and automatic page reload on session end.
Impersonation Backend
apps/api/src/impersonation/*, apps/api/src/auth/guards/impersonation.guard.ts, apps/api/src/auth/decorators/bypass-impersonation.decorator.ts
Complete impersonation infrastructure: controller exposes start/extend/end/status endpoints, service manages session lifecycle with 4-hour max duration and agency validation, guard validates impersonation context and rewrites request.user, bypass decorator marks endpoints exempt from context swapping.
Authorization Refactoring
apps/api/src/auth/decorators/admin-only.decorator.ts, apps/api/src/*/...-controller.ts (15+ files)
Unified @AdminOnly() decorator standardizes admin authorization across API by replacing scattered @UseGuards(AdminGuard) and @Roles('admin') patterns on ~25 controller methods and class-level declarations.
Contact Share Requests System
apps/api/src/contacts/contact-share-requests.*, apps/admin/src/app/contacts/[id]/_components/contact-share-request-button.tsx, apps/admin/src/app/contacts/[id]/page.tsx
New agent-to-agent contact access request workflow: frontend button initiates POST to share-requests endpoint with loading/success states; backend service validates ownership/access, prevents duplicates (30-day expiration), emits events; controller exposes create/list/resolve endpoints; access service now includes _accessLevel field on responses.
Security Audit Logging
apps/api/src/security-audit/*, apps/api/src/auth/guards/...
New security event logging: SecurityAuditService listens for security.* events and persists to database; SecurityExceptionFilter captures 401/403 exceptions and emits audit events; UsersService emits role change events; ImpersonationService and ContactShareRequestsService emit related events.
Email Authentication Error Handling
apps/admin/src/app/emails/inbox/page.tsx, apps/admin/src/app/profile/_components/email-tab.tsx, apps/api/src/email-accounts/imap-sync.service.ts, apps/api/src/email-accounts/email-sync.processor.ts, apps/api/src/hooks/use-emails.ts
IMAP authentication failures now detected and handled with specific error code 'IMAP_AUTH_FAILED': frontend shows destructive alert with credential update link; backend centralizes failure handling with HttpException throwing; error state persists to account; email sync skips on auth failures.
Database Schema
packages/database/src/migrations/202603201201..., packages/database/src/schema/*.schema.ts
Three new tables: impersonation_sessions (admin-user pairs, expiry, end reason), contact_share_requests (request workflow with pending/approved/denied states and 30-day expiration), security_audit_logs (event audit trail with metadata); all with appropriate indexes and constraints.
Frontend Access Control & UI
apps/admin/src/app/providers.tsx, apps/admin/src/app/settings/users/_components/users-table.tsx, apps/admin/src/app/settings/users/page.tsx, apps/admin/src/lib/constants/roles.ts
UI enhancements: ImpersonationBanner added to provider tree; users table includes impersonate action for admins on non-admin targets; role display names standardized via getRoleDisplayName() helper; useUser hook integration for admin/role checks.
Infrastructure & Types
apps/api/src/app.module.ts, apps/api/src/main.ts, apps/api/src/lib/api.ts, packages/shared-types/src/api/contacts.types.ts
Global guard registration adds ImpersonationGuard in correct order; SecurityAuditModule and ImpersonationModule wired into AppModule; ApiError extended with optional code field; getAuthHeaders now includes X-Impersonate-User-Id header from localStorage; ContactResponseDto includes _accessLevel field.
Documentation
docs/superpowers/specs/2026-03-20-rbac-hardening-design.md
Design specification documenting RBAC hardening standardization, contact share requests workflow, impersonation system architecture, security audit logging, endpoint authorization audit requirements, and out-of-scope items.

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin User
    participant UI as Frontend UI
    participant API as API Server
    participant DB as Database
    participant Events as Event System

    Admin->>UI: Click "Impersonate User"
    UI->>API: POST /admin/impersonate/{userId}
    API->>DB: Create impersonation_session
    API->>Events: Emit security.impersonation_started
    Events->>DB: Log security audit event
    API-->>UI: Session details + expiresAt
    UI->>UI: Store impersonate-user-id in localStorage
    UI->>UI: Redirect to /dashboard
    
    Note over UI: ImpersonationBanner appears<br/>with countdown timer
    
    Admin->>UI: Click "Extend" (before expiry)
    UI->>API: POST /admin/impersonate/extend
    API->>DB: Refresh expiresAt (max 4h from creation)
    API->>API: Fetch latest session status
    API-->>UI: Updated expiresAt
    UI->>UI: Reset countdown timer
    UI->>UI: Show success toast
    
    Admin->>UI: Click "Exit Impersonation"
    UI->>API: DELETE /admin/impersonate
    API->>DB: Set endedAt + endReason='manual'
    API->>Events: Emit security.impersonation_ended
    Events->>DB: Log audit + notify target user
    API-->>UI: Confirmation
    UI->>UI: Clear localStorage + reload page
    UI->>UI: Return to normal admin view
Loading
sequenceDiagram
    participant Agent as Agent User
    participant UI as Frontend UI
    participant API as API Server
    participant DB as Database
    participant ContactOwner as Contact Owner
    participant Events as Event System

    Agent->>UI: View contact with basic access
    UI->>UI: Display "Limited View" badge
    UI->>UI: Show "Request Access" button
    
    Agent->>UI: Click "Request Access"
    UI->>API: POST /contacts/{id}/share-requests
    API->>DB: Check contact exists in agency
    API->>DB: Verify not already owned by requester
    API->>DB: Check no existing pending request
    API->>DB: Create contact_share_requests record<br/>(status='pending')
    API->>Events: Emit contact.share_requested
    Events->>DB: Create audit log
    API-->>UI: Share request created
    UI->>UI: Button → "Request Sent" (disabled)
    UI->>UI: Show success toast
    
    ContactOwner->>UI: View pending share requests
    UI->>API: GET /share-requests/pending
    API->>DB: Fetch pending requests for owner
    API-->>UI: List of pending requests
    
    ContactOwner->>UI: Click "Approve" request
    UI->>API: PATCH /share-requests/{id}
    API->>DB: Validate request pending status
    API->>DB: Update status='approved'
    API->>DB: Set resolvedAt + resolvedBy
    API->>API: Create ContactShare (full access)
    API->>Events: Emit contact.share_approved
    Events->>DB: Log audit event
    API-->>UI: Request approved
    UI->>UI: Show success notification
    
    Agent->>UI: Refresh contact view
    UI->>API: GET /contacts/{id}
    API->>DB: Check ContactShare record exists
    API->>API: Set _accessLevel='full'
    API-->>UI: Contact with full access
    UI->>UI: Display full contact details
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #18: Modifies DashboardService.getStats signature and adds trip/contact filtering via access services; overlaps directly with dashboard controller/service changes in this PR for agent-scoped statistics.
  • PR #19: Updates dashboard controller/service call sites for getStats and getDateRanges parameters; shares dashboard refactoring scope with this PR's access-control additions.

Poem

🐰 Hop, hop! The admin now impersonates with grace,
While agents request access to contacts at their pace,
Security logs every move with audit cheer,
And share requests bloom when access draws near!
RBAC fortified, the warren's secure! 🔐✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/rbac-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant