Skip to content

Commit 1a53bb5

Browse files
authored
fix: Propagate tenant context when auth is disabled (#1252)
The metadataPropagationMiddleware strips incoming x-tenant-id headers (security) and re-sets them from the auth context. When AUTH_ENABLED=false, the auth context is empty, so the tenant header set by the tenant resolver was stripped and never restored. This caused all tenant-scoped API calls to fail with "tenant context missing" on the demo environment. Add a fallback that reads tenant from the request context (set by the tenant resolver middleware) when neither JWT nor API key identity is present. Also fix a frontend crash on the Positions page where .replace() was called on a non-string statusTracking.currentStatus value. Co-authored-by: Ben Coombs <bjcoombs@users.noreply.github.com>
1 parent cc01ffe commit 1a53bb5

4 files changed

Lines changed: 60 additions & 3 deletions

File tree

frontend/src/pages/positions/detail.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export function PositionDetailPage() {
208208
<SkeletonField />
209209
) : (
210210
<span>
211-
{log?.statusTracking?.currentStatus?.replace(/_/g, ' ') ?? '—'}
211+
{typeof log?.statusTracking?.currentStatus === 'string' ? log.statusTracking.currentStatus.replace(/_/g, ' ') : '—'}
212212
</span>
213213
)}
214214
</LabeledField>

frontend/src/pages/positions/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ export function PositionsPage() {
8484
accessorKey: 'statusTracking',
8585
header: 'Status',
8686
cell: ({ row }) => {
87-
const status = row.original.statusTracking?.currentStatus ?? '—'
88-
return <span className="text-sm">{status.replace(/_/g, ' ')}</span>
87+
const status = row.original.statusTracking?.currentStatus
88+
return <span className="text-sm">{typeof status === 'string' ? status.replace(/_/g, ' ') : '—'}</span>
8989
},
9090
},
9191
{

services/gateway/metadata.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66

77
"github.com/meridianhub/meridian/services/gateway/auth"
8+
"github.com/meridianhub/meridian/shared/platform/tenant"
89
)
910

1011
// metadataPropagationMiddleware is an HTTP middleware for the Vanguard transcoder
@@ -88,5 +89,14 @@ func writeIdentityMetadata(req *http.Request) {
8889
if tenantID, ok := auth.GetTenantIDFromContext(ctx); ok && tenantID != "" {
8990
req.Header.Set("x-tenant-id", tenantID)
9091
}
92+
return
93+
}
94+
95+
// Fall back to tenant resolver context. When auth is disabled
96+
// (AUTH_ENABLED=false), the tenant resolver middleware still injects the
97+
// tenant ID into the request context via tenant.WithTenant(). Propagate
98+
// it as a header so Vanguard forwards it as gRPC metadata.
99+
if tenantID, ok := tenant.FromContext(ctx); ok && !tenantID.IsEmpty() {
100+
req.Header.Set("x-tenant-id", string(tenantID))
91101
}
92102
}

services/gateway/metadata_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/meridianhub/meridian/services/gateway/auth"
10+
"github.com/meridianhub/meridian/shared/platform/tenant"
1011
"github.com/stretchr/testify/assert"
1112
)
1213

@@ -233,3 +234,49 @@ func TestMetadataPropagationMiddleware_JWTPrioritizedOverAPIKey(t *testing.T) {
233234
assert.Equal(t, AuthMethodJWT, capturedHeaders.Get("x-auth-method"))
234235
assert.Equal(t, "jwt-tenant", capturedHeaders.Get("x-tenant-id"))
235236
}
237+
238+
// TestMetadataPropagationMiddleware_TenantResolverFallback verifies that when
239+
// auth is disabled (no JWT or API key identity), the middleware falls back to
240+
// tenant context set by the tenant resolver middleware.
241+
func TestMetadataPropagationMiddleware_TenantResolverFallback(t *testing.T) {
242+
var capturedHeaders http.Header
243+
244+
handler := metadataPropagationMiddleware(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
245+
capturedHeaders = r.Header.Clone()
246+
}))
247+
248+
req := httptest.NewRequest(http.MethodPost, "/meridian.party.v1.PartyService/ListParties", nil)
249+
// No auth context (AUTH_ENABLED=false), but tenant resolver injected tenant
250+
ctx := tenant.WithTenant(req.Context(), tenant.TenantID("volterra_energy"))
251+
req = req.WithContext(ctx)
252+
253+
handler.ServeHTTP(httptest.NewRecorder(), req)
254+
255+
// Tenant ID propagated from resolver context
256+
assert.Equal(t, "volterra_energy", capturedHeaders.Get("x-tenant-id"))
257+
// No auth identity headers
258+
assert.Empty(t, capturedHeaders.Get("x-user-id"))
259+
assert.Empty(t, capturedHeaders.Get("x-auth-method"))
260+
}
261+
262+
// TestMetadataPropagationMiddleware_TenantResolverSpoofedHeaderStripped verifies
263+
// that spoofed x-tenant-id headers are stripped even when tenant resolver context
264+
// is used as fallback.
265+
func TestMetadataPropagationMiddleware_TenantResolverSpoofedHeaderStripped(t *testing.T) {
266+
var capturedHeaders http.Header
267+
268+
handler := metadataPropagationMiddleware(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
269+
capturedHeaders = r.Header.Clone()
270+
}))
271+
272+
req := httptest.NewRequest(http.MethodPost, "/test", nil)
273+
req.Header.Set("x-tenant-id", "spoofed-tenant")
274+
// Tenant resolver set the real tenant
275+
ctx := tenant.WithTenant(req.Context(), tenant.TenantID("real_tenant"))
276+
req = req.WithContext(ctx)
277+
278+
handler.ServeHTTP(httptest.NewRecorder(), req)
279+
280+
// Spoofed header replaced by resolver tenant
281+
assert.Equal(t, "real_tenant", capturedHeaders.Get("x-tenant-id"))
282+
}

0 commit comments

Comments
 (0)