authprovider: make it possible to provide custom AuthConfig providers

This enables client to provide their own functionality for
AuthConfig lookup, rather than just passing in dockercli.ConfigFile.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi committed 2025-12-10 18:22:37 -08:00
1 parent 31072ad09a
commit 339740431f
4 files changed
+114 -73

No files matched your search

+2 -2
View File
@@ -189,8 +189,8 @@ func buildAction(clicontext *cli.Context) error {
}
attachable := []session.Attachable{authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{
ConfigFile: dockerConfig,
TLSConfigs: tlsConfigs,
AuthConfigProvider: authprovider.LoadAuthConfig(dockerConfig),
TLSConfigs: tlsConfigs,
})}
if ssh := clicontext.StringSlice("ssh"); len(ssh) > 0 {
@@ -0,0 +1,58 @@
package authprovider
import (
"context"
"sync"
"time"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
)
func LoadAuthConfig(config *configfile.ConfigFile) AuthConfigProvider {
acp := &authConfigProvider{
config: config,
authConfigCache: map[string]authConfigCacheEntry{},
}
return acp.load
}
type authConfigProvider struct {
config *configfile.ConfigFile
authConfigCache map[string]authConfigCacheEntry
mu sync.Mutex
}
func (ap *authConfigProvider) load(ctx context.Context, host string, scopes []string, cacheExpireCheck ExpireCachedAuthCheck) (types.AuthConfig, error) {
ap.mu.Lock()
defer ap.mu.Unlock()
entry, exists := ap.authConfigCache[host]
if exists && (cacheExpireCheck == nil || !cacheExpireCheck(entry.Created, host)) {
return *entry.Auth, nil
}
hostKey := host
if host == DockerHubRegistryHost {
hostKey = DockerHubConfigfileKey
}
ac, err := ap.config.GetAuthConfig(hostKey)
if err != nil {
return types.AuthConfig{}, err
}
entry = authConfigCacheEntry{
Created: time.Now(),
Auth: &ac,
}
ap.authConfigCache[host] = entry
return ac, nil
}
type authConfigCacheEntry struct {
Created time.Time
Auth *types.AuthConfig
}
+44 -61
View File
@@ -19,7 +19,6 @@ import (
authutil "github.com/containerd/containerd/v2/core/remotes/docker/auth"
remoteserrors "github.com/containerd/containerd/v2/core/remotes/errors"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/moby/buildkit/session"
@@ -36,13 +35,17 @@ import (
const (
defaultExpiration = 60
dockerHubConfigfileKey = "https://index.docker.io/v1/"
dockerHubRegistryHost = "registry-1.docker.io"
DockerHubConfigfileKey = "https://index.docker.io/v1/"
DockerHubRegistryHost = "registry-1.docker.io"
)
type AuthConfigProvider func(ctx context.Context, host string, scope []string, cacheCheck ExpireCachedAuthCheck) (types.AuthConfig, error)
type ExpireCachedAuthCheck func(created time.Time, serverURL string) bool
type DockerAuthProviderConfig struct {
// ConfigFile is the docker config file
ConfigFile *configfile.ConfigFile
// AuthConfigProvider is a function that provides auth config for a given host and scope
AuthConfigProvider AuthConfigProvider
// TLSConfigs is a map of host to TLS config
TLSConfigs map[string]*AuthTLSConfig
// ExpireCachedAuth is a function that returns true auth config should be refreshed
@@ -50,12 +53,7 @@ type DockerAuthProviderConfig struct {
// If nil then the cached result will expire after 4 minutes and 50 seconds.
// The function is called with the time the cached auth config was created
// and the server URL the auth config is for.
ExpireCachedAuth func(created time.Time, serverURL string) bool
}
type authConfigCacheEntry struct {
Created time.Time
Auth *types.AuthConfig
ExpireCachedAuth ExpireCachedAuthCheck
}
func NewDockerAuthProvider(cfg DockerAuthProviderConfig) session.Attachable {
@@ -66,23 +64,21 @@ func NewDockerAuthProvider(cfg DockerAuthProviderConfig) session.Attachable {
}
}
return &authProvider{
authConfigCache: map[string]authConfigCacheEntry{},
expireAc: cfg.ExpireCachedAuth,
config: cfg.ConfigFile,
seeds: &tokenSeeds{dir: config.Dir()},
loggerCache: map[string]struct{}{},
tlsConfigs: cfg.TLSConfigs,
expireAc: cfg.ExpireCachedAuth,
provider: cfg.AuthConfigProvider,
seeds: &tokenSeeds{dir: config.Dir()},
loggerCache: map[string]struct{}{},
tlsConfigs: cfg.TLSConfigs,
}
}
type authProvider struct {
authConfigCache map[string]authConfigCacheEntry
expireAc func(time.Time, string) bool
config *configfile.ConfigFile
seeds *tokenSeeds
logger progresswriter.Logger
loggerCache map[string]struct{}
tlsConfigs map[string]*AuthTLSConfig
expireAc func(time.Time, string) bool
provider AuthConfigProvider
seeds *tokenSeeds
logger progresswriter.Logger
loggerCache map[string]struct{}
tlsConfigs map[string]*AuthTLSConfig
// The need for this mutex is not well understood.
// Without it, the docker cli on OS X hangs when
@@ -102,7 +98,7 @@ func (ap *authProvider) Register(server *grpc.Server) {
}
func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequest) (rr *auth.FetchTokenResponse, err error) {
ac, err := ap.getAuthConfig(ctx, req.Host)
ac, err := ap.getAuthConfig(ctx, req.Host, req.Scopes)
if err != nil {
return nil, err
}
@@ -112,11 +108,7 @@ func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequ
return toTokenResponse(ac.RegistryToken, time.Time{}, 0), nil
}
creds, err := ap.credentials(ctx, req.Host)
if err != nil {
return nil, err
}
creds := toCredentials(*ac)
to := authutil.TokenOptions{
Realm: req.Realm,
Service: req.Service,
@@ -215,11 +207,7 @@ func (ap *authProvider) tlsConfig(host string) (*tls.Config, error) {
return tc, nil
}
func (ap *authProvider) credentials(ctx context.Context, host string) (*auth.CredentialsResponse, error) {
ac, err := ap.getAuthConfig(ctx, host)
if err != nil {
return nil, err
}
func toCredentials(ac types.AuthConfig) *auth.CredentialsResponse {
res := &auth.CredentialsResponse{}
if ac.IdentityToken != "" {
res.Secret = ac.IdentityToken
@@ -227,12 +215,16 @@ func (ap *authProvider) credentials(ctx context.Context, host string) (*auth.Cre
res.Username = ac.Username
res.Secret = ac.Password
}
return res, nil
return res
}
func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) {
resp, err := ap.credentials(ctx, req.Host)
if err != nil || resp.Secret != "" {
ac, err := ap.getAuthConfig(ctx, req.Host, nil)
if err != nil {
return nil, err
}
resp := toCredentials(*ac)
if resp.Secret != "" {
ap.mu.Lock()
defer ap.mu.Unlock()
_, ok := ap.loggerCache[req.Host]
@@ -267,33 +259,22 @@ func (ap *authProvider) VerifyTokenAuthority(ctx context.Context, req *auth.Veri
return &auth.VerifyTokenAuthorityResponse{Signed: sign.Sign(nil, req.Payload, priv)}, nil
}
func (ap *authProvider) getAuthConfig(ctx context.Context, host string) (*types.AuthConfig, error) {
func (ap *authProvider) getAuthConfig(ctx context.Context, host string, scopes []string) (*types.AuthConfig, error) {
ap.mu.Lock()
defer ap.mu.Unlock()
if host == dockerHubRegistryHost {
host = dockerHubConfigfileKey
var ac types.AuthConfig
if ap.provider != nil {
span, _ := tracing.StartSpan(ctx, fmt.Sprintf("load credentials for %s", host))
res, err := ap.provider(ctx, host, scopes, ap.expireAc)
tracing.FinishWithError(span, err)
if err != nil {
return nil, err
}
ac = res
}
entry, exists := ap.authConfigCache[host]
if exists && !ap.expireAc(entry.Created, host) {
return entry.Auth, nil
}
span, _ := tracing.StartSpan(ctx, fmt.Sprintf("load credentials for %s", host))
ac, err := ap.config.GetAuthConfig(host)
tracing.FinishWithError(span, err)
if err != nil {
return nil, err
}
entry = authConfigCacheEntry{
Created: time.Now(),
Auth: &ac,
}
ap.authConfigCache[host] = entry
return entry.Auth, nil
return &ac, nil
}
func (ap *authProvider) getAuthorityKey(ctx context.Context, host string, salt []byte) (ed25519.PrivateKey, error) {
@@ -301,10 +282,12 @@ func (ap *authProvider) getAuthorityKey(ctx context.Context, host string, salt [
return nil, status.Errorf(codes.Unavailable, "client side tokens disabled")
}
creds, err := ap.credentials(ctx, host)
ac, err := ap.getAuthConfig(ctx, host, nil)
if err != nil {
return nil, err
}
creds := toCredentials(*ac)
seed, err := ap.seeds.getSeed(host)
if err != nil {
return nil, err
+10 -10
View File
@@ -16,21 +16,21 @@ func TestFetchTokenCaching(t *testing.T) {
newCfg := func() *configfile.ConfigFile {
return &configfile.ConfigFile{
AuthConfigs: map[string]types.AuthConfig{
dockerHubConfigfileKey: {Username: "user", RegistryToken: "hunter2"},
DockerHubConfigfileKey: {Username: "user", RegistryToken: "hunter2"},
},
}
}
cfg := newCfg()
p := NewDockerAuthProvider(DockerAuthProviderConfig{
ConfigFile: cfg,
AuthConfigProvider: LoadAuthConfig(cfg),
}).(*authProvider)
res, err := p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost})
res, err := p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: DockerHubRegistryHost})
require.NoError(t, err)
assert.Equal(t, "hunter2", res.Token)
cfg.AuthConfigs[dockerHubConfigfileKey] = types.AuthConfig{Username: "user", RegistryToken: "hunter3"}
res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost})
cfg.AuthConfigs[DockerHubConfigfileKey] = types.AuthConfig{Username: "user", RegistryToken: "hunter3"}
res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: DockerHubRegistryHost})
require.NoError(t, err)
// Verify that we cached the result instead of returning hunter3.
@@ -40,19 +40,19 @@ func TestFetchTokenCaching(t *testing.T) {
cfg = newCfg()
p = NewDockerAuthProvider(DockerAuthProviderConfig{
ConfigFile: cfg,
AuthConfigProvider: LoadAuthConfig(cfg),
ExpireCachedAuth: func(_ time.Time, host string) bool {
require.Equal(t, dockerHubConfigfileKey, host)
require.Equal(t, DockerHubRegistryHost, host)
return true
},
}).(*authProvider)
res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost})
res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: DockerHubRegistryHost})
require.NoError(t, err)
assert.Equal(t, "hunter2", res.Token)
cfg.AuthConfigs[dockerHubConfigfileKey] = types.AuthConfig{Username: "user", RegistryToken: "hunter3"}
res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: dockerHubRegistryHost})
cfg.AuthConfigs[DockerHubConfigfileKey] = types.AuthConfig{Username: "user", RegistryToken: "hunter3"}
res, err = p.FetchToken(context.Background(), &auth.FetchTokenRequest{Host: DockerHubRegistryHost})
require.NoError(t, err)
// Verify that we re-fetched the token after it expired.