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)
}
65 changes: 65 additions & 0 deletions registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
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"
"errors"
"fmt"

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

// Login provides the login functionality with the given credentials. The target
// 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 {
// create a clone of the original registry for login purpose
regClone := *reg
// we use the original client if applicable, otherwise use a default client
var authClient auth.Client
if reg.Client == nil {
authClient = *auth.DefaultClient
authClient.Cache = nil // no cache
} else if client, ok := reg.Client.(*auth.Client); ok {
authClient = *client
} else {
return errors.New("client type not supported")
}
regClone.Client = &authClient
// update credentials with the client
authClient.Credential = auth.StaticCredential(reg.Reference.Registry, cred)
// login and store credential
if err := regClone.Ping(ctx); err != nil {
return fmt.Errorf("unable to login to the registry %s: %w", regClone.Reference.Registry, err)
}
hostname := mapHostname(regClone.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 {
delete(t.storage, serverAddress)
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)
reg, err := remote.NewRegistry(uri.Host)
if err != nil {
t.Fatalf("cannot create test registry: %v", err)
}
reg.PlainHTTP = true
// create a test store
ns := &testStore{}
tests := []struct {
name string
ctx context.Context
registry *remote.Registry
cred auth.Credential
wantErr bool
}{
{
name: "login succeeds",
ctx: context.Background(),
registry: reg,
cred: auth.Credential{Username: testUsername, Password: testPassword},
wantErr: false,
},
{
name: "login fails (incorrect password)",
ctx: context.Background(),
registry: reg,
cred: auth.Credential{Username: testUsername, Password: "whatever"},
wantErr: true,
},
{
name: "login fails (nil context makes remote.Ping fails)",
ctx: nil,
registry: reg,
cred: auth.Credential{Username: testUsername, Password: testPassword},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 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)
}
ns.Delete(tt.ctx, reg.Reference.Registry)
})
}
}

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)
}
})
}
}