This repository has been archived by the owner on Apr 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Resolves #29 --------- Signed-off-by: wangxiaoxuan273 <[email protected]>
- Loading branch information
1 parent
0ad40e9
commit aead42c
Showing
4 changed files
with
257 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
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= | ||
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= | ||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
oras.land/oras-go/v2 v2.0.2 h1:3aSQdJ7EUC0ft2e9PjJB9Jzastz5ojPA4LzZ3Q4YbUc= | ||
oras.land/oras-go/v2 v2.0.2/go.mod h1:PWnWc/Kyyg7wUTUsDHshrsJkzuxXzreeMd6NrfdnFSo= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/* | ||
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" | ||
|
||
"github.com/docker/docker-credential-helpers/client" | ||
"github.com/docker/docker-credential-helpers/credentials" | ||
"oras.land/oras-go/v2/registry/remote/auth" | ||
) | ||
|
||
const ( | ||
remoteCredentialsPrefix = "docker-credential-" | ||
emptyUsername = "<token>" | ||
) | ||
|
||
// NativeStore implements a credentials store using native keychain to keep | ||
// credentials secure. | ||
type NativeStore struct { | ||
programFunc client.ProgramFunc | ||
} | ||
|
||
// NewNativeStore creates a new native store that uses a remote helper program to | ||
// manage credentials. | ||
func NewNativeStore(helperSuffix string) Store { | ||
return &NativeStore{ | ||
programFunc: client.NewShellProgramFunc(remoteCredentialsPrefix + helperSuffix), | ||
} | ||
} | ||
|
||
// 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) | ||
if err != nil { | ||
if credentials.IsErrCredentialsNotFound(err) { | ||
// do not return an error if the credentials are not in the keychain. | ||
return auth.EmptyCredential, nil | ||
} | ||
return auth.EmptyCredential, err | ||
} | ||
// bearer auth is used if the username is emptyUsername | ||
if dockerCred.Username == emptyUsername { | ||
cred.RefreshToken = dockerCred.Secret | ||
} else { | ||
cred.Username = dockerCred.Username | ||
cred.Password = dockerCred.Secret | ||
} | ||
return cred, nil | ||
} | ||
|
||
// Put saves credentials into the store | ||
func (ns *NativeStore) Put(_ context.Context, serverAddress string, cred auth.Credential) error { | ||
dockerCred := &credentials.Credentials{ | ||
ServerURL: serverAddress, | ||
Username: cred.Username, | ||
Secret: cred.Password, | ||
} | ||
if cred.RefreshToken != "" { | ||
dockerCred.Username = emptyUsername | ||
dockerCred.Secret = cred.RefreshToken | ||
} | ||
return client.Store(ns.programFunc, dockerCred) | ||
} | ||
|
||
// 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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
/* | ||
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/json" | ||
"fmt" | ||
"io" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/docker/docker-credential-helpers/client" | ||
"github.com/docker/docker-credential-helpers/credentials" | ||
"oras.land/oras-go/v2/registry/remote/auth" | ||
) | ||
|
||
const ( | ||
basicAuthHost = "localhost:2333" | ||
bearerAuthHost = "localhost:6666" | ||
testUsername = "test_username" | ||
testPassword = "test_password" | ||
testRefreshToken = "test_token" | ||
) | ||
|
||
var ( | ||
errCommandExited = fmt.Errorf("exited with error") | ||
) | ||
|
||
// testCommand implements the Program interface for testing purpose. | ||
// It simulates interactions between the docker client and a remote | ||
// credentials helper. | ||
type testCommand struct { | ||
arg string | ||
input io.Reader | ||
} | ||
|
||
// Output returns responses from the remote credentials helper. | ||
// It mocks those responses based in the input in the mock. | ||
func (m *testCommand) Output() ([]byte, error) { | ||
in, err := io.ReadAll(m.input) | ||
if err != nil { | ||
return nil, err | ||
} | ||
inS := string(in) | ||
switch m.arg { | ||
case "get": | ||
switch inS { | ||
case basicAuthHost: | ||
return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil | ||
case bearerAuthHost: | ||
return []byte(`{"Username": "<token>", "Secret": "test_token"}`), nil | ||
default: | ||
return []byte("program failed"), errCommandExited | ||
} | ||
case "store": | ||
var c credentials.Credentials | ||
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c) | ||
if err != nil { | ||
return []byte("program failed"), errCommandExited | ||
} | ||
switch c.ServerURL { | ||
case basicAuthHost, bearerAuthHost: | ||
return nil, nil | ||
default: | ||
return []byte("program failed"), errCommandExited | ||
} | ||
case "erase": | ||
switch inS { | ||
case basicAuthHost, bearerAuthHost: | ||
return nil, nil | ||
default: | ||
return []byte("program failed"), errCommandExited | ||
} | ||
} | ||
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited | ||
} | ||
|
||
// Input sets the input to send to a remote credentials helper. | ||
func (m *testCommand) Input(in io.Reader) { | ||
m.input = in | ||
} | ||
|
||
func testCommandFn(args ...string) client.Program { | ||
return &testCommand{ | ||
arg: args[0], | ||
} | ||
} | ||
|
||
func TestNativeStore_interface(t *testing.T) { | ||
var ns interface{} = &NativeStore{} | ||
if _, ok := ns.(Store); !ok { | ||
t.Error("&NativeStore{} does not conform Store") | ||
} | ||
} | ||
|
||
func TestNativeStore_basicAuth(t *testing.T) { | ||
ns := &NativeStore{ | ||
programFunc: testCommandFn, | ||
} | ||
// Put | ||
err := ns.Put(context.Background(), basicAuthHost, auth.Credential{Username: testUsername, Password: testPassword}) | ||
if err != nil { | ||
t.Fatalf("basic auth test ns.Put fails: %v", err) | ||
} | ||
// Get | ||
cred, err := ns.Get(context.Background(), basicAuthHost) | ||
if err != nil { | ||
t.Fatalf("basic auth test ns.Get fails: %v", err) | ||
} | ||
if cred.Username != testUsername { | ||
t.Fatal("incorrect username") | ||
} | ||
if cred.Password != testPassword { | ||
t.Fatal("incorrect password") | ||
} | ||
// Delete | ||
err = ns.Delete(context.Background(), basicAuthHost) | ||
if err != nil { | ||
t.Fatalf("basic auth test ns.Delete fails: %v", err) | ||
} | ||
} | ||
|
||
func TestNativeStore_refreshToken(t *testing.T) { | ||
ns := &NativeStore{ | ||
programFunc: testCommandFn, | ||
} | ||
// Put | ||
err := ns.Put(context.Background(), bearerAuthHost, auth.Credential{RefreshToken: testRefreshToken}) | ||
if err != nil { | ||
t.Fatalf("refresh token test ns.Put fails: %v", err) | ||
} | ||
// Get | ||
cred, err := ns.Get(context.Background(), bearerAuthHost) | ||
if err != nil { | ||
t.Fatalf("refresh token test ns.Get fails: %v", err) | ||
} | ||
if cred.Username != "" { | ||
t.Fatalf("expect username to be empty, got %s", cred.Username) | ||
} | ||
if cred.RefreshToken != testRefreshToken { | ||
t.Fatal("incorrect refresh token") | ||
} | ||
// Delete | ||
err = ns.Delete(context.Background(), basicAuthHost) | ||
if err != nil { | ||
t.Fatalf("refresh token test ns.Delete fails: %v", err) | ||
} | ||
} |