1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-13 16:01:28 +05:30

Implement credential programs reading from Stdin.

Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
David Calavera
2016-02-09 10:53:34 -08:00
parent de9748d6ed
commit f4a0e81b0b
7 changed files with 266 additions and 71 deletions
+7 -1
View File
@@ -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`.
+6
View File
@@ -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")
+1 -1
View File
@@ -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;
+11 -1
View File
@@ -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))
+99 -25
View File
@@ -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 <store|get|erase>", 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 <store|get|erase>", 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)
}
+142
View File
@@ -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")
}
}
-43
View File
@@ -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
}