-
Notifications
You must be signed in to change notification settings - Fork 63
fix(backend): API keys access user integrations via SA annotation #901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,16 @@ | |
| package server | ||
|
|
||
| import ( | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "fmt" | ||
| "log" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/gin-contrib/cors" | ||
| "github.com/gin-gonic/gin" | ||
| v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| // RouterFunc is a function that can register routes on a Gin router | ||
|
|
@@ -158,6 +161,76 @@ func forwardedIdentityMiddleware() gin.HandlerFunc { | |
| if v := c.GetHeader("X-Forwarded-Access-Token"); v != "" { | ||
| c.Set("forwardedAccessToken", v) | ||
| } | ||
|
|
||
| // Fallback: if userID is still empty, try to resolve it from the | ||
| // ServiceAccount's 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") == "" { | ||
| if ns, saName, ok := extractServiceAccountFromBearer(c); ok && K8sClient != nil { | ||
| sa, err := K8sClient.CoreV1().ServiceAccounts(ns).Get(c.Request.Context(), saName, v1.GetOptions{}) | ||
| if err == nil && sa.Annotations != nil { | ||
| if uid := sa.Annotations["ambient-code.io/created-by-user-id"]; uid != "" { | ||
| c.Set("userID", uid) | ||
| } | ||
| } | ||
|
Comment on lines
+169
to
+176
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not treat a mutable ServiceAccount annotation as authoritative user identity. Once the token is verified, this block copies 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| c.Next() | ||
| } | ||
| } | ||
|
|
||
| // extractServiceAccountFromBearer extracts namespace and SA name from a Bearer | ||
| // JWT's "sub" claim (format: "system:serviceaccount:<ns>:<name>"). | ||
| // Also checks the X-Remote-User header for the same format. | ||
| func extractServiceAccountFromBearer(c *gin.Context) (string, string, bool) { | ||
| // Check X-Remote-User header (OpenShift OAuth proxy format) | ||
| if remoteUser := c.GetHeader("X-Remote-User"); remoteUser != "" { | ||
| const prefix = "system:serviceaccount:" | ||
| if strings.HasPrefix(remoteUser, prefix) { | ||
| parts := strings.SplitN(strings.TrimPrefix(remoteUser, prefix), ":", 2) | ||
| if len(parts) == 2 { | ||
| return parts[0], parts[1], true | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Standard Authorization Bearer JWT parsing | ||
| rawAuth := c.GetHeader("Authorization") | ||
| parts := strings.SplitN(rawAuth, " ", 2) | ||
| if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") { | ||
| return "", "", false | ||
| } | ||
| token := strings.TrimSpace(parts[1]) | ||
|
Comment on lines
+188
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Query-string API-key auth still misses this fallback. This resolver only reads Possible fix func Run(registerRoutes RouterFunc) error {
// Setup Gin router with custom logger that redacts tokens
r := gin.New()
r.Use(gin.Recovery())
+ r.Use(func(c *gin.Context) {
+ if c.GetHeader("Authorization") == "" && c.GetHeader("X-Forwarded-Access-Token") == "" {
+ if qp := strings.TrimSpace(c.Query("token")); qp != "" {
+ c.Request.Header.Set("Authorization", "Bearer "+qp)
+ }
+ }
+ c.Next()
+ })
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {🤖 Prompt for AI Agents |
||
| if token == "" { | ||
| return "", "", false | ||
| } | ||
| segs := strings.Split(token, ".") | ||
| if len(segs) < 2 { | ||
| return "", "", false | ||
| } | ||
| payloadB64 := segs[1] | ||
| if m := len(payloadB64) % 4; m != 0 { | ||
| payloadB64 += strings.Repeat("=", 4-m) | ||
| } | ||
| data, err := base64.URLEncoding.DecodeString(payloadB64) | ||
| if err != nil { | ||
| return "", "", false | ||
| } | ||
| var payload map[string]interface{} | ||
| if err := json.Unmarshal(data, &payload); err != nil { | ||
| return "", "", false | ||
| } | ||
| sub, _ := payload["sub"].(string) | ||
| const prefix = "system:serviceaccount:" | ||
| if !strings.HasPrefix(sub, prefix) { | ||
| return "", "", false | ||
| } | ||
| rest := strings.TrimPrefix(sub, prefix) | ||
| parts2 := strings.SplitN(rest, ":", 2) | ||
| if len(parts2) != 2 { | ||
| return "", "", false | ||
| } | ||
| return parts2[0], parts2[1], true | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.