-
Notifications
You must be signed in to change notification settings - Fork 64
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 all commits
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 |
|---|---|---|
|
|
@@ -9,6 +9,8 @@ import ( | |
|
|
||
| "github.com/gin-contrib/cors" | ||
| "github.com/gin-gonic/gin" | ||
| authnv1 "k8s.io/api/authentication/v1" | ||
| v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| ) | ||
|
|
||
| // RouterFunc is a function that can register routes on a Gin router | ||
|
|
@@ -158,6 +160,72 @@ func forwardedIdentityMiddleware() gin.HandlerFunc { | |
| if v := c.GetHeader("X-Forwarded-Access-Token"); v != "" { | ||
| 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 { | ||
| 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 { | ||
| 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() | ||
| } | ||
| } | ||
|
|
||
| // resolveServiceAccountFromToken verifies the Bearer token via K8s TokenReview | ||
| // and extracts the ServiceAccount namespace and name from the authenticated identity. | ||
| // Returns (namespace, saName, true) when verified, otherwise ("","",false). | ||
| func resolveServiceAccountFromToken(c *gin.Context) (string, string, bool) { | ||
| 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 | ||
| } | ||
|
|
||
| tr := &authnv1.TokenReview{Spec: authnv1.TokenReviewSpec{Token: token}} | ||
| rv, err := K8sClient.AuthenticationV1().TokenReviews().Create(c.Request.Context(), tr, v1.CreateOptions{}) | ||
| if err != nil || !rv.Status.Authenticated || rv.Status.Error != "" { | ||
| return "", "", false | ||
| } | ||
|
|
||
| subj := strings.TrimSpace(rv.Status.User.Username) | ||
| const prefix = "system:serviceaccount:" | ||
| if !strings.HasPrefix(subj, prefix) { | ||
| return "", "", false | ||
| } | ||
| rest := strings.TrimPrefix(subj, prefix) | ||
| segs := strings.SplitN(rest, ":", 2) | ||
| if len(segs) != 2 { | ||
| return "", "", false | ||
| } | ||
| return segs[0], segs[1], true | ||
| } | ||
|
|
||
| // ExtractServiceAccountFromAuth extracts namespace and ServiceAccount name | ||
| // from the X-Remote-User header (OpenShift OAuth proxy format). | ||
| // Returns (namespace, saName, true) when a SA subject is present, otherwise ("","",false). | ||
| func ExtractServiceAccountFromAuth(c *gin.Context) (string, string, bool) { | ||
| 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 | ||
| } | ||
| } | ||
| } | ||
| return "", "", false | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep bearer-token ServiceAccount extraction in this compatibility wrapper.
server.ExtractServiceAccountFromAuthonly handlesX-Remote-User, but callers in this package still rely onExtractServiceAccountFromAuthfor bearer-token API-key requests (getUserSubjectFromContext), and the existing handler tests cover that path. After this delegation, those requests no longer round-trip tosystem:serviceaccount:<ns>:<name>and instead fall back to the creatoruserID, which changes behavior for any subject-based logic.🤖 Prompt for AI Agents