From 1002f2bdf0a03ba33d212b73b4d1605ce198567f Mon Sep 17 00:00:00 2001 From: Guzman Date: Wed, 22 Apr 2026 00:10:19 +0200 Subject: [PATCH] Fix identity resolution for ServiceAccount tokens behind OAuth proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a request uses a K8s ServiceAccount token (API key), the OAuth proxy sets X-Forwarded-User to the SA subject (e.g. "system:serviceaccount:ns:sa-name"). The middleware sanitized this into a synthetic userID, but the fallback that reads the SA's created-by-user-id annotation was guarded by `userID == ""` — which was never true because the proxy already set it. This caused credential RBAC failures for API key-authenticated requests (mobile apps, CLI tools) because the synthetic SA identity didn't match the session owner's identity, so the runner couldn't fetch GitHub/Jira/Google credentials. Fix: also trigger the annotation lookup when X-Forwarded-User is a ServiceAccount subject, overriding the synthetic ID with the real creating user's identity. Co-Authored-By: Claude Opus 4.6 --- components/backend/server/server.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/components/backend/server/server.go b/components/backend/server/server.go index 1bb440e5a..7ea1013fc 100755 --- a/components/backend/server/server.go +++ b/components/backend/server/server.go @@ -161,12 +161,23 @@ func forwardedIdentityMiddleware() gin.HandlerFunc { c.Set("forwardedAccessToken", v) } - // Fallback: if userID is still empty, verify the Bearer token via - // TokenReview to securely resolve the ServiceAccount identity, then - // read the created-by-user-id annotation. This enables API key- - // authenticated requests to inherit the creating user's identity - // so that integration credentials (GitHub, Jira, GitLab) are accessible. - if c.GetString("userID") == "" && K8sClient != nil { + // Resolve the creating user's identity for ServiceAccount tokens. + // The OAuth proxy sets X-Forwarded-User to the SA subject + // (e.g. "system:serviceaccount:ns:sa-name"), which the code above + // sanitizes into a synthetic userID. That synthetic ID doesn't match + // the session owner, breaking credential RBAC. We resolve the SA's + // created-by-user-id annotation to get the real human identity. + // + // This runs in two cases: + // 1. X-Forwarded-User was a SA subject (set by OAuth proxy for API key requests) + // 2. No X-Forwarded-User at all (direct API key requests bypassing proxy) + resolveNeeded := c.GetString("userID") == "" + if !resolveNeeded { + if orig := c.GetString("userIDOriginal"); strings.HasPrefix(orig, "system:serviceaccount:") { + resolveNeeded = true + } + } + if resolveNeeded && K8sClient != nil { if ns, saName, ok := resolveServiceAccountFromToken(c); ok { sa, err := K8sClient.CoreV1().ServiceAccounts(ns).Get(c.Request.Context(), saName, v1.GetOptions{}) if err == nil && sa.Annotations != nil {