-
Notifications
You must be signed in to change notification settings - Fork 18
Authorization for apiserver #6
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
Conversation
Summary of ChangesHello @YaoZengzeng, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a significant architectural change to the pico-apiserver's authorization system. It transitions from a rudimentary JWT-based authentication to a robust, Kubernetes-native Service Account TokenReview mechanism. This shift ensures that all API operations, particularly those involving Kubernetes resource management like sandbox lifecycles, are performed with the explicit permissions of the requesting user's Service Account and within their specific namespace. This change not only strengthens the security posture but also lays the groundwork for more sophisticated multi-tenant environments. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
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.
Code Review
This pull request is a significant improvement, transitioning the apiserver from a simple JWT secret to a robust Kubernetes-native authentication model using Service Account tokens and the TokenReview API. The shift to a caller-permission-based model, where actions are performed with the user's identity in their own namespace, is a great security enhancement. The code is well-structured, and the new example client with its detailed README is very helpful. My review includes a few suggestions to enhance the robustness of token parsing and context value extraction to make the new implementation even more solid.
| if authToken == "" { | ||
| log.Println("⚠️ WARNING: API_TOKEN environment variable not set") | ||
| log.Println(" Attempting to proceed without authentication token") | ||
| log.Println(" Set API_TOKEN to authenticate with the API server") | ||
| log.Println() | ||
| } else { | ||
| log.Println("✅ API authentication token loaded from environment") | ||
| log.Println() | ||
| } |
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.
Since the server now mandates an authentication token, the client will fail with a 401 Unauthorized error if API_TOKEN is not set. Instead of printing a warning and proceeding, it would provide clearer and more immediate feedback to the user if the client exits with a fatal error. This makes the example client's behavior consistent with the new server requirements.
if authToken == "" {
log.Fatalf("❌ ERROR: API_TOKEN environment variable not set. Please set it to a valid service account token.")
}
log.Println("✅ API authentication token loaded from environment")
log.Println()
pkg/apiserver/handlers.go
Outdated
| userToken, _ := r.Context().Value(contextKeyUserToken).(string) | ||
| userNamespace, _ := r.Context().Value(contextKeyNamespace).(string) | ||
| serviceAccount, _ := r.Context().Value(contextKeyServiceAccount).(string) | ||
|
|
||
| if userToken == "" || userNamespace == "" { | ||
| respondError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unable to extract user credentials") | ||
| return | ||
| } |
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.
It's more idiomatic in Go to check the ok boolean returned from a type assertion when retrieving values from a context. While the current code works because a failed assertion for a string results in an empty string, explicitly checking ok makes the intent clearer and the code more robust.
| userToken, _ := r.Context().Value(contextKeyUserToken).(string) | |
| userNamespace, _ := r.Context().Value(contextKeyNamespace).(string) | |
| serviceAccount, _ := r.Context().Value(contextKeyServiceAccount).(string) | |
| if userToken == "" || userNamespace == "" { | |
| respondError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unable to extract user credentials") | |
| return | |
| } | |
| userToken, ok1 := r.Context().Value(contextKeyUserToken).(string) | |
| userNamespace, ok2 := r.Context().Value(contextKeyNamespace).(string) | |
| serviceAccount, _ := r.Context().Value(contextKeyServiceAccount).(string) | |
| if !ok1 || !ok2 || userToken == "" || userNamespace == "" { | |
| respondError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unable to extract user credentials") | |
| return | |
| } |
pkg/apiserver/handlers.go
Outdated
| userToken, _ := r.Context().Value(contextKeyUserToken).(string) | ||
| userNamespace, _ := r.Context().Value(contextKeyNamespace).(string) | ||
| serviceAccount, _ := r.Context().Value(contextKeyServiceAccount).(string) | ||
|
|
||
| if userToken == "" || userNamespace == "" { | ||
| respondError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unable to extract user credentials") | ||
| return | ||
| } |
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.
Similar to the handleCreateSession handler, it's more idiomatic and robust to check the ok boolean from the type assertion when extracting values from the request context. This improves code clarity and ensures that the absence of a value is handled explicitly.
| userToken, _ := r.Context().Value(contextKeyUserToken).(string) | |
| userNamespace, _ := r.Context().Value(contextKeyNamespace).(string) | |
| serviceAccount, _ := r.Context().Value(contextKeyServiceAccount).(string) | |
| if userToken == "" || userNamespace == "" { | |
| respondError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unable to extract user credentials") | |
| return | |
| } | |
| userToken, ok1 := r.Context().Value(contextKeyUserToken).(string) | |
| userNamespace, ok2 := r.Context().Value(contextKeyNamespace).(string) | |
| serviceAccount, _ := r.Context().Value(contextKeyServiceAccount).(string) | |
| if !ok1 || !ok2 || userToken == "" || userNamespace == "" { | |
| respondError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unable to extract user credentials") | |
| return | |
| } |
b9bf44a to
7c46eb9
Compare
example/README.md
Outdated
|
|
||
| ## Permission Model | ||
|
|
||
| **Important:** pico-apiserver now uses a caller-permission-based operation model. This means: |
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.
check all pico-apiserver and update to agentcube-apiserver
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.
good catch, maybe the rebase issue.
example/README.md
Outdated
| rules: | ||
| - apiGroups: ["agents.x-k8s.io"] | ||
| resources: ["sandboxes"] | ||
| verbs: ["create", "delete", "get", "list", "watch"] |
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.
we may not need watch
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.
Pull Request Overview
This PR refactors the authentication and authorization model to implement caller-permission-based operations using Kubernetes Service Account tokens. The key change is moving from a centralized permission model to one where operations are executed using the caller's own Service Account credentials.
- Replaces JWT-based authentication with Kubernetes TokenReview API validation
- Introduces per-user Kubernetes clients that execute operations with caller's permissions
- Updates sandbox operations to use user namespaces and credentials
Reviewed Changes
Copilot reviewed 9 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pkg/apiserver/k8s_client.go | Adds UserK8sClient type and baseConfig storage for creating per-user Kubernetes clients |
| pkg/apiserver/handlers.go | Updates create/delete handlers to extract user credentials and use UserK8sClient for operations |
| pkg/apiserver/auth.go | Replaces JWT validation with Kubernetes TokenReview API and extracts service account details |
| pkg/apiserver/config.go | Removes deprecated JWTSecret field |
| cmd/apiserver/main.go | Removes jwt-secret command line flag |
| k8s/agentcube-apiserver.yaml | Adds RBAC permissions for TokenReview API access |
| example/client.go | Updates to use API_TOKEN environment variable with improved messaging |
| example/README.md | Documents new permission model and RBAC configuration requirements |
| .gitignore | Updates test binary path |
| go.mod, go.sum | Adds indirect dependencies (kr/fs, pkg/sftp) |
Comments suppressed due to low confidence (3)
go.mod:3
- Go version 1.24.4 does not exist. As of January 2025, the latest Go versions are in the 1.23.x series. This appears to be a future version that is not yet released. Please use a valid Go version such as 1.23.x or earlier.
go 1.24.4
go.mod:5
- Go toolchain version 1.24.9 does not exist. As of January 2025, the latest Go versions are in the 1.23.x series. This appears to be a future version that is not yet released. Please use a valid Go toolchain version such as 1.23.x or earlier.
toolchain go1.24.9
example/client.go:61
- The authToken variable is read twice from the environment (line 61 and line 66). The second assignment on line 66 is redundant and should be removed to avoid confusion.
authToken = os.Getenv("API_TOKEN")
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pkg/apiserver/handlers.go
Outdated
| } | ||
|
|
||
| // Create K8s client with user's token | ||
| userClient, err := s.k8sClient.NewUserK8sClient(userToken, userNamespace) |
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.
This would create one client per call. I am concerned, It will create too many clients.
hzxuzhonghu
left a comment
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.
And it seems you donot protect the tunnel handler
|
@hzxuzhonghu updated |
cmd/apiserver/main.go
Outdated
| enableTLS = flag.Bool("enable-tls", false, "Enable TLS (HTTPS)") | ||
| tlsCert = flag.String("tls-cert", "", "Path to TLS certificate file") | ||
| tlsKey = flag.String("tls-key", "", "Path to TLS key file") | ||
| agentCubeAPIServiceAccount = flag.String("agentcube-api-service-account", "agentcube-apiserver", "Service account name for agentcube-apiserver (has admin privileges)") |
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.
admin privileges seems confusing
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.
BTW, what token it this?
cmd/apiserver/main.go
Outdated
| jwtSecret = flag.String("jwt-secret", "", "JWT secret for token validation (optional)") | ||
| port = flag.String("port", "8080", "API server port") | ||
| namespace = flag.String("namespace", "default", "Kubernetes namespace for sandboxes") | ||
| sshUsername = flag.String("ssh-username", "sandbox", "Default SSH username for sandbox pods") |
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.
nit: i think this name should be passed from client, it is common client can use their own images, which can run as customized users
pkg/apiserver/auth.go
Outdated
| // - User is the creator of the sandbox | ||
| func (s *Server) checkSandboxAccess(sandbox *Sandbox, serviceAccountName string) bool { | ||
| // Check if user is agentcube-apiserver (has admin access) | ||
| if s.config.AgentCubeAPIServiceAccount != "" && serviceAccountName == s.config.AgentCubeAPIServiceAccount { |
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.
I am not quite understand why compare with s.config.AgentCubeAPIServiceAccount.
pkg/apiserver/client_cache.go
Outdated
|
|
||
| // Check if entry is expired | ||
| if time.Since(entry.lastAccess) > c.ttl { | ||
| return nil |
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.
I donot see where you call the cleanup funciton, could do lazy cleanup here
pkg/apiserver/k8s_client.go
Outdated
|
|
||
| // If cache entry exists but token doesn't match, remove it | ||
| // This handles token rotation scenarios | ||
| if c.clientCache.Get(cacheKey) != nil { |
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.
duplicate with L131?
|
|
||
| // NewUserK8sClient creates a K8s client using the provided user token | ||
| // This method creates a new client without using cache (for direct creation) | ||
| func (c *K8sClient) NewUserK8sClient(userToken, namespace string) (*UserK8sClient, error) { |
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.
From my perspective, we can only cache dynamicClient.
Signed-off-by: YaoZengzeng <yaozengzeng@huawei.com>
Signed-off-by: YaoZengzeng <yaozengzeng@huawei.com>
Signed-off-by: YaoZengzeng <yaozengzeng@huawei.com>
|
@hzxuzhonghu updated |
|
/lgtm We need to keep track of sdk side in sync with server side update |
|
[APPROVALNOTIFIER] This PR is APPROVED Approval requirements bypassed by manually added approval. This pull-request has been approved by: hzxuzhonghu The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
@hzxuzhonghu please merge |
|
[needs-rebase] label prevented the bot merging |
I have rebase the PR, but the bot doesn't detect it and not remove the label, weird. |
No description provided.