Skip to content

RFC: Auth and Router Refactor #643

@alexluong

Description

@alexluong

Summary

Clean up router logic after recent changes consolidating routes under /tenants/:tenantID. The current structure has historical artifacts that no longer make sense.

Problem

The current auth model creates confusion in route registration:

// Current route groupings in router.go
nonTenantRoutes      // Admin-only, no :tenantID
tenantUpsertRoute    // Admin-only, but HAS :tenantID in path
portalRoutes         // Admin-only, manually adds RequireTenantMiddleware
tenantAgnosticRoutes // AdminOrTenant - why separate from...
tenantSpecificRoutes // AdminOrTenant - also has tenant in path?

Issues:

  1. Confusing route groupings - The distinction between tenantAgnosticRoutes and tenantSpecificRoutes is unclear. Both use AuthScopeAdminOrTenant, both have :tenantID in path. Historical artifact from when there were two route versions (/:tenantID/destinations vs /destinations with JWT).

  2. Inconsistent tenant middleware - Some routes manually add RequireTenantMiddleware, others don't. Not clear from AuthScope whether a route needs tenant context.

  3. AuthScope doesn't capture tenant requirement - AuthScopeAdmin on a route with :tenantID doesn't tell you if tenant lookup is needed.

  4. Handlers do their own tenant lookup - Each handler fetches tenant and handles 404 separately.

Goals

  1. Simplify the mental model for auth
  2. Centralize tenant lookup in middleware
  3. Handlers receive validated *Tenant, not raw ID
  4. Remove confusing route groupings and RouteMode

Proposed Design

Auth Modes (3 total)

const (
    AuthPublic AuthMode = "public"  // No auth required
    AuthTenant AuthMode = "tenant"  // Tenant JWT required (admin can also access)
    AuthAdmin  AuthMode = "admin"   // Admin only
)

TenantScoped as Contract

type RouteDefinition struct {
    Method       string
    Path         string
    Handler      gin.HandlerFunc
    Auth         AuthMode      // Public, Tenant, Admin
    TenantScoped bool          // Contract: handler receives *Tenant
    Middlewares  []gin.HandlerFunc
}

TenantScoped: true means middleware injects *Tenant into context, handlers call TenantFromContext(c).

Remove RouteMode

Replace with conditional registration:

routes := baseRoutes
if cfg.PortalEnabled() {
    routes = append(routes, portalRoutes...)
}

Simplified Route Registration

routes := []RouteDefinition{
    {Path: "/healthz", Auth: AuthPublic, TenantScoped: false},
    {Path: "/topics", Auth: AuthTenant, TenantScoped: false},
    {Path: "/tenants", Auth: AuthAdmin, TenantScoped: false},
    {Path: "/tenants/:tenantID", Auth: AuthAdmin, TenantScoped: true},
    {Path: "/tenants/:tenantID/destinations", Auth: AuthTenant, TenantScoped: true},
}

Each route is self-describing - no confusing groupings.

Prerequisites

Before refactoring, verify test coverage for auth at the API level:

  • API key auth (valid, invalid, missing)
  • JWT auth (valid, invalid, expired, wrong tenant)
  • Tenant not found scenarios

If coverage is weak, add tests first.


Full spec: .ralph/specs/auth-refactor.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions