From f4a0e81b0b49c63323b9d7657fccc108c5211ecd Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 9 Feb 2016 10:53:34 -0800 Subject: [PATCH] Implement credential programs reading from Stdin. Signed-off-by: David Calavera --- README.md | 8 +- credentials/helper.go | 6 ++ osxkeychain/osxkeychain_darwin.c | 2 +- osxkeychain/osxkeychain_darwin.go | 12 ++- plugin/plugin.go | 124 ++++++++++++++++++++------ plugin/plugin_test.go | 142 ++++++++++++++++++++++++++++++ plugin/server.go | 43 --------- 7 files changed, 266 insertions(+), 71 deletions(-) create mode 100644 plugin/plugin_test.go delete mode 100644 plugin/server.go diff --git a/README.md b/README.md index 707b502..b6f5627 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,13 @@ Set the `credsStore` option in your `.docker/config.json` file with the suffix o ## Development -Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory. +A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are three valid values: + +- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Password`. +- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`. +- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`. + +This repository also includes libraries to implement new credentials programs in Go. Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory. 1. Implement the interface `credentials.Helper` in `YOUR_PACKAGE/YOUR_PACKAGE_$GOOS.go` 2. Create a main program in `YOUR_PACKAGE/cmd/main_$GOOS.go`. diff --git a/credentials/helper.go b/credentials/helper.go index 3315d7e..ed696fc 100644 --- a/credentials/helper.go +++ b/credentials/helper.go @@ -1,5 +1,7 @@ package credentials +import "errors" + // Credentials holds the information shared between docker and the credentials store. type Credentials struct { ServerURL string @@ -13,3 +15,7 @@ type Helper interface { Delete(serverURL string) error Get(serverURL string) (string, string, error) } + +// Standarize the not found error, so every helper returns +// the same message and docker can handle it properly. +var NotFoundError = errors.New("credentials not found in native keychain") diff --git a/osxkeychain/osxkeychain_darwin.c b/osxkeychain/osxkeychain_darwin.c index 803f96f..d00f07f 100644 --- a/osxkeychain/osxkeychain_darwin.c +++ b/osxkeychain/osxkeychain_darwin.c @@ -4,7 +4,7 @@ char *get_error(OSStatus status) { char *buf = malloc(128); CFStringRef str = SecCopyErrorMessageString(status, NULL); int success = CFStringGetCString(str, buf, 128, kCFStringEncodingUTF8); - if (success) { + if (!success) { strncpy(buf, "Unknown error", 128); } return buf; diff --git a/osxkeychain/osxkeychain_darwin.go b/osxkeychain/osxkeychain_darwin.go index cf0d5f4..94fc34b 100644 --- a/osxkeychain/osxkeychain_darwin.go +++ b/osxkeychain/osxkeychain_darwin.go @@ -18,6 +18,10 @@ import ( "github.com/calavera/docker-credential-helpers/credentials" ) +// notFoundError is the specific error message returned by OS X +// when the credentials are not in the keychain. +const notFoundError = "The specified item could not be found in the keychain." + type osxkeychain struct{} // New creates a new osxkeychain. @@ -82,7 +86,13 @@ func (h osxkeychain) Get(serverURL string) (string, string, error) { errMsg := C.keychain_get(s, &usernameLen, &username, &passwordLen, &password) if errMsg != nil { defer C.free(unsafe.Pointer(errMsg)) - return "", "", errors.New(C.GoString(errMsg)) + goMsg := C.GoString(errMsg) + + if goMsg == notFoundError { + return "", "", credentials.NotFoundError + } + + return "", "", errors.New(goMsg) } user := C.GoStringN(username, C.int(usernameLen)) diff --git a/plugin/plugin.go b/plugin/plugin.go index e19c6ab..5df3d0e 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -1,38 +1,112 @@ package plugin import ( - "net/rpc" + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "strings" "github.com/calavera/docker-credential-helpers/credentials" - "github.com/hashicorp/go-plugin" ) -var handshakeConfig = plugin.HandshakeConfig{ - ProtocolVersion: 1, - MagicCookieKey: "DOCKER_CREDENTIAL_PLUGIN", - MagicCookieValue: "nyzGgJQpfOYO$oUVHo4RsLaYaNmCqeWLEqZnZG}peMVq4nXdFp", +type credentialsGetResponse struct { + Username string + Password string } -type credentialsPlugin struct { - helper credentials.Helper -} - -func (p *credentialsPlugin) Server(*plugin.MuxBroker) (interface{}, error) { - return p, nil -} - -func (*credentialsPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { - return nil, nil -} - -// Serve initializes the socket connection to a store helper. +// Serve initializes the store helper and parses the action argument. func Serve(helper credentials.Helper) { - pluginMap := map[string]plugin.Plugin{ - "credentials": &credentialsPlugin{helper}, + if err := handleCommand(helper); err != nil { + fmt.Fprintf(os.Stdout, "%v\n", err) + os.Exit(1) + } +} + +func handleCommand(helper credentials.Helper) error { + if len(os.Args) != 2 { + return fmt.Errorf("Usage: %s ", os.Args[0]) } - plugin.Serve(&plugin.ServeConfig{ - HandshakeConfig: handshakeConfig, - Plugins: pluginMap, - }) + switch os.Args[1] { + case "store": + return store(helper, os.Stdin) + case "get": + return get(helper, os.Stdin, os.Stdout) + case "erase": + return erase(helper, os.Stdin) + } + return fmt.Errorf("Usage: %s ", os.Args[0]) +} + +func store(helper credentials.Helper, reader io.Reader) error { + scanner := bufio.NewScanner(reader) + + buffer := new(bytes.Buffer) + for scanner.Scan() { + buffer.Write(scanner.Bytes()) + } + + if err := scanner.Err(); err != nil && err != io.EOF { + return err + } + + var creds credentials.Credentials + if err := json.NewDecoder(buffer).Decode(&creds); err != nil { + return err + } + + return helper.Add(&creds) +} + +func get(helper credentials.Helper, reader io.Reader, writer io.Writer) error { + scanner := bufio.NewScanner(reader) + + buffer := new(bytes.Buffer) + for scanner.Scan() { + buffer.Write(scanner.Bytes()) + } + + if err := scanner.Err(); err != nil && err != io.EOF { + return err + } + + serverURL := strings.TrimSpace(buffer.String()) + + username, password, err := helper.Get(serverURL) + if err != nil { + return err + } + + resp := credentialsGetResponse{ + Username: username, + Password: password, + } + + buffer.Reset() + if err := json.NewEncoder(buffer).Encode(resp); err != nil { + return err + } + + fmt.Fprint(writer, buffer.String()) + return nil +} + +func erase(helper credentials.Helper, reader io.Reader) error { + scanner := bufio.NewScanner(reader) + + buffer := new(bytes.Buffer) + for scanner.Scan() { + buffer.Write(scanner.Bytes()) + } + + if err := scanner.Err(); err != nil && err != io.EOF { + return err + } + + serverURL := strings.TrimSpace(buffer.String()) + + return helper.Delete(serverURL) } diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go new file mode 100644 index 0000000..89f40d2 --- /dev/null +++ b/plugin/plugin_test.go @@ -0,0 +1,142 @@ +package plugin + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "testing" + + "github.com/calavera/docker-credential-helpers/credentials" +) + +type memoryStore struct { + creds map[string]*credentials.Credentials +} + +func newMemoryStore() *memoryStore { + return &memoryStore{ + creds: make(map[string]*credentials.Credentials), + } +} + +func (m *memoryStore) Add(creds *credentials.Credentials) error { + m.creds[creds.ServerURL] = creds + return nil +} + +func (m *memoryStore) Delete(serverURL string) error { + delete(m.creds, serverURL) + return nil +} + +func (m *memoryStore) Get(serverURL string) (string, string, error) { + c, ok := m.creds[serverURL] + if !ok { + return "", "", fmt.Errorf("creds not found for %s", serverURL) + } + return c.Username, c.Password, nil +} + +func TestStore(t *testing.T) { + serverURL := "https://index.docker.io/v1/" + creds := &credentials.Credentials{ + ServerURL: serverURL, + Username: "foo", + Password: "bar", + } + b, err := json.Marshal(creds) + if err != nil { + t.Fatal(err) + } + in := bytes.NewReader(b) + + h := newMemoryStore() + if err := store(h, in); err != nil { + t.Fatal(err) + } + + c, ok := h.creds[serverURL] + if !ok { + t.Fatalf("creds not found for %s\n", serverURL) + } + + if c.Username != "foo" { + t.Fatalf("expected username foo, got %s\n", c.Username) + } + + if c.Password != "bar" { + t.Fatalf("expected username bar, got %s\n", c.Password) + } +} + +func TestGet(t *testing.T) { + serverURL := "https://index.docker.io/v1/" + creds := &credentials.Credentials{ + ServerURL: serverURL, + Username: "foo", + Password: "bar", + } + b, err := json.Marshal(creds) + if err != nil { + t.Fatal(err) + } + in := bytes.NewReader(b) + + h := newMemoryStore() + if err := store(h, in); err != nil { + t.Fatal(err) + } + + buf := strings.NewReader(serverURL) + w := new(bytes.Buffer) + if err := get(h, buf, w); err != nil { + t.Fatal(err) + } + + if w.Len() == 0 { + t.Fatalf("expected output in the writer, got %d", w.Len()) + } + + var c credentialsGetResponse + if err := json.NewDecoder(w).Decode(&c); err != nil { + t.Fatal(err) + } + + if c.Username != "foo" { + t.Fatalf("expected username foo, got %s\n", c.Username) + } + + if c.Password != "bar" { + t.Fatalf("expected username bar, got %s\n", c.Password) + } +} + +func TestErase(t *testing.T) { + serverURL := "https://index.docker.io/v1/" + creds := &credentials.Credentials{ + ServerURL: serverURL, + Username: "foo", + Password: "bar", + } + b, err := json.Marshal(creds) + if err != nil { + t.Fatal(err) + } + in := bytes.NewReader(b) + + h := newMemoryStore() + if err := store(h, in); err != nil { + t.Fatal(err) + } + + buf := strings.NewReader(serverURL) + if err := erase(h, buf); err != nil { + t.Fatal(err) + } + + w := new(bytes.Buffer) + if err := get(h, buf, w); err == nil { + t.Fatal("expected error getting missing creds, got empty") + } +} diff --git a/plugin/server.go b/plugin/server.go deleted file mode 100644 index 2decbc1..0000000 --- a/plugin/server.go +++ /dev/null @@ -1,43 +0,0 @@ -package plugin - -import "github.com/calavera/docker-credential-helpers/credentials" - -// CredentialsGetResponse holds the information sent to docker after -// a request for credentials. -type CredentialsGetResponse struct { - Error string - Username string - Password string -} - -func (p *credentialsPlugin) Get(c *credentials.Credentials, resp *CredentialsGetResponse) error { - username, password, err := p.helper.Get(c.ServerURL) - if err != nil { - *resp = CredentialsGetResponse{ - Error: err.Error(), - } - return nil - } - - *resp = CredentialsGetResponse{ - Username: username, - Password: password, - } - return nil -} - -func (p *credentialsPlugin) Add(c *credentials.Credentials, resp *string) error { - err := p.helper.Add(c) - if err != nil { - *resp = err.Error() - } - return nil -} - -func (p *credentialsPlugin) Delete(c *credentials.Credentials, resp *string) error { - err := p.helper.Delete(c.ServerURL) - if err != nil { - *resp = err.Error() - } - return nil -}