blob: 9204245e4f93347545ecaf466e751dce4d23d950 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 creds_test
import (
"context"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/apache/dubbo-kubernetes/app/dubboctl/internal/docker"
"github.com/apache/dubbo-kubernetes/app/dubboctl/internal/docker/creds"
"github.com/docker/docker-credential-helpers/credentials"
. "github.com/apache/dubbo-kubernetes/app/dubboctl/internal/testing"
)
func Test_registryEquals(t *testing.T) {
tests := []struct {
name string
urlA string
urlB string
want bool
}{
{"no port matching host", "quay.io", "quay.io", true},
{"non-matching host added sub-domain", "sub.quay.io", "quay.io", false},
{"non-matching host different sub-domain", "sub.quay.io", "sub3.quay.io", false},
{"localhost", "localhost", "localhost", true},
{"localhost with standard ports", "localhost:80", "localhost:443", false},
{"localhost with matching port", "https://localhost:1234", "http://localhost:1234", true},
{"localhost with match by default port 80", "http://localhost", "localhost:80", true},
{"localhost with match by default port 443", "https://localhost", "localhost:443", true},
{"localhost with mismatch by non-default port 5000", "https://localhost", "localhost:5000", false},
{"localhost with match by empty ports", "https://localhost", "http://localhost", true},
{"docker.io matching host https", "https://docker.io", "docker.io", true},
{"docker.io matching host http", "http://docker.io", "docker.io", true},
{"docker.io with path", "docker.io/v1/", "docker.io", true},
{"docker.io with protocol and path", "https://docker.io/v1/", "docker.io", true},
{"docker.io with subdomain index.", "https://index.docker.io/v1/", "docker.io", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := creds.RegistryEquals(tt.urlA, tt.urlB); got != tt.want {
t.Errorf("to2ndLevelDomain() = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckAuth(t *testing.T) {
const (
uname = "testuser"
pwd = "testpwd"
incorrectPwd = "badpwd"
)
localhost, localhostTLS := startServer(t, uname, pwd)
_, portTLS, err := net.SplitHostPort(localhostTLS)
if err != nil {
t.Fatal(err)
}
nonLocalhostTLS := "test.io:" + portTLS
type args struct {
ctx context.Context
username string
password string
registry string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "correct credentials localhost no-TLS",
args: args{
ctx: context.Background(),
username: uname,
password: pwd,
registry: localhost,
},
wantErr: false,
},
{
name: "correct credentials localhost",
args: args{
ctx: context.Background(),
username: uname,
password: pwd,
registry: localhostTLS,
},
wantErr: false,
},
{
name: "correct credentials non-localhost",
args: args{
ctx: context.Background(),
username: uname,
password: pwd,
registry: nonLocalhostTLS,
},
wantErr: false,
},
{
name: "incorrect credentials localhost no-TLS",
args: args{
ctx: context.Background(),
username: uname,
password: incorrectPwd,
registry: localhost,
},
wantErr: true,
},
{
name: "incorrect credentials localhost",
args: args{
ctx: context.Background(),
username: uname,
password: incorrectPwd,
registry: localhostTLS,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := docker.Credentials{
Username: tt.args.username,
Password: tt.args.password,
}
if err := creds.CheckAuth(tt.args.ctx, tt.args.registry+"/someorg/someimage:sometag", c, http.DefaultTransport); (err != nil) != tt.wantErr {
t.Errorf("CheckAuth() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestCheckAuthEmptyCreds(t *testing.T) {
localhost, _ := startServer(t, "", "")
err := creds.CheckAuth(context.Background(), localhost+"/someorg/someimage:sometag", docker.Credentials{}, http.DefaultTransport)
if err != nil {
t.Error(err)
}
}
func startServer(t *testing.T, uname, pwd string) (addr, addrTLS string) {
// TODO: this should be refactored to use OS-chosen ports so as not to
// fail when a user is running a function on the default port.)
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
addr = listener.Addr().String()
listenerTLS, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
addrTLS = listenerTLS.Addr().String()
handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
if uname == "" || pwd == "" {
if req.Method == http.MethodPost {
resp.WriteHeader(http.StatusCreated)
} else {
resp.WriteHeader(http.StatusOK)
}
return
}
// TODO add also test for token based auth
resp.Header().Add("WWW-Authenticate", "basic")
if u, p, ok := req.BasicAuth(); ok {
if u == uname && p == pwd {
if req.Method == http.MethodPost {
resp.WriteHeader(http.StatusCreated)
} else {
resp.WriteHeader(http.StatusOK)
}
return
}
}
resp.WriteHeader(http.StatusUnauthorized)
})
randReader := rand.Reader
caPublicKey, caPrivateKey, err := ed25519.GenerateKey(randReader)
if err != nil {
t.Fatal(err)
}
ca := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "localhost",
},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
DNSNames: []string{"localhost", "test.io"},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
ExtraExtensions: []pkix.Extension{},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caBytes, err := x509.CreateCertificate(randReader, ca, ca, caPublicKey, caPrivateKey)
if err != nil {
t.Fatal(err)
}
ca, err = x509.ParseCertificate(caBytes)
if err != nil {
t.Fatal(err)
}
cert := tls.Certificate{
Certificate: [][]byte{caBytes},
PrivateKey: caPrivateKey,
Leaf: ca,
}
server := http.Server{
Handler: handler,
TLSConfig: &tls.Config{
ServerName: "localhost",
Certificates: []tls.Certificate{cert},
},
}
go func() {
err := server.ServeTLS(listenerTLS, "", "")
if err != nil && !strings.Contains(err.Error(), "Server closed") {
panic(err)
}
}()
go func() {
err := server.Serve(listener)
if err != nil && !strings.Contains(err.Error(), "Server closed") {
panic(err)
}
}()
// make the testing CA trusted by default HTTP transport/client
oldDefaultTransport := http.DefaultTransport
newDefaultTransport := http.DefaultTransport.(*http.Transport).Clone()
http.DefaultTransport = newDefaultTransport
caPool := x509.NewCertPool()
caPool.AddCert(ca)
newDefaultTransport.TLSClientConfig.RootCAs = caPool
dc := newDefaultTransport.DialContext
newDefaultTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
h, p, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if h == "test.io" {
h = "localhost"
}
addr = net.JoinHostPort(h, p)
return dc(ctx, network, addr)
}
t.Cleanup(func() {
err := server.Shutdown(context.Background())
if err != nil {
t.Fatal(err)
}
http.DefaultTransport = oldDefaultTransport
})
return addr, addrTLS
}
const (
dockerIoUser = "testUser1"
dockerIoUserPwd = "goodPwd1"
quayIoUser = "testUser2"
quayIoUserPwd = "goodPwd2"
)
type Credentials = docker.Credentials
func TestNewCredentialsProvider(t *testing.T) {
withCleanHome(t)
helperWithQuayIO := newInMemoryHelper()
err := helperWithQuayIO.Add(&credentials.Credentials{
ServerURL: "quay.io",
Username: quayIoUser,
Secret: quayIoUserPwd,
})
if err != nil {
t.Fatal(err)
}
type args struct {
promptUser creds.CredentialsCallback
verifyCredentials creds.VerifyCredentialsCallback
additionalLoaders []creds.CredentialsCallback
registry string
setUpEnv setUpEnv
}
tests := []struct {
name string
args args
want Credentials
}{
{
name: "test user callback correct password on first try",
args: args{
promptUser: correctPwdCallback,
verifyCredentials: correctVerifyCbk,
registry: "docker.io",
},
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
},
{
name: "test user callback correct password on second try",
args: args{
promptUser: pwdCbkFirstWrongThenCorrect(t),
verifyCredentials: correctVerifyCbk,
registry: "docker.io",
},
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
},
{
name: "get quay-io credentials with func config populated",
args: args{
promptUser: pwdCbkThatShallNotBeCalled(t),
verifyCredentials: correctVerifyCbk,
registry: "quay.io",
setUpEnv: withPopulatedFuncAuthConfig,
},
want: Credentials{Username: quayIoUser, Password: quayIoUserPwd},
},
{
name: "get docker-io credentials with func config populated",
args: args{
promptUser: pwdCbkThatShallNotBeCalled(t),
verifyCredentials: correctVerifyCbk,
registry: "docker.io",
setUpEnv: withPopulatedFuncAuthConfig,
},
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
},
{
name: "get quay-io credentials with docker config populated",
args: args{
promptUser: pwdCbkThatShallNotBeCalled(t),
verifyCredentials: correctVerifyCbk,
registry: "quay.io",
setUpEnv: all(
withPopulatedDockerAuthConfig,
setUpMockHelper("docker-credential-mock", helperWithQuayIO)),
},
want: Credentials{Username: quayIoUser, Password: quayIoUserPwd},
},
{
name: "get docker-io credentials from custom loader",
args: args{
promptUser: pwdCbkThatShallNotBeCalled(t),
verifyCredentials: correctVerifyCbk,
registry: "docker.io",
additionalLoaders: []creds.CredentialsCallback{correctPwdCallback},
},
want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer cleanUpConfigs(t)
if tt.args.setUpEnv != nil {
tt.args.setUpEnv(t)
}
credentialsProvider := creds.NewCredentialsProvider(
testConfigPath(t),
creds.WithPromptForCredentials(tt.args.promptUser),
creds.WithVerifyCredentials(tt.args.verifyCredentials),
creds.WithAdditionalCredentialLoaders(tt.args.additionalLoaders...))
got, err := credentialsProvider(context.Background(), tt.args.registry+"/someorg/someimage:sometag")
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if got != tt.want {
t.Errorf("credentialsProvider() = %v, want %v", got, tt.want)
}
})
}
}
func TestNewCredentialsProviderEmptyCreds(t *testing.T) {
withCleanHome(t)
credentialsProvider := creds.NewCredentialsProvider(testConfigPath(t), creds.WithVerifyCredentials(func(ctx context.Context, image string, credentials docker.Credentials) error {
if image == "localhost:5555/someorg/someimage:sometag" && credentials == (docker.Credentials{}) {
return nil
}
t.Fatal("unreachable")
return nil
}))
c, err := credentialsProvider(context.Background(), "localhost:5555/someorg/someimage:sometag")
if err != nil {
t.Error(err)
}
if c != (docker.Credentials{}) {
t.Error("unexpected credentials")
}
}
func TestCredentialsProviderSavingFromUserInput(t *testing.T) {
withCleanHome(t)
helper := newInMemoryHelper()
setUpMockHelper("docker-credential-mock", helper)(t)
var pwdCbkInvocations int
pwdCbk := func(r string) (Credentials, error) {
pwdCbkInvocations++
return correctPwdCallback(r)
}
chooseNoStore := func(available []string) (string, error) {
if len(available) < 1 {
t.Errorf("this should have been invoked with non empty list")
}
return "", nil
}
chooseMockStore := func(available []string) (string, error) {
if len(available) < 1 {
t.Errorf("this should have been invoked with non empty list")
}
return "docker-credential-mock", nil
}
shallNotBeInvoked := func(available []string) (string, error) {
t.Fatal("this choose helper callback shall not be invoked")
return "", errors.New("this callback shall not be invoked")
}
credentialsProvider := creds.NewCredentialsProvider(
testConfigPath(t),
creds.WithPromptForCredentials(pwdCbk),
creds.WithVerifyCredentials(correctVerifyCbk),
creds.WithPromptForCredentialStore(chooseNoStore))
_, err := credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag")
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
// now credentials should not be saved because no helper was provided
l, err := helper.List()
if err != nil {
t.Fatal(err)
}
credsInStore := len(l)
if credsInStore != 0 {
t.Errorf("expected to have zero credentials in store, but has: %d", credsInStore)
}
credentialsProvider = creds.NewCredentialsProvider(
testConfigPath(t),
creds.WithPromptForCredentials(pwdCbk),
creds.WithVerifyCredentials(correctVerifyCbk),
creds.WithPromptForCredentialStore(chooseMockStore))
_, err = credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag")
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if pwdCbkInvocations != 2 {
t.Errorf("the pwd callback should have been invoked exactly twice but was invoked %d time", pwdCbkInvocations)
}
// now credentials should be saved in the mock secure store
l, err = helper.List()
if err != nil {
t.Fatal(err)
}
credsInStore = len(l)
if len(l) != 1 {
t.Errorf("expected to have exactly one credentials in store, but has: %d", credsInStore)
}
credentialsProvider = creds.NewCredentialsProvider(
testConfigPath(t),
creds.WithPromptForCredentials(pwdCbkThatShallNotBeCalled(t)),
creds.WithVerifyCredentials(correctVerifyCbk),
creds.WithPromptForCredentialStore(shallNotBeInvoked))
_, err = credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag")
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
}
func cleanUpConfigs(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
}
os.RemoveAll(filepath.Join(home, ".docker"))
}
type setUpEnv = func(t *testing.T)
func withPopulatedDockerAuthConfig(t *testing.T) {
t.Helper()
home, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
}
dockerConfigDir := filepath.Join(home, ".docker")
dockerConfigPath := filepath.Join(dockerConfigDir, "config.json")
err = os.MkdirAll(filepath.Dir(dockerConfigPath), 0o700)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(dockerConfigDir) })
configJSON := `{
"auths": {
"docker.io": { "auth": "%s" },
"quay.io": {}
},
"credsStore": "mock"
}`
configJSON = fmt.Sprintf(configJSON, base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd)))
err = os.WriteFile(dockerConfigPath, []byte(configJSON), 0o600)
if err != nil {
t.Fatal(err)
}
}
func withPopulatedFuncAuthConfig(t *testing.T) {
t.Helper()
var err error
authConfig := filepath.Join(testConfigPath(t), "auth.json")
err = os.MkdirAll(filepath.Dir(authConfig), 0o700)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.RemoveAll(authConfig) })
authJSON := `{
"auths": {
"docker.io": { "auth": "%s" },
"quay.io": { "auth": "%s" }
}
}`
authJSON = fmt.Sprintf(authJSON,
base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd)),
base64.StdEncoding.EncodeToString([]byte(quayIoUser+":"+quayIoUserPwd)))
err = os.WriteFile(authConfig, []byte(authJSON), 0o600)
if err != nil {
t.Fatal(err)
}
}
func pwdCbkThatShallNotBeCalled(t *testing.T) creds.CredentialsCallback {
t.Helper()
return func(registry string) (Credentials, error) {
return Credentials{}, errors.New("this pwd cbk code shall not be called")
}
}
func pwdCbkFirstWrongThenCorrect(t *testing.T) func(registry string) (Credentials, error) {
t.Helper()
var firstInvocation bool
return func(registry string) (Credentials, error) {
if registry != "index.docker.io" && registry != "quay.io" {
return Credentials{}, fmt.Errorf("unexpected registry: %s", registry)
}
if firstInvocation {
firstInvocation = false
return Credentials{Username: dockerIoUser, Password: "badPwd"}, nil
}
return correctPwdCallback(registry)
}
}
func correctPwdCallback(registry string) (Credentials, error) {
if registry == "index.docker.io" {
return Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, nil
}
if registry == "quay.io" {
return Credentials{Username: quayIoUser, Password: quayIoUserPwd}, nil
}
return Credentials{}, errors.New("this cbk don't know the pwd")
}
func correctVerifyCbk(ctx context.Context, image string, credentials Credentials) error {
username, password := credentials.Username, credentials.Password
if username == dockerIoUser && password == dockerIoUserPwd && image == "docker.io/someorg/someimage:sometag" {
return nil
}
if username == quayIoUser && password == quayIoUserPwd && image == "quay.io/someorg/someimage:sometag" {
return nil
}
return creds.ErrUnauthorized
}
func testHomeEnvName(t *testing.T) string {
t.Helper()
if runtime.GOOS == "windows" {
return "USERPROFILE"
}
return "HOME"
}
func withCleanHome(t *testing.T) {
t.Helper()
tmpHome := t.TempDir()
t.Setenv(testHomeEnvName(t), tmpHome)
if runtime.GOOS == "linux" {
t.Setenv("XDG_CONFIG_HOME", filepath.Join(tmpHome, ".config"))
}
}
func testConfigPath(t *testing.T) string {
t.Helper()
home := os.Getenv(testHomeEnvName(t))
configPath := filepath.Join(home, ".config", "func")
if err := os.MkdirAll(configPath, os.ModePerm); err != nil {
t.Fatal(err)
}
return configPath
}
func handlerForCredHelper(t *testing.T, credHelper credentials.Helper) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
defer request.Body.Close()
var err error
var outBody interface{}
uri := strings.Trim(request.RequestURI, "/")
var serverURL string
if uri == "get" || uri == "erase" {
data, err := io.ReadAll(request.Body)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
return
}
serverURL = string(data)
serverURL = strings.Trim(serverURL, "\n\r\t ")
}
switch uri {
case "list":
var list map[string]string
list, err = credHelper.List()
if err == nil {
outBody = &list
}
case "store":
creds := credentials.Credentials{}
dec := json.NewDecoder(request.Body)
err = dec.Decode(&creds)
if err != nil {
break
}
err = credHelper.Add(&creds)
case "get":
var user, secret string
user, secret, err = credHelper.Get(serverURL)
if err == nil {
outBody = &credentials.Credentials{ServerURL: serverURL, Username: user, Secret: secret}
}
case "erase":
err = credHelper.Delete(serverURL)
default:
writer.WriteHeader(http.StatusNotFound)
return
}
if err != nil {
if credentials.IsErrCredentialsNotFound(err) {
writer.WriteHeader(http.StatusNotFound)
} else {
writer.WriteHeader(http.StatusInternalServerError)
writer.Header().Add("Content-Type", "text/plain")
fmt.Fprintf(writer, "error: %+v\n", err)
}
return
}
if outBody != nil {
var data []byte
data, err = json.Marshal(outBody)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
return
}
writer.Header().Add("Content-Type", "application/json")
_, err = writer.Write(data)
if err != nil {
t.Fatal(err)
}
}
})
}
// Go source code of mock docker-credential-helper implementation.
// Its storage is backed by inMemoryHelper instantiated in test and exposed via HTTP.
const helperGoSrc = `package main
import (
"errors"
"io"
"log"
"net/http"
"os"
)
func main() {
var (
baseURL = os.Getenv("HELPER_BASE_URL")
resp *http.Response
err error
)
cmd := os.Args[1]
switch cmd {
case "list":
resp, err = http.Get(baseURL + "/" + cmd)
if err != nil {
log.Fatal(err)
}
io.Copy(os.Stdout, resp.Body)
case "get", "erase":
resp, err = http.Post(baseURL+ "/" + cmd, "text/plain", os.Stdin)
if err != nil {
log.Fatal(err)
}
io.Copy(os.Stdout, resp.Body)
case "store":
resp, err = http.Post(baseURL+ "/" + cmd, "application/json", os.Stdin)
if err != nil {
log.Fatal(err)
}
default:
log.Fatal(errors.New("unknown cmd: " + cmd))
}
if resp.StatusCode != http.StatusOK {
log.Fatal(errors.New(resp.Status))
}
return
}
`
// Creates executable with name determined by the helperName parameter and puts it on $PATH.
//
// The executable behaves like docker credential helper (https://github.com/docker/docker-credential-helpers).
//
// The content of the store presented by the executable is backed by the helper parameter.
func setUpMockHelper(helperName string, helper credentials.Helper) func(t *testing.T) {
return func(t *testing.T) {
WithExecutable(t, helperName, helperGoSrc)
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = listener.Close() })
baseURL := fmt.Sprintf("http://%s", listener.Addr().String())
t.Setenv("HELPER_BASE_URL", baseURL)
server := http.Server{Handler: handlerForCredHelper(t, helper)}
servErrChan := make(chan error)
go func() {
servErrChan <- server.Serve(listener)
}()
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_ = server.Shutdown(ctx)
e := <-servErrChan
if !errors.Is(e, http.ErrServerClosed) {
t.Fatal(e)
}
})
}
}
// combines multiple setUp routines into one setUp routine
func all(fns ...setUpEnv) setUpEnv {
return func(t *testing.T) {
t.Helper()
for _, fn := range fns {
fn(t)
}
}
}
func newInMemoryHelper() *inMemoryHelper {
return &inMemoryHelper{lock: &sync.Mutex{}, credentials: make(map[string]credentials.Credentials)}
}
type inMemoryHelper struct {
credentials map[string]credentials.Credentials
lock sync.Locker
}
func (i *inMemoryHelper) Add(credentials *credentials.Credentials) error {
i.lock.Lock()
defer i.lock.Unlock()
i.credentials[credentials.ServerURL] = *credentials
return nil
}
func (i *inMemoryHelper) Get(serverURL string) (string, string, error) {
i.lock.Lock()
defer i.lock.Unlock()
if result, ok := i.credentials[serverURL]; ok {
return result.Username, result.Secret, nil
}
return "", "", credentials.NewErrCredentialsNotFound()
}
func (i *inMemoryHelper) List() (map[string]string, error) {
i.lock.Lock()
defer i.lock.Unlock()
result := make(map[string]string, len(i.credentials))
for k, v := range i.credentials {
result[k] = v.Username
}
return result, nil
}
func (i *inMemoryHelper) Delete(serverURL string) error {
i.lock.Lock()
defer i.lock.Unlock()
if _, ok := i.credentials[serverURL]; ok {
delete(i.credentials, serverURL)
return nil
}
return credentials.NewErrCredentialsNotFound()
}