1
0
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:
David Calavera
2016-05-24 21:07:53 -07:00
parent 00703eb6db
commit c4fc9c07dd
13 changed files with 369 additions and 24 deletions
+17 -2
View File
@@ -27,6 +27,8 @@ $ make osxkeychain
## Usage ## 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`. 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 ```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 ### Available programs
1. osxkeychain: Provides a helper to use the OS X keychain as credentials store. 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. 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. 3. wincred: Provides a helper to use Windows credentials manager as store.
## Development ## Development
+70
View File
@@ -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
}
+192
View File
@@ -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)
}
}
+37
View File
@@ -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
}
+7
View File
@@ -10,6 +10,13 @@ import (
"strings" "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. // Serve initializes the credentials helper and parses the action argument.
// This function is designed to be called from a command line interface. // This function is designed to be called from a command line interface.
// It uses os.Args[1] as the key for the action. // It uses os.Args[1] as the key for the action.
+37
View File
@@ -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
}
-13
View File
@@ -1,14 +1,5 @@
package credentials 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. // Helper is the interface a credentials store helper must implement.
type Helper interface { type Helper interface {
// Add appends credentials to the store. // Add appends credentials to the store.
@@ -19,7 +10,3 @@ type Helper interface {
// It returns username and secret as strings. // It returns username and secret as strings.
Get(serverURL string) (string, string, error) 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")
+1 -1
View File
@@ -85,7 +85,7 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
goMsg := C.GoString(errMsg) goMsg := C.GoString(errMsg)
if goMsg == errCredentialsNotFound { if goMsg == errCredentialsNotFound {
return "", "", credentials.ErrCredentialsNotFound return "", "", credentials.NewErrCredentialsNotFound()
} }
return "", "", errors.New(goMsg) return "", "", errors.New(goMsg)
+2 -2
View File
@@ -39,7 +39,7 @@ func TestOSXKeychainHelper(t *testing.T) {
func TestMissingCredentials(t *testing.T) { func TestMissingCredentials(t *testing.T) {
helper := Osxkeychain{} helper := Osxkeychain{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
if err != credentials.ErrCredentialsNotFound { if !credentials.IsErrCredentialsNotFound(err) {
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err) t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
} }
} }
+1 -1
View File
@@ -74,7 +74,7 @@ func (h Secretservice) Get(serverURL string) (string, string, error) {
user := C.GoString(username) user := C.GoString(username)
pass := C.GoString(secret) pass := C.GoString(secret)
if pass == "" { if pass == "" {
return "", "", credentials.ErrCredentialsNotFound return "", "", credentials.NewErrCredentialsNotFound()
} }
return user, pass, nil return user, pass, nil
} }
+2 -2
View File
@@ -43,7 +43,7 @@ func TestMissingCredentials(t *testing.T) {
helper := Secretservice{} helper := Secretservice{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
if err != credentials.ErrCredentialsNotFound { if !credentials.IsErrCredentialsNotFound(err) {
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err) t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ func (h Wincred) Delete(serverURL string) error {
func (h Wincred) Get(serverURL string) (string, string, error) { func (h Wincred) Get(serverURL string) (string, string, error) {
g, _ := winc.GetGenericCredential(serverURL) g, _ := winc.GetGenericCredential(serverURL)
if g == nil { if g == nil {
return "", "", credentials.ErrCredentialsNotFound return "", "", credentials.NewErrCredentialsNotFound()
} }
return g.UserName, string(g.CredentialBlob), nil return g.UserName, string(g.CredentialBlob), nil
} }
+2 -2
View File
@@ -39,7 +39,7 @@ func TestWinCredHelper(t *testing.T) {
func TestMissingCredentials(t *testing.T) { func TestMissingCredentials(t *testing.T) {
helper := Wincred{} helper := Wincred{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
if err != credentials.ErrCredentialsNotFound { if !credentials.IsErrCredentialsNotFound(err) {
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err) t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
} }
} }