-
Notifications
You must be signed in to change notification settings - Fork 86
refactor: resolve Docker credentials on CLI side instead of copying config #590
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
base: main
Are you sure you want to change the base?
Conversation
Instead of copying ~/.docker/config.json into the container, credentials are now resolved on the CLI side using the Docker credential helpers and sent with pull/push requests. This simplifies container setup and avoids exposing the full Docker config inside the container. Signed-off-by: Dorin Geman <dorin.geman@docker.com>
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.
Hey - I've found 2 issues, and left some high level feedback:
- In
Client.Pull/Push, consider threading the request context down intoresolveCredentialsinstead of usingcontext.Background()so timeouts/cancellations and tracing are respected during credential resolution. - In
handlePushModel, JSON decoding errors forModelPushRequestare silently ignored and the request proceeds; it would be clearer to return a 400 Bad Request (and/or log) when the body is present but invalid JSON. - In
exchangeForToken, treating a registry with noWWW-Authenticaterealm as an error ("no auth required") causes a fallback to username/password; consider treating this as a non-error and using anonymous access instead of basic auth when no auth is required.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `Client.Pull`/`Push`, consider threading the request context down into `resolveCredentials` instead of using `context.Background()` so timeouts/cancellations and tracing are respected during credential resolution.
- In `handlePushModel`, JSON decoding errors for `ModelPushRequest` are silently ignored and the request proceeds; it would be clearer to return a 400 Bad Request (and/or log) when the body is present but invalid JSON.
- In `exchangeForToken`, treating a registry with no `WWW-Authenticate` realm as an error (`"no auth required"`) causes a fallback to username/password; consider treating this as a non-error and using anonymous access instead of basic auth when no auth is required.
## Individual Comments
### Comment 1
<location> `cmd/cli/desktop/desktop.go:98` </location>
<code_context>
+ return "", fmt.Errorf("no auth required")
+ }
+
+ scope := ref.Scope(remote.PushScope)
+ token, err := remote.Exchange(ctx, ref.Context().Registry, auth, nil, []string{scope}, pr)
+ if err != nil {
</code_context>
<issue_to_address>
**🚨 issue (security):** Using PushScope for all token exchanges may be too restrictive for pull-only operations
resolveCredentials is used for both pull and push, but exchangeForToken always requests remote.PushScope. For pull-only callers this can both fail unnecessarily and request overly broad (write) tokens. Consider making the scope a parameter (pull vs push) or deriving it from the caller so we only request the minimum required scope.
</issue_to_address>
### Comment 2
<location> `pkg/distribution/distribution/client.go:240-249` </location>
<code_context>
+func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Authentication precedence differs between PullModel and PushModel and may be surprising
In PullModel, basic auth currently takes precedence over BearerToken, while PushModel does the opposite. This inconsistent precedence means callers who set both fields get different auth behavior between pull and push. Consider standardizing the precedence across both methods or rejecting calls where multiple auth fields are set to avoid confusion and unintended auth schemes.
Suggested implementation:
```golang
// PullModel pulls a model from a registry and returns the local file path
func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error {
// Ensure callers don't mix auth mechanisms; this matches PushModel behavior and avoids
// surprising precedence differences when multiple credential fields are set.
if creds != nil {
hasBearer := creds.BearerToken != ""
// NOTE: adjust these field names if your Credentials type uses different ones
hasBasic := creds.Username != "" || creds.Password != ""
if hasBearer && hasBasic {
return fmt.Errorf("multiple authentication methods configured: use either bearer token or basic auth, not both")
}
}
// Store original reference before normalization (needed for case-sensitive HuggingFace API)
originalReference := reference
// Normalize the model reference
reference = c.normalizeModelName(reference)
c.log.Infoln("Starting model pull:", utils.SanitizeForLog(reference))
```
1. Ensure the `Credentials` struct has `Username` and `Password` fields; if they differ (e.g. `User` / `Pass`), update the `hasBasic` line accordingly.
2. Add the `fmt` package to the imports in `pkg/distribution/distribution/client.go`:
- `import "fmt"`
3. For full consistency with `PushModel`, confirm that `PushModel` either:
- Uses the same mutual-exclusion rule (rejecting mixed auth), or
- If it assumes a specific precedence (e.g. bearer over basic), document that and potentially mirror that behavior here instead of rejecting.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| return "", fmt.Errorf("no auth required") | ||
| } | ||
|
|
||
| scope := ref.Scope(remote.PushScope) |
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.
🚨 issue (security): Using PushScope for all token exchanges may be too restrictive for pull-only operations
resolveCredentials is used for both pull and push, but exchangeForToken always requests remote.PushScope. For pull-only callers this can both fail unnecessarily and request overly broad (write) tokens. Consider making the scope a parameter (pull vs push) or deriving it from the caller so we only request the minimum required scope.
| func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error { | ||
| // Store original reference before normalization (needed for case-sensitive HuggingFace API) | ||
| originalReference := reference | ||
| // Normalize the model reference | ||
| reference = c.normalizeModelName(reference) | ||
| c.log.Infoln("Starting model pull:", utils.SanitizeForLog(reference)) | ||
|
|
||
| // Handle bearer token for registry authentication | ||
| var token string | ||
| if len(bearerToken) > 0 && bearerToken[0] != "" { | ||
| token = bearerToken[0] | ||
| } | ||
|
|
||
| // HuggingFace references always use native pull (download raw files from HF Hub) | ||
| if isHuggingFaceReference(originalReference) { | ||
| c.log.Infoln("Using native HuggingFace pull for:", utils.SanitizeForLog(reference)) |
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.
suggestion (bug_risk): Authentication precedence differs between PullModel and PushModel and may be surprising
In PullModel, basic auth currently takes precedence over BearerToken, while PushModel does the opposite. This inconsistent precedence means callers who set both fields get different auth behavior between pull and push. Consider standardizing the precedence across both methods or rejecting calls where multiple auth fields are set to avoid confusion and unintended auth schemes.
Suggested implementation:
// PullModel pulls a model from a registry and returns the local file path
func (c *Client) PullModel(ctx context.Context, reference string, progressWriter io.Writer, creds *Credentials) error {
// Ensure callers don't mix auth mechanisms; this matches PushModel behavior and avoids
// surprising precedence differences when multiple credential fields are set.
if creds != nil {
hasBearer := creds.BearerToken != ""
// NOTE: adjust these field names if your Credentials type uses different ones
hasBasic := creds.Username != "" || creds.Password != ""
if hasBearer && hasBasic {
return fmt.Errorf("multiple authentication methods configured: use either bearer token or basic auth, not both")
}
}
// Store original reference before normalization (needed for case-sensitive HuggingFace API)
originalReference := reference
// Normalize the model reference
reference = c.normalizeModelName(reference)
c.log.Infoln("Starting model pull:", utils.SanitizeForLog(reference))- Ensure the
Credentialsstruct hasUsernameandPasswordfields; if they differ (e.g.User/Pass), update thehasBasicline accordingly. - Add the
fmtpackage to the imports inpkg/distribution/distribution/client.go:import "fmt"
- For full consistency with
PushModel, confirm thatPushModeleither:- Uses the same mutual-exclusion rule (rejecting mixed auth), or
- If it assumes a specific precedence (e.g. bearer over basic), document that and potentially mirror that behavior here instead of rejecting.
ilopezluna
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.
I wonder if we should enable TLS by default. What do you think?
ilopezluna
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.
make -C cmd/cli docs :D
Instead of copying ~/.docker/config.json into the container, credentials are now resolved on the CLI side using the Docker credential helpers and sent with pull/push requests. This simplifies container setup and avoids exposing the full Docker config inside the container.
To test following the next steps, you'd need to either connect to Offload or use
_MODEL_RUNNER_TREAT_DESKTOP_AS_MOBY=1.Resolves #560.
I'd merge this once we get #582 merged as well.