fix(gateway/services): admin node/render pages 500 — redirect loop + is_admin claim
Build backend images / build content-svc (push) Failing after 56s
Build backend images / build file-svc (push) Failing after 54s
Build backend images / build gateway (push) Failing after 55s
Build backend images / build identity-svc (push) Failing after 48s
Build backend images / build notification-svc (push) Failing after 55s
Build backend images / build render-svc (push) Failing after 57s
Build backend images / build studio-svc (push) Failing after 44s
Build backend images / build content-svc (push) Failing after 56s
Build backend images / build file-svc (push) Failing after 54s
Build backend images / build gateway (push) Failing after 55s
Build backend images / build identity-svc (push) Failing after 48s
Build backend images / build notification-svc (push) Failing after 55s
Build backend images / build render-svc (push) Failing after 57s
Build backend images / build studio-svc (push) Failing after 44s
- gateway proxy: trim trailing slash before forwarding upstream. gin's RedirectTrailingSlash adds /nodes → /nodes/ while render-svc redirects /nodes/ → /nodes, forming an infinite redirect loop (admin pages 500'd) - accept is_admin as bool OR string "true" in render/file/notification/gateway auth middleware (identity emits it as a string) — admin endpoints were 403'ing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,13 @@ func Auth(jwtSecret string) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
tenantID, _ := uuid.Parse(claims["tenant_id"].(string))
|
||||
isAdmin, _ := claims["is_admin"].(bool)
|
||||
isAdmin := false
|
||||
switch v := claims["is_admin"].(type) {
|
||||
case bool:
|
||||
isAdmin = v
|
||||
case string:
|
||||
isAdmin = v == "true"
|
||||
}
|
||||
|
||||
c.Set(KeyUserID, userID)
|
||||
c.Set(KeyTenantID, tenantID)
|
||||
|
||||
@@ -75,7 +75,13 @@ func validateAndInject(c *gin.Context, secret string) bool {
|
||||
claims, _ := token.Claims.(jwt.MapClaims)
|
||||
userID, _ := uuid.Parse(fmt.Sprintf("%v", claims["sub"]))
|
||||
tenantID, _ := uuid.Parse(fmt.Sprintf("%v", claims["tenant_id"]))
|
||||
isAdmin, _ := claims["is_admin"].(bool)
|
||||
isAdmin := false
|
||||
switch v := claims["is_admin"].(type) {
|
||||
case bool:
|
||||
isAdmin = v
|
||||
case string:
|
||||
isAdmin = v == "true"
|
||||
}
|
||||
role, _ := claims["role"].(string)
|
||||
|
||||
c.Set(CtxUserID, userID)
|
||||
@@ -116,7 +122,13 @@ func OptionalJWTAuth(secret string) gin.HandlerFunc {
|
||||
claims, _ := token.Claims.(jwt.MapClaims)
|
||||
userID, _ := uuid.Parse(fmt.Sprintf("%v", claims["sub"]))
|
||||
tenantID, _ := uuid.Parse(fmt.Sprintf("%v", claims["tenant_id"]))
|
||||
isAdmin, _ := claims["is_admin"].(bool)
|
||||
isAdmin := false
|
||||
switch v := claims["is_admin"].(type) {
|
||||
case bool:
|
||||
isAdmin = v
|
||||
case string:
|
||||
isAdmin = v == "true"
|
||||
}
|
||||
c.Request.Header.Set(HeaderUserID, userID.String())
|
||||
c.Request.Header.Set(HeaderTenantID, tenantID.String())
|
||||
if isAdmin {
|
||||
|
||||
@@ -38,6 +38,13 @@ func New(name, target string) *Upstream {
|
||||
orig := rp.Director
|
||||
rp.Director = func(req *http.Request) {
|
||||
orig(req)
|
||||
// Normalise away a trailing slash (gin's RedirectTrailingSlash adds one when a
|
||||
// route is registered as catch-all, e.g. /v1/nodes/*path, but upstream services
|
||||
// register the canonical no-slash form and redirect /nodes/ → /nodes — without
|
||||
// this the two redirects form an infinite loop).
|
||||
if len(req.URL.Path) > 1 && strings.HasSuffix(req.URL.Path, "/") {
|
||||
req.URL.Path = strings.TrimRight(req.URL.Path, "/")
|
||||
}
|
||||
req.Header.Set("X-Forwarded-Host", req.Host)
|
||||
req.Header.Del("X-Forwarded-For") // let httputil re-add it properly
|
||||
req.Host = u.Host
|
||||
|
||||
@@ -37,7 +37,13 @@ func JWTAuth(secret string) gin.HandlerFunc {
|
||||
claims, _ := token.Claims.(jwt.MapClaims)
|
||||
userID, _ := uuid.Parse(fmt.Sprintf("%v", claims["sub"]))
|
||||
tenantID, _ := uuid.Parse(fmt.Sprintf("%v", claims["tenant_id"]))
|
||||
isAdmin, _ := claims["is_admin"].(bool)
|
||||
isAdmin := false
|
||||
switch v := claims["is_admin"].(type) {
|
||||
case bool:
|
||||
isAdmin = v
|
||||
case string:
|
||||
isAdmin = v == "true"
|
||||
}
|
||||
c.Set(CtxUserID, userID)
|
||||
c.Set(CtxTenantID, tenantID)
|
||||
c.Set(CtxIsAdmin, isAdmin)
|
||||
|
||||
@@ -43,7 +43,15 @@ func JWTAuth(secret string) gin.HandlerFunc {
|
||||
}
|
||||
userID, _ := uuid.Parse(fmt.Sprintf("%v", claims["sub"]))
|
||||
tenantID, _ := uuid.Parse(fmt.Sprintf("%v", claims["tenant_id"]))
|
||||
isAdmin, _ := claims["is_admin"].(bool)
|
||||
// is_admin may arrive as a JSON bool or as the string "true" (identity emits a
|
||||
// string). Accept both so [RequireAdmin] works regardless of token encoding.
|
||||
isAdmin := false
|
||||
switch v := claims["is_admin"].(type) {
|
||||
case bool:
|
||||
isAdmin = v
|
||||
case string:
|
||||
isAdmin = v == "true"
|
||||
}
|
||||
role, _ := claims["role"].(string)
|
||||
|
||||
c.Set(CtxUserID, userID)
|
||||
|
||||
Reference in New Issue
Block a user