diff --git a/cmd/notation/common.go b/cmd/notation/common.go index f8aac178f..fc2eb7ade 100644 --- a/cmd/notation/common.go +++ b/cmd/notation/common.go @@ -4,6 +4,7 @@ import ( "os" "github.com/spf13/pflag" + "oras.land/oras-go/v2/registry/remote/auth" ) const ( @@ -55,3 +56,16 @@ func (opts *SecureFlagOpts) ApplyFlags(fs *pflag.FlagSet) { opts.Username = os.Getenv(defaultUsernameEnv) opts.Password = os.Getenv(defaultPasswordEnv) } + +// Credential returns an auth.Credential from opts.Username and opts.Password. +func (opts *SecureFlagOpts) Credential() auth.Credential { + if opts.Username == "" { + return auth.Credential{ + RefreshToken: opts.Password, + } + } + return auth.Credential{ + Username: opts.Username, + Password: opts.Password, + } +} diff --git a/cmd/notation/common_test.go b/cmd/notation/common_test.go new file mode 100644 index 000000000..a524ecd56 --- /dev/null +++ b/cmd/notation/common_test.go @@ -0,0 +1,61 @@ +package main + +import ( + "reflect" + "testing" + + "oras.land/oras-go/v2/registry/remote/auth" +) + +func TestSecureFlagOpts_Credential(t *testing.T) { + tests := []struct { + name string + opts *SecureFlagOpts + want auth.Credential + }{ + { + name: "Username and password", + opts: &SecureFlagOpts{ + Username: "username", + Password: "password", + }, + want: auth.Credential{ + Username: "username", + Password: "password", + }, + }, + { + name: "Username only", + opts: &SecureFlagOpts{ + Username: "username", + }, + want: auth.Credential{ + Username: "username", + }, + }, + { + name: "Password only", + opts: &SecureFlagOpts{ + Password: "token", + }, + want: auth.Credential{ + RefreshToken: "token", + }, + }, + { + name: "Empty username and password", + opts: &SecureFlagOpts{ + Username: "", + Password: "", + }, + want: auth.EmptyCredential, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.opts.Credential(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SecureFlagOpts.Credential() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/notation/login.go b/cmd/notation/login.go index 3497853b5..e44da7f76 100644 --- a/cmd/notation/login.go +++ b/cmd/notation/login.go @@ -9,13 +9,16 @@ import ( "os" "strings" + "github.com/notaryproject/notation-go/log" + "github.com/notaryproject/notation/internal/auth" "github.com/notaryproject/notation/internal/cmd" - "github.com/notaryproject/notation/pkg/auth" + credentials "github.com/oras-project/oras-credentials-go" "github.com/spf13/cobra" "golang.org/x/term" - orasauth "oras.land/oras-go/v2/registry/remote/auth" ) +const urlDocHowToAuthenticate = "https://notaryproject.dev/docs/how-to/registry-authentication/" + type loginOpts struct { cmd.LoggingFlagOpts SecureFlagOpts @@ -70,61 +73,69 @@ func runLogin(ctx context.Context, opts *loginOpts) error { // input username and password by prompt reader := bufio.NewReader(os.Stdin) var err error - if opts.Username == "" { - opts.Username, err = readUsernameFromPrompt(reader) - if err != nil { - return err - } - } - if opts.Password == "" { - opts.Password, err = readPasswordFromPrompt(reader) + var isToken bool + if opts.Username == "" { + opts.Username, err = readUsernameFromPrompt(reader) + if err != nil { + return err + } + if opts.Username == "" { + // the username is empty, the password is used as a token + isToken = true + } + } + opts.Password, err = readPasswordFromPrompt(reader, isToken) if err != nil { return err } + if opts.Password == "" { + if isToken { + return errors.New("token required") + } + return errors.New("password required") + } } + cred := opts.Credential() - if err := validateAuthConfig(ctx, opts, serverAddress); err != nil { - return err + credsStore, err := auth.NewCredentialsStore() + if err != nil { + return fmt.Errorf("failed to get credentials store: %v", err) } - - nativeStore, err := auth.GetCredentialsStore(ctx, serverAddress) + registry, err := getRegistryLoginClient(ctx, &opts.SecureFlagOpts, serverAddress) if err != nil { - return fmt.Errorf("could not get the credentials store: %v", err) + return fmt.Errorf("failed to get registry client: %v", err) } + if err := credentials.Login(ctx, credsStore, registry, cred); err != nil { + registryName := registry.Reference.Registry + if !errors.Is(err, credentials.ErrPlaintextPutDisabled) { + return fmt.Errorf("failed to log in to %s: %v", registryName, err) + } - // init creds - creds := newCredentialFromInput( - opts.Username, - opts.Password, - ) - if err = nativeStore.Store(serverAddress, creds); err != nil { - return fmt.Errorf("failed to store credentials: %v", err) + // ErrPlaintextPutDisabled returned by Login() indicates that the + // credential is validated but is not saved because there is no native + // credentials store available + if savedCred, err := credsStore.Get(ctx, registryName); err != nil || savedCred != cred { + if err != nil { + // if we fail to get the saved credential, log a warning + // but do not throw the GET error, as the error could be + // confusing to users + logger := log.GetLogger(ctx) + logger.Warnf("Failed to get the existing credentials for %s: %v", registryName, err) + } + return fmt.Errorf("failed to log in to %s: the credential could not be saved because a credentials store is required to securely store the password. See %s", + registryName, urlDocHowToAuthenticate) + } + + // the credential already exists somewhere, ignore the saving error + fmt.Fprintf(os.Stderr, "Warning: The credentials store is not set up. It is recommended to configure the credentials store to securely store your credentials. See %s.\n", urlDocHowToAuthenticate) + fmt.Println("Authenticated with existing credentials") } fmt.Println("Login Succeeded") return nil } -func validateAuthConfig(ctx context.Context, opts *loginOpts, serverAddress string) error { - registry, err := getRegistryClient(ctx, &opts.SecureFlagOpts, serverAddress) - if err != nil { - return err - } - return registry.Ping(ctx) -} - -func newCredentialFromInput(username, password string) orasauth.Credential { - c := orasauth.Credential{ - Username: username, - Password: password, - } - if c.Username == "" { - c.RefreshToken = password - } - return c -} - func readPassword(opts *loginOpts) error { if opts.passwordStdin { password, err := readLine(os.Stdin) @@ -156,19 +167,27 @@ func readUsernameFromPrompt(reader *bufio.Reader) (string, error) { return username, nil } -func readPasswordFromPrompt(reader *bufio.Reader) (string, error) { - fmt.Print("Password: ") +func readPasswordFromPrompt(reader *bufio.Reader, isToken bool) (string, error) { + var passwordType string + if isToken { + passwordType = "token" + fmt.Print("Token: ") + } else { + passwordType = "password" + fmt.Print("Password: ") + } + if term.IsTerminal(int(os.Stdin.Fd())) { bytePassword, err := term.ReadPassword(int(os.Stdin.Fd())) if err != nil { - return "", fmt.Errorf("error reading password: %w", err) + return "", fmt.Errorf("error reading %s: %w", passwordType, err) } fmt.Println() return string(bytePassword), nil } else { password, err := readLine(reader) if err != nil { - return "", fmt.Errorf("error reading password: %w", err) + return "", fmt.Errorf("error reading %s: %w", passwordType, err) } fmt.Println() return password, nil diff --git a/cmd/notation/logout.go b/cmd/notation/logout.go index 252b02117..fd13b5b80 100644 --- a/cmd/notation/logout.go +++ b/cmd/notation/logout.go @@ -5,8 +5,9 @@ import ( "errors" "fmt" + "github.com/notaryproject/notation/internal/auth" "github.com/notaryproject/notation/internal/cmd" - "github.com/notaryproject/notation/pkg/auth" + credentials "github.com/oras-project/oras-credentials-go" "github.com/spf13/cobra" ) @@ -40,16 +41,12 @@ func logoutCommand(opts *logoutOpts) *cobra.Command { func runLogout(ctx context.Context, opts *logoutOpts) error { // set log level ctx = opts.LoggingFlagOpts.SetLoggerLevel(ctx) - - // initialize - serverAddress := opts.server - nativeStore, err := auth.GetCredentialsStore(ctx, serverAddress) + credsStore, err := auth.NewCredentialsStore() if err != nil { - return err + return fmt.Errorf("failed to get credentials store: %v", err) } - err = nativeStore.Erase(serverAddress) - if err != nil { - return err + if err := credentials.Logout(ctx, credsStore, opts.server); err != nil { + return fmt.Errorf("failed to log out of %s: %v", opts.server, err) } fmt.Println("Logout Succeeded") diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 299a62db3..c1080f11d 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -3,16 +3,18 @@ package main import ( "context" "errors" + "fmt" "net" "net/http" "github.com/notaryproject/notation-go/log" notationregistry "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation/cmd/notation/internal/experimental" + notationauth "github.com/notaryproject/notation/internal/auth" "github.com/notaryproject/notation/internal/trace" "github.com/notaryproject/notation/internal/version" - loginauth "github.com/notaryproject/notation/pkg/auth" "github.com/notaryproject/notation/pkg/configutil" + credentials "github.com/oras-project/oras-credentials-go" "github.com/sirupsen/logrus" "oras.land/oras-go/v2/registry" "oras.land/oras-go/v2/registry/remote" @@ -80,7 +82,7 @@ func getRemoteRepository(ctx context.Context, opts *SecureFlagOpts, reference st } func getRepositoryClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Reference) (*remote.Repository, error) { - authClient, insecureRegistry, err := getAuthClient(ctx, opts, ref) + authClient, insecureRegistry, err := getAuthClient(ctx, opts, ref, true) if err != nil { return nil, err } @@ -92,13 +94,13 @@ func getRepositoryClient(ctx context.Context, opts *SecureFlagOpts, ref registry }, nil } -func getRegistryClient(ctx context.Context, opts *SecureFlagOpts, serverAddress string) (*remote.Registry, error) { +func getRegistryLoginClient(ctx context.Context, opts *SecureFlagOpts, serverAddress string) (*remote.Registry, error) { reg, err := remote.NewRegistry(serverAddress) if err != nil { return nil, err } - reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, opts, reg.Reference) + reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, opts, reg.Reference, false) if err != nil { return nil, err } @@ -118,7 +120,15 @@ func setHttpDebugLog(ctx context.Context, authClient *auth.Client) { authClient.Client.Transport = trace.NewTransport(authClient.Client.Transport) } -func getAuthClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Reference) (*auth.Client, bool, error) { +// getAuthClient returns an *auth.Client and a bool indicating if the registry +// is insecure. +// +// If withCredential is true, the returned *auth.Client will have its Credential +// function configured. +// +// If withCredential is false, the returned *auth.Client will have a nil +// Credential function. +func getAuthClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Reference, withCredential bool) (*auth.Client, bool, error) { var insecureRegistry bool if opts.InsecureRegistry { insecureRegistry = opts.InsecureRegistry @@ -130,49 +140,29 @@ func getAuthClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Refer } } } - cred := auth.Credential{ - Username: opts.Username, - Password: opts.Password, - } - if cred.Username == "" { - cred = auth.Credential{ - RefreshToken: cred.Password, - } - } - if cred == auth.EmptyCredential { - var err error - cred, err = getSavedCreds(ctx, ref.Registry) - // local registry may not need credentials - if err != nil && !errors.Is(err, loginauth.ErrCredentialsConfigNotSet) { - return nil, false, err - } - } + // build authClient authClient := &auth.Client{ - Credential: func(ctx context.Context, registry string) (auth.Credential, error) { - switch registry { - case ref.Host(): - return cred, nil - default: - return auth.EmptyCredential, nil - } - }, Cache: auth.NewCache(), ClientID: "notation", } authClient.SetUserAgent("notation/" + version.GetVersion()) - - // update authClient setHttpDebugLog(ctx, authClient) - - return authClient, insecureRegistry, nil -} - -func getSavedCreds(ctx context.Context, serverAddress string) (auth.Credential, error) { - nativeStore, err := loginauth.GetCredentialsStore(ctx, serverAddress) - if err != nil { - return auth.EmptyCredential, err + if !withCredential { + return authClient, insecureRegistry, nil } - return nativeStore.Get(serverAddress) + cred := opts.Credential() + if cred != auth.EmptyCredential { + // use the specified credential + authClient.Credential = auth.StaticCredential(ref.Host(), cred) + } else { + // use saved credentials + credsStore, err := notationauth.NewCredentialsStore() + if err != nil { + return nil, false, fmt.Errorf("failed to get credentials store: %w", err) + } + authClient.Credential = credentials.Credential(credsStore) + } + return authClient, insecureRegistry, nil } diff --git a/go.mod b/go.mod index 878635c4f..17485ee90 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,21 @@ module github.com/notaryproject/notation go 1.20 require ( - github.com/docker/docker-credential-helpers v0.7.0 github.com/notaryproject/notation-core-go v1.0.0-rc.3 github.com/notaryproject/notation-go v1.0.0-rc.5 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc.3 + github.com/oras-project/oras-credentials-go v0.1.1-0.20230517070118-6b1be42cc39f github.com/sirupsen/logrus v1.9.2 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 golang.org/x/term v0.8.0 - oras.land/oras-go/v2 v2.1.0 + oras.land/oras-go/v2 v2.1.1-0.20230510085343-0e20275bbbd7 ) require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-ldap/ldap/v3 v3.4.4 // indirect @@ -26,6 +27,6 @@ require ( github.com/x448/float16 v0.8.4 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/sync v0.1.0 // indirect + golang.org/x/sync v0.2.0 // indirect golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index df1bd4779..9b6493cba 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc.3 h1:GT9Xon8YrLxz6N7sErbN81V8J4lOQKGUZQmI3ioviqU= github.com/opencontainers/image-spec v1.1.0-rc.3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/oras-project/oras-credentials-go v0.1.1-0.20230517070118-6b1be42cc39f h1:8XfuRi2ZM2RxF/O7KfH9CcC26etFSCGx2Y10LsMEvn0= +github.com/oras-project/oras-credentials-go v0.1.1-0.20230517070118-6b1be42cc39f/go.mod h1:ptY3018+JzNg2Y/epvqxJp09vhGNxG6UpSqqz9UduM4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -48,8 +50,8 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0 golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -65,5 +67,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -oras.land/oras-go/v2 v2.1.0 h1:1nS8BIeEP6CBVQifwxrsth2bkuD+cYfjp7Hf7smUcS8= -oras.land/oras-go/v2 v2.1.0/go.mod h1:v5ZSAPIMEJYnZjZ6rTGPAyaonH+rCFmbE95IAzCTeGU= +oras.land/oras-go/v2 v2.1.1-0.20230510085343-0e20275bbbd7 h1:pPUovCp5UsgacsDm+k0HRJ6/aKOJVm9GT3G8sXpinnc= +oras.land/oras-go/v2 v2.1.1-0.20230510085343-0e20275bbbd7/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8= diff --git a/internal/auth/credentials.go b/internal/auth/credentials.go new file mode 100644 index 000000000..27f259549 --- /dev/null +++ b/internal/auth/credentials.go @@ -0,0 +1,29 @@ +package auth + +import ( + "fmt" + + "github.com/notaryproject/notation-go/dir" + credentials "github.com/oras-project/oras-credentials-go" +) + +// NewCredentialsStore returns a new credentials store from the settings in the +// configuration file. +func NewCredentialsStore() (credentials.Store, error) { + configPath, err := dir.ConfigFS().SysPath(dir.PathConfigFile) + if err != nil { + return nil, fmt.Errorf("failed to load config file: %w", err) + } + + opts := credentials.StoreOptions{AllowPlaintextPut: false} + primaryStore, err := credentials.NewStore(configPath, opts) + if err != nil { + return nil, fmt.Errorf("failed to create credential store from config file: %w", err) + } + + fallbackStore, err := credentials.NewStoreFromDocker(opts) + if err != nil { + return nil, fmt.Errorf("failed to create credential store from docker config file: %w", err) + } + return credentials.NewStoreWithFallbacks(primaryStore, fallbackStore), nil +} diff --git a/pkg/auth/api.go b/pkg/auth/api.go deleted file mode 100644 index bf5c6cb7b..000000000 --- a/pkg/auth/api.go +++ /dev/null @@ -1,13 +0,0 @@ -package auth - -import "oras.land/oras-go/v2/registry/remote/auth" - -// CredentialStore is the interface that any credentials store must implement. -type CredentialStore interface { - // Store saves credentials into the store - Store(serverAddress string, credsConf auth.Credential) error - // Erase removes credentials from the store for the given server - Erase(serverAddress string) error - // Get retrieves credentials from the store for the given server - Get(serverAddress string) (auth.Credential, error) -} diff --git a/pkg/auth/credential.go b/pkg/auth/credential.go deleted file mode 100644 index ae4dd181a..000000000 --- a/pkg/auth/credential.go +++ /dev/null @@ -1,75 +0,0 @@ -package auth - -import ( - "errors" - "io/fs" - - "github.com/docker/docker-credential-helpers/credentials" - "github.com/notaryproject/notation-go/config" - "github.com/notaryproject/notation/pkg/configutil" - "oras.land/oras-go/v2/registry/remote/auth" -) - -// ErrorCodeCredentialsConfigNotSet indicates the credentials store config was not set up -var ErrCredentialsConfigNotSet = errors.New("credentials store config was not set up") - -// var for unit tests -var ( - loadOrDefault = configutil.LoadConfigOnce - loadDockerConfig = configutil.LoadDockerConfig -) - -// LoadConfig loads the configuration from the config file -func LoadConfig() (*config.Config, error) { - // load notation config first - config, err := loadOrDefault() - if err != nil { - return nil, err - } - if config != nil && containsAuth(config) { - return config, nil - } - - config, err = loadDockerCredentials() - if errors.Is(err, fs.ErrNotExist) { - return nil, ErrCredentialsConfigNotSet - } - if err != nil { - return nil, err - } - if containsAuth(config) { - return config, nil - } - return nil, ErrCredentialsConfigNotSet -} - -// loadDockerCredentials loads the configuration from the config file under .docker -// directory -func loadDockerCredentials() (*config.Config, error) { - dockerConfig, err := loadDockerConfig() - if err != nil { - return nil, err - } - return &config.Config{ - CredentialHelpers: dockerConfig.CredentialHelpers, - CredentialsStore: dockerConfig.CredentialsStore, - }, nil -} - -// containsAuth returns whether there is authentication configured in this file -// or not. -func containsAuth(configFile *config.Config) bool { - return configFile.CredentialsStore != "" || len(configFile.CredentialHelpers) > 0 -} - -// newCredentialFromDockerCreds creates a new auth.Credential from the docker-cli credentials -func newCredentialFromDockerCreds(dockerCreds *credentials.Credentials) auth.Credential { - var credsConf auth.Credential - if dockerCreds.Username == tokenUsername { - credsConf.RefreshToken = dockerCreds.Secret - } else { - credsConf.Password = dockerCreds.Secret - credsConf.Username = dockerCreds.Username - } - return credsConf -} diff --git a/pkg/auth/credential_test.go b/pkg/auth/credential_test.go deleted file mode 100644 index e25620ded..000000000 --- a/pkg/auth/credential_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package auth - -import ( - "fmt" - "testing" - - "github.com/notaryproject/notation-go/config" - "github.com/notaryproject/notation/pkg/configutil" -) - -const ( - errMsg = "error message" - validStore = "pass" -) - -func TestLoadConfig_LoadNotationConfigFailed(t *testing.T) { - loadOrDefault = func() (*config.Config, error) { - return nil, fmt.Errorf(errMsg) - } - _, err := LoadConfig() - if err == nil || err.Error() != errMsg { - t.Fatalf("Didn't get the expected error, but got: %v", err) - } -} - -func TestLoadConfig_NotationConfigContainsAuth(t *testing.T) { - loadOrDefault = func() (*config.Config, error) { - return &config.Config{CredentialsStore: validStore}, nil - } - file, err := LoadConfig() - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if file == nil || file.CredentialsStore != validStore { - t.Fatalf("Should contain auth") - } -} - -func TestLoadConfig_LoadDockerConfigFailed(t *testing.T) { - loadOrDefault = func() (*config.Config, error) { - return nil, nil - } - loadDockerConfig = func() (*configutil.DockerConfigFile, error) { - return nil, fmt.Errorf(errMsg) - } - _, err := LoadConfig() - if err == nil || err.Error() != errMsg { - t.Fatalf("Didn't get the expected error, but got: %v", err) - } -} - -func TestLoadConfig_DockerConfigContainsAuth(t *testing.T) { - loadOrDefault = func() (*config.Config, error) { - return nil, nil - } - loadDockerConfig = func() (*configutil.DockerConfigFile, error) { - return &configutil.DockerConfigFile{ - CredentialsStore: validStore, - }, nil - } - file, err := LoadConfig() - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - if file == nil || file.CredentialsStore != validStore { - t.Fatalf("Should contain auth") - } -} - -func TestLoadConfig_DockerConfigEmptyAuth(t *testing.T) { - loadOrDefault = func() (*config.Config, error) { - return nil, nil - } - loadDockerConfig = func() (*configutil.DockerConfigFile, error) { - return &configutil.DockerConfigFile{}, nil - } - _, err := LoadConfig() - if err == nil { - t.Fatalf("expect error but got nil") - } -} diff --git a/pkg/auth/native_store.go b/pkg/auth/native_store.go deleted file mode 100644 index 421ec7ed7..000000000 --- a/pkg/auth/native_store.go +++ /dev/null @@ -1,97 +0,0 @@ -package auth - -import ( - "context" - "fmt" - - "github.com/docker/docker-credential-helpers/client" - "github.com/docker/docker-credential-helpers/credentials" - "github.com/notaryproject/notation-go/config" - "github.com/notaryproject/notation-go/log" - "oras.land/oras-go/v2/registry/remote/auth" -) - -const ( - remoteCredentialsPrefix = "docker-credential-" - tokenUsername = "" -) - -// var for unit testing. -var loadConfig = LoadConfig - -// nativeAuthStore implements a credentials store using native keychain to keep -// credentials secure. -type nativeAuthStore struct { - programFunc client.ProgramFunc -} - -// GetCredentialsStore returns a new credentials store from the settings in the -// configuration file -func GetCredentialsStore(ctx context.Context, registryHostname string) (CredentialStore, error) { - configFile, err := loadConfig() - if err != nil { - return nil, fmt.Errorf("failed to load config file, error: %w", err) - } - if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" { - return newNativeAuthStore(ctx, helper), nil - } - return nil, fmt.Errorf("could not get the configured credentials store for registry: %s", registryHostname) -} - -// newNativeAuthStore creates a new native store that uses a remote helper -// program to manage credentials. Note: it's different from the nativeStore in -// docker-cli which may fall back to plain text store -func newNativeAuthStore(ctx context.Context, helperSuffix string) CredentialStore { - logger := log.GetLogger(ctx) - name := remoteCredentialsPrefix + helperSuffix - logger.Infoln("Executing remote credential helper program:", name) - return &nativeAuthStore{ - programFunc: client.NewShellProgramFunc(name), - } -} - -// getConfiguredCredentialStore returns the credential helper configured for the -// given registry, the default credsStore, or the empty string if neither are -// configured. -func getConfiguredCredentialStore(c *config.Config, registryHostname string) string { - if c.CredentialHelpers != nil && registryHostname != "" { - if helper, exists := c.CredentialHelpers[registryHostname]; exists { - return helper - } - } - return c.CredentialsStore -} - -// Store saves credentials into the native store -func (s *nativeAuthStore) Store(serverAddress string, authCreds auth.Credential) error { - creds := &credentials.Credentials{ - ServerURL: serverAddress, - Username: authCreds.Username, - Secret: authCreds.Password, - } - - if authCreds.RefreshToken != "" { - creds.Username = tokenUsername - creds.Secret = authCreds.RefreshToken - } - - return client.Store(s.programFunc, creds) -} - -// Get retrieves credentials from the store for the given server -func (s *nativeAuthStore) Get(serverAddress string) (auth.Credential, error) { - creds, err := client.Get(s.programFunc, serverAddress) - if err != nil { - if credentials.IsErrCredentialsNotFound(err) { - // do not return an error if the credentials are not in the keychain. - return auth.EmptyCredential, nil - } - return auth.EmptyCredential, err - } - return newCredentialFromDockerCreds(creds), nil -} - -// Erase removes credentials from the store for the given server -func (s *nativeAuthStore) Erase(serverAddress string) error { - return client.Erase(s.programFunc, serverAddress) -} diff --git a/pkg/auth/native_store_test.go b/pkg/auth/native_store_test.go deleted file mode 100644 index ededc6f09..000000000 --- a/pkg/auth/native_store_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package auth - -import ( - "context" - "encoding/json" - "fmt" - "io" - "strings" - "testing" - - "github.com/docker/docker-credential-helpers/client" - "github.com/docker/docker-credential-helpers/credentials" - "github.com/notaryproject/notation-go/config" - "oras.land/oras-go/v2/registry/remote/auth" -) - -const ( - validServerAddress = "https://index.docker.io/v1" - validServerAddress2 = "https://example.com:5002" - invalidServerAddress = "https://foobar.example.com" - missingCredsAddress = "https://missing.docker.io/v1" - Username = "Username" - Secret = "Secret" - validUsername = "username" - validPassword = "password" - validIdentityToken = "identityToken" - validHelper = "helper" -) - -var ( - errCommandExited = fmt.Errorf("exited 1") -) - -// mockCommand simulates interactions between the docker client and a remote -// credentials helper. -// Unit tests inject this mocked command into the remote to control execution. -type mockCommand struct { - arg string - input io.Reader -} - -// Output returns responses from the remote credentials helper. -// It mocks those responses based in the input in the mock. -func (m *mockCommand) Output() ([]byte, error) { - in, err := io.ReadAll(m.input) - if err != nil { - return nil, err - } - inS := string(in) - - switch m.arg { - case "erase": - switch inS { - case validServerAddress: - return nil, nil - default: - return []byte("program failed"), errCommandExited - } - case "get": - switch inS { - case validServerAddress: - return []byte(`{"Username": "username", "Secret": "password"}`), nil - case invalidServerAddress: - return []byte("program failed"), errCommandExited - case validServerAddress2: - return []byte(`{"Username": "", "Secret": "identityToken"}`), nil - } - case "store": - var c credentials.Credentials - err := json.NewDecoder(strings.NewReader(inS)).Decode(&c) - if err != nil { - return []byte("program failed"), errCommandExited - } - switch c.ServerURL { - case validServerAddress, validServerAddress2: - return nil, nil - default: - return []byte("program failed"), errCommandExited - } - } - - return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited -} - -// Input sets the input to send to a remote credentials helper. -func (m *mockCommand) Input(in io.Reader) { - m.input = in -} - -func mockCommandFn(args ...string) client.Program { - return &mockCommand{ - arg: args[0], - } -} - -func TestNativeStore_StoreGetErase(t *testing.T) { - creds := auth.Credential{ - Username: validUsername, - Password: validPassword, - } - s := &nativeAuthStore{ - programFunc: mockCommandFn, - } - - // store creds - err := s.Store(validServerAddress, creds) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - - // get creds - fetchedCreds, err := s.Get(validServerAddress) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - if fetchedCreds != creds { - t.Fatalf("expected %+v, got %+v", creds, fetchedCreds) - } - - // erase creds - err = s.Erase(validServerAddress) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - fetchedCreds, err = s.Get(validServerAddress) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - if fetchedCreds == auth.EmptyCredential { - t.Fatalf("expect empty conf, but got: %+v", fetchedCreds) - } -} - -func TestNativeStore_StoreIdentityToken(t *testing.T) { - creds := auth.Credential{ - RefreshToken: validIdentityToken, - } - s := &nativeAuthStore{ - programFunc: mockCommandFn, - } - - // store creds - err := s.Store(validServerAddress, creds) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - - // get creds - fetchedCreds, err := s.Get(validServerAddress2) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - if fetchedCreds.RefreshToken != creds.RefreshToken { - t.Fatalf("expected %+v, got %+v", creds, fetchedCreds) - } -} - -func TestNativeStore_FailedGet(t *testing.T) { - s := &nativeAuthStore{ - programFunc: mockCommandFn, - } - _, err := s.Get(invalidServerAddress) - if err == nil { - t.Fatalf("expect error, got nil") - } -} - -func TestNativeStore_GetCredentialsStore_LoadConfigFailed(t *testing.T) { - loadConfig = func() (*config.Config, error) { - return nil, fmt.Errorf("loadConfig err") - } - _, err := GetCredentialsStore(context.Background(), validServerAddress) - if err == nil { - t.Fatalf("expect error, got nil") - } -} - -func TestNativeStore_GetCredentialsStore_NoHelperSet(t *testing.T) { - loadConfig = func() (*config.Config, error) { - return &config.Config{}, nil - } - _, err := GetCredentialsStore(context.Background(), validServerAddress) - if err == nil || err.Error() != "could not get the configured credentials store for registry: "+validServerAddress { - t.Fatalf("Didn't get the expected error, but got: %v", err) - } -} - -func TestNativeStore_GetCredentialsStore_HelperSet(t *testing.T) { - loadConfig = func() (*config.Config, error) { - return &config.Config{ - CredentialHelpers: map[string]string{ - validServerAddress: validHelper, - }, - }, nil - } - _, err := GetCredentialsStore(context.Background(), validServerAddress) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} diff --git a/pkg/configutil/docker_config.go b/pkg/configutil/docker_config.go deleted file mode 100644 index df2dd424f..000000000 --- a/pkg/configutil/docker_config.go +++ /dev/null @@ -1,69 +0,0 @@ -package configutil - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" -) - -const ( - // dockerConfigFileName is the name of config file - dockerConfigFileName = "config.json" - dockerConfigFileDir = ".docker" -) - -// DockerConfigFile is the minimized configuration of the Docker daemon, only -// credentails store related configs are included -type DockerConfigFile struct { - CredentialsStore string `json:"credsStore,omitempty"` - CredentialHelpers map[string]string `json:"credHelpers,omitempty"` -} - -// Load reads the configuration files in the given directory, and sets up -// the auth config information and returns values. -func LoadDockerConfig() (*DockerConfigFile, error) { - configDir, err := getDockerConfigDir() - if err != nil { - return nil, err - } - - filename := filepath.Join(configDir, dockerConfigFileName) - - // load latest config file - file, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("%s: %w", filename, err) - } - defer file.Close() - - configFile := &DockerConfigFile{} - err = configFile.loadFromReader(file) - if err != nil { - return nil, fmt.Errorf("%s: %w", filename, err) - } - return configFile, err -} - -func getDockerConfigDir() (string, error) { - configDir := os.Getenv("DOCKER_CONFIG") - if configDir == "" { - homeDir, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("%s, %w", "Could not get home directory", err) - } - configDir = filepath.Join(homeDir, dockerConfigFileDir) - } - return configDir, nil -} - -// loadFromReader reads the configuration data given and sets up the auth config -// information with given directory and populates the receiver object -func (configFile *DockerConfigFile) loadFromReader(configData io.Reader) error { - if err := json.NewDecoder(configData).Decode(configFile); err != nil && !errors.Is(err, io.EOF) { - return err - } - return nil -} diff --git a/pkg/configutil/docker_config_test.go b/pkg/configutil/docker_config_test.go deleted file mode 100644 index a3afb2a06..000000000 --- a/pkg/configutil/docker_config_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package configutil - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -const ( - validJson = `{ - "credHelpers": { - "localhost:5000": "pass" - }, - "credsStore": "pass" - }` - invalidJson = `{` -) - -func TestLoadDockerConfig_noErrors(t *testing.T) { - // Create temp directory - dockerConfigDir := t.TempDir() - t.Setenv("DOCKER_CONFIG", dockerConfigDir) - - // Create config.json - f, err := os.Create(filepath.Join(dockerConfigDir, dockerConfigFileName)) - if err != nil { - t.Fatalf("Failed to mock docker config, err: %v", err) - } - defer f.Close() - data := []byte(validJson) - if _, err := f.Write(data); err != nil { - t.Fatalf("Failed to mock docker config, err: %v", err) - } - - // Load docker config - config, err := LoadDockerConfig() - if err != nil { - t.Fatalf("Unexpected error loading config.json: %v", err) - } - if config.CredentialsStore != "pass" { - t.Fatalf("Expected credentials store to be 'pass', but got %v", config.CredentialsStore) - } -} - -func TestLoadDockerConfig_noConfigFile(t *testing.T) { - // Create temp directory - dockerConfigDir := t.TempDir() - t.Setenv("DOCKER_CONFIG", dockerConfigDir) - - // Load docker config - _, err := LoadDockerConfig() - if err == nil { - t.Fatalf("Expected error not returned") - } -} - -func TestLoadDockerConfig_invalidConfigFile(t *testing.T) { - // Create temp directory - dockerConfigDir := t.TempDir() - t.Setenv("DOCKER_CONFIG", dockerConfigDir) - - // Create config.json - f, err := os.Create(filepath.Join(dockerConfigDir, dockerConfigFileName)) - if err != nil { - t.Fatalf("Failed to mock docker config, err: %v", err) - } - defer f.Close() - data := []byte(invalidJson) - if _, err := f.Write(data); err != nil { - t.Fatalf("Failed to mock docker config, err: %v", err) - } - - // Load docker config - _, err = LoadDockerConfig() - if err == nil || !strings.HasSuffix(err.Error(), "unexpected EOF") { - t.Fatalf("Expected error not returned") - } -}