From 8aa532bf1c4551d6193676f3288f3306b160c5f4 Mon Sep 17 00:00:00 2001 From: Billy Zha Date: Thu, 27 Apr 2023 13:18:06 +0800 Subject: [PATCH] feat: use oras-credential-go to do auth (#932) Signed-off-by: Billy Zha --- cmd/oras/internal/option/remote.go | 15 +--- cmd/oras/root/login.go | 20 ++---- cmd/oras/root/logout.go | 10 +-- go.mod | 5 +- go.sum | 35 +-------- internal/credential/store.go | 112 +++-------------------------- internal/credential/store_test.go | 85 ---------------------- 7 files changed, 22 insertions(+), 260 deletions(-) delete mode 100644 internal/credential/store_test.go diff --git a/cmd/oras/internal/option/remote.go b/cmd/oras/internal/option/remote.go index 05fbe58d2..c4261a880 100644 --- a/cmd/oras/internal/option/remote.go +++ b/cmd/oras/internal/option/remote.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" + credentials "github.com/oras-project/oras-credentials-go" "github.com/spf13/pflag" "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" @@ -219,19 +220,7 @@ func (opts *Remote) authClient(registry string, debug bool) (client *auth.Client if err != nil { return nil, err } - // For a user case with a registry from 'docker.io', the hostname is "registry-1.docker.io" - // According to the the behavior of Docker CLI, - // credential under key "https://index.docker.io/v1/" should be provided - if registry == "docker.io" { - client.Credential = func(ctx context.Context, hostname string) (auth.Credential, error) { - if hostname == "registry-1.docker.io" { - hostname = "https://index.docker.io/v1/" - } - return store.Credential(ctx, hostname) - } - } else { - client.Credential = store.Credential - } + client.Credential = credentials.Credential(store) } return } diff --git a/cmd/oras/root/login.go b/cmd/oras/root/login.go index e015ca19a..aa862fd4d 100644 --- a/cmd/oras/root/login.go +++ b/cmd/oras/root/login.go @@ -22,6 +22,7 @@ import ( "os" "strings" + credentials "github.com/oras-project/oras-credentials-go" "github.com/spf13/cobra" "golang.org/x/term" "oras.land/oras/cmd/oras/internal/option" @@ -103,28 +104,15 @@ func runLogin(ctx context.Context, opts loginOptions) (err error) { } } - // Ping to ensure credential is valid - remote, err := opts.Remote.NewRegistry(opts.Hostname, opts.Common) + store, err := credential.NewStore(opts.Configs...) if err != nil { return err } - if err = remote.Ping(ctx); err != nil { - return err - } - - // Store the validated credential - store, err := credential.NewStore(opts.Configs...) + remote, err := opts.Remote.NewRegistry(opts.Hostname, opts.Common) if err != nil { return err } - // For a user case that login 'docker.io', - // According the the behavior of Docker CLI, - // credential should be added under key "https://index.docker.io/v1/" - hostname := opts.Hostname - if hostname == "docker.io" { - hostname = "https://index.docker.io/v1/" - } - if err := store.Store(hostname, opts.Credential()); err != nil { + if err = credentials.Login(ctx, store, remote, opts.Credential()); err != nil { return err } fmt.Println("Login Succeeded") diff --git a/cmd/oras/root/logout.go b/cmd/oras/root/logout.go index a4ee857f4..bdb39d1f5 100644 --- a/cmd/oras/root/logout.go +++ b/cmd/oras/root/logout.go @@ -18,6 +18,7 @@ package root import ( "context" + credentials "github.com/oras-project/oras-credentials-go" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "oras.land/oras/internal/credential" @@ -61,12 +62,5 @@ func runLogout(ctx context.Context, opts logoutOptions) error { if err != nil { return err } - // For a user case that logout from 'docker.io', - // According the the behavior of Docker CLI, - // credential under key "https://index.docker.io/v1/" should be removed - hostname := opts.hostname - if hostname == "docker.io" { - hostname = "https://index.docker.io/v1/" - } - return store.Erase(hostname) + return credentials.Logout(ctx, store, opts.hostname) } diff --git a/go.mod b/go.mod index e74bc9eff..d83a38860 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module oras.land/oras go 1.20 require ( - github.com/docker/cli v23.0.4+incompatible github.com/need-being/go-tree v0.1.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc2 + github.com/oras-project/oras-credentials-go v0.0.0-20230424070720-ba6b33c40845 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 @@ -16,11 +16,8 @@ require ( ) require ( - github.com/docker/docker v23.0.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pkg/errors v0.9.1 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.7.0 // indirect - gotest.tools/v3 v3.4.0 // indirect ) diff --git a/go.sum b/go.sum index 6eb26335d..35c8b4c81 100644 --- a/go.sum +++ b/go.sum @@ -2,14 +2,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/cli v23.0.4+incompatible h1:xClB7PsiATttDHj8ce5qvJcikiApNy7teRR1XkoBZGs= -github.com/docker/cli v23.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v23.0.3+incompatible h1:9GhVsShNWz1hO//9BNg/dpMnZW25KydO4wtVxWAIbho= -github.com/docker/docker v23.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/need-being/go-tree v0.1.0 h1:blQrtD006cFm97UDeMUfixwPc9o06A6c+uLaUskdNNw= @@ -18,8 +12,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-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/oras-project/oras-credentials-go v0.0.0-20230424070720-ba6b33c40845 h1:lE/TKx3cblnIRMazUdXgHmoM8TXs4fZqv3EXvEF3A2I= +github.com/oras-project/oras-credentials-go v0.0.0-20230424070720-ba6b33c40845/go.mod h1:yww9XqMCWjNh4Z7S5Ek7KeV9j/yhbyVxgSinGdNNwEg= 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= @@ -32,42 +26,17 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= oras.land/oras-go/v2 v2.0.2 h1:3aSQdJ7EUC0ft2e9PjJB9Jzastz5ojPA4LzZ3Q4YbUc= oras.land/oras-go/v2 v2.0.2/go.mod h1:PWnWc/Kyyg7wUTUsDHshrsJkzuxXzreeMd6NrfdnFSo= diff --git a/internal/credential/store.go b/internal/credential/store.go index 94660d7fa..5c61d8726 100644 --- a/internal/credential/store.go +++ b/internal/credential/store.go @@ -16,114 +16,24 @@ limitations under the License. package credential import ( - "context" - "fmt" - "os" - - "github.com/docker/cli/cli/config" - "github.com/docker/cli/cli/config/configfile" - "github.com/docker/cli/cli/config/credentials" - "github.com/docker/cli/cli/config/types" - "oras.land/oras-go/v2/registry/remote/auth" + credentials "github.com/oras-project/oras-credentials-go" ) -// Store provides credential CRUD operations. -type Store struct { - configs []*configfile.ConfigFile -} - -// NewStore generates a store based on the passed in config file path. -func NewStore(configPaths ...string) (*Store, error) { +// NewStore generates a store based on the passed-in config file paths. +func NewStore(configPaths ...string) (credentials.Store, error) { + opts := credentials.StoreOptions{AllowPlaintextPut: true} if len(configPaths) == 0 { - // No config path passed, load default docker config file. - cfg, err := config.Load(config.Dir()) - if err != nil { - return nil, err - } - if !cfg.ContainsAuth() { - cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore) - } - - return &Store{ - configs: []*configfile.ConfigFile{cfg}, - }, nil + // use default docker config file path + return credentials.NewStoreFromDocker(opts) } - var configs []*configfile.ConfigFile - for _, path := range configPaths { - cfg, err := loadConfigFile(path) - if err != nil { - return nil, fmt.Errorf("%s: %w", path, err) - } - configs = append(configs, cfg) - } - - return &Store{ - configs: configs, - }, nil -} - -// loadConfigFile reads the credential-related configurationfrom the given path. -func loadConfigFile(path string) (*configfile.ConfigFile, error) { - var cfg *configfile.ConfigFile - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - cfg = configfile.New(path) - } else { - return nil, err - } - } else { - file, err := os.Open(path) + var stores []credentials.Store + for _, config := range configPaths { + store, err := credentials.NewStore(config, opts) if err != nil { return nil, err } - defer file.Close() - cfg = configfile.New(path) - if err := cfg.LoadFromReader(file); err != nil { - return nil, err - } - } - - if !cfg.ContainsAuth() { - cfg.CredentialsStore = credentials.DetectDefaultStore(cfg.CredentialsStore) - } - return cfg, nil -} - -// Store stores a credential for a given registry. -func (s *Store) Store(registry string, cred auth.Credential) error { - authConf := types.AuthConfig{ - Username: cred.Username, - Password: cred.Password, - ServerAddress: registry, - IdentityToken: cred.RefreshToken, - RegistryToken: cred.AccessToken, - } - return s.configs[0].GetCredentialsStore(registry).Store(authConf) -} - -// Erase erases a credential for a given registry. -func (s *Store) Erase(registry string) error { - return s.configs[0].GetCredentialsStore(registry).Erase(registry) -} - -// Credential iterates all the config files, returns the first non-empty -// credential in a best-effort way. -func (s *Store) Credential(ctx context.Context, registry string) (auth.Credential, error) { - for _, c := range s.configs { - authConf, err := c.GetCredentialsStore(registry).Get(registry) - if err != nil { - return auth.EmptyCredential, err - } - cred := auth.Credential{ - Username: authConf.Username, - Password: authConf.Password, - AccessToken: authConf.RegistryToken, - RefreshToken: authConf.IdentityToken, - } - if cred != auth.EmptyCredential { - return cred, nil - } + stores = append(stores, store) } - return auth.EmptyCredential, nil + return credentials.NewStoreWithFallbacks(stores[0], stores[1:]...), nil } diff --git a/internal/credential/store_test.go b/internal/credential/store_test.go deleted file mode 100644 index 91e544075..000000000 --- a/internal/credential/store_test.go +++ /dev/null @@ -1,85 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package credential_test - -import ( - "context" - "math/rand" - "path/filepath" - "strconv" - "testing" - - "oras.land/oras-go/v2/registry/remote/auth" - "oras.land/oras/internal/credential" -) - -func TestStore_storeGetErase(t *testing.T) { - tempDir := t.TempDir() - confFileName := "test.json" - configPath := filepath.Join(tempDir, confFileName) - regName := "test" - cred := auth.Credential{ - Username: "username", - Password: "password", - } - - // store - store, err := credential.NewStore(configPath) - if err != nil { - t.Fatalf("unexpected error: %v", err.Error()) - } - err = store.Store(regName, cred) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - // get cred - got, err := store.Credential(context.Background(), regName) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != cred { - t.Fatalf("expect: %v, got: %v", cred, got) - } - - // erase - err = store.Erase(regName) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - got, err = store.Credential(context.Background(), regName) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != auth.EmptyCredential { - t.Fatalf("expect: %v, got: %v", auth.EmptyCredential, got) - } -} - -func TestStore_getEmptyCred(t *testing.T) { - store, err := credential.NewStore() - if err != nil { - t.Fatalf("Failed to create store with default config path: %v", err.Error()) - } - - got, err := store.Credential(context.Background(), strconv.Itoa(rand.Int())) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if got != auth.EmptyCredential { - t.Fatalf("expect: %v, got: %v", auth.EmptyCredential, got) - } -}