diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 219acfa0d..57322cab6 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -68,7 +68,7 @@ func main() { repo.Client = &auth.Client{ Client: retry.DefaultClient, Cache: auth.NewCache(), - Credential: auth.StaticCredential(repo.Reference.Registry, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(repo.Reference.Registry, credentials.Credential{ Username: "username", Password: "password", }), @@ -287,7 +287,7 @@ func main() { repo.Client = &auth.Client{ Client: retry.DefaultClient, Cache: auth.NewCache(), - Credential: auth.StaticCredential(repo.Reference.Registry, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(repo.Reference.Registry, credentials.Credential{ Username: "username", Password: "password", }), diff --git a/example_copy_test.go b/example_copy_test.go index ece69b4d9..0d54e1afe 100644 --- a/example_copy_test.go +++ b/example_copy_test.go @@ -37,6 +37,7 @@ import ( "github.com/oras-project/oras-go/v3/internal/spec" "github.com/oras-project/oras-go/v3/registry/remote" "github.com/oras-project/oras-go/v3/registry/remote/auth" + "github.com/oras-project/oras-go/v3/registry/remote/credentials" "github.com/oras-project/oras-go/v3/registry/remote/retry" ) @@ -451,7 +452,7 @@ func Example_extendedCopyArtifactAndReferrersToRepository() { repo.Client = &auth.Client{ Client: retry.DefaultClient, Cache: auth.NewCache(), - Credential: auth.StaticCredential(registry, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(registry, credentials.Credential{ Username: "username", Password: "password", }), diff --git a/example_test.go b/example_test.go index c7f0c9320..1e14ffc61 100644 --- a/example_test.go +++ b/example_test.go @@ -50,7 +50,7 @@ func Example_pullFilesFromRemoteRepository() { repo.Client = &auth.Client{ Client: retry.DefaultClient, Cache: auth.NewCache(), - Credential: auth.StaticCredential(reg, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(reg, credentials.Credential{ Username: "username", Password: "password", }), @@ -85,7 +85,7 @@ func Example_pullImageFromRemoteRepository() { repo.Client = &auth.Client{ Client: retry.DefaultClient, Cache: auth.NewCache(), - Credential: auth.StaticCredential(reg, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(reg, credentials.Credential{ Username: "username", Password: "password", }), @@ -125,9 +125,9 @@ func Example_pullImageUsingDockerCredentials() { panic(err) } repo.Client = &auth.Client{ - Client: retry.DefaultClient, - Cache: auth.NewCache(), - Credential: credentials.Credential(credStore), // Use the credentials store + Client: retry.DefaultClient, + Cache: auth.NewCache(), + CredentialFunc: remote.GetCredentialFunc(credStore), // Use the credentials store } // 2. Copy from the remote repository to the OCI layout store @@ -190,7 +190,7 @@ func Example_pushFilesToRemoteRepository() { repo.Client = &auth.Client{ Client: retry.DefaultClient, Cache: auth.NewCache(), - Credential: auth.StaticCredential(reg, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(reg, credentials.Credential{ Username: "username", Password: "password", }), @@ -218,7 +218,7 @@ func Example_attachBlobToRemoteRepository() { repo.Client = &auth.Client{ Client: retry.DefaultClient, Cache: auth.NewCache(), - Credential: auth.StaticCredential(registry, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(registry, credentials.Credential{ Username: "username", Password: "password", }), diff --git a/registry/remote/credentials/registry.go b/registry/remote/auth.go similarity index 83% rename from registry/remote/credentials/registry.go rename to registry/remote/auth.go index 920230e94..bd515008e 100644 --- a/registry/remote/credentials/registry.go +++ b/registry/remote/auth.go @@ -13,15 +13,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package credentials +package remote import ( "context" "errors" "fmt" - "github.com/oras-project/oras-go/v3/registry/remote" "github.com/oras-project/oras-go/v3/registry/remote/auth" + "github.com/oras-project/oras-go/v3/registry/remote/credentials" ) // ErrClientTypeUnsupported is thrown by Login() when the registry's client type @@ -32,7 +32,7 @@ var ErrClientTypeUnsupported = errors.New("client type not supported") // registry's client should be nil or of type *auth.Client. Login uses // a client local to the function and will not modify the original client of // the registry. -func Login(ctx context.Context, store Store, reg *remote.Registry, cred auth.Credential) error { +func Login(ctx context.Context, store credentials.Store, reg *Registry, cred credentials.Credential) error { // create a clone of the original registry for login purpose regClone := *reg // we use the original client if applicable, otherwise use a default client @@ -47,7 +47,7 @@ func Login(ctx context.Context, store Store, reg *remote.Registry, cred auth.Cre } regClone.Client = &authClient // update credentials with the client - authClient.Credential = auth.StaticCredential(reg.Reference.Registry, cred) + authClient.CredentialFunc = credentials.StaticCredentialFunc(reg.Reference.Registry, cred) // validate and store the credential if err := regClone.Ping(ctx); err != nil { return fmt.Errorf("failed to validate the credentials for %s: %w", regClone.Reference.Registry, err) @@ -60,7 +60,7 @@ func Login(ctx context.Context, store Store, reg *remote.Registry, cred auth.Cre } // Logout provides the logout functionality given the registry name. -func Logout(ctx context.Context, store Store, registryName string) error { +func Logout(ctx context.Context, store credentials.Store, registryName string) error { registryName = ServerAddressFromRegistry(registryName) if err := store.Delete(ctx, registryName); err != nil { return fmt.Errorf("failed to delete the credential for %s: %w", registryName, err) @@ -68,12 +68,12 @@ func Logout(ctx context.Context, store Store, registryName string) error { return nil } -// Credential returns a Credential() function that can be used by auth.Client. -func Credential(store Store) auth.CredentialFunc { - return func(ctx context.Context, hostport string) (auth.Credential, error) { +// GetCredentialFunc returns a GetCredentialFunc() function that can be used by auth.Client. +func GetCredentialFunc(store credentials.Store) credentials.CredentialFunc { + return func(ctx context.Context, hostport string) (credentials.Credential, error) { hostport = ServerAddressFromHostname(hostport) if hostport == "" { - return auth.EmptyCredential, nil + return credentials.EmptyCredential, nil } return store.Get(ctx, hostport) } diff --git a/registry/remote/auth/client.go b/registry/remote/auth/client.go index fb803b3f5..35826ebfe 100644 --- a/registry/remote/auth/client.go +++ b/registry/remote/auth/client.go @@ -27,6 +27,7 @@ import ( "net/url" "strings" + "github.com/oras-project/oras-go/v3/registry/remote/credentials" "github.com/oras-project/oras-go/v3/registry/remote/internal/errutil" "github.com/oras-project/oras-go/v3/registry/remote/retry" ) @@ -66,29 +67,6 @@ var maxResponseBytes int64 = 128 * 1024 // 128 KiB // See also ClientID. var defaultClientID = "oras-go" -// CredentialFunc represents a function that resolves the credential for the -// given registry (i.e. host:port). -// -// [EmptyCredential] is a valid return value and should not be considered as -// an error. -type CredentialFunc func(ctx context.Context, hostport string) (Credential, error) - -// StaticCredential specifies static credentials for the given host. -func StaticCredential(registry string, cred Credential) CredentialFunc { - if registry == "docker.io" { - // it is expected that traffic targeting "docker.io" will be redirected - // to "registry-1.docker.io" - // reference: https://github.com/moby/moby/blob/v24.0.0-beta.2/registry/config.go#L25-L48 - registry = "registry-1.docker.io" - } - return func(_ context.Context, hostport string) (Credential, error) { - if hostport == registry { - return cred, nil - } - return EmptyCredential, nil - } -} - // Client is an auth-decorated HTTP client. // Its zero value is a usable client that uses http.DefaultClient with no cache. type Client struct { @@ -105,12 +83,12 @@ type Client struct { // Header contains the custom headers to be added to each request. Header http.Header - // Credential specifies the function for resolving the credential for the + // CredentialFunc specifies the function for resolving the credential for the // given registry (i.e. host:port). // EmptyCredential is a valid return value and should not be considered as // an error. // If nil, the credential is always resolved to EmptyCredential. - Credential CredentialFunc + CredentialFunc credentials.CredentialFunc // Cache caches credentials for direct accessing the remote registry. // If nil, no cache is used. @@ -148,11 +126,11 @@ func (c *Client) send(req *http.Request) (*http.Response, error) { } // credential resolves the credential for the given registry. -func (c *Client) credential(ctx context.Context, reg string) (Credential, error) { - if c.Credential == nil { - return EmptyCredential, nil +func (c *Client) credential(ctx context.Context, reg string) (credentials.Credential, error) { + if c.CredentialFunc == nil { + return credentials.EmptyCredential, nil } - return c.Credential(ctx, reg) + return c.CredentialFunc(ctx, reg) } // cache resolves the cache. @@ -291,7 +269,7 @@ func (c *Client) fetchBasicAuth(ctx context.Context, registry string) (string, e if err != nil { return "", fmt.Errorf("failed to resolve credential: %w", err) } - if cred == EmptyCredential { + if cred == credentials.EmptyCredential { return "", ErrBasicCredentialNotFound } if cred.Username == "" || cred.Password == "" { @@ -310,7 +288,7 @@ func (c *Client) fetchBearerToken(ctx context.Context, registry, realm, service if cred.AccessToken != "" { return cred.AccessToken, nil } - if cred == EmptyCredential || (cred.RefreshToken == "" && !c.ForceAttemptOAuth2) { + if cred == credentials.EmptyCredential || (cred.RefreshToken == "" && !c.ForceAttemptOAuth2) { return c.fetchDistributionToken(ctx, realm, service, scopes, cred.Username, cred.Password) } return c.fetchOAuth2Token(ctx, realm, service, scopes, cred) @@ -370,7 +348,7 @@ func (c *Client) fetchDistributionToken(ctx context.Context, realm, service stri // fetchOAuth2Token fetches an OAuth2 access token. // Reference: https://distribution.github.io/distribution/spec/auth/oauth/ -func (c *Client) fetchOAuth2Token(ctx context.Context, realm, service string, scopes []string, cred Credential) (string, error) { +func (c *Client) fetchOAuth2Token(ctx context.Context, realm, service string, scopes []string, cred credentials.Credential) (string, error) { form := url.Values{} if cred.RefreshToken != "" { form.Set("grant_type", "refresh_token") diff --git a/registry/remote/auth/client_test.go b/registry/remote/auth/client_test.go index 6a9c1fe77..cb52b81cf 100644 --- a/registry/remote/auth/client_test.go +++ b/registry/remote/auth/client_test.go @@ -28,6 +28,7 @@ import ( "sync/atomic" "testing" + "github.com/oras-project/oras-go/v3/registry/remote/credentials" "github.com/oras-project/oras-go/v3/registry/remote/errcode" ) @@ -99,13 +100,13 @@ func TestClient_Do_Basic_Auth(t *testing.T) { } client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -180,13 +181,13 @@ func TestClient_Do_Basic_Auth_Cached(t *testing.T) { } client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -289,13 +290,13 @@ func TestClient_Do_Bearer_AccessToken(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ AccessToken: accessToken, }, nil }, @@ -376,13 +377,13 @@ func TestClient_Do_Bearer_AccessToken_Cached(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ AccessToken: accessToken, }, nil }, @@ -484,7 +485,7 @@ func TestClient_Do_Bearer_AccessToken_Cached_PerHost(t *testing.T) { } service1 = uri1.Host client1 := &Client{ - Credential: StaticCredential(uri1.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ AccessToken: accessToken1, }), Cache: NewCache(), @@ -519,7 +520,7 @@ func TestClient_Do_Bearer_AccessToken_Cached_PerHost(t *testing.T) { } service2 = uri2.Host client2 := &Client{ - Credential: StaticCredential(uri2.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ AccessToken: accessToken2, }), Cache: NewCache(), @@ -608,7 +609,7 @@ func TestClient_Do_Bearer_AccessToken_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client1.Credential = StaticCredential(uri1.Host, Credential{ + client1.CredentialFunc = credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ AccessToken: accessToken1, }) resp1, err = client1.Do(req1) @@ -630,7 +631,7 @@ func TestClient_Do_Bearer_AccessToken_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client2.Credential = StaticCredential(uri2.Host, Credential{ + client2.CredentialFunc = credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ AccessToken: accessToken2, }) resp2, err = client2.Do(req2) @@ -713,13 +714,13 @@ func TestClient_Do_Bearer_Auth(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -839,13 +840,13 @@ func TestClient_Do_Bearer_Auth_Cached(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -988,7 +989,7 @@ func TestClient_Do_Bearer_Auth_Cached_PerHost(t *testing.T) { } service1 = uri1.Host client1 := &Client{ - Credential: StaticCredential(uri1.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ Username: username1, Password: password1, }), @@ -1058,7 +1059,7 @@ func TestClient_Do_Bearer_Auth_Cached_PerHost(t *testing.T) { } service2 = uri2.Host client2 := &Client{ - Credential: StaticCredential(uri2.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ Username: username2, Password: password2, }), @@ -1164,7 +1165,7 @@ func TestClient_Do_Bearer_Auth_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client1.Credential = StaticCredential(uri1.Host, Credential{ + client1.CredentialFunc = credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ Username: username1, Password: password1, }) @@ -1193,7 +1194,7 @@ func TestClient_Do_Bearer_Auth_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client2.Credential = StaticCredential(uri2.Host, Credential{ + client2.CredentialFunc = credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ Username: username2, Password: password2, }) @@ -1300,13 +1301,13 @@ func TestClient_Do_Bearer_OAuth2_Password(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -1447,13 +1448,13 @@ func TestClient_Do_Bearer_OAuth2_Password_Cached(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -1617,7 +1618,7 @@ func TestClient_Do_Bearer_OAuth2_Password_Cached_PerHost(t *testing.T) { } service1 = uri1.Host client1 := &Client{ - Credential: StaticCredential(uri1.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ Username: username1, Password: password1, }), @@ -1707,7 +1708,7 @@ func TestClient_Do_Bearer_OAuth2_Password_Cached_PerHost(t *testing.T) { } service2 = uri2.Host client2 := &Client{ - Credential: StaticCredential(uri2.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ Username: username2, Password: password2, }), @@ -1812,7 +1813,7 @@ func TestClient_Do_Bearer_OAuth2_Password_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client1.Credential = StaticCredential(uri1.Host, Credential{ + client1.CredentialFunc = credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ Username: username1, Password: password1, }) @@ -1840,7 +1841,7 @@ func TestClient_Do_Bearer_OAuth2_Password_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client2.Credential = StaticCredential(uri2.Host, Credential{ + client2.CredentialFunc = credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ Username: username2, Password: password2, }) @@ -1941,13 +1942,13 @@ func TestClient_Do_Bearer_OAuth2_RefreshToken(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ RefreshToken: refreshToken, }, nil }, @@ -2079,13 +2080,13 @@ func TestClient_Do_Bearer_OAuth2_RefreshToken_Cached(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ RefreshToken: refreshToken, }, nil }, @@ -2240,7 +2241,7 @@ func TestClient_Do_Bearer_OAuth2_RefreshToken_Cached_PerHost(t *testing.T) { } service1 = uri1.Host client1 := &Client{ - Credential: StaticCredential(uri1.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ RefreshToken: refreshToken1, }), Cache: NewCache(), @@ -2323,7 +2324,7 @@ func TestClient_Do_Bearer_OAuth2_RefreshToken_Cached_PerHost(t *testing.T) { } service2 = uri2.Host client2 := &Client{ - Credential: StaticCredential(uri2.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ RefreshToken: refreshToken2, }), Cache: NewCache(), @@ -2426,7 +2427,7 @@ func TestClient_Do_Bearer_OAuth2_RefreshToken_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client1.Credential = StaticCredential(uri1.Host, Credential{ + client1.CredentialFunc = credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ RefreshToken: refreshToken1, }) resp1, err = client1.Do(req1) @@ -2452,7 +2453,7 @@ func TestClient_Do_Bearer_OAuth2_RefreshToken_Cached_PerHost(t *testing.T) { if err != nil { t.Fatalf("failed to create test request: %v", err) } - client2.Credential = StaticCredential(uri2.Host, Credential{ + client2.CredentialFunc = credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ RefreshToken: refreshToken2, }) resp2, err = client2.Do(req2) @@ -2552,13 +2553,13 @@ func TestClient_Do_Token_Expire(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ RefreshToken: refreshToken, }, nil }, @@ -2690,7 +2691,7 @@ func TestClient_Do_Token_Expire_PerHost(t *testing.T) { } service1 = uri1.Host client1 := &Client{ - Credential: StaticCredential(uri1.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ RefreshToken: refreshToken1, }), Cache: NewCache(), @@ -2772,7 +2773,7 @@ func TestClient_Do_Token_Expire_PerHost(t *testing.T) { } service2 = uri2.Host client2 := &Client{ - Credential: StaticCredential(uri2.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ RefreshToken: refreshToken2, }), Cache: NewCache(), @@ -2958,13 +2959,13 @@ func TestClient_Do_Scope_Hint_Mismatch(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -3109,7 +3110,7 @@ func TestClient_Do_Scope_Hint_Mismatch_PerHost(t *testing.T) { } service1 = uri1.Host client1 := &Client{ - Credential: StaticCredential(uri1.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri1.Host, credentials.Credential{ Username: username1, Password: password1, }), @@ -3203,7 +3204,7 @@ func TestClient_Do_Scope_Hint_Mismatch_PerHost(t *testing.T) { } service2 = uri2.Host client2 := &Client{ - Credential: StaticCredential(uri2.Host, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(uri2.Host, credentials.Credential{ Username: username2, Password: password2, }), @@ -3337,13 +3338,13 @@ func TestClient_Do_Invalid_Credential_Basic(t *testing.T) { } client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: "bad credential", }, nil @@ -3422,13 +3423,13 @@ func TestClient_Do_Invalid_Credential_Bearer(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: "bad credential", }, nil @@ -3606,13 +3607,13 @@ func TestClient_Do_Scheme_Change(t *testing.T) { service = uri.Host client := &Client{ - Credential: func(ctx context.Context, reg string) (Credential, error) { + CredentialFunc: func(ctx context.Context, reg string) (credentials.Credential, error) { if reg != uri.Host { err := fmt.Errorf("registry mismatch: got %v, want %v", reg, uri.Host) t.Error(err) - return EmptyCredential, err + return credentials.EmptyCredential, err } - return Credential{ + return credentials.Credential{ Username: username, Password: password, }, nil @@ -3671,18 +3672,18 @@ func TestStaticCredential(t *testing.T) { name string registry string target string - cred Credential - want Credential + cred credentials.Credential + want credentials.Credential }{ { name: "Matched credential for regular registry", registry: "registry.example.com", target: "registry.example.com", - cred: Credential{ + cred: credentials.Credential{ Username: "username", Password: "password", }, - want: Credential{ + want: credentials.Credential{ Username: "username", Password: "password", }, @@ -3691,11 +3692,11 @@ func TestStaticCredential(t *testing.T) { name: "Matched credential for docker.io", registry: "docker.io", target: "registry-1.docker.io", - cred: Credential{ + cred: credentials.Credential{ Username: "username", Password: "password", }, - want: Credential{ + want: credentials.Credential{ Username: "username", Password: "password", }, @@ -3704,35 +3705,35 @@ func TestStaticCredential(t *testing.T) { name: "Mismatched credential for regular registry", registry: "registry.example.com", target: "whatever.example.com", - cred: Credential{ + cred: credentials.Credential{ Username: "username", Password: "password", }, - want: EmptyCredential, + want: credentials.EmptyCredential, }, { name: "Mismatched credential for docker.io", registry: "docker.io", target: "whatever.docker.io", - cred: Credential{ + cred: credentials.Credential{ Username: "username", Password: "password", }, - want: EmptyCredential, + want: credentials.EmptyCredential, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := &Client{ - Credential: StaticCredential(tt.registry, tt.cred), + CredentialFunc: credentials.StaticCredentialFunc(tt.registry, tt.cred), } ctx := context.Background() - got, err := client.Credential(ctx, tt.target) + got, err := client.CredentialFunc(ctx, tt.target) if err != nil { - t.Fatal("Client.Credential() error =", err) + t.Fatal("Client.CredentialFunc() error =", err) } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Client.Credential() = %v, want %v", got, tt.want) + t.Errorf("Client.CredentialFunc() = %v, want %v", got, tt.want) } }) } @@ -3769,7 +3770,7 @@ func TestClient_StaticCredential_basicAuth(t *testing.T) { // create a test client with the correct credentials clientValid := &Client{ - Credential: StaticCredential(hostAddress, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(hostAddress, credentials.Credential{ Username: testUsername, Password: testPassword, }), @@ -3788,7 +3789,7 @@ func TestClient_StaticCredential_basicAuth(t *testing.T) { // create a test client with incorrect credentials clientInvalid := &Client{ - Credential: StaticCredential(hostAddress, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(hostAddress, credentials.Credential{ Username: "foo", Password: "bar", }), @@ -3841,7 +3842,7 @@ func TestClient_StaticCredential_withAccessToken(t *testing.T) { // create a test client with the correct credentials clientValid := &Client{ - Credential: StaticCredential(hostAddress, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(hostAddress, credentials.Credential{ AccessToken: testAccessToken, }), } @@ -3859,7 +3860,7 @@ func TestClient_StaticCredential_withAccessToken(t *testing.T) { // create a test client with incorrect credentials clientInvalid := &Client{ - Credential: StaticCredential(hostAddress, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(hostAddress, credentials.Credential{ AccessToken: "foo", }), } @@ -3935,7 +3936,7 @@ func TestClient_StaticCredential_withRefreshToken(t *testing.T) { // create a test client with the correct credentials clientValid := &Client{ - Credential: StaticCredential(hostAddress, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(hostAddress, credentials.Credential{ RefreshToken: testRefreshToken, }), } @@ -3953,7 +3954,7 @@ func TestClient_StaticCredential_withRefreshToken(t *testing.T) { // create a test client with incorrect credentials clientInvalid := &Client{ - Credential: StaticCredential(hostAddress, Credential{ + CredentialFunc: credentials.StaticCredentialFunc(hostAddress, credentials.Credential{ RefreshToken: "bar", }), } @@ -3967,8 +3968,8 @@ func TestClient_StaticCredential_withRefreshToken(t *testing.T) { func TestClient_fetchBasicAuth(t *testing.T) { c := &Client{ - Credential: func(ctx context.Context, registry string) (Credential, error) { - return EmptyCredential, nil + CredentialFunc: func(ctx context.Context, registry string) (credentials.Credential, error) { + return credentials.EmptyCredential, nil }, } _, err := c.fetchBasicAuth(context.Background(), "") diff --git a/registry/remote/auth/credential.go b/registry/remote/auth/credential.go deleted file mode 100644 index 044bcaecd..000000000 --- a/registry/remote/auth/credential.go +++ /dev/null @@ -1,40 +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 auth - -// EmptyCredential represents an empty credential. -var EmptyCredential Credential - -// Credential contains authentication credentials used to access remote -// registries. -type Credential struct { - // Username is the name of the user for the remote registry. - Username string - - // Password is the secret associated with the username. - Password string - - // RefreshToken is a bearer token to be sent to the authorization service - // for fetching access tokens. - // A refresh token is often referred as an identity token. - // Reference: https://distribution.github.io/distribution/spec/auth/oauth/ - RefreshToken string - - // AccessToken is a bearer token to be sent to the registry. - // An access token is often referred as a registry token. - // Reference: https://distribution.github.io/distribution/spec/auth/token/ - AccessToken string -} diff --git a/registry/remote/auth/example_test.go b/registry/remote/auth/example_test.go index 918681d5e..5ad5f12c6 100644 --- a/registry/remote/auth/example_test.go +++ b/registry/remote/auth/example_test.go @@ -12,7 +12,7 @@ limitations under the License. */ // Package auth_test includes the testable examples for the http client. -package auth_test +package auth import ( "context" @@ -26,7 +26,7 @@ import ( "testing" . "github.com/oras-project/oras-go/v3/registry/internal/doc" - "github.com/oras-project/oras-go/v3/registry/remote/auth" + "github.com/oras-project/oras-go/v3/registry/remote/credentials" ) const ( @@ -141,7 +141,7 @@ func TestMain(m *testing.M) { // ExampleClient_Do_minimalClient gives an example of a minimal working client. func ExampleClient_Do_minimalClient() { - var client auth.Client + var client Client // targetURL can be any URL. For example, https://registry.wabbit-networks.io/v2/ req, err := http.NewRequest(http.MethodGet, targetURL, nil) if err != nil { @@ -159,9 +159,9 @@ func ExampleClient_Do_minimalClient() { // ExampleClient_Do_basicAuth gives an example of using client with credentials. func ExampleClient_Do_basicAuth() { - client := &auth.Client{ + client := &Client{ // expectedHostAddress is of form ipaddr:port - Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(expectedHostAddress, credentials.Credential{ Username: username, Password: password, }), @@ -184,16 +184,16 @@ func ExampleClient_Do_basicAuth() { // ExampleClient_Do_clientConfigurations shows the client configurations available, // including using cache, setting user agent and configuring OAuth2. func ExampleClient_Do_clientConfigurations() { - client := &auth.Client{ + client := &Client{ // expectedHostAddress is of form ipaddr:port - Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(expectedHostAddress, credentials.Credential{ Username: username, Password: password, }), // ForceAttemptOAuth2 controls whether to follow OAuth2 with password grant. ForceAttemptOAuth2: true, // Cache caches credentials for accessing the remote registry. - Cache: auth.NewCache(), + Cache: NewCache(), } // SetUserAgent sets the user agent for all out-going requests. client.SetUserAgent("example user agent") @@ -205,7 +205,7 @@ func ExampleClient_Do_clientConfigurations() { "repository:src:pull", } // WithScopes returns a context with scopes added. - ctx := auth.WithScopes(context.Background(), scopes...) + ctx := WithScopes(context.Background(), scopes...) // clientConfigTargetURL can be any URL. For example, https://registry.wabbit-networks.io/v2/ req, err := http.NewRequestWithContext(ctx, http.MethodGet, clientConfigTargetURL, nil) @@ -224,9 +224,9 @@ func ExampleClient_Do_clientConfigurations() { // ExampleClient_Do_withAccessToken gives an example of using client with an access token. func ExampleClient_Do_withAccessToken() { - client := &auth.Client{ + client := &Client{ // expectedHostAddress is of form ipaddr:port - Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(expectedHostAddress, credentials.Credential{ AccessToken: accessToken, }), } @@ -247,9 +247,9 @@ func ExampleClient_Do_withAccessToken() { // ExampleClient_Do_withRefreshToken gives an example of using client with a refresh token. func ExampleClient_Do_withRefreshToken() { - client := &auth.Client{ + client := &Client{ // expectedHostAddress is of form ipaddr:port - Credential: auth.StaticCredential(expectedHostAddress, auth.Credential{ + CredentialFunc: credentials.StaticCredentialFunc(expectedHostAddress, credentials.Credential{ RefreshToken: refreshToken, }), } diff --git a/registry/remote/credentials/registry_test.go b/registry/remote/auth_test.go similarity index 79% rename from registry/remote/credentials/registry_test.go rename to registry/remote/auth_test.go index df5a7ca37..97ebfb0f1 100644 --- a/registry/remote/credentials/registry_test.go +++ b/registry/remote/auth_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package credentials +package remote import ( "context" @@ -25,22 +25,25 @@ import ( "reflect" "testing" - "github.com/oras-project/oras-go/v3/registry/remote" "github.com/oras-project/oras-go/v3/registry/remote/auth" + "github.com/oras-project/oras-go/v3/registry/remote/credentials" ) +var testUsername = "username" +var testPassword = "password" + // testStore implements the Store interface, used for testing purpose. type testStore struct { - storage map[string]auth.Credential + storage map[string]credentials.Credential } -func (t *testStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { +func (t *testStore) Get(ctx context.Context, serverAddress string) (credentials.Credential, error) { return t.storage[serverAddress], nil } -func (t *testStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { +func (t *testStore) Put(ctx context.Context, serverAddress string, cred credentials.Credential) error { if len(t.storage) == 0 { - t.storage = make(map[string]auth.Credential) + t.storage = make(map[string]credentials.Credential) } t.storage[serverAddress] = cred return nil @@ -63,7 +66,7 @@ func TestLogin(t *testing.T) { })) defer ts.Close() uri, _ := url.Parse(ts.URL) - reg, err := remote.NewRegistry(uri.Host) + reg, err := NewRegistry(uri.Host) if err != nil { t.Fatalf("cannot create test registry: %v", err) } @@ -73,26 +76,26 @@ func TestLogin(t *testing.T) { tests := []struct { name string ctx context.Context - registry *remote.Registry - cred auth.Credential + registry *Registry + cred credentials.Credential wantErr bool }{ { name: "login succeeds", ctx: context.Background(), - cred: auth.Credential{Username: testUsername, Password: testPassword}, + cred: credentials.Credential{Username: testUsername, Password: testPassword}, wantErr: false, }, { name: "login fails (incorrect password)", ctx: context.Background(), - cred: auth.Credential{Username: testUsername, Password: "whatever"}, + cred: credentials.Credential{Username: testUsername, Password: "whatever"}, wantErr: true, }, { name: "login fails (nil context makes remote.Ping fails)", ctx: nil, - cred: auth.Credential{Username: testUsername, Password: testPassword}, + cred: credentials.Credential{Username: testUsername, Password: testPassword}, wantErr: true, }, } @@ -116,7 +119,7 @@ func TestLogin(t *testing.T) { func TestLogin_unsupportedClient(t *testing.T) { var testClient http.Client - reg, err := remote.NewRegistry("whatever") + reg, err := NewRegistry("whatever") if err != nil { t.Fatalf("cannot create test registry: %v", err) } @@ -125,7 +128,7 @@ func TestLogin_unsupportedClient(t *testing.T) { ctx := context.Background() s := &testStore{} - cred := auth.EmptyCredential + cred := credentials.EmptyCredential err = Login(ctx, s, reg, cred) if wantErr := ErrClientTypeUnsupported; !errors.Is(err, wantErr) { t.Errorf("Login() error = %v, wantErr %v", err, wantErr) @@ -135,14 +138,14 @@ func TestLogin_unsupportedClient(t *testing.T) { func TestLogout(t *testing.T) { // create a test store s := &testStore{} - s.storage = map[string]auth.Credential{ + s.storage = map[string]credentials.Credential{ "localhost:2333": {Username: "test_user", Password: "test_word"}, "https://index.docker.io/v1/": {Username: "user", Password: "word"}, } tests := []struct { name string ctx context.Context - store Store + store credentials.Store registryName string wantErr bool }{ @@ -164,7 +167,7 @@ func TestLogout(t *testing.T) { if err := Logout(tt.ctx, s, tt.registryName); (err != nil) != tt.wantErr { t.Fatalf("Logout() error = %v, wantErr %v", err, tt.wantErr) } - if s.storage[tt.registryName] != auth.EmptyCredential { + if s.storage[tt.registryName] != credentials.EmptyCredential { t.Error("Credentials are not deleted") } }) @@ -205,47 +208,47 @@ func Test_mapHostname(t *testing.T) { func TestCredential(t *testing.T) { // create a test store s := &testStore{} - s.storage = map[string]auth.Credential{ + s.storage = map[string]credentials.Credential{ "localhost:2333": {Username: "test_user", Password: "test_word"}, "https://index.docker.io/v1/": {Username: "user", Password: "word"}, } - // create a test client using Credential + // create a test client using GetCredentialFunc testClient := &auth.Client{} - testClient.Credential = Credential(s) + testClient.CredentialFunc = GetCredentialFunc(s) tests := []struct { name string registry string - wantCredential auth.Credential + wantCredential credentials.Credential }{ { name: "get credentials for localhost:2333", registry: "localhost:2333", - wantCredential: auth.Credential{Username: "test_user", Password: "test_word"}, + wantCredential: credentials.Credential{Username: "test_user", Password: "test_word"}, }, { name: "get credentials for registry-1.docker.io", registry: "registry-1.docker.io", - wantCredential: auth.Credential{Username: "user", Password: "word"}, + wantCredential: credentials.Credential{Username: "user", Password: "word"}, }, { name: "get credentials for a registry not stored", registry: "localhost:6666", - wantCredential: auth.EmptyCredential, + wantCredential: credentials.EmptyCredential, }, { name: "get credentials for an empty string", registry: "", - wantCredential: auth.EmptyCredential, + wantCredential: credentials.EmptyCredential, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := testClient.Credential(context.Background(), tt.registry) + got, err := testClient.CredentialFunc(context.Background(), tt.registry) if err != nil { t.Errorf("could not get credential: %v", err) } if !reflect.DeepEqual(got, tt.wantCredential) { - t.Errorf("Credential() = %v, want %v", got, tt.wantCredential) + t.Errorf("GetCredentialFunc() = %v, want %v", got, tt.wantCredential) } }) } diff --git a/registry/remote/credentials/credential.go b/registry/remote/credentials/credential.go new file mode 100644 index 000000000..654d0eaed --- /dev/null +++ b/registry/remote/credentials/credential.go @@ -0,0 +1,97 @@ +/* +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 credentials + +import ( + "context" + "fmt" + + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration" +) + +// Credential contains authentication credentials used to access remote +// registries. +type Credential struct { + // Username is the name of the user for the remote registry. + Username string + + // Password is the secret associated with the username. + Password string + + // RefreshToken is a bearer token to be sent to the authorization service + // for fetching access tokens. + // A refresh token is often referred as an identity token. + // Reference: https://distribution.github.io/distribution/spec/auth/oauth/ + RefreshToken string + + // AccessToken is a bearer token to be sent to the registry. + // An access token is often referred as a registry token. + // Reference: https://distribution.github.io/distribution/spec/auth/token/ + AccessToken string +} + +// EmptyCredential represents an empty credential. +var EmptyCredential Credential + +// IsEmpty returns true if the credential has no values +func (c *Credential) IsEmpty() bool { + if *c == EmptyCredential { + return true + } + return false +} + +// CredentialFunc represents a function that resolves the credential for the +// given registry (i.e. host:port). +// +// [EmptyCredential] is a valid return value and should not be considered as +// an error. +type CredentialFunc func(ctx context.Context, hostport string) (Credential, error) + +// StaticCredentialFunc specifies static credentials for the given host. +func StaticCredentialFunc(registry string, cred Credential) CredentialFunc { + if registry == "docker.io" { + // it is expected that traffic targeting "docker.io" will be redirected + // to "registry-1.docker.io" + // reference: https://github.com/moby/moby/blob/v24.0.0-beta.2/registry/config.go#L25-L48 + registry = "registry-1.docker.io" + } + return func(_ context.Context, hostport string) (Credential, error) { + if hostport == registry { + return cred, nil + } + return EmptyCredential, nil + } +} + +// NewCredential creates a Credential based on authCfg. +func NewCredential(ac configuration.AuthConfig) (Credential, error) { + cred := Credential{ + Username: ac.Username, + Password: ac.Password, + RefreshToken: ac.IdentityToken, + AccessToken: ac.RegistryToken, + } + if ac.Auth != "" { + var err error + // override username and password + cred.Username, cred.Password, err = ac.DecodeAuth() + if err != nil { + return EmptyCredential, fmt.Errorf("failed to decode auth field: %w: %v", configuration.ErrInvalidConfigFormat, err) + } + } + return cred, nil +} diff --git a/registry/remote/credentials/credential_test.go b/registry/remote/credentials/credential_test.go new file mode 100644 index 000000000..73692d4d1 --- /dev/null +++ b/registry/remote/credentials/credential_test.go @@ -0,0 +1,373 @@ +/* +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 credentials + +import ( + "context" + "testing" + + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration" +) + +func TestStaticCredential_BasicAuth(t *testing.T) { + ctx := context.Background() + registry := "example.com:5000" + expectedCred := Credential{ + Username: "testuser", + Password: "testpass", + } + + credFunc := StaticCredentialFunc(registry, expectedCred) + + // Test matching registry + cred, err := credFunc(ctx, registry) + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != expectedCred { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, expectedCred) + } + + // Test non-matching registry + cred, err = credFunc(ctx, "different.com:5000") + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != EmptyCredential { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, EmptyCredential) + } +} + +func TestStaticCredential_BearerToken(t *testing.T) { + ctx := context.Background() + registry := "registry.example.com" + expectedCred := Credential{ + RefreshToken: "refresh_token_123", + AccessToken: "access_token_456", + } + + credFunc := StaticCredentialFunc(registry, expectedCred) + + cred, err := credFunc(ctx, registry) + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != expectedCred { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, expectedCred) + } +} + +func TestStaticCredential_DockerIORedirect(t *testing.T) { + ctx := context.Background() + expectedCred := Credential{ + Username: "dockeruser", + Password: "dockerpass", + } + + // Create credential function for docker.io + credFunc := StaticCredentialFunc("docker.io", expectedCred) + + // Test that docker.io is redirected to registry-1.docker.io + cred, err := credFunc(ctx, "registry-1.docker.io") + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != expectedCred { + t.Errorf("StaticCredentialFunc() for registry-1.docker.io = %+v, want %+v", cred, expectedCred) + } + + // Test that docker.io itself doesn't match (because it gets redirected) + cred, err = credFunc(ctx, "docker.io") + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != EmptyCredential { + t.Errorf("StaticCredentialFunc() for docker.io = %+v, want %+v", cred, EmptyCredential) + } +} + +func TestStaticCredential_EmptyCredential(t *testing.T) { + ctx := context.Background() + registry := "test.registry.io" + + credFunc := StaticCredentialFunc(registry, EmptyCredential) + + cred, err := credFunc(ctx, registry) + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != EmptyCredential { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, EmptyCredential) + } +} + +func TestStaticCredential_MixedCredential(t *testing.T) { + ctx := context.Background() + registry := "mixed.example.com" + expectedCred := Credential{ + Username: "mixeduser", + RefreshToken: "mixed_refresh", + } + + credFunc := StaticCredentialFunc(registry, expectedCred) + + cred, err := credFunc(ctx, registry) + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != expectedCred { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, expectedCred) + } +} + +func TestStaticCredential_CaseSensitive(t *testing.T) { + ctx := context.Background() + registry := "Example.Com:5000" + expectedCred := Credential{ + Username: "testuser", + Password: "testpass", + } + + credFunc := StaticCredentialFunc(registry, expectedCred) + + // Test exact match (case-sensitive) + cred, err := credFunc(ctx, registry) + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != expectedCred { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, expectedCred) + } + + // Test different case should not match + cred, err = credFunc(ctx, "example.com:5000") + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + if cred != EmptyCredential { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, EmptyCredential) + } +} + +func TestStaticCredential_WithPort(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + registry string + hostport string + shouldMatch bool + }{ + { + name: "exact match with port", + registry: "example.com:5000", + hostport: "example.com:5000", + shouldMatch: true, + }, + { + name: "different port", + registry: "example.com:5000", + hostport: "example.com:443", + shouldMatch: false, + }, + { + name: "missing port in hostport", + registry: "example.com:5000", + hostport: "example.com", + shouldMatch: false, + }, + { + name: "missing port in registry", + registry: "example.com", + hostport: "example.com:443", + shouldMatch: false, + }, + } + + expectedCred := Credential{ + Username: "testuser", + Password: "testpass", + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + credFunc := StaticCredentialFunc(tt.registry, expectedCred) + cred, err := credFunc(ctx, tt.hostport) + if err != nil { + t.Fatalf("StaticCredentialFunc() error = %v, want nil", err) + } + + if tt.shouldMatch { + if cred != expectedCred { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, expectedCred) + } + } else { + if cred != EmptyCredential { + t.Errorf("StaticCredentialFunc() = %+v, want %+v", cred, EmptyCredential) + } + } + }) + } +} + +func TestCredentialFunc_Interface(t *testing.T) { + // Test that CredentialFunc is a valid function type + var credFunc CredentialFunc = func(ctx context.Context, hostport string) (Credential, error) { + return EmptyCredential, nil + } + + ctx := context.Background() + cred, err := credFunc(ctx, "test.example.com") + if err != nil { + t.Fatalf("CredentialFunc() error = %v, want nil", err) + } + if cred != EmptyCredential { + t.Errorf("CredentialFunc() = %+v, want %+v", cred, EmptyCredential) + } +} + +func TestCredential(t *testing.T) { + tests := []struct { + name string + authCfg configuration.AuthConfig + want Credential + wantErr bool + }{ + { + name: "Username and password", + authCfg: configuration.AuthConfig{ + Username: "username", + Password: "password", + }, + want: Credential{ + Username: "username", + Password: "password", + }, + }, + { + name: "Identity token", + authCfg: configuration.AuthConfig{ + IdentityToken: "identity_token", + }, + want: Credential{ + RefreshToken: "identity_token", + }, + }, + { + name: "Registry token", + authCfg: configuration.AuthConfig{ + RegistryToken: "registry_token", + }, + want: Credential{ + AccessToken: "registry_token", + }, + }, + { + name: "All fields", + authCfg: configuration.AuthConfig{ + Username: "username", + Password: "password", + IdentityToken: "identity_token", + RegistryToken: "registry_token", + }, + want: Credential{ + Username: "username", + Password: "password", + RefreshToken: "identity_token", + AccessToken: "registry_token", + }, + }, + { + name: "Empty auth config", + authCfg: configuration.AuthConfig{}, + want: Credential{}, + }, + { + name: "Auth field overrides username and password", + authCfg: configuration.AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", // username:password + Username: "old_username", + Password: "old_password", + }, + want: Credential{ + Username: "username", + Password: "password", + }, + }, + { + name: "Auth field with identity and registry tokens", + authCfg: configuration.AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", // username:password + IdentityToken: "identity_token", + RegistryToken: "registry_token", + }, + want: Credential{ + Username: "username", + Password: "password", + RefreshToken: "identity_token", + AccessToken: "registry_token", + }, + }, + { + name: "Invalid auth field", + authCfg: configuration.AuthConfig{ + Auth: "invalid_base64!@#", + }, + want: EmptyCredential, + wantErr: true, + }, + { + name: "Auth field bad format", + authCfg: configuration.AuthConfig{ + Auth: "d2hhdGV2ZXI=", // whatever (no colon) + }, + want: EmptyCredential, + wantErr: true, + }, + { + name: "Auth field username only", + authCfg: configuration.AuthConfig{ + Auth: "dXNlcm5hbWU6", // username: + }, + want: Credential{ + Username: "username", + Password: "", + }, + }, + { + name: "Auth field password only", + authCfg: configuration.AuthConfig{ + Auth: "OnBhc3N3b3Jk", // :password + }, + want: Credential{ + Username: "", + Password: "password", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewCredential(tt.authCfg) + if (err != nil) != tt.wantErr { + t.Errorf("Credential() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Credential() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/registry/remote/credentials/example_test.go b/registry/remote/credentials/example_test.go index 824f1fd49..85696265b 100644 --- a/registry/remote/credentials/example_test.go +++ b/registry/remote/credentials/example_test.go @@ -18,9 +18,14 @@ import ( "fmt" "net/http" + . "github.com/oras-project/oras-go/v3/registry/internal/doc" "github.com/oras-project/oras-go/v3/registry/remote" "github.com/oras-project/oras-go/v3/registry/remote/auth" - credentials "github.com/oras-project/oras-go/v3/registry/remote/credentials" + "github.com/oras-project/oras-go/v3/registry/remote/credentials" +) + +const ( + _ = ExampleUnplayable ) func ExampleNewNativeStore() { @@ -28,7 +33,7 @@ func ExampleNewNativeStore() { ctx := context.Background() // save credentials into the store - err := ns.Put(ctx, "localhost:5000", auth.Credential{ + err := ns.Put(ctx, "localhost:5000", credentials.Credential{ Username: "username-example", Password: "password-example", }) @@ -58,7 +63,7 @@ func ExampleNewFileStore() { ctx := context.Background() // save credentials into the store - err = fs.Put(ctx, "localhost:5000", auth.Credential{ + err = fs.Put(ctx, "localhost:5000", credentials.Credential{ Username: "username-example", Password: "password-example", }) @@ -96,7 +101,7 @@ func ExampleNewStore() { ctx := context.Background() // save credentials into the store - err = store.Put(ctx, "localhost:5000", auth.Credential{ + err = store.Put(ctx, "localhost:5000", credentials.Credential{ Username: "username-example", Password: "password-example", }) @@ -128,7 +133,7 @@ func ExampleNewStoreFromDocker() { ctx := context.Background() // save credentials into the store - err = ds.Put(ctx, "localhost:5000", auth.Credential{ + err = ds.Put(ctx, "localhost:5000", credentials.Credential{ Username: "username-example", Password: "password-example", }) @@ -162,7 +167,7 @@ func ExampleNewStoreWithFallbacks_configAsPrimaryStoreDockerAsFallback() { ctx := context.Background() // save credentials into the store - err = sf.Put(ctx, "localhost:5000", auth.Credential{ + err = sf.Put(ctx, "localhost:5000", credentials.Credential{ Username: "username-example", Password: "password-example", }) @@ -195,11 +200,11 @@ func ExampleLogin() { if err != nil { panic(err) } - cred := auth.Credential{ + cred := credentials.Credential{ Username: "username-example", Password: "password-example", } - err = credentials.Login(context.Background(), store, registry, cred) + err = remote.Login(context.Background(), store, registry, cred) if err != nil { panic(err) } @@ -211,7 +216,7 @@ func ExampleLogout() { if err != nil { panic(err) } - err = credentials.Logout(context.Background(), store, "localhost:5000") + err = remote.Logout(context.Background(), store, "localhost:5000") if err != nil { panic(err) } @@ -225,7 +230,7 @@ func ExampleCredential() { } client := auth.DefaultClient - client.Credential = credentials.Credential(store) + client.CredentialFunc = remote.GetCredentialFunc(store) request, err := http.NewRequest(http.MethodGet, "localhost:5000", nil) if err != nil { diff --git a/registry/remote/credentials/file_store.go b/registry/remote/credentials/file_store.go index d2a6e852b..6659f23fb 100644 --- a/registry/remote/credentials/file_store.go +++ b/registry/remote/credentials/file_store.go @@ -21,8 +21,7 @@ import ( "fmt" "strings" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/config" + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration" ) // FileStore implements a credentials store using the docker configuration file @@ -34,7 +33,7 @@ type FileStore struct { // If DisablePut is set to true, Put() will return ErrPlaintextPutDisabled. DisablePut bool - config *config.Config + config *configuration.Config } var ( @@ -50,7 +49,7 @@ var ( // // Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties func NewFileStore(configPath string) (*FileStore, error) { - cfg, err := config.Load(configPath) + cfg, err := configuration.Load(configPath) if err != nil { return nil, err } @@ -58,18 +57,22 @@ func NewFileStore(configPath string) (*FileStore, error) { } // newFileStore creates a file credentials store based on the given config instance. -func newFileStore(cfg *config.Config) *FileStore { +func newFileStore(cfg *configuration.Config) *FileStore { return &FileStore{config: cfg} } // Get retrieves credentials from the store for the given server address. -func (fs *FileStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) { - return fs.config.GetCredential(serverAddress) +func (fs *FileStore) Get(_ context.Context, serverAddress string) (Credential, error) { + authCfg, err := fs.config.GetAuthConfig(serverAddress) + if err != nil { + return EmptyCredential, err + } + return NewCredential(authCfg) } // Put saves credentials into the store for the given server address. // Returns ErrPlaintextPutDisabled if fs.DisablePut is set to true. -func (fs *FileStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error { +func (fs *FileStore) Put(_ context.Context, serverAddress string, cred Credential) error { if fs.DisablePut { return ErrPlaintextPutDisabled } @@ -77,16 +80,23 @@ func (fs *FileStore) Put(_ context.Context, serverAddress string, cred auth.Cred return err } - return fs.config.PutCredential(serverAddress, cred) + authCfg := configuration.NewAuthConfig( + cred.Username, + cred.Password, + cred.RefreshToken, + cred.AccessToken, + ) + + return fs.config.PutAuthConfig(serverAddress, authCfg) } // Delete removes credentials from the store for the given server address. func (fs *FileStore) Delete(_ context.Context, serverAddress string) error { - return fs.config.DeleteCredential(serverAddress) + return fs.config.DeleteAuthConfig(serverAddress) } // validateCredentialFormat validates the format of cred. -func validateCredentialFormat(cred auth.Credential) error { +func validateCredentialFormat(cred Credential) error { if strings.ContainsRune(cred.Username, ':') { // Username and password will be encoded in the base64(username:password) // format in the file. The decoded result will be wrong if username diff --git a/registry/remote/credentials/file_store_test.go b/registry/remote/credentials/file_store_test.go index 47ba8f0a9..c957430f6 100644 --- a/registry/remote/credentials/file_store_test.go +++ b/registry/remote/credentials/file_store_test.go @@ -24,8 +24,7 @@ import ( "reflect" "testing" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/config/configtest" + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration/configtest" ) func TestNewFileStore_badPath(t *testing.T) { @@ -66,17 +65,17 @@ func TestNewFileStore_badFormat(t *testing.T) { }{ { name: "Bad JSON format", - configPath: "testdata/bad_config", + configPath: "../internal/configuration/testdata/bad_config", wantErr: true, }, { name: "Invalid auths format", - configPath: "testdata/invalid_auths_config.json", + configPath: "../internal/configuration/testdata/invalid_auths_config.json", wantErr: true, }, { name: "No auths field", - configPath: "testdata/no_auths_config.json", + configPath: "../internal/configuration/testdata/no_auths_config.json", wantErr: false, }, } @@ -93,7 +92,7 @@ func TestNewFileStore_badFormat(t *testing.T) { func TestFileStore_Get_validConfig(t *testing.T) { ctx := context.Background() - fs, err := NewFileStore("testdata/valid_auths_config.json") + fs, err := NewFileStore("../internal/configuration/testdata/valid_auths_config.json") if err != nil { t.Fatal("NewFileStore() error =", err) } @@ -101,13 +100,13 @@ func TestFileStore_Get_validConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want Credential wantErr bool }{ { name: "Username and password", serverAddress: "registry1.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", }, @@ -115,21 +114,21 @@ func TestFileStore_Get_validConfig(t *testing.T) { { name: "Identity token", serverAddress: "registry2.example.com", - want: auth.Credential{ + want: Credential{ RefreshToken: "identity_token", }, }, { name: "Registry token", serverAddress: "registry3.example.com", - want: auth.Credential{ + want: Credential{ AccessToken: "registry_token", }, }, { name: "Username and password, identity token and registry token", serverAddress: "registry4.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", RefreshToken: "identity_token", @@ -139,12 +138,12 @@ func TestFileStore_Get_validConfig(t *testing.T) { { name: "Empty credential", serverAddress: "registry5.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, }, { name: "Username and password, no auth", serverAddress: "registry6.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", }, @@ -152,7 +151,7 @@ func TestFileStore_Get_validConfig(t *testing.T) { { name: "Auth overriding Username and password", serverAddress: "registry7.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", }, @@ -160,12 +159,12 @@ func TestFileStore_Get_validConfig(t *testing.T) { { name: "Not in auths", serverAddress: "foo.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, }, { name: "No record", serverAddress: "registry999.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, }, } for _, tt := range tests { @@ -184,7 +183,7 @@ func TestFileStore_Get_validConfig(t *testing.T) { func TestFileStore_Get_invalidConfig(t *testing.T) { ctx := context.Background() - fs, err := NewFileStore("testdata/invalid_auths_entry_config.json") + fs, err := NewFileStore("../internal/configuration/testdata/invalid_auths_entry_config.json") if err != nil { t.Fatal("NewFileStore() error =", err) } @@ -192,25 +191,25 @@ func TestFileStore_Get_invalidConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want Credential wantErr bool }{ { name: "Invalid auth encode", serverAddress: "registry1.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, wantErr: true, }, { name: "Invalid auths format", serverAddress: "registry2.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, wantErr: true, }, { name: "Invalid type", serverAddress: "registry3.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, wantErr: true, }, } @@ -230,7 +229,7 @@ func TestFileStore_Get_invalidConfig(t *testing.T) { func TestFileStore_Get_emptyConfig(t *testing.T) { ctx := context.Background() - fs, err := NewFileStore("testdata/empty_config.json") + fs, err := NewFileStore("../internal/configuration/testdata/empty.json") if err != nil { t.Fatal("NewFileStore() error =", err) } @@ -238,13 +237,13 @@ func TestFileStore_Get_emptyConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want Credential wantErr error }{ { name: "Not found", serverAddress: "registry.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, wantErr: nil, }, } @@ -272,13 +271,13 @@ func TestFileStore_Get_notExistConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want Credential wantErr error }{ { name: "Not found", serverAddress: "registry.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, wantErr: nil, }, } @@ -307,7 +306,7 @@ func TestFileStore_Put_notExistConfig(t *testing.T) { } server := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", RefreshToken: "refresh_token", @@ -360,7 +359,7 @@ func TestFileStore_Put_addNew(t *testing.T) { // prepare test content server1 := "registry1.example.com" - cred1 := auth.Credential{ + cred1 := Credential{ Username: "username", Password: "password", RefreshToken: "refresh_token", @@ -392,7 +391,7 @@ func TestFileStore_Put_addNew(t *testing.T) { t.Fatal("NewFileStore() error =", err) } server2 := "registry2.example.com" - cred2 := auth.Credential{ + cred2 := Credential{ Username: "username_2", Password: "password_2", RefreshToken: "refresh_token_2", @@ -481,7 +480,7 @@ func TestFileStore_Put_updateOld(t *testing.T) { if err != nil { t.Fatal("NewFileStore() error =", err) } - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", AccessToken: "access_token", @@ -535,7 +534,7 @@ func TestFileStore_Put_disablePut(t *testing.T) { fs.DisablePut = true server := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", RefreshToken: "refresh_token", @@ -557,7 +556,7 @@ func TestFileStore_Put_usernameContainsColon(t *testing.T) { t.Fatal("NewFileStore() error =", err) } serverAddr := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "x:y", Password: "z", } @@ -576,7 +575,7 @@ func TestFileStore_Put_passwordContainsColon(t *testing.T) { t.Fatal("NewFileStore() error =", err) } serverAddr := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "y", Password: "y:z", } @@ -599,14 +598,14 @@ func TestFileStore_Delete(t *testing.T) { // prepare test content server1 := "registry1.example.com" - cred1 := auth.Credential{ + cred1 := Credential{ Username: "username", Password: "password", RefreshToken: "refresh_token", AccessToken: "access_token", } server2 := "registry2.example.com" - cred2 := auth.Credential{ + cred2 := Credential{ Username: "username_2", Password: "password_2", RefreshToken: "refresh_token_2", @@ -686,7 +685,7 @@ func TestFileStore_Delete(t *testing.T) { if err != nil { t.Fatalf("FileStore.Get() error = %v", err) } - if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) { + if want := EmptyCredential; !reflect.DeepEqual(got, want) { t.Errorf("FileStore.Get(%s) = %v, want %v", server1, got, want) } got, err = fs.Get(ctx, server2) @@ -705,7 +704,7 @@ func TestFileStore_Delete_lastConfig(t *testing.T) { // prepare test content server := "registry1.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", RefreshToken: "refresh_token", @@ -771,7 +770,7 @@ func TestFileStore_Delete_lastConfig(t *testing.T) { if err != nil { t.Fatalf("FileStore.Get() error = %v", err) } - if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) { + if want := EmptyCredential; !reflect.DeepEqual(got, want) { t.Errorf("FileStore.Get(%s) = %v, want %v", server, got, want) } } @@ -783,7 +782,7 @@ func TestFileStore_Delete_notExistRecord(t *testing.T) { // prepare test content server := "registry1.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", RefreshToken: "refresh_token", @@ -881,12 +880,12 @@ func TestFileStore_Delete_notExistConfig(t *testing.T) { func Test_validateCredentialFormat(t *testing.T) { tests := []struct { name string - cred auth.Credential + cred Credential wantErr error }{ { name: "Username contains colon", - cred: auth.Credential{ + cred: Credential{ Username: "x:y", Password: "z", }, @@ -894,7 +893,7 @@ func Test_validateCredentialFormat(t *testing.T) { }, { name: "Password contains colon", - cred: auth.Credential{ + cred: Credential{ Username: "x", Password: "y:z", }, diff --git a/registry/remote/credentials/memory_store.go b/registry/remote/credentials/memory_store.go index 4366d49a4..4f80917ec 100644 --- a/registry/remote/credentials/memory_store.go +++ b/registry/remote/credentials/memory_store.go @@ -21,8 +21,7 @@ import ( "fmt" "sync" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/config" + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration" ) // memoryStore is a store that keeps credentials in memory. @@ -40,17 +39,17 @@ func NewMemoryStore() Store { // Reference: https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties func NewMemoryStoreFromDockerConfig(c []byte) (Store, error) { cfg := struct { - Auths map[string]config.AuthConfig `json:"auths"` + Auths map[string]configuration.AuthConfig `json:"auths"` }{} if err := json.Unmarshal(c, &cfg); err != nil { - return nil, fmt.Errorf("failed to unmarshal auth field: %w: %v", config.ErrInvalidConfigFormat, err) + return nil, fmt.Errorf("failed to unmarshal auth field: %w: %v", configuration.ErrInvalidConfigFormat, err) } s := &memoryStore{} for addr, auth := range cfg.Auths { // Normalize the auth key to hostname. - hostname := config.ToHostname(addr) - cred, err := auth.Credential() + hostname := configuration.ToHostname(addr) + cred, err := NewCredential(auth) if err != nil { return nil, err } @@ -60,16 +59,16 @@ func NewMemoryStoreFromDockerConfig(c []byte) (Store, error) { } // Get retrieves credentials from the store for the given server address. -func (ms *memoryStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) { +func (ms *memoryStore) Get(_ context.Context, serverAddress string) (Credential, error) { cred, found := ms.store.Load(serverAddress) if !found { - return auth.EmptyCredential, nil + return EmptyCredential, nil } - return cred.(auth.Credential), nil + return cred.(Credential), nil } // Put saves credentials into the store for the given server address. -func (ms *memoryStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error { +func (ms *memoryStore) Put(_ context.Context, serverAddress string, cred Credential) error { ms.store.Store(serverAddress, cred) return nil } diff --git a/registry/remote/credentials/memory_store_from_config_test.go b/registry/remote/credentials/memory_store_from_config_test.go index 66ba66f7b..3bf42c356 100644 --- a/registry/remote/credentials/memory_store_from_config_test.go +++ b/registry/remote/credentials/memory_store_from_config_test.go @@ -22,24 +22,23 @@ import ( "reflect" "testing" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/config" + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration" ) func TestMemoryStore_Create_fromInvalidConfig(t *testing.T) { - f, err := os.ReadFile("testdata/invalid_auths_entry_config.json") + f, err := os.ReadFile("../internal/configuration/testdata/invalid_auths_entry_config.json") if err != nil { t.Fatalf("failed to read file: %v", err) } _, err = NewMemoryStoreFromDockerConfig(f) - if !errors.Is(err, config.ErrInvalidConfigFormat) { - t.Fatalf("Error: %s is expected", config.ErrInvalidConfigFormat) + if !errors.Is(err, configuration.ErrInvalidConfigFormat) { + t.Fatalf("Error: %s is expected", configuration.ErrInvalidConfigFormat) } } func TestMemoryStore_Get_validConfig(t *testing.T) { ctx := context.Background() - f, err := os.ReadFile("testdata/valid_auths_config.json") + f, err := os.ReadFile("../internal/configuration/testdata/valid_auths_config.json") if err != nil { t.Fatalf("failed to read file: %v", err) } @@ -51,13 +50,13 @@ func TestMemoryStore_Get_validConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want Credential wantErr bool }{ { name: "Username and password", serverAddress: "registry1.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", }, @@ -65,21 +64,21 @@ func TestMemoryStore_Get_validConfig(t *testing.T) { { name: "Identity token", serverAddress: "registry2.example.com", - want: auth.Credential{ + want: Credential{ RefreshToken: "identity_token", }, }, { name: "Registry token", serverAddress: "registry3.example.com", - want: auth.Credential{ + want: Credential{ AccessToken: "registry_token", }, }, { name: "Username and password, identity token and registry token", serverAddress: "registry4.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", RefreshToken: "identity_token", @@ -89,12 +88,12 @@ func TestMemoryStore_Get_validConfig(t *testing.T) { { name: "Empty credential", serverAddress: "registry5.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, }, { name: "Username and password, no auth", serverAddress: "registry6.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", }, @@ -102,7 +101,7 @@ func TestMemoryStore_Get_validConfig(t *testing.T) { { name: "Auth overriding Username and password", serverAddress: "registry7.example.com", - want: auth.Credential{ + want: Credential{ Username: "username", Password: "password", }, @@ -110,12 +109,12 @@ func TestMemoryStore_Get_validConfig(t *testing.T) { { name: "Not in auths", serverAddress: "foo.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, }, { name: "No record", serverAddress: "registry999.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, }, } for _, tt := range tests { @@ -143,13 +142,13 @@ func TestMemoryStore_Get_emptyConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want Credential wantErr error }{ { name: "Not found", serverAddress: "registry.example.com", - want: auth.EmptyCredential, + want: EmptyCredential, wantErr: nil, }, } diff --git a/registry/remote/credentials/memory_store_test.go b/registry/remote/credentials/memory_store_test.go index 51117d50b..197dcce3d 100644 --- a/registry/remote/credentials/memory_store_test.go +++ b/registry/remote/credentials/memory_store_test.go @@ -19,8 +19,6 @@ import ( "context" "reflect" "testing" - - "github.com/oras-project/oras-go/v3/registry/remote/auth" ) func TestMemoryStore_Get_notExistRecord(t *testing.T) { @@ -33,8 +31,8 @@ func TestMemoryStore_Get_notExistRecord(t *testing.T) { t.Errorf("MemoryStore.Get() error = %v", err) return } - if !reflect.DeepEqual(got, auth.EmptyCredential) { - t.Errorf("MemoryStore.Get() = %v, want %v", got, auth.EmptyCredential) + if !reflect.DeepEqual(got, EmptyCredential) { + t.Errorf("MemoryStore.Get() = %v, want %v", got, EmptyCredential) } } @@ -43,7 +41,7 @@ func TestMemoryStore_Get_validRecord(t *testing.T) { ms := NewMemoryStore().(*memoryStore) serverAddress := "registry.example.com" - want := auth.Credential{ + want := Credential{ Username: "username", Password: "password", RefreshToken: "identity_token", @@ -67,7 +65,7 @@ func TestMemoryStore_Put_addNew(t *testing.T) { // Test Put server1 := "registry.example.com" - cred1 := auth.Credential{ + cred1 := Credential{ Username: "username", Password: "password", RefreshToken: "identity_token", @@ -79,7 +77,7 @@ func TestMemoryStore_Put_addNew(t *testing.T) { } server2 := "registry2.example.com" - cred2 := auth.Credential{ + cred2 := Credential{ Username: "username2", Password: "password2", RefreshToken: "identity_token2", @@ -118,7 +116,7 @@ func TestMemoryStore_Put_update(t *testing.T) { // Test Put serverAddress := "registry.example.com" - cred1 := auth.Credential{ + cred1 := Credential{ Username: "username", Password: "password", RefreshToken: "identity_token", @@ -129,7 +127,7 @@ func TestMemoryStore_Put_update(t *testing.T) { return } - cred2 := auth.Credential{ + cred2 := Credential{ Username: "username2", Password: "password2", RefreshToken: "identity_token2", @@ -157,7 +155,7 @@ func TestMemoryStore_Delete_existRecord(t *testing.T) { // Test Put serverAddress := "registry.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", RefreshToken: "identity_token", @@ -191,8 +189,8 @@ func TestMemoryStore_Delete_existRecord(t *testing.T) { t.Errorf("MemoryStore.Get() error = %v", err) return } - if !reflect.DeepEqual(got, auth.EmptyCredential) { - t.Errorf("MemoryStore.Get() = %v, want %v", got, auth.EmptyCredential) + if !reflect.DeepEqual(got, EmptyCredential) { + t.Errorf("MemoryStore.Get() = %v, want %v", got, EmptyCredential) return } } @@ -203,7 +201,7 @@ func TestMemoryStore_Delete_notExistRecord(t *testing.T) { // Test Put serverAddress := "registry.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", RefreshToken: "identity_token", diff --git a/registry/remote/credentials/native_store.go b/registry/remote/credentials/native_store.go index 746dffbba..0ae1a5d74 100644 --- a/registry/remote/credentials/native_store.go +++ b/registry/remote/credentials/native_store.go @@ -22,7 +22,6 @@ import ( "os/exec" "strings" - "github.com/oras-project/oras-go/v3/registry/remote/auth" "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/executer" ) @@ -80,19 +79,19 @@ func NewDefaultNativeStore() (Store, bool) { } // Get retrieves credentials from the store for the given server. -func (ns *nativeStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { - var cred auth.Credential +func (ns *nativeStore) Get(ctx context.Context, serverAddress string) (Credential, error) { + var cred Credential out, err := ns.exec.Execute(ctx, strings.NewReader(serverAddress), "get") if err != nil { if err.Error() == errCredentialsNotFoundMessage { // do not return an error if the credentials are not in the keychain. - return auth.EmptyCredential, nil + return EmptyCredential, nil } - return auth.EmptyCredential, err + return EmptyCredential, err } var dockerCred dockerCredentials if err := json.Unmarshal(out, &dockerCred); err != nil { - return auth.EmptyCredential, err + return EmptyCredential, err } // bearer auth is used if the username is "" if dockerCred.Username == emptyUsername { @@ -105,7 +104,7 @@ func (ns *nativeStore) Get(ctx context.Context, serverAddress string) (auth.Cred } // Put saves credentials into the store. -func (ns *nativeStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { +func (ns *nativeStore) Put(ctx context.Context, serverAddress string, cred Credential) error { dockerCred := &dockerCredentials{ ServerURL: serverAddress, Username: cred.Username, diff --git a/registry/remote/credentials/native_store_test.go b/registry/remote/credentials/native_store_test.go index ca71ef6e0..19ec8743b 100644 --- a/registry/remote/credentials/native_store_test.go +++ b/registry/remote/credentials/native_store_test.go @@ -24,7 +24,6 @@ import ( "strings" "testing" - "github.com/oras-project/oras-go/v3/registry/remote/auth" "github.com/oras-project/oras-go/v3/registry/remote/credentials/trace" ) @@ -143,7 +142,7 @@ func TestNativeStore_basicAuth(t *testing.T) { &testExecuter{}, } // Put - err := ns.Put(context.Background(), basicAuthHost, auth.Credential{Username: testUsername, Password: testPassword}) + err := ns.Put(context.Background(), basicAuthHost, Credential{Username: testUsername, Password: testPassword}) if err != nil { t.Fatalf("basic auth test ns.Put fails: %v", err) } @@ -170,7 +169,7 @@ func TestNativeStore_refreshToken(t *testing.T) { &testExecuter{}, } // Put - err := ns.Put(context.Background(), bearerAuthHost, auth.Credential{RefreshToken: testRefreshToken}) + err := ns.Put(context.Background(), bearerAuthHost, Credential{RefreshToken: testRefreshToken}) if err != nil { t.Fatalf("refresh token test ns.Put fails: %v", err) } @@ -238,7 +237,7 @@ func TestNativeStore_trace(t *testing.T) { } ctx := trace.WithExecutableTrace(context.Background(), traceHook) // Test ns.Put trace - err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword}) + err := ns.Put(ctx, traceHost, Credential{Username: testUsername, Password: testPassword}) if err != nil { t.Fatalf("trace test ns.Put fails: %v", err) } @@ -274,7 +273,7 @@ func TestNativeStore_noTrace(t *testing.T) { &testExecuter{}, } // Put - err := ns.Put(context.Background(), traceHost, auth.Credential{Username: testUsername, Password: testPassword}) + err := ns.Put(context.Background(), traceHost, Credential{Username: testUsername, Password: testPassword}) if err != nil { t.Fatalf("basic auth test ns.Put fails: %v", err) } @@ -304,7 +303,7 @@ func TestNativeStore_emptyTrace(t *testing.T) { traceHook := &trace.ExecutableTrace{} ctx := trace.WithExecutableTrace(context.Background(), traceHook) // Put - err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword}) + err := ns.Put(ctx, traceHost, Credential{Username: testUsername, Password: testPassword}) if err != nil { t.Fatalf("basic auth test ns.Put fails: %v", err) } @@ -354,7 +353,7 @@ func TestNativeStore_multipleTrace(t *testing.T) { trace3 := &trace.ExecutableTrace{} ctx = trace.WithExecutableTrace(ctx, trace3) // Test ns.Put trace - err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword}) + err := ns.Put(ctx, traceHost, Credential{Username: testUsername, Password: testPassword}) if err != nil { t.Fatalf("trace test ns.Put fails: %v", err) } diff --git a/registry/remote/credentials/store.go b/registry/remote/credentials/store.go index 555a9e4c4..ee006e3dd 100644 --- a/registry/remote/credentials/store.go +++ b/registry/remote/credentials/store.go @@ -27,8 +27,7 @@ import ( "path/filepath" "github.com/oras-project/oras-go/v3/internal/syncutil" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/config" + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration" ) const ( @@ -40,9 +39,9 @@ const ( // Store is the interface that any credentials store must implement. type Store interface { // Get retrieves credentials from the store for the given server address. - Get(ctx context.Context, serverAddress string) (auth.Credential, error) + Get(ctx context.Context, serverAddress string) (Credential, error) // Put saves credentials into the store for the given server address. - Put(ctx context.Context, serverAddress string, cred auth.Credential) error + Put(ctx context.Context, serverAddress string, cred Credential) error // Delete removes credentials from the store for the given server address. Delete(ctx context.Context, serverAddress string) error } @@ -50,7 +49,7 @@ type Store interface { // DynamicStore dynamically determines which store to use based on the settings // in the config file. type DynamicStore struct { - config *config.Config + config *configuration.Config options StoreOptions detectedCredsStore string setCredsStoreOnce syncutil.OnceOrRetry @@ -95,7 +94,7 @@ type StoreOptions struct { // - https://docs.docker.com/engine/reference/commandline/login/#credentials-store // - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties func NewStore(configPath string, opts StoreOptions) (*DynamicStore, error) { - cfg, err := config.Load(configPath) + cfg, err := configuration.Load(configPath) if err != nil { return nil, err } @@ -129,14 +128,14 @@ func NewStoreFromDocker(opt StoreOptions) (*DynamicStore, error) { } // Get retrieves credentials from the store for the given server address. -func (ds *DynamicStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { +func (ds *DynamicStore) Get(ctx context.Context, serverAddress string) (Credential, error) { return ds.getStore(serverAddress).Get(ctx, serverAddress) } // Put saves credentials into the store for the given server address. // Put returns ErrPlaintextPutDisabled if native store is not available and // [StoreOptions].AllowPlaintextPut is set to false. -func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { +func (ds *DynamicStore) Put(ctx context.Context, serverAddress string, cred Credential) error { if err := ds.getStore(serverAddress).Put(ctx, serverAddress, cred); err != nil { return err } @@ -236,22 +235,22 @@ func NewStoreWithFallbacks(primary Store, fallbacks ...Store) Store { // Get retrieves credentials from the StoreWithFallbacks for the given server. // It searches the primary and the fallback stores for the credentials of serverAddress // and returns when it finds the credentials in any of the stores. -func (sf *storeWithFallbacks) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { +func (sf *storeWithFallbacks) Get(ctx context.Context, serverAddress string) (Credential, error) { for _, s := range sf.stores { cred, err := s.Get(ctx, serverAddress) if err != nil { - return auth.EmptyCredential, err + return EmptyCredential, err } - if cred != auth.EmptyCredential { + if cred != EmptyCredential { return cred, nil } } - return auth.EmptyCredential, nil + return EmptyCredential, nil } // Put saves credentials into the StoreWithFallbacks. It puts // the credentials into the primary store. -func (sf *storeWithFallbacks) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { +func (sf *storeWithFallbacks) Put(ctx context.Context, serverAddress string, cred Credential) error { return sf.stores[0].Put(ctx, serverAddress, cred) } diff --git a/registry/remote/credentials/store_test.go b/registry/remote/credentials/store_test.go index 2ad798c81..e80ae1013 100644 --- a/registry/remote/credentials/store_test.go +++ b/registry/remote/credentials/store_test.go @@ -24,21 +24,42 @@ import ( "reflect" "testing" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/config/configtest" + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration/configtest" ) +// testStore implements the Store interface, used for testing purpose. +type testStore struct { + storage map[string]Credential +} + +func (t *testStore) Get(ctx context.Context, serverAddress string) (Credential, error) { + return t.storage[serverAddress], nil +} + +func (t *testStore) Put(ctx context.Context, serverAddress string, cred Credential) error { + if len(t.storage) == 0 { + t.storage = make(map[string]Credential) + } + t.storage[serverAddress] = cred + return nil +} + +func (t *testStore) Delete(ctx context.Context, serverAddress string) error { + delete(t.storage, serverAddress) + return nil +} + type badStore struct{} var errBadStore = errors.New("bad store!") // Get retrieves credentials from the store for the given server address. -func (s *badStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { - return auth.EmptyCredential, errBadStore +func (s *badStore) Get(ctx context.Context, serverAddress string) (Credential, error) { + return EmptyCredential, errBadStore } // Put saves credentials into the store for the given server address. -func (s *badStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { +func (s *badStore) Put(ctx context.Context, serverAddress string, cred Credential) error { return errBadStore } @@ -207,7 +228,7 @@ func Test_DynamicStore_authConfigured(t *testing.T) { } serverAddr := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", } @@ -245,7 +266,7 @@ func Test_DynamicStore_authConfigured(t *testing.T) { if err != nil { t.Fatal("DynamicStore.Get() error =", err) } - if want := auth.EmptyCredential; got != want { + if want := EmptyCredential; got != want { t.Errorf("DynamicStore.Get() = %v, want %v", got, want) } } @@ -284,7 +305,7 @@ func Test_DynamicStore_authConfigured_DetectDefaultNativeStore(t *testing.T) { } serverAddr := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", } @@ -322,7 +343,7 @@ func Test_DynamicStore_authConfigured_DetectDefaultNativeStore(t *testing.T) { if err != nil { t.Fatal("DynamicStore.Get() error =", err) } - if want := auth.EmptyCredential; got != want { + if want := EmptyCredential; got != want { t.Errorf("DynamicStore.Get() = %v, want %v", got, want) } } @@ -354,7 +375,7 @@ func Test_DynamicStore_noAuthConfigured(t *testing.T) { } serverAddr := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", } @@ -397,7 +418,7 @@ func Test_DynamicStore_noAuthConfigured(t *testing.T) { if err != nil { t.Fatal("DynamicStore.Get() error =", err) } - if want := auth.EmptyCredential; got != want { + if want := EmptyCredential; got != want { t.Errorf("DynamicStore.Get() = %v, want %v", got, want) } } @@ -433,7 +454,7 @@ func Test_DynamicStore_noAuthConfigured_DetectDefaultNativeStore(t *testing.T) { } serverAddr := "test.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", } @@ -482,7 +503,7 @@ func Test_DynamicStore_noAuthConfigured_DetectDefaultNativeStore(t *testing.T) { if err != nil { t.Fatal("DynamicStore.Get() error =", err) } - if want := auth.EmptyCredential; got != want { + if want := EmptyCredential; got != want { t.Errorf("DynamicStore.Get() = %v, want %v", got, want) } } @@ -492,7 +513,7 @@ func Test_DynamicStore_fileStore_AllowPlainTextPut(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") serverAddr := "newtest.example.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", } @@ -564,37 +585,37 @@ func Test_DynamicStore_getHelperSuffix(t *testing.T) { }{ { name: "Get cred helper: registry_helper1", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "registry1.example.com", want: "registry1-helper", }, { name: "Get cred helper: registry_helper2", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "registry2.example.com", want: "registry2-helper", }, { name: "Empty cred helper configured", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "registry3.example.com", want: "", }, { name: "No cred helper and creds store configured", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "whatever.example.com", want: "", }, { name: "Choose cred helper over creds store", - configPath: "testdata/credsStore_config.json", + configPath: "../internal/configuration/testdata/credsStore_config.json", serverAddress: "test.example.com", want: "test-helper", }, { name: "No cred helper configured, choose cred store", - configPath: "testdata/credsStore_config.json", + configPath: "../internal/configuration/testdata/credsStore_config.json", serverAddress: "whatever.example.com", want: "teststore", }, @@ -613,7 +634,7 @@ func Test_DynamicStore_getHelperSuffix(t *testing.T) { } func Test_DynamicStore_ConfigPath(t *testing.T) { - path := "../../testdata/credsStore_config.json" + path := "../internal/configuration/testdata/credsStore_config.json" var err error store, err := NewStore(path, StoreOptions{}) if err != nil { @@ -633,22 +654,22 @@ func Test_DynamicStore_getStore_nativeStore(t *testing.T) { }{ { name: "Cred helper configured for registry1.example.com", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "registry1.example.com", }, { name: "Cred helper configured for registry2.example.com", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "registry2.example.com", }, { name: "Cred helper configured for test.example.com", - configPath: "testdata/credsStore_config.json", + configPath: "../internal/configuration/testdata/credsStore_config.json", serverAddress: "test.example.com", }, { name: "No cred helper configured, use creds store", - configPath: "testdata/credsStore_config.json", + configPath: "../internal/configuration/testdata/credsStore_config.json", serverAddress: "whaterver.example.com", }, } @@ -674,12 +695,12 @@ func Test_DynamicStore_getStore_fileStore(t *testing.T) { }{ { name: "Empty cred helper configured for registry3.example.com", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "registry3.example.com", }, { name: "No cred helper configured", - configPath: "testdata/credHelpers_config.json", + configPath: "../internal/configuration/testdata/credHelpers_config.json", serverAddress: "whatever.example.com", }, } @@ -711,23 +732,23 @@ func Test_DynamicStore_getStore_fileStore(t *testing.T) { func Test_storeWithFallbacks_Get(t *testing.T) { // prepare test content server1 := "foo.registry.com" - cred1 := auth.Credential{ + cred1 := Credential{ Username: "username", Password: "password", } server2 := "bar.registry.com" - cred2 := auth.Credential{ + cred2 := Credential{ RefreshToken: "identity_token", } primaryStore := &testStore{} fallbackStore1 := &testStore{ - storage: map[string]auth.Credential{ + storage: map[string]Credential{ server1: cred1, }, } fallbackStore2 := &testStore{ - storage: map[string]auth.Credential{ + storage: map[string]Credential{ server2: cred2, }, } @@ -755,7 +776,7 @@ func Test_storeWithFallbacks_Get(t *testing.T) { if err != nil { t.Fatal("storeWithFallbacks.Get() error =", err) } - if want := auth.EmptyCredential; got != want { + if want := EmptyCredential; got != want { t.Errorf("storeWithFallbacks.Get() = %v, want %v", got, want) } } @@ -799,7 +820,7 @@ func Test_storeWithFallbacks_Put(t *testing.T) { ctx := context.Background() server := "example.registry.com" - cred := auth.Credential{ + cred := Credential{ Username: "username", Password: "password", } @@ -824,7 +845,7 @@ func Test_storeWithFallbacks_Put_throwError(t *testing.T) { ctx := context.Background() // test Put(): should thrown error - err := sf.Put(ctx, "whatever", auth.Credential{}) + err := sf.Put(ctx, "whatever", Credential{}) if wantErr := errBadStore; !errors.Is(err, wantErr) { t.Errorf("storeWithFallback.Put() error = %v, wantErr %v", err, wantErr) } @@ -833,17 +854,17 @@ func Test_storeWithFallbacks_Put_throwError(t *testing.T) { func Test_storeWithFallbacks_Delete(t *testing.T) { // prepare test content server1 := "foo.registry.com" - cred1 := auth.Credential{ + cred1 := Credential{ Username: "username", Password: "password", } server2 := "bar.registry.com" - cred2 := auth.Credential{ + cred2 := Credential{ RefreshToken: "identity_token", } primaryStore := &testStore{ - storage: map[string]auth.Credential{ + storage: map[string]Credential{ server1: cred1, server2: cred2, }, @@ -857,7 +878,7 @@ func Test_storeWithFallbacks_Delete(t *testing.T) { t.Fatal("storeWithFallback.Delete()") } // verify primary store - if want := map[string]auth.Credential{server2: cred2}; !reflect.DeepEqual(primaryStore.storage, want) { + if want := map[string]Credential{server2: cred2}; !reflect.DeepEqual(primaryStore.storage, want) { t.Errorf("primaryStore.storage = %v, want %v", primaryStore.storage, want) } @@ -866,7 +887,7 @@ func Test_storeWithFallbacks_Delete(t *testing.T) { t.Fatal("storeWithFallback.Delete()") } // verify primary store - if want := map[string]auth.Credential{}; !reflect.DeepEqual(primaryStore.storage, want) { + if want := map[string]Credential{}; !reflect.DeepEqual(primaryStore.storage, want) { t.Errorf("primaryStore.storage = %v, want %v", primaryStore.storage, want) } } @@ -923,7 +944,7 @@ func TestNewStoreFromDocker(t *testing.T) { t.Setenv("DOCKER_CONFIG", tempDir) serverAddr1 := "test.example.com" - cred1 := auth.Credential{ + cred1 := Credential{ Username: "foo", Password: "bar", } @@ -961,7 +982,7 @@ func TestNewStoreFromDocker(t *testing.T) { // test putting a new credential serverAddr2 := "newtest.example.com" - cred2 := auth.Credential{ + cred2 := Credential{ Username: "username", Password: "password", } @@ -989,7 +1010,7 @@ func TestNewStoreFromDocker(t *testing.T) { if err != nil { t.Fatal("DynamicStore.Get() error =", err) } - if want := auth.EmptyCredential; got != want { + if want := EmptyCredential; got != want { t.Errorf("DynamicStore.Get() = %v, want %v", got, want) } } diff --git a/registry/remote/credentials/trace/example_test.go b/registry/remote/credentials/trace/example_test.go index 79392dc54..ab5bacaca 100644 --- a/registry/remote/credentials/trace/example_test.go +++ b/registry/remote/credentials/trace/example_test.go @@ -17,7 +17,6 @@ import ( "context" "fmt" - "github.com/oras-project/oras-go/v3/registry/remote/auth" "github.com/oras-project/oras-go/v3/registry/remote/credentials" "github.com/oras-project/oras-go/v3/registry/remote/credentials/trace" ) @@ -47,7 +46,7 @@ func Example() { // Get, Put and Delete credentials from store. If any credential helper // executable is run, traceHooks is executed. - err = store.Put(ctx, "localhost:5000", auth.Credential{Username: "testUsername", Password: "testPassword"}) + err = store.Put(ctx, "localhost:5000", credentials.Credential{Username: "testUsername", Password: "testPassword"}) if err != nil { panic(err) } diff --git a/registry/remote/internal/configuration/authconfig.go b/registry/remote/internal/configuration/authconfig.go new file mode 100644 index 000000000..03cf86278 --- /dev/null +++ b/registry/remote/internal/configuration/authconfig.go @@ -0,0 +1,74 @@ +/* +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 configuration + +import ( + "encoding/base64" + "fmt" + "strings" +) + +// AuthConfig contains authorization information for connecting to a Registry. +// References: +// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L45 +// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/types/authconfig.go#L3-L22 +type AuthConfig struct { + // Auth is a base64-encoded string of "{username}:{password}". + Auth string `json:"auth,omitempty"` + // IdentityToken is used to authenticate the user and get an access token + // for the registry. + IdentityToken string `json:"identitytoken,omitempty"` + // RegistryToken is a bearer token to be sent to a registry. + RegistryToken string `json:"registrytoken,omitempty"` + + Username string `json:"username,omitempty"` // legacy field for compatibility + Password string `json:"password,omitempty"` // legacy field for compatibility +} + +// NewAuthConfig creates an AuthConfig based on cred. +func NewAuthConfig(username, password, refreshToken, accessToken string) AuthConfig { + return AuthConfig{ + Auth: EncodeAuth(username, password), + IdentityToken: refreshToken, + RegistryToken: accessToken, + } +} + +// EncodeAuth base64-encodes username and password into base64(username:password). +func EncodeAuth(username, password string) string { + if username == "" && password == "" { + return "" + } + return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) +} + +// DecodeAuth decodes the base64-encoded Auth field and returns username and password. +func (ac AuthConfig) DecodeAuth() (username string, password string, err error) { + if ac.Auth == "" { + return "", "", nil + } + + decoded, err := base64.StdEncoding.DecodeString(ac.Auth) + if err != nil { + return "", "", err + } + decodedStr := string(decoded) + username, password, ok := strings.Cut(decodedStr, ":") + if !ok { + return "", "", fmt.Errorf("auth '%s' does not conform the base64(username:password) format", decodedStr) + } + return username, password, nil +} diff --git a/registry/remote/internal/configuration/authconfig_test.go b/registry/remote/internal/configuration/authconfig_test.go new file mode 100644 index 000000000..fb3da35a3 --- /dev/null +++ b/registry/remote/internal/configuration/authconfig_test.go @@ -0,0 +1,125 @@ +/* +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 configuration + +import ( + "testing" +) + +func Test_EncodeAuth(t *testing.T) { + tests := []struct { + name string + username string + password string + want string + }{ + { + name: "Username and password", + username: "username", + password: "password", + want: "dXNlcm5hbWU6cGFzc3dvcmQ=", + }, + { + name: "Username only", + username: "username", + password: "", + want: "dXNlcm5hbWU6", + }, + { + name: "Password only", + username: "", + password: "password", + want: "OnBhc3N3b3Jk", + }, + { + name: "Empty username and empty password", + username: "", + password: "", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := EncodeAuth(tt.username, tt.password); got != tt.want { + t.Errorf("EncodeAuth() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAuthConfig_DecodeAuth(t *testing.T) { + tests := []struct { + name string + authStr string + username string + password string + wantErr bool + }{ + { + name: "Valid base64", + authStr: "dXNlcm5hbWU6cGFzc3dvcmQ=", // username:password + username: "username", + password: "password", + }, + { + name: "Valid base64, username only", + authStr: "dXNlcm5hbWU6", // username: + username: "username", + }, + { + name: "Valid base64, password only", + authStr: "OnBhc3N3b3Jk", // :password + password: "password", + }, + { + name: "Valid base64, bad format", + authStr: "d2hhdGV2ZXI=", // whatever + username: "", + password: "", + wantErr: true, + }, + { + name: "Invalid base64", + authStr: "whatever", + username: "", + password: "", + wantErr: true, + }, + { + name: "Empty string", + authStr: "", + username: "", + password: "", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + authCfg := AuthConfig{Auth: tt.authStr} + gotUsername, gotPassword, err := authCfg.DecodeAuth() + if (err != nil) != tt.wantErr { + t.Errorf("AuthConfig.DecodeAuth() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotUsername != tt.username { + t.Errorf("AuthConfig.DecodeAuth() got username = %v, want %v", gotUsername, tt.username) + } + if gotPassword != tt.password { + t.Errorf("AuthConfig.DecodeAuth() got password = %v, want %v", gotPassword, tt.password) + } + }) + } +} diff --git a/registry/remote/credentials/internal/config/config.go b/registry/remote/internal/configuration/config.go similarity index 72% rename from registry/remote/credentials/internal/config/config.go rename to registry/remote/internal/configuration/config.go index d71c26760..4733763a4 100644 --- a/registry/remote/credentials/internal/config/config.go +++ b/registry/remote/internal/configuration/config.go @@ -13,11 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package config +package configuration import ( "bytes" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -27,8 +26,7 @@ import ( "strings" "sync" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/ioutil" + "github.com/oras-project/oras-go/v3/registry/remote/internal/ioutil" ) const ( @@ -44,51 +42,6 @@ const ( // ErrInvalidConfigFormat is returned when the config format is invalid. var ErrInvalidConfigFormat = errors.New("invalid config format") -// AuthConfig contains authorization information for connecting to a Registry. -// References: -// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/configfile/file.go#L17-L45 -// - https://github.com/docker/cli/blob/v24.0.0-beta.2/cli/config/types/authconfig.go#L3-L22 -type AuthConfig struct { - // Auth is a base64-encoded string of "{username}:{password}". - Auth string `json:"auth,omitempty"` - // IdentityToken is used to authenticate the user and get an access token - // for the registry. - IdentityToken string `json:"identitytoken,omitempty"` - // RegistryToken is a bearer token to be sent to a registry. - RegistryToken string `json:"registrytoken,omitempty"` - - Username string `json:"username,omitempty"` // legacy field for compatibility - Password string `json:"password,omitempty"` // legacy field for compatibility -} - -// NewAuthConfig creates an authConfig based on cred. -func NewAuthConfig(cred auth.Credential) AuthConfig { - return AuthConfig{ - Auth: encodeAuth(cred.Username, cred.Password), - IdentityToken: cred.RefreshToken, - RegistryToken: cred.AccessToken, - } -} - -// Credential returns an auth.Credential based on ac. -func (ac AuthConfig) Credential() (auth.Credential, error) { - cred := auth.Credential{ - Username: ac.Username, - Password: ac.Password, - RefreshToken: ac.IdentityToken, - AccessToken: ac.RegistryToken, - } - if ac.Auth != "" { - var err error - // override username and password - cred.Username, cred.Password, err = decodeAuth(ac.Auth) - if err != nil { - return auth.EmptyCredential, fmt.Errorf("failed to decode auth field: %w: %v", ErrInvalidConfigFormat, err) - } - } - return cred, nil -} - // Config represents a docker configuration file. // References: // - https://docs.docker.com/engine/reference/commandline/cli/#docker-cli-configuration-file-configjson-properties @@ -162,8 +115,8 @@ func Load(configPath string) (*Config, error) { return cfg, nil } -// GetAuthConfig returns an auth.Credential for serverAddress. -func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error) { +// GetAuthConfig returns an AuthConfig for serverAddress. +func (cfg *Config) GetAuthConfig(serverAddress string) (AuthConfig, error) { cfg.rwLock.RLock() defer cfg.rwLock.RUnlock() @@ -181,22 +134,21 @@ func (cfg *Config) GetCredential(serverAddress string) (auth.Credential, error) } } if !matched { - return auth.EmptyCredential, nil + return AuthConfig{}, nil } } var authCfg AuthConfig if err := json.Unmarshal(authCfgBytes, &authCfg); err != nil { - return auth.EmptyCredential, fmt.Errorf("failed to unmarshal auth field: %w: %v", ErrInvalidConfigFormat, err) + return AuthConfig{}, fmt.Errorf("failed to unmarshal auth field: %w: %v", ErrInvalidConfigFormat, err) } - return authCfg.Credential() + return authCfg, nil } -// PutAuthConfig puts cred for serverAddress. -func (cfg *Config) PutCredential(serverAddress string, cred auth.Credential) error { +// PutAuthConfig puts authCfg for serverAddress. +func (cfg *Config) PutAuthConfig(serverAddress string, authCfg AuthConfig) error { cfg.rwLock.Lock() defer cfg.rwLock.Unlock() - authCfg := NewAuthConfig(cred) authCfgBytes, err := json.Marshal(authCfg) if err != nil { return fmt.Errorf("failed to marshal auth field: %w", err) @@ -206,7 +158,7 @@ func (cfg *Config) PutCredential(serverAddress string, cred auth.Credential) err } // DeleteAuthConfig deletes the corresponding credential for serverAddress. -func (cfg *Config) DeleteCredential(serverAddress string) error { +func (cfg *Config) DeleteAuthConfig(serverAddress string) error { cfg.rwLock.Lock() defer cfg.rwLock.Unlock() @@ -300,32 +252,6 @@ func (cfg *Config) saveFile() (returnErr error) { return nil } -// encodeAuth base64-encodes username and password into base64(username:password). -func encodeAuth(username, password string) string { - if username == "" && password == "" { - return "" - } - return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) -} - -// decodeAuth decodes a base64 encoded string and returns username and password. -func decodeAuth(authStr string) (username string, password string, err error) { - if authStr == "" { - return "", "", nil - } - - decoded, err := base64.StdEncoding.DecodeString(authStr) - if err != nil { - return "", "", err - } - decodedStr := string(decoded) - username, password, ok := strings.Cut(decodedStr, ":") - if !ok { - return "", "", fmt.Errorf("auth '%s' does not conform the base64(username:password) format", decodedStr) - } - return username, password, nil -} - // ToHostname normalizes a server address to just its hostname, removing // the scheme and the path parts. // It is used to match keys in the auths map, which may be either stored as diff --git a/registry/remote/credentials/internal/config/config_test.go b/registry/remote/internal/configuration/config_test.go similarity index 60% rename from registry/remote/credentials/internal/config/config_test.go rename to registry/remote/internal/configuration/config_test.go index b9309f0b8..60af37150 100644 --- a/registry/remote/credentials/internal/config/config_test.go +++ b/registry/remote/internal/configuration/config_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package config +package configuration import ( "encoding/json" @@ -23,8 +23,7 @@ import ( "reflect" "testing" - "github.com/oras-project/oras-go/v3/registry/remote/auth" - "github.com/oras-project/oras-go/v3/registry/remote/credentials/internal/config/configtest" + "github.com/oras-project/oras-go/v3/registry/remote/internal/configuration/configtest" ) func TestLoad_badPath(t *testing.T) { @@ -65,17 +64,17 @@ func TestLoad_badFormat(t *testing.T) { }{ { name: "Bad JSON format", - configPath: "../../testdata/bad_config", + configPath: "./testdata/bad_config", wantErr: true, }, { name: "Invalid auths format", - configPath: "../../testdata/invalid_auths_config.json", + configPath: "./testdata/invalid_auths_config.json", wantErr: true, }, { name: "No auths field", - configPath: "../../testdata/no_auths_config.json", + configPath: "./testdata/no_auths_config.json", wantErr: false, }, } @@ -90,8 +89,8 @@ func TestLoad_badFormat(t *testing.T) { } } -func TestConfig_GetCredential_validConfig(t *testing.T) { - cfg, err := Load("../../testdata/valid_auths_config.json") +func TestConfig_GetAuthConfig_validConfig(t *testing.T) { + cfg, err := Load("./testdata/valid_auths_config.json") if err != nil { t.Fatal("Load() error =", err) } @@ -99,50 +98,48 @@ func TestConfig_GetCredential_validConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want AuthConfig wantErr bool }{ { name: "Username and password", serverAddress: "registry1.example.com", - want: auth.Credential{ - Username: "username", - Password: "password", + want: AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", }, }, { name: "Identity token", serverAddress: "registry2.example.com", - want: auth.Credential{ - RefreshToken: "identity_token", + want: AuthConfig{ + IdentityToken: "identity_token", }, }, { name: "Registry token", serverAddress: "registry3.example.com", - want: auth.Credential{ - AccessToken: "registry_token", + want: AuthConfig{ + RegistryToken: "registry_token", }, }, { name: "Username and password, identity token and registry token", serverAddress: "registry4.example.com", - want: auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "identity_token", - AccessToken: "registry_token", + want: AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + IdentityToken: "identity_token", + RegistryToken: "registry_token", }, }, { name: "Empty credential", serverAddress: "registry5.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, }, { name: "Username and password, no auth", serverAddress: "registry6.example.com", - want: auth.Credential{ + want: AuthConfig{ Username: "username", Password: "password", }, @@ -150,38 +147,39 @@ func TestConfig_GetCredential_validConfig(t *testing.T) { { name: "Auth overriding Username and password", serverAddress: "registry7.example.com", - want: auth.Credential{ - Username: "username", - Password: "password", + want: AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + Username: "foo", + Password: "bar", }, }, { name: "Not in auths", serverAddress: "foo.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, }, { name: "No record", serverAddress: "registry999.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) + got, err := cfg.GetAuthConfig(tt.serverAddress) if (err != nil) != tt.wantErr { - t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Config.GetAuthConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want) + t.Errorf("Config.GetAuthConfig() = %v, want %v", got, tt.want) } }) } } -func TestConfig_GetCredential_legacyConfig(t *testing.T) { - cfg, err := Load("../../testdata/legacy_auths_config.json") +func TestConfig_GetAuthConfig_legacyConfig(t *testing.T) { + cfg, err := Load("./testdata/legacy_auths_config.json") if err != nil { t.Fatal("Load() error =", err) } @@ -189,87 +187,80 @@ func TestConfig_GetCredential_legacyConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want AuthConfig wantErr bool }{ { name: "Regular address matched", serverAddress: "registry1.example.com", - want: auth.Credential{ - Username: "username1", - Password: "password1", + want: AuthConfig{ + Auth: "dXNlcm5hbWUxOnBhc3N3b3JkMQ==", }, }, { name: "Another entry for the same address matched", serverAddress: "https://registry1.example.com/", - want: auth.Credential{ - Username: "foo", - Password: "bar", + want: AuthConfig{ + Auth: "Zm9vOmJhcg==", }, }, { name: "Address with different scheme unmached", serverAddress: "http://registry1.example.com/", - want: auth.EmptyCredential, + want: AuthConfig{}, }, { name: "Address with http prefix matched", serverAddress: "registry2.example.com", - want: auth.Credential{ - Username: "username2", - Password: "password2", + want: AuthConfig{ + Auth: "dXNlcm5hbWUyOnBhc3N3b3JkMg==", }, }, { name: "Address with https prefix matched", serverAddress: "registry3.example.com", - want: auth.Credential{ - Username: "username3", - Password: "password3", + want: AuthConfig{ + Auth: "dXNlcm5hbWUzOnBhc3N3b3JkMw==", }, }, { name: "Address with http prefix and / suffix matched", serverAddress: "registry4.example.com", - want: auth.Credential{ - Username: "username4", - Password: "password4", + want: AuthConfig{ + Auth: "dXNlcm5hbWU0OnBhc3N3b3JkNA==", }, }, { name: "Address with https prefix and / suffix matched", serverAddress: "registry5.example.com", - want: auth.Credential{ - Username: "username5", - Password: "password5", + want: AuthConfig{ + Auth: "dXNlcm5hbWU1OnBhc3N3b3JkNQ==", }, }, { name: "Address with https prefix and path suffix matched", serverAddress: "registry6.example.com", - want: auth.Credential{ - Username: "username6", - Password: "password6", + want: AuthConfig{ + Auth: "dXNlcm5hbWU2OnBhc3N3b3JkNg==", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) + got, err := cfg.GetAuthConfig(tt.serverAddress) if (err != nil) != tt.wantErr { - t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Config.GetAuthConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want) + t.Errorf("Config.GetAuthConfig() = %v, want %v", got, tt.want) } }) } } -func TestConfig_GetCredential_invalidConfig(t *testing.T) { - cfg, err := Load("../../testdata/invalid_auths_entry_config.json") +func TestConfig_GetAuthConfig_invalidConfig(t *testing.T) { + cfg, err := Load("./testdata/invalid_auths_entry_config.json") if err != nil { t.Fatal("Load() error =", err) } @@ -277,44 +268,46 @@ func TestConfig_GetCredential_invalidConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want AuthConfig wantErr bool }{ { name: "Invalid auth encode", serverAddress: "registry1.example.com", - want: auth.EmptyCredential, - wantErr: true, + want: AuthConfig{ + Auth: "username:password", + }, + wantErr: false, }, { name: "Invalid auths format", serverAddress: "registry2.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, wantErr: true, }, { name: "Invalid type", serverAddress: "registry3.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) + got, err := cfg.GetAuthConfig(tt.serverAddress) if (err != nil) != tt.wantErr { - t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Config.GetAuthConfig() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want) + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("Config.GetAuthConfig() = %v, want %v", got, tt.want) } }) } } -func TestConfig_GetCredential_empty(t *testing.T) { - cfg, err := Load("../../testdata/empty.json") +func TestConfig_GetAuthConfig_empty(t *testing.T) { + cfg, err := Load("./testdata/empty.json") if err != nil { t.Fatal("Load() error =", err) } @@ -322,32 +315,32 @@ func TestConfig_GetCredential_empty(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want AuthConfig wantErr error }{ { name: "Not found", serverAddress: "registry.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) + got, err := cfg.GetAuthConfig(tt.serverAddress) if !errors.Is(err, tt.wantErr) { - t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Config.GetAuthConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want) + t.Errorf("Config.GetAuthConfig() = %v, want %v", got, tt.want) } }) } } -func TestConfig_GetCredential_whiteSpace(t *testing.T) { - cfg, err := Load("../../testdata/whitespace.json") +func TestConfig_GetAuthConfig_whiteSpace(t *testing.T) { + cfg, err := Load("./testdata/whitespace.json") if err != nil { t.Fatal("Load() error =", err) } @@ -355,31 +348,31 @@ func TestConfig_GetCredential_whiteSpace(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want AuthConfig wantErr error }{ { name: "Not found", serverAddress: "registry.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) + got, err := cfg.GetAuthConfig(tt.serverAddress) if !errors.Is(err, tt.wantErr) { - t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Config.GetAuthConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want) + t.Errorf("Config.GetAuthConfig() = %v, want %v", got, tt.want) } }) } } -func TestConfig_GetCredential_notExistConfig(t *testing.T) { +func TestConfig_GetAuthConfig_notExistConfig(t *testing.T) { cfg, err := Load("whatever") if err != nil { t.Fatal("Load() error =", err) @@ -388,31 +381,31 @@ func TestConfig_GetCredential_notExistConfig(t *testing.T) { tests := []struct { name string serverAddress string - want auth.Credential + want AuthConfig wantErr error }{ { name: "Not found", serverAddress: "registry.example.com", - want: auth.EmptyCredential, + want: AuthConfig{}, wantErr: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := cfg.GetCredential(tt.serverAddress) + got, err := cfg.GetAuthConfig(tt.serverAddress) if !errors.Is(err, tt.wantErr) { - t.Errorf("Config.GetCredential() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Config.GetAuthConfig() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, tt.want) + t.Errorf("Config.GetAuthConfig() = %v, want %v", got, tt.want) } }) } } -func TestConfig_PutCredential_notExistConfig(t *testing.T) { +func TestConfig_PutAuthConfig_notExistConfig(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") @@ -422,16 +415,15 @@ func TestConfig_PutCredential_notExistConfig(t *testing.T) { } server := "test.example.com" - cred := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", + authCfg := AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + IdentityToken: "refresh_token", + RegistryToken: "access_token", } // test put - if err := cfg.PutCredential(server, cred); err != nil { - t.Fatalf("Config.PutCredential() error = %v", err) + if err := cfg.PutAuthConfig(server, authCfg); err != nil { + t.Fatalf("Config.PutAuthConfig() error = %v", err) } // verify config file @@ -459,25 +451,24 @@ func TestConfig_PutCredential_notExistConfig(t *testing.T) { } // verify get - got, err := cfg.GetCredential(server) + got, err := cfg.GetAuthConfig(server) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } - if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential() = %v, want %v", got, want) + if !reflect.DeepEqual(got, authCfg) { + t.Errorf("Config.GetAuthConfig() = %v, want %v", got, authCfg) } } -func TestConfig_PutCredential_addNew(t *testing.T) { +func TestConfig_PutAuthConfig_addNew(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") // prepare test content server1 := "registry1.example.com" - cred1 := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", + authCfg1 := AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + IdentityToken: "refresh_token", + RegistryToken: "access_token", } testCfg := configtest.Config{ @@ -485,8 +476,8 @@ func TestConfig_PutCredential_addNew(t *testing.T) { server1: { SomeAuthField: "whatever", Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred1.RefreshToken, - RegistryToken: cred1.AccessToken, + IdentityToken: "refresh_token", + RegistryToken: "access_token", }, }, SomeConfigField: 123, @@ -505,14 +496,13 @@ func TestConfig_PutCredential_addNew(t *testing.T) { t.Fatal("Load() error =", err) } server2 := "registry2.example.com" - cred2 := auth.Credential{ - Username: "username_2", - Password: "password_2", - RefreshToken: "refresh_token_2", - AccessToken: "access_token_2", + authCfg2 := AuthConfig{ + Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y", + IdentityToken: "refresh_token_2", + RegistryToken: "access_token_2", } - if err := cfg.PutCredential(server2, cred2); err != nil { - t.Fatalf("Config.PutCredential() error = %v", err) + if err := cfg.PutAuthConfig(server2, authCfg2); err != nil { + t.Fatalf("Config.PutAuthConfig() error = %v", err) } // verify config file @@ -530,8 +520,8 @@ func TestConfig_PutCredential_addNew(t *testing.T) { server1: { SomeAuthField: "whatever", Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred1.RefreshToken, - RegistryToken: cred1.AccessToken, + IdentityToken: "refresh_token", + RegistryToken: "access_token", }, server2: { Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y", @@ -546,24 +536,24 @@ func TestConfig_PutCredential_addNew(t *testing.T) { } // verify get - got, err := cfg.GetCredential(server1) + got, err := cfg.GetAuthConfig(server1) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } - if want := cred1; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server1, got, want) + if !reflect.DeepEqual(got, authCfg1) { + t.Errorf("Config.GetAuthConfig(%s) = %v, want %v", server1, got, authCfg1) } - got, err = cfg.GetCredential(server2) + got, err = cfg.GetAuthConfig(server2) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } - if want := cred2; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server2, got, want) + if !reflect.DeepEqual(got, authCfg2) { + t.Errorf("Config.GetAuthConfig(%s) = %v, want %v", server2, got, authCfg2) } } -func TestConfig_PutCredential_updateOld(t *testing.T) { +func TestConfig_PutAuthConfig_updateOld(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") @@ -593,13 +583,12 @@ func TestConfig_PutCredential_updateOld(t *testing.T) { if err != nil { t.Fatal("Load() error =", err) } - cred := auth.Credential{ - Username: "username", - Password: "password", - AccessToken: "access_token", + authCfg := AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + RegistryToken: "access_token", } - if err := cfg.PutCredential(server, cred); err != nil { - t.Fatalf("Config.PutCredential() error = %v", err) + if err := cfg.PutAuthConfig(server, authCfg); err != nil { + t.Fatalf("Config.PutAuthConfig() error = %v", err) } // verify config file @@ -626,41 +615,39 @@ func TestConfig_PutCredential_updateOld(t *testing.T) { } // verify get - got, err := cfg.GetCredential(server) + got, err := cfg.GetAuthConfig(server) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } - if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) + if !reflect.DeepEqual(got, authCfg) { + t.Errorf("Config.GetAuthConfig(%s).Credential() = %v, want %v", server, got, authCfg) } } -func TestConfig_DeleteCredential(t *testing.T) { +func TestConfig_DeleteAuthConfig(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") // prepare test content server1 := "registry1.example.com" - cred1 := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", + cred1 := AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + IdentityToken: "refresh_token", + RegistryToken: "access_token", } server2 := "registry2.example.com" - cred2 := auth.Credential{ - Username: "username_2", - Password: "password_2", - RefreshToken: "refresh_token_2", - AccessToken: "access_token_2", + cred2 := AuthConfig{ + Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y", + IdentityToken: "refresh_token_2", + RegistryToken: "access_token_2", } testCfg := configtest.Config{ AuthConfigs: map[string]configtest.AuthConfig{ server1: { Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred1.RefreshToken, - RegistryToken: cred1.AccessToken, + IdentityToken: cred1.IdentityToken, + RegistryToken: cred1.RegistryToken, }, server2: { Auth: "dXNlcm5hbWVfMjpwYXNzd29yZF8y", @@ -683,24 +670,26 @@ func TestConfig_DeleteCredential(t *testing.T) { t.Fatal("Load() error =", err) } // test get - got, err := cfg.GetCredential(server1) + got, err := cfg.GetAuthConfig(server1) if err != nil { - t.Fatalf("FileStore.GetCredential() error = %v", err) + t.Fatalf("FileStore.GetAuthConfig() error = %v", err) } + if want := cred1; !reflect.DeepEqual(got, want) { - t.Errorf("FileStore.GetCredential(%s) = %v, want %v", server1, got, want) + t.Errorf("FileStore.GetAuthConfig(%s).Credential() = %v, want %v", server1, got, want) } - got, err = cfg.GetCredential(server2) + got, err = cfg.GetAuthConfig(server2) if err != nil { - t.Fatalf("FileStore.GetCredential() error = %v", err) + t.Fatalf("FileStore.GetAuthConfig() error = %v", err) } + if want := cred2; !reflect.DeepEqual(got, want) { - t.Errorf("FileStore.Get(%s) = %v, want %v", server2, got, want) + t.Errorf("FileStore.GetAuthConfig(%s).Credential() = %v, want %v", server2, got, want) } // test delete - if err := cfg.DeleteCredential(server1); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) + if err := cfg.DeleteAuthConfig(server1); err != nil { + t.Fatalf("Config.DeleteAuthConfig() error = %v", err) } // verify config file @@ -724,41 +713,43 @@ func TestConfig_DeleteCredential(t *testing.T) { } // test get again - got, err = cfg.GetCredential(server1) + got, err = cfg.GetAuthConfig(server1) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } - if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server1, got, want) + + want := AuthConfig{} + if !reflect.DeepEqual(got, want) { + t.Errorf("Config.GetAuthConfig(%s).Credential() = %v, want %v", server1, got, want) } - got, err = cfg.GetCredential(server2) + got, err = cfg.GetAuthConfig(server2) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } + if want := cred2; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server2, got, want) + t.Errorf("Config.GetAuthConfig(%s).Credential() = %v, want %v", server2, got, want) } } -func TestConfig_DeleteCredential_lastConfig(t *testing.T) { +func TestConfig_DeleteAuthConfig_lastConfig(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") // prepare test content server := "registry1.example.com" - cred := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", + cred := AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + IdentityToken: "refresh_token", + RegistryToken: "access_token", } testCfg := configtest.Config{ AuthConfigs: map[string]configtest.AuthConfig{ server: { Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred.RefreshToken, - RegistryToken: cred.AccessToken, + IdentityToken: cred.IdentityToken, + RegistryToken: cred.RegistryToken, }, }, SomeConfigField: 123, @@ -776,17 +767,17 @@ func TestConfig_DeleteCredential_lastConfig(t *testing.T) { t.Fatal("Load() error =", err) } // test get - got, err := cfg.GetCredential(server) + got, err := cfg.GetAuthConfig(server) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) + t.Errorf("Config.GetAuthConfig(%s).Credential() = %v, want %v", server, got, want) } // test delete - if err := cfg.DeleteCredential(server); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) + if err := cfg.DeleteAuthConfig(server); err != nil { + t.Fatalf("Config.DeleteAuthConfig() error = %v", err) } // verify config file @@ -808,33 +799,33 @@ func TestConfig_DeleteCredential_lastConfig(t *testing.T) { } // test get again - got, err = cfg.GetCredential(server) + got, err = cfg.GetAuthConfig(server) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } - if want := auth.EmptyCredential; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) + want := AuthConfig{} + if !reflect.DeepEqual(got, want) { + t.Errorf("Config.GetAuthConfig(%s).Credential() = %v, want %v", server, got, want) } } -func TestConfig_DeleteCredential_notExistRecord(t *testing.T) { +func TestConfig_DeleteAuthConfig_notExistRecord(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") // prepare test content server := "registry1.example.com" - cred := auth.Credential{ - Username: "username", - Password: "password", - RefreshToken: "refresh_token", - AccessToken: "access_token", + cred := AuthConfig{ + Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", + IdentityToken: "refresh_token", + RegistryToken: "access_token", } testCfg := configtest.Config{ AuthConfigs: map[string]configtest.AuthConfig{ server: { Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - IdentityToken: cred.RefreshToken, - RegistryToken: cred.AccessToken, + IdentityToken: cred.IdentityToken, + RegistryToken: cred.RegistryToken, }, }, SomeConfigField: 123, @@ -852,17 +843,17 @@ func TestConfig_DeleteCredential_notExistRecord(t *testing.T) { t.Fatal("Load() error =", err) } // test get - got, err := cfg.GetCredential(server) + got, err := cfg.GetAuthConfig(server) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) + t.Errorf("Config.GetAuthConfig(%s).Credential() = %v, want %v", server, got, want) } // test delete - if err := cfg.DeleteCredential("test.example.com"); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) + if err := cfg.DeleteAuthConfig("test.example.com"); err != nil { + t.Fatalf("Config.DeleteAuthConfig() error = %v", err) } // verify config file @@ -886,16 +877,16 @@ func TestConfig_DeleteCredential_notExistRecord(t *testing.T) { } // test get again - got, err = cfg.GetCredential(server) + got, err = cfg.GetAuthConfig(server) if err != nil { - t.Fatalf("Config.GetCredential() error = %v", err) + t.Fatalf("Config.GetAuthConfig() error = %v", err) } if want := cred; !reflect.DeepEqual(got, want) { - t.Errorf("Config.GetCredential(%s) = %v, want %v", server, got, want) + t.Errorf("Config.GetAuthConfig(%s).Credential() = %v, want %v", server, got, want) } } -func TestConfig_DeleteCredential_notExistConfig(t *testing.T) { +func TestConfig_DeleteAuthConfig_notExistConfig(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.json") @@ -906,8 +897,8 @@ func TestConfig_DeleteCredential_notExistConfig(t *testing.T) { server := "test.example.com" // test delete - if err := cfg.DeleteCredential(server); err != nil { - t.Fatalf("Config.DeleteCredential() error = %v", err) + if err := cfg.DeleteAuthConfig(server); err != nil { + t.Fatalf("Config.DeleteAuthConfig() error = %v", err) } // verify config file is not created @@ -918,7 +909,7 @@ func TestConfig_DeleteCredential_notExistConfig(t *testing.T) { } func TestConfig_GetCredentialHelper(t *testing.T) { - cfg, err := Load("../../testdata/credHelpers_config.json") + cfg, err := Load("./testdata/credHelpers_config.json") if err != nil { t.Fatal("Load() error =", err) } @@ -966,12 +957,12 @@ func TestConfig_CredentialsStore(t *testing.T) { }{ { name: "creds store configured", - configPath: "../../testdata/credsStore_config.json", + configPath: "./testdata/credsStore_config.json", want: "teststore", }, { name: "No creds store configured", - configPath: "../../testdata/credsHelpers_config.json", + configPath: "./testdata/credsHelpers_config.json", want: "", }, } @@ -1014,8 +1005,8 @@ func TestConfig_SetCredentialsStore(t *testing.T) { } // verify - if got := cfg.credentialsStore; got != credsStore { - t.Errorf("Config.credentialsStore = %v, want %v", got, credsStore) + if got := cfg.CredentialsStore(); got != credsStore { + t.Errorf("Config.CredentialsStore() = %v, want %v", got, credsStore) } // verify config file configFile, err := os.Open(configPath) @@ -1044,8 +1035,8 @@ func TestConfig_SetCredentialsStore(t *testing.T) { t.Fatal("Config.SetCredentialsStore() error =", err) } // verify - if got := cfg.credentialsStore; got != "" { - t.Errorf("Config.credentialsStore = %v, want empty", got) + if got := cfg.CredentialsStore(); got != "" { + t.Errorf("Config.CredentialsStore() = %v, want empty", got) } // verify config file configFile, err = os.Open(configPath) @@ -1199,243 +1190,6 @@ func TestConfig_IsAuthConfigured(t *testing.T) { } } -func TestConfig_saveFile(t *testing.T) { - tempDir := t.TempDir() - tests := []struct { - name string - fileName string - shouldCreateFile bool - oldCfg configtest.Config - newCfg configtest.Config - wantCfg configtest.Config - }{ - { - name: "set credsStore in a non-existing file", - fileName: "config.json", - oldCfg: configtest.Config{}, - newCfg: configtest.Config{ - CredentialsStore: "teststore", - }, - wantCfg: configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - CredentialsStore: "teststore", - }, - shouldCreateFile: false, - }, - { - name: "set credsStore in empty file", - fileName: "empty.json", - oldCfg: configtest.Config{}, - newCfg: configtest.Config{ - CredentialsStore: "teststore", - }, - wantCfg: configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - CredentialsStore: "teststore", - }, - shouldCreateFile: true, - }, - { - name: "set credsStore in a no-auth-configured file", - fileName: "empty.json", - oldCfg: configtest.Config{ - SomeConfigField: 123, - }, - newCfg: configtest.Config{ - CredentialsStore: "teststore", - }, - wantCfg: configtest.Config{ - SomeConfigField: 123, - AuthConfigs: make(map[string]configtest.AuthConfig), - CredentialsStore: "teststore", - }, - shouldCreateFile: true, - }, - { - name: "Set credsStore and credHelpers in an auth-configured file", - fileName: "auth_configured.json", - oldCfg: configtest.Config{ - SomeConfigField: 123, - AuthConfigs: map[string]configtest.AuthConfig{ - "registry1.example.com": { - SomeAuthField: "something", - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - }, - }, - CredentialsStore: "oldstore", - CredentialHelpers: map[string]string{ - "registry2.example.com": "testhelper", - }, - }, - newCfg: configtest.Config{ - AuthConfigs: make(map[string]configtest.AuthConfig), - SomeConfigField: 123, - CredentialsStore: "newstore", - CredentialHelpers: map[string]string{ - "xxx": "yyy", - }, - }, - wantCfg: configtest.Config{ - SomeConfigField: 123, - AuthConfigs: map[string]configtest.AuthConfig{ - "registry1.example.com": { - SomeAuthField: "something", - Auth: "dXNlcm5hbWU6cGFzc3dvcmQ=", - }, - }, - CredentialsStore: "newstore", - CredentialHelpers: map[string]string{ - "registry2.example.com": "testhelper", // cred helpers will not be updated - }, - }, - shouldCreateFile: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // prepare test content - configPath := filepath.Join(tempDir, tt.fileName) - if tt.shouldCreateFile { - jsonStr, err := json.Marshal(tt.oldCfg) - if err != nil { - t.Fatalf("failed to marshal config: %v", err) - } - if err := os.WriteFile(configPath, jsonStr, 0666); err != nil { - t.Fatalf("failed to write config file: %v", err) - } - } - - cfg, err := Load(configPath) - if err != nil { - t.Fatal("Load() error =", err) - } - cfg.credentialsStore = tt.newCfg.CredentialsStore - cfg.credentialHelpers = tt.newCfg.CredentialHelpers - if err := cfg.saveFile(); err != nil { - t.Fatal("saveFile() error =", err) - } - - // verify config file - configFile, err := os.Open(configPath) - if err != nil { - t.Fatalf("failed to open config file: %v", err) - } - defer configFile.Close() - var gotCfg configtest.Config - if err := json.NewDecoder(configFile).Decode(&gotCfg); err != nil { - t.Fatalf("failed to decode config file: %v", err) - } - if !reflect.DeepEqual(gotCfg, tt.wantCfg) { - t.Errorf("Decoded config = %v, want %v", gotCfg, tt.wantCfg) - } - }) - } -} - -func Test_encodeAuth(t *testing.T) { - tests := []struct { - name string - username string - password string - want string - }{ - { - name: "Username and password", - username: "username", - password: "password", - want: "dXNlcm5hbWU6cGFzc3dvcmQ=", - }, - { - name: "Username only", - username: "username", - password: "", - want: "dXNlcm5hbWU6", - }, - { - name: "Password only", - username: "", - password: "password", - want: "OnBhc3N3b3Jk", - }, - { - name: "Empty username and empty password", - username: "", - password: "", - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := encodeAuth(tt.username, tt.password); got != tt.want { - t.Errorf("encodeAuth() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_decodeAuth(t *testing.T) { - tests := []struct { - name string - authStr string - username string - password string - wantErr bool - }{ - { - name: "Valid base64", - authStr: "dXNlcm5hbWU6cGFzc3dvcmQ=", // username:password - username: "username", - password: "password", - }, - { - name: "Valid base64, username only", - authStr: "dXNlcm5hbWU6", // username: - username: "username", - }, - { - name: "Valid base64, password only", - authStr: "OnBhc3N3b3Jk", // :password - password: "password", - }, - { - name: "Valid base64, bad format", - authStr: "d2hhdGV2ZXI=", // whatever - username: "", - password: "", - wantErr: true, - }, - { - name: "Invalid base64", - authStr: "whatever", - username: "", - password: "", - wantErr: true, - }, - { - name: "Empty string", - authStr: "", - username: "", - password: "", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotUsername, gotPassword, err := decodeAuth(tt.authStr) - if (err != nil) != tt.wantErr { - t.Errorf("decodeAuth() error = %v, wantErr %v", err, tt.wantErr) - return - } - if gotUsername != tt.username { - t.Errorf("decodeAuth() got = %v, want %v", gotUsername, tt.username) - } - if gotPassword != tt.password { - t.Errorf("decodeAuth() got1 = %v, want %v", gotPassword, tt.password) - } - }) - } -} - func Test_toHostname(t *testing.T) { tests := []struct { name string @@ -1485,9 +1239,10 @@ func Test_toHostname(t *testing.T) { } func TestConfig_Path(t *testing.T) { - mockedPath := "/path/to/config.json" - config := Config{ - path: mockedPath, + mockedPath := "testdata/valid_auths_config.json" + config, err := Load(mockedPath) + if err != nil { + t.Fatal("Load() error =", err) } if got := config.Path(); got != mockedPath { t.Errorf("Config.Path() = %v, want %v", got, mockedPath) diff --git a/registry/remote/credentials/internal/config/configtest/config.go b/registry/remote/internal/configuration/configtest/config.go similarity index 100% rename from registry/remote/credentials/internal/config/configtest/config.go rename to registry/remote/internal/configuration/configtest/config.go diff --git a/registry/remote/credentials/testdata/bad_config b/registry/remote/internal/configuration/testdata/bad_config similarity index 100% rename from registry/remote/credentials/testdata/bad_config rename to registry/remote/internal/configuration/testdata/bad_config diff --git a/registry/remote/credentials/testdata/credHelpers_config.json b/registry/remote/internal/configuration/testdata/credHelpers_config.json similarity index 100% rename from registry/remote/credentials/testdata/credHelpers_config.json rename to registry/remote/internal/configuration/testdata/credHelpers_config.json diff --git a/registry/remote/credentials/testdata/credsStore_config.json b/registry/remote/internal/configuration/testdata/credsStore_config.json similarity index 100% rename from registry/remote/credentials/testdata/credsStore_config.json rename to registry/remote/internal/configuration/testdata/credsStore_config.json diff --git a/registry/remote/credentials/testdata/empty.json b/registry/remote/internal/configuration/testdata/empty.json similarity index 100% rename from registry/remote/credentials/testdata/empty.json rename to registry/remote/internal/configuration/testdata/empty.json diff --git a/registry/remote/credentials/testdata/invalid_auths_config.json b/registry/remote/internal/configuration/testdata/invalid_auths_config.json similarity index 100% rename from registry/remote/credentials/testdata/invalid_auths_config.json rename to registry/remote/internal/configuration/testdata/invalid_auths_config.json diff --git a/registry/remote/credentials/testdata/invalid_auths_entry_config.json b/registry/remote/internal/configuration/testdata/invalid_auths_entry_config.json similarity index 100% rename from registry/remote/credentials/testdata/invalid_auths_entry_config.json rename to registry/remote/internal/configuration/testdata/invalid_auths_entry_config.json diff --git a/registry/remote/credentials/testdata/legacy_auths_config.json b/registry/remote/internal/configuration/testdata/legacy_auths_config.json similarity index 100% rename from registry/remote/credentials/testdata/legacy_auths_config.json rename to registry/remote/internal/configuration/testdata/legacy_auths_config.json diff --git a/registry/remote/credentials/testdata/no_auths_config.json b/registry/remote/internal/configuration/testdata/no_auths_config.json similarity index 100% rename from registry/remote/credentials/testdata/no_auths_config.json rename to registry/remote/internal/configuration/testdata/no_auths_config.json diff --git a/registry/remote/credentials/testdata/valid_auths_config.json b/registry/remote/internal/configuration/testdata/valid_auths_config.json similarity index 100% rename from registry/remote/credentials/testdata/valid_auths_config.json rename to registry/remote/internal/configuration/testdata/valid_auths_config.json diff --git a/registry/remote/credentials/testdata/whitespace.json b/registry/remote/internal/configuration/testdata/whitespace.json similarity index 100% rename from registry/remote/credentials/testdata/whitespace.json rename to registry/remote/internal/configuration/testdata/whitespace.json diff --git a/registry/remote/credentials/internal/ioutil/ioutil.go b/registry/remote/internal/ioutil/ioutil.go similarity index 100% rename from registry/remote/credentials/internal/ioutil/ioutil.go rename to registry/remote/internal/ioutil/ioutil.go