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