Skip to content
This repository has been archived by the owner on Apr 7, 2024. It is now read-only.

feat: support login #42

Merged
merged 14 commits into from
Apr 13, 2023
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ require (
)

require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
Expand Down
6 changes: 3 additions & 3 deletions native_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func NewNativeStore(helperSuffix string) Store {
}
}

// Get retrieves credentials from the store for the given server
// Get retrieves credentials from the store for the given server.
func (ns *NativeStore) Get(_ context.Context, serverAddress string) (auth.Credential, error) {
var cred auth.Credential
dockerCred, err := client.Get(ns.programFunc, serverAddress)
Expand All @@ -63,7 +63,7 @@ func (ns *NativeStore) Get(_ context.Context, serverAddress string) (auth.Creden
return cred, nil
}

// Put saves credentials into the store
// Put saves credentials into the store.
func (ns *NativeStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error {
dockerCred := &credentials.Credentials{
ServerURL: serverAddress,
Expand All @@ -77,7 +77,7 @@ func (ns *NativeStore) Put(_ context.Context, serverAddress string, cred auth.Cr
return client.Store(ns.programFunc, dockerCred)
}

// Delete removes credentials from the store for the given server
// Delete removes credentials from the store for the given server.
func (ns *NativeStore) Delete(_ context.Context, serverAddress string) error {
return client.Erase(ns.programFunc, serverAddress)
}
51 changes: 51 additions & 0 deletions registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
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"

"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/retry"
)

// Login provides the login functionality with the given credentials.
func Login(ctx context.Context, store Store, registry remote.Registry, cred auth.Credential) error {
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
registry.Client = &auth.Client{
Client: retry.DefaultClient,
Cache: auth.DefaultCache,
Credential: auth.StaticCredential(registry.Reference.Registry, cred),
}
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
if err := registry.Ping(ctx); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cred is never used in Ping().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word registry is a bit overloaded here although I don't have a better suggestion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word registry is a bit overloaded here although I don't have a better suggestion.

Registry is the proper name here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The word registry is a bit overloaded here although I don't have a better suggestion.

reg might be good enough

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to reg

return fmt.Errorf("unable to login to the registry %s: %w", registry.Reference.Registry, err)
}
hostname := mapHostname(registry.Reference.Registry)
if err := store.Put(ctx, hostname, cred); err != nil {
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("unable to store the credential for %s: %w", hostname, err)
}
return nil
}

func mapHostname(hostname string) string {
shizhMSFT marked this conversation as resolved.
Show resolved Hide resolved
// The Docker CLI expects that the 'docker.io' credential
// will be added under the key "https://index.docker.io/v1/"
if hostname == "docker.io" {
return "https://index.docker.io/v1/"
}
return hostname
}
143 changes: 143 additions & 0 deletions registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
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"
"encoding/base64"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"

"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
)

// testStore implements the Store interface, used for testing purpose.
type testStore struct {
storage map[string]auth.Credential
}

func (t *testStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) {
return t.storage[serverAddress], nil
}

func (t *testStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error {
if len(t.storage) == 0 {
t.storage = make(map[string]auth.Credential)
}
t.storage[serverAddress] = cred
return nil
}

func (t *testStore) Delete(ctx context.Context, serverAddress string) error {
return nil
}

func TestLogin(t *testing.T) {
// create a test registry
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
wantedAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(testUsername+":"+testPassword))
authHeader := r.Header.Get("Authorization")
if authHeader != wantedAuthHeader {
w.Header().Set("Www-Authenticate", `Basic realm="Test Server"`)
w.WriteHeader(http.StatusUnauthorized)
}
}))
defer ts.Close()
uri, _ := url.Parse(ts.URL)

// create a test store
ns := &testStore{}
tests := []struct {
name string
ctx context.Context
registry string
cred auth.Credential
wantErr bool
}{
{
name: "login fails (incorrect password)",
ctx: context.Background(),
registry: uri.Host,
cred: auth.Credential{Username: testUsername, Password: "whatever"},
wantErr: true,
},
{
name: "login fails (nil context makes remote.Ping fails)",
ctx: nil,
registry: uri.Host,
cred: auth.Credential{Username: testUsername, Password: testPassword},
wantErr: true,
},
{
name: "login succeeds",
ctx: context.Background(),
registry: uri.Host,
cred: auth.Credential{Username: testUsername, Password: testPassword},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// create test registry
reg, err := remote.NewRegistry(tt.registry)
if err != nil {
t.Fatalf("cannot create test registry: %v", err)
}
reg.PlainHTTP = true
// login to test registry
err = Login(tt.ctx, ns, *reg, tt.cred)
if (err != nil) != tt.wantErr {
t.Fatalf("Login() error = %v, wantErr %v", err, tt.wantErr)
}
if err != nil {
return
}
if got := ns.storage[reg.Reference.Registry]; !reflect.DeepEqual(got, tt.cred) {
t.Fatalf("Stored credential = %v, want %v", got, tt.cred)
}
})
}
}

func Test_mapHostname(t *testing.T) {
tests := []struct {
name string
host string
want string
}{
{
"map docker.io to https://index.docker.io/v1/",
"docker.io",
"https://index.docker.io/v1/",
},
{
"do not map other host names",
"localhost:2333",
"localhost:2333",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := mapHostname(tt.host); got != tt.want {
t.Errorf("mapHostname() = %v, want %v", got, tt.want)
}
})
}
}