mirror of
https://github.com/docker/docker-credential-helpers.git
synced 2026-06-13 16:01:28 +05:30
Add client functions to allow integrations within other CLIs.
This is a simplification of how the docker engine implements this feature, but it will be ported there once this is merged. Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
@@ -27,6 +27,8 @@ $ make osxkeychain
|
||||
|
||||
## Usage
|
||||
|
||||
### With the Docker Engine
|
||||
|
||||
Set the `credsStore` option in your `.docker/config.json` file with the suffix of the program you want to use. For instance, set it to `osxkeychain` if you want to use `docker-credential-osxkeychain`.
|
||||
|
||||
```json
|
||||
@@ -35,11 +37,24 @@ Set the `credsStore` option in your `.docker/config.json` file with the suffix o
|
||||
}
|
||||
```
|
||||
|
||||
### With other command line applications
|
||||
|
||||
The sub-package [client](https://godoc.org/github.com/docker/docker-credential-helpers/client) includes
|
||||
functions to call external programs from your own command line applications.
|
||||
|
||||
There are three things you need to know if you need to interact with a helper:
|
||||
|
||||
1. The name of the program to execute, for instance `docker-credential-osxkeychain`.
|
||||
2. The server address to identify the credentials, for instance `https://example.com`.
|
||||
3. The username and secret to store, when you want to store credentials.
|
||||
|
||||
You can see examples of each function in the [client](https://godoc.org/github.com/docker/docker-credential-helpers/client) documentation.
|
||||
|
||||
### Available programs
|
||||
|
||||
1. osxkeychain: Provides a helper to use the OS X keychain as credentials store.
|
||||
1. secretservice: Provides a helper to use the D-Bus secret service as credentials store.
|
||||
2. wincred: Provides a helper to use Windows credentials manager as store.
|
||||
2. secretservice: Provides a helper to use the D-Bus secret service as credentials store.
|
||||
3. wincred: Provides a helper to use Windows credentials manager as store.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
// Store uses an external program to save credentials.
|
||||
func Store(program ProgramFunc, credentials *credentials.Credentials) error {
|
||||
cmd := program("store")
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(buffer).Encode(credentials); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Input(buffer)
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t := strings.TrimSpace(string(out))
|
||||
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get executes an external program to get the credentials from a native store.
|
||||
func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) {
|
||||
cmd := program("get")
|
||||
cmd.Input(strings.NewReader(serverURL))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t := strings.TrimSpace(string(out))
|
||||
|
||||
if credentials.IsErrCredentialsNotFoundMessage(t) {
|
||||
return nil, credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
|
||||
}
|
||||
|
||||
resp := &credentials.Credentials{
|
||||
ServerURL: serverURL,
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Erase executes a program to remove the server credentails from the native store.
|
||||
func Erase(program ProgramFunc, serverURL string) error {
|
||||
cmd := program("erase")
|
||||
cmd.Input(strings.NewReader(serverURL))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
t := strings.TrimSpace(string(out))
|
||||
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
validServerAddress = "https://index.docker.io/v1"
|
||||
validServerAddress2 = "https://example.com:5002"
|
||||
invalidServerAddress = "https://foobar.example.com"
|
||||
missingCredsAddress = "https://missing.docker.io/v1"
|
||||
)
|
||||
|
||||
var errProgramExited = fmt.Errorf("exited 1")
|
||||
|
||||
// mockProgram simulates interactions between the docker client and a remote
|
||||
// credentials helper.
|
||||
// Unit tests inject this mocked command into the remote to control execution.
|
||||
type mockProgram 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 *mockProgram) Output() ([]byte, error) {
|
||||
in, err := ioutil.ReadAll(m.input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inS := string(in)
|
||||
|
||||
switch m.arg {
|
||||
case "erase":
|
||||
switch inS {
|
||||
case validServerAddress:
|
||||
return nil, nil
|
||||
default:
|
||||
return []byte("program failed"), errProgramExited
|
||||
}
|
||||
case "get":
|
||||
switch inS {
|
||||
case validServerAddress:
|
||||
return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
|
||||
case validServerAddress2:
|
||||
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
||||
case missingCredsAddress:
|
||||
return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited
|
||||
case invalidServerAddress:
|
||||
return []byte("program failed"), errProgramExited
|
||||
}
|
||||
case "store":
|
||||
var c credentials.Credentials
|
||||
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
|
||||
if err != nil {
|
||||
return []byte("error storing credentials"), errProgramExited
|
||||
}
|
||||
switch c.ServerURL {
|
||||
case validServerAddress:
|
||||
return nil, nil
|
||||
case validServerAddress2:
|
||||
return nil, nil
|
||||
default:
|
||||
return []byte("error storing credentials"), errProgramExited
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
|
||||
}
|
||||
|
||||
// Input sets the input to send to a remote credentials helper.
|
||||
func (m *mockProgram) Input(in io.Reader) {
|
||||
m.input = in
|
||||
}
|
||||
|
||||
func mockProgramFn(args ...string) Program {
|
||||
return &mockProgram{
|
||||
arg: args[0],
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleStore() {
|
||||
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||
|
||||
c := &credentials.Credentials{
|
||||
ServerURL: "https://example.com",
|
||||
Username: "calavera",
|
||||
Secret: "my super secret token",
|
||||
}
|
||||
|
||||
if err := Store(p, c); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStore(t *testing.T) {
|
||||
valid := []credentials.Credentials{
|
||||
{validServerAddress, "foo", "bar"},
|
||||
{validServerAddress2, "<token>", "abcd1234"},
|
||||
}
|
||||
|
||||
for _, v := range valid {
|
||||
if err := Store(mockProgramFn, &v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []credentials.Credentials{
|
||||
{invalidServerAddress, "foo", "bar"},
|
||||
}
|
||||
|
||||
for _, v := range invalid {
|
||||
if err := Store(mockProgramFn, &v); err == nil {
|
||||
t.Fatalf("Expected error for server %s, got nil", v.ServerURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleGet() {
|
||||
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||
|
||||
creds, err := Get(p, "https://example.com")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Got credentials for user `%s` in `%s`\n", creds.Username, creds.ServerURL)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
valid := []credentials.Credentials{
|
||||
{validServerAddress, "foo", "bar"},
|
||||
{validServerAddress2, "<token>", "abcd1234"},
|
||||
}
|
||||
|
||||
for _, v := range valid {
|
||||
c, err := Get(mockProgramFn, v.ServerURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.Username != v.Username {
|
||||
t.Fatalf("expected username `%s`, got %s", v.Username, c.Username)
|
||||
}
|
||||
if c.Secret != v.Secret {
|
||||
t.Fatalf("expected secret `%s`, got %s", v.Secret, c.Secret)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []struct {
|
||||
serverURL string
|
||||
err string
|
||||
}{
|
||||
{missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()},
|
||||
{invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"},
|
||||
}
|
||||
|
||||
for _, v := range invalid {
|
||||
_, err := Get(mockProgramFn, v.serverURL)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error for server %s, got nil", v.serverURL)
|
||||
}
|
||||
if err.Error() != v.err {
|
||||
t.Fatalf("Expected error `%s`, got `%v`", v.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleErase() {
|
||||
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||
|
||||
if err := Erase(p, "https://example.com"); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErase(t *testing.T) {
|
||||
if err := Erase(mockProgramFn, validServerAddress); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := Erase(mockProgramFn, invalidServerAddress); err == nil {
|
||||
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Program is an interface to execute external programs.
|
||||
type Program interface {
|
||||
Output() ([]byte, error)
|
||||
Input(in io.Reader)
|
||||
}
|
||||
|
||||
// ProgramFunc is a type of function that initializes programs based on arguments.
|
||||
type ProgramFunc func(args ...string) Program
|
||||
|
||||
// NewShellProgramFunc creates programs that are executed in a Shell.
|
||||
func NewShellProgramFunc(name string) ProgramFunc {
|
||||
return func(args ...string) Program {
|
||||
return &Shell{cmd: exec.Command(name, args...)}
|
||||
}
|
||||
}
|
||||
|
||||
// Shell invokes shell commands to talk with a remote credentials helper.
|
||||
type Shell struct {
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// Output returns responses from the remote credentials helper.
|
||||
func (s *Shell) Output() ([]byte, error) {
|
||||
return s.cmd.Output()
|
||||
}
|
||||
|
||||
// Input sets the input to send to a remote credentials helper.
|
||||
func (s *Shell) Input(in io.Reader) {
|
||||
s.cmd.Stdin = in
|
||||
}
|
||||
@@ -10,6 +10,13 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Credentials holds the information shared between docker and the credentials store.
|
||||
type Credentials struct {
|
||||
ServerURL string
|
||||
Username string
|
||||
Secret string
|
||||
}
|
||||
|
||||
// Serve initializes the credentials helper and parses the action argument.
|
||||
// This function is designed to be called from a command line interface.
|
||||
// It uses os.Args[1] as the key for the action.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package credentials
|
||||
|
||||
// ErrCredentialsNotFound standarizes the not found error, so every helper returns
|
||||
// the same message and docker can handle it properly.
|
||||
const errCredentialsNotFoundMessage = "credentials not found in native keychain"
|
||||
|
||||
// errCredentialsNotFound represents an error
|
||||
// raised when credentials are not in the store.
|
||||
type errCredentialsNotFound struct{}
|
||||
|
||||
// Error returns the standard error message
|
||||
// for when the credentials are not in the store.
|
||||
func (errCredentialsNotFound) Error() string {
|
||||
return errCredentialsNotFoundMessage
|
||||
}
|
||||
|
||||
// NewErrCredentialsNotFound creates a new error
|
||||
// for when the credentials are not in the store.
|
||||
func NewErrCredentialsNotFound() error {
|
||||
return errCredentialsNotFound{}
|
||||
}
|
||||
|
||||
// IsErrCredentialsNotFound returns true if the error
|
||||
// was caused by not having a set of credentials in a store.
|
||||
func IsErrCredentialsNotFound(err error) bool {
|
||||
_, ok := err.(errCredentialsNotFound)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsErrCredentialsNotFoundMessage returns true if the error
|
||||
// was caused by not having a set of credentials in a store.
|
||||
//
|
||||
// This function helps to check messages returned by an
|
||||
// external program via its standard output.
|
||||
func IsErrCredentialsNotFoundMessage(err string) bool {
|
||||
return err == errCredentialsNotFoundMessage
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
package credentials
|
||||
|
||||
import "errors"
|
||||
|
||||
// Credentials holds the information shared between docker and the credentials store.
|
||||
type Credentials struct {
|
||||
ServerURL string
|
||||
Username string
|
||||
Secret string
|
||||
}
|
||||
|
||||
// Helper is the interface a credentials store helper must implement.
|
||||
type Helper interface {
|
||||
// Add appends credentials to the store.
|
||||
@@ -19,7 +10,3 @@ type Helper interface {
|
||||
// It returns username and secret as strings.
|
||||
Get(serverURL string) (string, string, error)
|
||||
}
|
||||
|
||||
// ErrCredentialsNotFound standarizes the not found error, so every helper returns
|
||||
// the same message and docker can handle it properly.
|
||||
var ErrCredentialsNotFound = errors.New("credentials not found in native keychain")
|
||||
|
||||
@@ -85,7 +85,7 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
|
||||
goMsg := C.GoString(errMsg)
|
||||
|
||||
if goMsg == errCredentialsNotFound {
|
||||
return "", "", credentials.ErrCredentialsNotFound
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
|
||||
return "", "", errors.New(goMsg)
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestOSXKeychainHelper(t *testing.T) {
|
||||
func TestMissingCredentials(t *testing.T) {
|
||||
helper := Osxkeychain{}
|
||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||
if err != credentials.ErrCredentialsNotFound {
|
||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func (h Secretservice) Get(serverURL string) (string, string, error) {
|
||||
user := C.GoString(username)
|
||||
pass := C.GoString(secret)
|
||||
if pass == "" {
|
||||
return "", "", credentials.ErrCredentialsNotFound
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
return user, pass, nil
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestMissingCredentials(t *testing.T) {
|
||||
|
||||
helper := Secretservice{}
|
||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||
if err != credentials.ErrCredentialsNotFound {
|
||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func (h Wincred) Delete(serverURL string) error {
|
||||
func (h Wincred) Get(serverURL string) (string, string, error) {
|
||||
g, _ := winc.GetGenericCredential(serverURL)
|
||||
if g == nil {
|
||||
return "", "", credentials.ErrCredentialsNotFound
|
||||
return "", "", credentials.NewErrCredentialsNotFound()
|
||||
}
|
||||
return g.UserName, string(g.CredentialBlob), nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestWinCredHelper(t *testing.T) {
|
||||
func TestMissingCredentials(t *testing.T) {
|
||||
helper := Wincred{}
|
||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||
if err != credentials.ErrCredentialsNotFound {
|
||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
||||
if !credentials.IsErrCredentialsNotFound(err) {
|
||||
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user