package auth import ( "context" "log/slog" "net/http" "github.com/google/uuid" ) // TenantLookup resolves and verifies tenant access for a user. // Defined as an interface to avoid circular dependency with services. type TenantLookup interface { FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error) VerifyAccess(ctx context.Context, userID, tenantID uuid.UUID) (bool, error) GetUserRole(ctx context.Context, userID, tenantID uuid.UUID) (string, error) } // TenantResolver is middleware that resolves the tenant from X-Tenant-ID header // or defaults to the user's first tenant. Always verifies user has access. type TenantResolver struct { lookup TenantLookup } func NewTenantResolver(lookup TenantLookup) *TenantResolver { return &TenantResolver{lookup: lookup} } func (tr *TenantResolver) Resolve(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID, ok := UserFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } var tenantID uuid.UUID if header := r.Header.Get("X-Tenant-ID"); header != "" { parsed, err := uuid.Parse(header) if err != nil { http.Error(w, `{"error":"invalid X-Tenant-ID"}`, http.StatusBadRequest) return } // Verify user has access and get their role role, err := tr.lookup.GetUserRole(r.Context(), userID, parsed) if err != nil { slog.Error("tenant access check failed", "error", err, "user_id", userID, "tenant_id", parsed) http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError) return } if role == "" { http.Error(w, `{"error":"no access to tenant"}`, http.StatusForbidden) return } tenantID = parsed r = r.WithContext(ContextWithUserRole(r.Context(), role)) } else { // Default to user's first tenant (role already set by middleware) first, err := tr.lookup.FirstTenantForUser(r.Context(), userID) if err != nil { slog.Error("failed to resolve default tenant", "error", err, "user_id", userID) http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError) return } if first == nil { http.Error(w, `{"error":"no tenant found for user"}`, http.StatusBadRequest) return } tenantID = *first } ctx := ContextWithTenantID(r.Context(), tenantID) next.ServeHTTP(w, r.WithContext(ctx)) }) }