Skip to content

Commit f4a0e81

Browse files
committed
Implement credential programs reading from Stdin.
Signed-off-by: David Calavera <david.calavera@gmail.com>
1 parent de9748d commit f4a0e81

File tree

7 files changed

+261
-66
lines changed

7 files changed

+261
-66
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ Set the `credsStore` option in your `.docker/config.json` file with the suffix o
4141

4242
## Development
4343

44-
Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory.
44+
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:
45+
46+
- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Password`.
47+
- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
48+
- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
49+
50+
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.
4551

4652
1. Implement the interface `credentials.Helper` in `YOUR_PACKAGE/YOUR_PACKAGE_$GOOS.go`
4753
2. Create a main program in `YOUR_PACKAGE/cmd/main_$GOOS.go`.

credentials/helper.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package credentials
22

3+
import "errors"
4+
35
// Credentials holds the information shared between docker and the credentials store.
46
type Credentials struct {
57
ServerURL string
@@ -13,3 +15,7 @@ type Helper interface {
1315
Delete(serverURL string) error
1416
Get(serverURL string) (string, string, error)
1517
}
18+
19+
// Standarize the not found error, so every helper returns
20+
// the same message and docker can handle it properly.
21+
var NotFoundError = errors.New("credentials not found in native keychain")

osxkeychain/osxkeychain_darwin.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ char *get_error(OSStatus status) {
44
char *buf = malloc(128);
55
CFStringRef str = SecCopyErrorMessageString(status, NULL);
66
int success = CFStringGetCString(str, buf, 128, kCFStringEncodingUTF8);
7-
if (success) {
7+
if (!success) {
88
strncpy(buf, "Unknown error", 128);
99
}
1010
return buf;

osxkeychain/osxkeychain_darwin.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import (
1818
"github.com/calavera/docker-credential-helpers/credentials"
1919
)
2020

21+
// notFoundError is the specific error message returned by OS X
22+
// when the credentials are not in the keychain.
23+
const notFoundError = "The specified item could not be found in the keychain."
24+
2125
type osxkeychain struct{}
2226

2327
// New creates a new osxkeychain.
@@ -82,7 +86,13 @@ func (h osxkeychain) Get(serverURL string) (string, string, error) {
8286
errMsg := C.keychain_get(s, &usernameLen, &username, &passwordLen, &password)
8387
if errMsg != nil {
8488
defer C.free(unsafe.Pointer(errMsg))
85-
return "", "", errors.New(C.GoString(errMsg))
89+
goMsg := C.GoString(errMsg)
90+
91+
if goMsg == notFoundError {
92+
return "", "", credentials.NotFoundError
93+
}
94+
95+
return "", "", errors.New(goMsg)
8696
}
8797

8898
user := C.GoStringN(username, C.int(usernameLen))

plugin/plugin.go

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,112 @@
11
package plugin
22

33
import (
4-
"net/rpc"
4+
"bufio"
5+
"bytes"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"os"
10+
"strings"
511

612
"github.com/calavera/docker-credential-helpers/credentials"
7-
"github.com/hashicorp/go-plugin"
813
)
914

10-
var handshakeConfig = plugin.HandshakeConfig{
11-
ProtocolVersion: 1,
12-
MagicCookieKey: "DOCKER_CREDENTIAL_PLUGIN",
13-
MagicCookieValue: "nyzGgJQpfOYO$oUVHo4RsLaYaNmCqeWLEqZnZG}peMVq4nXdFp",
15+
type credentialsGetResponse struct {
16+
Username string
17+
Password string
1418
}
1519

16-
type credentialsPlugin struct {
17-
helper credentials.Helper
20+
// Serve initializes the store helper and parses the action argument.
21+
func Serve(helper credentials.Helper) {
22+
if err := handleCommand(helper); err != nil {
23+
fmt.Fprintf(os.Stdout, "%v\n", err)
24+
os.Exit(1)
25+
}
1826
}
1927

20-
func (p *credentialsPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
21-
return p, nil
28+
func handleCommand(helper credentials.Helper) error {
29+
if len(os.Args) != 2 {
30+
return fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
31+
}
32+
33+
switch os.Args[1] {
34+
case "store":
35+
return store(helper, os.Stdin)
36+
case "get":
37+
return get(helper, os.Stdin, os.Stdout)
38+
case "erase":
39+
return erase(helper, os.Stdin)
40+
}
41+
return fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
2242
}
2343

24-
func (*credentialsPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
25-
return nil, nil
44+
func store(helper credentials.Helper, reader io.Reader) error {
45+
scanner := bufio.NewScanner(reader)
46+
47+
buffer := new(bytes.Buffer)
48+
for scanner.Scan() {
49+
buffer.Write(scanner.Bytes())
50+
}
51+
52+
if err := scanner.Err(); err != nil && err != io.EOF {
53+
return err
54+
}
55+
56+
var creds credentials.Credentials
57+
if err := json.NewDecoder(buffer).Decode(&creds); err != nil {
58+
return err
59+
}
60+
61+
return helper.Add(&creds)
2662
}
2763

28-
// Serve initializes the socket connection to a store helper.
29-
func Serve(helper credentials.Helper) {
30-
pluginMap := map[string]plugin.Plugin{
31-
"credentials": &credentialsPlugin{helper},
64+
func get(helper credentials.Helper, reader io.Reader, writer io.Writer) error {
65+
scanner := bufio.NewScanner(reader)
66+
67+
buffer := new(bytes.Buffer)
68+
for scanner.Scan() {
69+
buffer.Write(scanner.Bytes())
70+
}
71+
72+
if err := scanner.Err(); err != nil && err != io.EOF {
73+
return err
3274
}
3375

34-
plugin.Serve(&plugin.ServeConfig{
35-
HandshakeConfig: handshakeConfig,
36-
Plugins: pluginMap,
37-
})
76+
serverURL := strings.TrimSpace(buffer.String())
77+
78+
username, password, err := helper.Get(serverURL)
79+
if err != nil {
80+
return err
81+
}
82+
83+
resp := credentialsGetResponse{
84+
Username: username,
85+
Password: password,
86+
}
87+
88+
buffer.Reset()
89+
if err := json.NewEncoder(buffer).Encode(resp); err != nil {
90+
return err
91+
}
92+
93+
fmt.Fprint(writer, buffer.String())
94+
return nil
95+
}
96+
97+
func erase(helper credentials.Helper, reader io.Reader) error {
98+
scanner := bufio.NewScanner(reader)
99+
100+
buffer := new(bytes.Buffer)
101+
for scanner.Scan() {
102+
buffer.Write(scanner.Bytes())
103+
}
104+
105+
if err := scanner.Err(); err != nil && err != io.EOF {
106+
return err
107+
}
108+
109+
serverURL := strings.TrimSpace(buffer.String())
110+
111+
return helper.Delete(serverURL)
38112
}

plugin/plugin_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package plugin
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
"testing"
9+
10+
"github.com/calavera/docker-credential-helpers/credentials"
11+
)
12+
13+
type memoryStore struct {
14+
creds map[string]*credentials.Credentials
15+
}
16+
17+
func newMemoryStore() *memoryStore {
18+
return &memoryStore{
19+
creds: make(map[string]*credentials.Credentials),
20+
}
21+
}
22+
23+
func (m *memoryStore) Add(creds *credentials.Credentials) error {
24+
m.creds[creds.ServerURL] = creds
25+
return nil
26+
}
27+
28+
func (m *memoryStore) Delete(serverURL string) error {
29+
delete(m.creds, serverURL)
30+
return nil
31+
}
32+
33+
func (m *memoryStore) Get(serverURL string) (string, string, error) {
34+
c, ok := m.creds[serverURL]
35+
if !ok {
36+
return "", "", fmt.Errorf("creds not found for %s", serverURL)
37+
}
38+
return c.Username, c.Password, nil
39+
}
40+
41+
func TestStore(t *testing.T) {
42+
serverURL := "https://index.docker.io/v1/"
43+
creds := &credentials.Credentials{
44+
ServerURL: serverURL,
45+
Username: "foo",
46+
Password: "bar",
47+
}
48+
b, err := json.Marshal(creds)
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
in := bytes.NewReader(b)
53+
54+
h := newMemoryStore()
55+
if err := store(h, in); err != nil {
56+
t.Fatal(err)
57+
}
58+
59+
c, ok := h.creds[serverURL]
60+
if !ok {
61+
t.Fatalf("creds not found for %s\n", serverURL)
62+
}
63+
64+
if c.Username != "foo" {
65+
t.Fatalf("expected username foo, got %s\n", c.Username)
66+
}
67+
68+
if c.Password != "bar" {
69+
t.Fatalf("expected username bar, got %s\n", c.Password)
70+
}
71+
}
72+
73+
func TestGet(t *testing.T) {
74+
serverURL := "https://index.docker.io/v1/"
75+
creds := &credentials.Credentials{
76+
ServerURL: serverURL,
77+
Username: "foo",
78+
Password: "bar",
79+
}
80+
b, err := json.Marshal(creds)
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
in := bytes.NewReader(b)
85+
86+
h := newMemoryStore()
87+
if err := store(h, in); err != nil {
88+
t.Fatal(err)
89+
}
90+
91+
buf := strings.NewReader(serverURL)
92+
w := new(bytes.Buffer)
93+
if err := get(h, buf, w); err != nil {
94+
t.Fatal(err)
95+
}
96+
97+
if w.Len() == 0 {
98+
t.Fatalf("expected output in the writer, got %d", w.Len())
99+
}
100+
101+
var c credentialsGetResponse
102+
if err := json.NewDecoder(w).Decode(&c); err != nil {
103+
t.Fatal(err)
104+
}
105+
106+
if c.Username != "foo" {
107+
t.Fatalf("expected username foo, got %s\n", c.Username)
108+
}
109+
110+
if c.Password != "bar" {
111+
t.Fatalf("expected username bar, got %s\n", c.Password)
112+
}
113+
}
114+
115+
func TestErase(t *testing.T) {
116+
serverURL := "https://index.docker.io/v1/"
117+
creds := &credentials.Credentials{
118+
ServerURL: serverURL,
119+
Username: "foo",
120+
Password: "bar",
121+
}
122+
b, err := json.Marshal(creds)
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
in := bytes.NewReader(b)
127+
128+
h := newMemoryStore()
129+
if err := store(h, in); err != nil {
130+
t.Fatal(err)
131+
}
132+
133+
buf := strings.NewReader(serverURL)
134+
if err := erase(h, buf); err != nil {
135+
t.Fatal(err)
136+
}
137+
138+
w := new(bytes.Buffer)
139+
if err := get(h, buf, w); err == nil {
140+
t.Fatal("expected error getting missing creds, got empty")
141+
}
142+
}

plugin/server.go

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)