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
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
"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.
|
||||||
|
|||||||
@@ -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
|
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")
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user