From 0f74c2fb36728b56b95bcf8d311749a21f549759 Mon Sep 17 00:00:00 2001 From: Binbin Li Date: Mon, 27 Jun 2022 10:29:48 +0000 Subject: [PATCH 1/5] feat: support notation login Signed-off-by: Binbin Li --- cmd/notation/login.go | 67 ++++++++++++++++++++++++++++ cmd/notation/logout.go | 28 ++++++++++++ cmd/notation/main.go | 2 + cmd/notation/manifest.go | 5 ++- cmd/notation/registry.go | 51 +++++++++++++++++++-- go.mod | 6 +-- go.sum | 4 +- pkg/auth/api.go | 13 ++++++ pkg/auth/credential.go | 66 ++++++++++++++++++++++++++++ pkg/auth/native_store.go | 88 +++++++++++++++++++++++++++++++++++++ pkg/auth/types.go | 6 +++ pkg/config/config.go | 2 + pkg/config/docker_config.go | 69 +++++++++++++++++++++++++++++ 13 files changed, 398 insertions(+), 9 deletions(-) create mode 100644 cmd/notation/login.go create mode 100644 cmd/notation/logout.go create mode 100644 pkg/auth/api.go create mode 100644 pkg/auth/credential.go create mode 100644 pkg/auth/native_store.go create mode 100644 pkg/auth/types.go create mode 100644 pkg/config/docker_config.go diff --git a/cmd/notation/login.go b/cmd/notation/login.go new file mode 100644 index 000000000..45630276d --- /dev/null +++ b/cmd/notation/login.go @@ -0,0 +1,67 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/notaryproject/notation/pkg/auth" + "github.com/urfave/cli/v2" + orasauth "oras.land/oras-go/v2/registry/remote/auth" +) + +var loginCommand = &cli.Command{ + Name: "login", + Usage: "Log in the specified registry hostname", + ArgsUsage: "", + Flags: []cli.Flag{ + flagUsername, + flagPassword, + flagPlainHTTP, + }, + Action: runLogin, +} + +func runLogin(ctx *cli.Context) error { + // initialize + if !ctx.Args().Present() { + return errors.New("no hostname specified") + } + serverAddress := ctx.Args().First() + + if err := validateAuthConfig(ctx, serverAddress); err != nil { + return err + } + + nativeStore, err := auth.GetCredentialsStore(serverAddress) + if err != nil { + return fmt.Errorf("could not get the credentials store: %v", err) + } + // init creds + creds := newCredentialFromInput( + ctx.String(flagUsername.Name), + ctx.String(flagPassword.Name), + ) + if err = nativeStore.Store(serverAddress, creds); err != nil { + return fmt.Errorf("failed to store credentials: %v", err) + } + return nil +} + +func validateAuthConfig(ctx *cli.Context, serverAddress string) error { + registry, err := getRegistryClient(ctx, serverAddress) + if err != nil { + return err + } + return registry.Ping(ctx.Context) +} + +func newCredentialFromInput(username, password string) orasauth.Credential { + c := orasauth.Credential{ + Username: username, + Password: password, + } + if c.Username == "" { + c.RefreshToken = password + } + return c +} diff --git a/cmd/notation/logout.go b/cmd/notation/logout.go new file mode 100644 index 000000000..615f8e50c --- /dev/null +++ b/cmd/notation/logout.go @@ -0,0 +1,28 @@ +package main + +import ( + "errors" + + "github.com/notaryproject/notation/pkg/auth" + "github.com/urfave/cli/v2" +) + +var logoutCommand = &cli.Command{ + Name: "logout", + Usage: "Log out the specified registry hostname", + ArgsUsage: "", + Action: runLogout, +} + +func runLogout(ctx *cli.Context) error { + // initialize + if !ctx.Args().Present() { + return errors.New("no hostname specified") + } + serverAddress := ctx.Args().First() + nativeStore, err := auth.GetCredentialsStore(serverAddress) + if err != nil { + return err + } + return nativeStore.Erase(serverAddress) +} diff --git a/cmd/notation/main.go b/cmd/notation/main.go index e5d653f6e..2b7beab16 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -28,6 +28,8 @@ func main() { keyCommand, cacheCommand, pluginCommand, + loginCommand, + logoutCommand, }, } if err := app.Run(os.Args); err != nil { diff --git a/cmd/notation/manifest.go b/cmd/notation/manifest.go index 0b527d973..cce70cb87 100644 --- a/cmd/notation/manifest.go +++ b/cmd/notation/manifest.go @@ -37,7 +37,10 @@ func getManifestDescriptorFromReference(ctx *cli.Context, reference string) (not if err != nil { return notation.Descriptor{}, err } - repo := getRepositoryClient(ctx, ref) + repo, err := getRepositoryClient(ctx, ref) + if err != nil { + return notation.Descriptor{}, err + } return repo.Resolve(ctx.Context, ref.ReferenceOrDefault()) } diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index f63ec345e..6b42632ca 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -6,9 +6,11 @@ import ( notationregistry "github.com/notaryproject/notation-go/registry" "github.com/notaryproject/notation/internal/version" + loginauth "github.com/notaryproject/notation/pkg/auth" "github.com/notaryproject/notation/pkg/config" "github.com/urfave/cli/v2" "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" ) @@ -17,10 +19,33 @@ func getSignatureRepository(ctx *cli.Context, reference string) (notationregistr if err != nil { return nil, err } - return getRepositoryClient(ctx, ref), nil + return getRepositoryClient(ctx, ref) } -func getRepositoryClient(ctx *cli.Context, ref registry.Reference) *notationregistry.RepositoryClient { +func getRegistryClient(ctx *cli.Context, serverAddress string) (*remote.Registry, error) { + reg, err := remote.NewRegistry(serverAddress) + if err != nil { + return nil, err + } + ref := registry.Reference{ + Registry: serverAddress, + } + reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, ref) + if err != nil { + return nil, err + } + return reg, nil +} + +func getRepositoryClient(ctx *cli.Context, ref registry.Reference) (*notationregistry.RepositoryClient, error) { + authClient, plainHTTP, err := getAuthClient(ctx, ref) + if err != nil { + return nil, err + } + return notationregistry.NewRepositoryClient(authClient, ref, plainHTTP), nil +} + +func getAuthClient(ctx *cli.Context, ref registry.Reference) (*auth.Client, bool, error) { var plainHTTP bool if ctx.IsSet(flagPlainHTTP.Name) { plainHTTP = ctx.Bool(flagPlainHTTP.Name) @@ -42,6 +67,13 @@ func getRepositoryClient(ctx *cli.Context, ref registry.Reference) *notationregi RefreshToken: cred.Password, } } + if cred == auth.EmptyCredential { + var err error + if cred, err = getSavedCreds(ref.Registry); err != nil { + return nil, false, err + } + } + authClient := &auth.Client{ Credential: func(ctx context.Context, registry string) (auth.Credential, error) { switch registry { @@ -56,5 +88,18 @@ func getRepositoryClient(ctx *cli.Context, ref registry.Reference) *notationregi } authClient.SetUserAgent("notation/" + version.GetVersion()) - return notationregistry.NewRepositoryClient(authClient, ref, plainHTTP) + return authClient, plainHTTP, nil +} + +func getSavedCreds(serverAddress string) (auth.Credential, error) { + nativeStore, err := loginauth.GetCredentialsStore(serverAddress) + if err != nil { + return auth.EmptyCredential, err + } + + creds, err := nativeStore.Get(serverAddress) + if err != nil { + return auth.EmptyCredential, err + } + return creds, nil } diff --git a/go.mod b/go.mod index 61367697e..5d2e5af30 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,22 @@ go 1.18 require ( github.com/distribution/distribution/v3 v3.0.0-20210804104954-38ab4c606ee3 github.com/docker/cli v20.10.17+incompatible + github.com/docker/docker-credential-helpers v0.6.4 github.com/notaryproject/notation-core-go v0.0.0-20220712013708-3c4b3efa03c5 github.com/notaryproject/notation-go v0.9.0-alpha.1.0.20220712175603-962d79cd4090 github.com/opencontainers/go-digest v1.0.0 github.com/spf13/cobra v1.5.0 github.com/spf13/pflag v1.0.5 github.com/urfave/cli/v2 v2.11.0 - oras.land/oras-go/v2 v2.0.0-20220620164807-8b2a54608a94 + oras.land/oras-go/v2 v2.0.0-20220620164807-8b2a54608a94 // TODO: upgrade to v2.0.0-rc.1 in the next PR ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/docker/docker v20.10.8+incompatible // indirect - github.com/docker/docker-credential-helpers v0.6.4 // indirect github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect github.com/oras-project/artifacts-spec v1.0.0-rc.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 492594e5f..ba344e8ae 100644 --- a/go.sum +++ b/go.sum @@ -81,8 +81,8 @@ github.com/notaryproject/notation-go v0.9.0-alpha.1.0.20220712175603-962d79cd409 github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= -github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/oras-project/artifacts-spec v1.0.0-rc.1 h1:bCHf9mPbrgiNwQFyVzBX79BYZVAl0OUrmvICZOCOwts= github.com/oras-project/artifacts-spec v1.0.0-rc.1/go.mod h1:Xch2aLzSwtkhbFFN6LUzTfLtukYvMMdXJ4oZ8O7BOdc= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/auth/api.go b/pkg/auth/api.go new file mode 100644 index 000000000..bf5c6cb7b --- /dev/null +++ b/pkg/auth/api.go @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000..dc45851b1 --- /dev/null +++ b/pkg/auth/credential.go @@ -0,0 +1,66 @@ +package auth + +import ( + "fmt" + + "github.com/docker/docker-credential-helpers/credentials" + "github.com/notaryproject/notation/pkg/config" + "oras.land/oras-go/v2/registry/remote/auth" +) + +// LoadConfig loads the configuration from the config file +func LoadConfig() (*config.File, error) { + // load notation config first + config, err := config.LoadOrDefault() + if err != nil { + return nil, err + } + if config != nil && containsAuth(config) { + fmt.Println("Using notation config file") + return config, nil + } + + config, err = loadDockerConfig() + if err != nil { + return nil, err + } + if config != nil && containsAuth(config) { + fmt.Println("Using docker config file") + return config, nil + } + if !containsAuth(config) { + return nil, fmt.Errorf("credentials store config is not set up") + } + return config, nil +} + +// loadDockerConfig loads the configuration from the config file under .docker +// directory +func loadDockerConfig() (*config.File, error) { + dockerConfig, err := config.LoadDockerConfig() + if err != nil { + return nil, err + } + return &config.File{ + CredentialHelpers: dockerConfig.CredentialHelpers, + CredentialsStore: dockerConfig.CredentialsStore, + }, nil +} + +// containsAuth returns whether there is authentication configured in this file +// or not. +func containsAuth(configFile *config.File) 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/native_store.go b/pkg/auth/native_store.go new file mode 100644 index 000000000..280f8dd68 --- /dev/null +++ b/pkg/auth/native_store.go @@ -0,0 +1,88 @@ +package auth + +import ( + "fmt" + + "github.com/docker/docker-credential-helpers/client" + "github.com/docker/docker-credential-helpers/credentials" + "github.com/notaryproject/notation/pkg/config" + "oras.land/oras-go/v2/registry/remote/auth" +) + +// nativeAuthStore implements a credentials store using native keychain to keep +// credentials secure. +type nativeAuthStore struct { + programFunc client.ProgramFunc +} + +// 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(helperSuffix string) CredentialStore { + name := remoteCredentialsPrefix + helperSuffix + return &nativeAuthStore{ + programFunc: client.NewShellProgramFunc(name), + } +} + +// GetCredentialsStore returns a new credentials store from the settings in the +// configuration file +func GetCredentialsStore(registryHostname string) (CredentialStore, error) { + configFile, err := LoadConfig() + if err != nil { + return nil, fmt.Errorf("failed to load config file, error: %v", err) + } + if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" { + return newNativeStore(helper), nil + } + return nil, fmt.Errorf("could not get the configured credentials store for registry: %s", registryHostname) +} + +// var for unit testing. +var newNativeStore = NewNativeAuthStore + +// 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.File, 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/types.go b/pkg/auth/types.go new file mode 100644 index 000000000..ff0d5adfb --- /dev/null +++ b/pkg/auth/types.go @@ -0,0 +1,6 @@ +package auth + +const ( + remoteCredentialsPrefix = "docker-credential-" + tokenUsername = "" +) diff --git a/pkg/config/config.go b/pkg/config/config.go index fc1bf1873..16d59b759 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -48,6 +48,8 @@ type File struct { VerificationCertificates VerificationCertificates `json:"verificationCerts"` SigningKeys SigningKeys `json:"signingKeys,omitempty"` InsecureRegistries []string `json:"insecureRegistries"` + CredentialsStore string `json:"credsStore,omitempty"` + CredentialHelpers map[string]string `json:"credHelpers,omitempty"` } // VerificationCertificates is a collection of public certs used for verification. diff --git a/pkg/config/docker_config.go b/pkg/config/docker_config.go new file mode 100644 index 000000000..a8af2436c --- /dev/null +++ b/pkg/config/docker_config.go @@ -0,0 +1,69 @@ +package config + +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) { + configFile := &DockerConfigFile{} + configDir, err := getDockerConfigDir() + if err != nil { + return configFile, err + } + + filename := filepath.Join(configDir, dockerConfigFileName) + + // load latest config file + file, err := os.Open(filename) + if err != nil { + return configFile, fmt.Errorf("%s: %w", filename, err) + } + + defer file.Close() + err = configFile.loadFromReader(file) + if err != nil { + err = 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 +} From f2c034a6e78cf0043d343dbe9d6ab51b0f4ce4b5 Mon Sep 17 00:00:00 2001 From: Binbin Li Date: Fri, 8 Jul 2022 03:55:10 +0000 Subject: [PATCH 2/5] refactor: Refactor for readablity Signed-off-by: Binbin Li --- cmd/notation/registry.go | 6 +----- pkg/auth/credential.go | 2 -- pkg/auth/native_store.go | 5 +++++ pkg/auth/types.go | 6 ------ pkg/config/docker_config.go | 10 +++++----- 5 files changed, 11 insertions(+), 18 deletions(-) delete mode 100644 pkg/auth/types.go diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 6b42632ca..2aef9754e 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -97,9 +97,5 @@ func getSavedCreds(serverAddress string) (auth.Credential, error) { return auth.EmptyCredential, err } - creds, err := nativeStore.Get(serverAddress) - if err != nil { - return auth.EmptyCredential, err - } - return creds, nil + return nativeStore.Get(serverAddress) } diff --git a/pkg/auth/credential.go b/pkg/auth/credential.go index dc45851b1..9438bb56c 100644 --- a/pkg/auth/credential.go +++ b/pkg/auth/credential.go @@ -16,7 +16,6 @@ func LoadConfig() (*config.File, error) { return nil, err } if config != nil && containsAuth(config) { - fmt.Println("Using notation config file") return config, nil } @@ -25,7 +24,6 @@ func LoadConfig() (*config.File, error) { return nil, err } if config != nil && containsAuth(config) { - fmt.Println("Using docker config file") return config, nil } if !containsAuth(config) { diff --git a/pkg/auth/native_store.go b/pkg/auth/native_store.go index 280f8dd68..750d2d247 100644 --- a/pkg/auth/native_store.go +++ b/pkg/auth/native_store.go @@ -9,6 +9,11 @@ import ( "oras.land/oras-go/v2/registry/remote/auth" ) +const ( + remoteCredentialsPrefix = "docker-credential-" + tokenUsername = "" +) + // nativeAuthStore implements a credentials store using native keychain to keep // credentials secure. type nativeAuthStore struct { diff --git a/pkg/auth/types.go b/pkg/auth/types.go deleted file mode 100644 index ff0d5adfb..000000000 --- a/pkg/auth/types.go +++ /dev/null @@ -1,6 +0,0 @@ -package auth - -const ( - remoteCredentialsPrefix = "docker-credential-" - tokenUsername = "" -) diff --git a/pkg/config/docker_config.go b/pkg/config/docker_config.go index a8af2436c..3f5c66c92 100644 --- a/pkg/config/docker_config.go +++ b/pkg/config/docker_config.go @@ -25,10 +25,9 @@ type DockerConfigFile struct { // Load reads the configuration files in the given directory, and sets up // the auth config information and returns values. func LoadDockerConfig() (*DockerConfigFile, error) { - configFile := &DockerConfigFile{} configDir, err := getDockerConfigDir() if err != nil { - return configFile, err + return nil, err } filename := filepath.Join(configDir, dockerConfigFileName) @@ -36,13 +35,14 @@ func LoadDockerConfig() (*DockerConfigFile, error) { // load latest config file file, err := os.Open(filename) if err != nil { - return configFile, fmt.Errorf("%s: %w", filename, err) + return nil, fmt.Errorf("%s: %w", filename, err) } - defer file.Close() + + configFile := &DockerConfigFile{} err = configFile.loadFromReader(file) if err != nil { - err = fmt.Errorf("%s: %w", filename, err) + return nil, fmt.Errorf("%s: %w", filename, err) } return configFile, err } From a8286994b77f3de4038b2e962b531c8ec5b0f15a Mon Sep 17 00:00:00 2001 From: Binbin Li Date: Fri, 8 Jul 2022 04:27:28 +0000 Subject: [PATCH 3/5] chore: refactor registry.go Signed-off-by: Binbin Li --- cmd/notation/registry.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/notation/registry.go b/cmd/notation/registry.go index 2aef9754e..551398add 100644 --- a/cmd/notation/registry.go +++ b/cmd/notation/registry.go @@ -27,10 +27,8 @@ func getRegistryClient(ctx *cli.Context, serverAddress string) (*remote.Registry if err != nil { return nil, err } - ref := registry.Reference{ - Registry: serverAddress, - } - reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, ref) + + reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, reg.Reference) if err != nil { return nil, err } From e29ad0fc27d5c7bca3eff7bbae7cd3981361c092 Mon Sep 17 00:00:00 2001 From: Binbin Li Date: Thu, 14 Jul 2022 04:41:28 +0000 Subject: [PATCH 4/5] feat: update CMDs to be consistent with specs Signed-off-by: Binbin Li --- cmd/notation/cache.go | 3 --- cmd/notation/common.go | 6 +++--- cmd/notation/list.go | 1 - cmd/notation/login.go | 43 ++++++++++++++++++++++++++++++++++++++---- cmd/notation/logout.go | 2 +- cmd/notation/main.go | 3 +++ cmd/notation/pull.go | 1 - cmd/notation/push.go | 1 - cmd/notation/sign.go | 1 - cmd/notation/verify.go | 1 - 10 files changed, 46 insertions(+), 16 deletions(-) diff --git a/cmd/notation/cache.go b/cmd/notation/cache.go index fd163df4f..f42fd612c 100644 --- a/cmd/notation/cache.go +++ b/cmd/notation/cache.go @@ -34,7 +34,6 @@ var ( flagLocal, flagUsername, flagPassword, - flagPlainHTTP, }, ArgsUsage: "[reference|manifest_digest]", Action: listCachedSignatures, @@ -62,7 +61,6 @@ var ( flagLocal, flagUsername, flagPassword, - flagPlainHTTP, }, Action: pruneCachedSignatures, } @@ -76,7 +74,6 @@ var ( flagLocal, flagUsername, flagPassword, - flagPlainHTTP, }, Action: removeCachedSignatures, } diff --git a/cmd/notation/common.go b/cmd/notation/common.go index 04161ecc3..473295dc6 100644 --- a/cmd/notation/common.go +++ b/cmd/notation/common.go @@ -6,18 +6,18 @@ var ( flagUsername = &cli.StringFlag{ Name: "username", Aliases: []string{"u"}, - Usage: "username for generic remote access", + Usage: "Username for registry operations", EnvVars: []string{"NOTATION_USERNAME"}, } flagPassword = &cli.StringFlag{ Name: "password", Aliases: []string{"p"}, - Usage: "password for generic remote access", + Usage: "Password for registry operations", EnvVars: []string{"NOTATION_PASSWORD"}, } flagPlainHTTP = &cli.BoolFlag{ Name: "plain-http", - Usage: "remote access via plain HTTP", + Usage: "Registry access via plain HTTP", } flagMediaType = &cli.StringFlag{ Name: "media-type", diff --git a/cmd/notation/list.go b/cmd/notation/list.go index aa5b29602..05400e1c6 100644 --- a/cmd/notation/list.go +++ b/cmd/notation/list.go @@ -15,7 +15,6 @@ var listCommand = &cli.Command{ Flags: []cli.Flag{ flagUsername, flagPassword, - flagPlainHTTP, }, Action: runList, } diff --git a/cmd/notation/login.go b/cmd/notation/login.go index 45630276d..3d1329817 100644 --- a/cmd/notation/login.go +++ b/cmd/notation/login.go @@ -3,6 +3,9 @@ package main import ( "errors" "fmt" + "io" + "os" + "strings" "github.com/notaryproject/notation/pkg/auth" "github.com/urfave/cli/v2" @@ -10,14 +13,25 @@ import ( ) var loginCommand = &cli.Command{ - Name: "login", - Usage: "Log in the specified registry hostname", - ArgsUsage: "", + Name: "login", + Usage: "Provides credentials for authenticated registry operations", + UsageText: `notation login [options] [server] + +Example - Login with provided username and password: + notation login -u -p registry.example.com + +Example - Login using $NOTATION_USERNAME $NOTATION_PASSWORD variables: + notation login registry.example.com`, + ArgsUsage: "[server]", Flags: []cli.Flag{ flagUsername, flagPassword, - flagPlainHTTP, + &cli.BoolFlag{ + Name: "password-stdin", + Usage: "Take the password from stdin", + }, }, + Before: readPassword, Action: runLogin, } @@ -65,3 +79,24 @@ func newCredentialFromInput(username, password string) orasauth.Credential { } return c } + +func readPassword(ctx *cli.Context) error { + if ctx.Bool("password-stdin") { + password, err := readLine() + if err != nil { + return err + } + ctx.Set(flagPassword.Name, password) + } + return nil +} + +func readLine() (string, error) { + passwordBytes, err := io.ReadAll(os.Stdin) + if err != nil { + return "", err + } + password := strings.TrimSuffix(string(passwordBytes), "\n") + password = strings.TrimSuffix(password, "\r") + return password, nil +} diff --git a/cmd/notation/logout.go b/cmd/notation/logout.go index 615f8e50c..5eb707210 100644 --- a/cmd/notation/logout.go +++ b/cmd/notation/logout.go @@ -10,7 +10,7 @@ import ( var logoutCommand = &cli.Command{ Name: "logout", Usage: "Log out the specified registry hostname", - ArgsUsage: "", + ArgsUsage: "[server]", Action: runLogout, } diff --git a/cmd/notation/main.go b/cmd/notation/main.go index 2b7beab16..fd2bea3a4 100644 --- a/cmd/notation/main.go +++ b/cmd/notation/main.go @@ -18,6 +18,9 @@ func main() { Name: "CNCF Notary Project", }, }, + Flags: []cli.Flag{ + flagPlainHTTP, + }, Commands: []*cli.Command{ signCommand, verifyCommand, diff --git a/cmd/notation/pull.go b/cmd/notation/pull.go index 9a3b52d36..3d4051dcc 100644 --- a/cmd/notation/pull.go +++ b/cmd/notation/pull.go @@ -26,7 +26,6 @@ var pullCommand = &cli.Command{ flagOutput, flagUsername, flagPassword, - flagPlainHTTP, }, Action: runPull, } diff --git a/cmd/notation/push.go b/cmd/notation/push.go index 3147a67dc..1d5eab871 100644 --- a/cmd/notation/push.go +++ b/cmd/notation/push.go @@ -19,7 +19,6 @@ var pushCommand = &cli.Command{ flagSignature, flagUsername, flagPassword, - flagPlainHTTP, }, Action: runPush, } diff --git a/cmd/notation/sign.go b/cmd/notation/sign.go index 31c51c920..47104f218 100644 --- a/cmd/notation/sign.go +++ b/cmd/notation/sign.go @@ -36,7 +36,6 @@ var signCommand = &cli.Command{ }, flagUsername, flagPassword, - flagPlainHTTP, flagMediaType, cmd.FlagPluginConfig, }, diff --git a/cmd/notation/verify.go b/cmd/notation/verify.go index 085cdec45..ad4ad0492 100644 --- a/cmd/notation/verify.go +++ b/cmd/notation/verify.go @@ -40,7 +40,6 @@ var verifyCommand = &cli.Command{ flagLocal, flagUsername, flagPassword, - flagPlainHTTP, flagMediaType, }, Action: runVerify, From e705d73e4be58f2859d25a649bee58a9ba8cc6ad Mon Sep 17 00:00:00 2001 From: Binbin Li Date: Mon, 18 Jul 2022 02:48:24 +0000 Subject: [PATCH 5/5] chore: refactor load config logic Signed-off-by: Binbin Li --- pkg/auth/credential.go | 23 +++++++++++++---------- pkg/auth/native_store.go | 28 ++++++++++++++-------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/pkg/auth/credential.go b/pkg/auth/credential.go index 9438bb56c..54e514e70 100644 --- a/pkg/auth/credential.go +++ b/pkg/auth/credential.go @@ -8,10 +8,16 @@ import ( "oras.land/oras-go/v2/registry/remote/auth" ) +// var for unit tests +var ( + loadOrDefault = config.LoadOrDefault + loadDockerConfig = config.LoadDockerConfig +) + // LoadConfig loads the configuration from the config file func LoadConfig() (*config.File, error) { // load notation config first - config, err := config.LoadOrDefault() + config, err := loadOrDefault() if err != nil { return nil, err } @@ -19,23 +25,20 @@ func LoadConfig() (*config.File, error) { return config, nil } - config, err = loadDockerConfig() + config, err = loadDockerCredentials() if err != nil { return nil, err } - if config != nil && containsAuth(config) { + if containsAuth(config) { return config, nil } - if !containsAuth(config) { - return nil, fmt.Errorf("credentials store config is not set up") - } - return config, nil + return nil, fmt.Errorf("credentials store config is not set up") } -// loadDockerConfig loads the configuration from the config file under .docker +// loadDockerCredentials loads the configuration from the config file under .docker // directory -func loadDockerConfig() (*config.File, error) { - dockerConfig, err := config.LoadDockerConfig() +func loadDockerCredentials() (*config.File, error) { + dockerConfig, err := loadDockerConfig() if err != nil { return nil, err } diff --git a/pkg/auth/native_store.go b/pkg/auth/native_store.go index 750d2d247..adfc6c022 100644 --- a/pkg/auth/native_store.go +++ b/pkg/auth/native_store.go @@ -14,37 +14,37 @@ const ( 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 } -// 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(helperSuffix string) CredentialStore { - name := remoteCredentialsPrefix + helperSuffix - return &nativeAuthStore{ - programFunc: client.NewShellProgramFunc(name), - } -} - // GetCredentialsStore returns a new credentials store from the settings in the // configuration file func GetCredentialsStore(registryHostname string) (CredentialStore, error) { - configFile, err := LoadConfig() + configFile, err := loadConfig() if err != nil { return nil, fmt.Errorf("failed to load config file, error: %v", err) } if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" { - return newNativeStore(helper), nil + return newNativeAuthStore(helper), nil } return nil, fmt.Errorf("could not get the configured credentials store for registry: %s", registryHostname) } -// var for unit testing. -var newNativeStore = NewNativeAuthStore +// 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(helperSuffix string) CredentialStore { + name := remoteCredentialsPrefix + helperSuffix + 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