version bump for docker-credential-helpers

Signed-off-by: Tycho Andersen <tycho@docker.com>
This commit is contained in:
Tycho Andersen 2017-08-16 14:02:38 -06:00
parent 4cf1849418
commit 6bd5c63c78
10 changed files with 341 additions and 28 deletions

View File

@ -5,7 +5,7 @@ github.com/cpuguy83/go-md2man a65d4d2de4d5f7c74868dfa9b202a3c8be315aaa
github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76
github.com/docker/distribution edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c github.com/docker/distribution edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c
github.com/docker/docker 84144a8c66c1bb2af8fa997288f51ef2719971b4 github.com/docker/docker 84144a8c66c1bb2af8fa997288f51ef2719971b4
github.com/docker/docker-credential-helpers v0.5.1 github.com/docker/docker-credential-helpers 3c90bd29a46b943b2a9842987b58fb91a7c1819b
# the docker/go package contains a customized version of canonical/json # the docker/go package contains a customized version of canonical/json
# and is used by Notary. The package is periodically rebased on current Go versions. # and is used by Notary. The package is periodically rebased on current Go versions.

View File

@ -1,6 +1,7 @@
package client package client
import ( import (
"fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@ -17,15 +18,26 @@ type ProgramFunc func(args ...string) Program
// NewShellProgramFunc creates programs that are executed in a Shell. // NewShellProgramFunc creates programs that are executed in a Shell.
func NewShellProgramFunc(name string) ProgramFunc { func NewShellProgramFunc(name string) ProgramFunc {
return NewShellProgramFuncWithEnv(name, nil)
}
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc {
return func(args ...string) Program { return func(args ...string) Program {
return &Shell{cmd: newCmdRedirectErr(name, args)} return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)}
} }
} }
func newCmdRedirectErr(name string, args []string) *exec.Cmd { func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd {
newCmd := exec.Command(name, args...) programCmd := exec.Command(commandName, args...)
newCmd.Stderr = os.Stderr programCmd.Env = os.Environ()
return newCmd if env != nil {
for k, v := range *env {
programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v))
}
}
programCmd.Stderr = os.Stderr
return programCmd
} }
// Shell invokes shell commands to talk with a remote credentials helper. // Shell invokes shell commands to talk with a remote credentials helper.

View File

@ -33,11 +33,12 @@ func (c *Credentials) isValid() (bool, error) {
return true, nil return true, nil
} }
// Docker credentials should be labeled as such in credentials stores that allow labelling. // CredsLabel holds the way Docker credentials should be labeled as such in credentials stores that allow labelling.
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain, // That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
// Windows credentials manager and Linux libsecret. Default value is "Docker Credentials" // Windows credentials manager and Linux libsecret. Default value is "Docker Credentials"
var CredsLabel = "Docker Credentials" var CredsLabel = "Docker Credentials"
// SetCredsLabel is a simple setter for CredsLabel
func SetCredsLabel(label string) { func SetCredsLabel(label string) {
CredsLabel = label CredsLabel = label
} }
@ -50,7 +51,7 @@ func SetCredsLabel(label string) {
func Serve(helper Helper) { func Serve(helper Helper) {
var err error var err error
if len(os.Args) != 2 { if len(os.Args) != 2 {
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0]) err = fmt.Errorf("Usage: %s <store|get|erase|list|version>", os.Args[0])
} }
if err == nil { if err == nil {
@ -74,6 +75,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error
return Erase(helper, in) return Erase(helper, in)
case "list": case "list":
return List(helper, out) return List(helper, out)
case "version":
return PrintVersion(out)
} }
return fmt.Errorf("Unknown credential action `%s`", key) return fmt.Errorf("Unknown credential action `%s`", key)
} }
@ -131,8 +134,8 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
resp := Credentials{ resp := Credentials{
ServerURL: serverURL, ServerURL: serverURL,
Username: username, Username: username,
Secret: secret, Secret: secret,
} }
buffer.Reset() buffer.Reset()
@ -175,3 +178,9 @@ func List(helper Helper, writer io.Writer) error {
} }
return json.NewEncoder(writer).Encode(accts) return json.NewEncoder(writer).Encode(accts)
} }
//PrintVersion outputs the current version.
func PrintVersion(writer io.Writer) error {
fmt.Fprintln(writer, Version)
return nil
}

View File

@ -8,7 +8,7 @@ const (
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize // ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
// invalid credentials or credentials management operations // invalid credentials or credentials management operations
errCredentialsMissingServerURLMessage = "no credentials server URL" errCredentialsMissingServerURLMessage = "no credentials server URL"
errCredentialsMissingUsernameMessage = "no credentials username" errCredentialsMissingUsernameMessage = "no credentials username"
) )
// errCredentialsNotFound represents an error // errCredentialsNotFound represents an error
@ -43,7 +43,6 @@ func IsErrCredentialsNotFoundMessage(err string) bool {
return err == errCredentialsNotFoundMessage return err == errCredentialsNotFoundMessage
} }
// errCredentialsMissingServerURL represents an error raised // errCredentialsMissingServerURL represents an error raised
// when the credentials object has no server URL or when no // when the credentials object has no server URL or when no
// server URL is provided to a credentials operation requiring // server URL is provided to a credentials operation requiring
@ -64,7 +63,6 @@ func (errCredentialsMissingUsername) Error() string {
return errCredentialsMissingUsernameMessage return errCredentialsMissingUsernameMessage
} }
// NewErrCredentialsMissingServerURL creates a new error for // NewErrCredentialsMissingServerURL creates a new error for
// errCredentialsMissingServerURL. // errCredentialsMissingServerURL.
func NewErrCredentialsMissingServerURL() error { func NewErrCredentialsMissingServerURL() error {
@ -77,7 +75,6 @@ func NewErrCredentialsMissingUsername() error {
return errCredentialsMissingUsername{} return errCredentialsMissingUsername{}
} }
// IsCredentialsMissingServerURL returns true if the error // IsCredentialsMissingServerURL returns true if the error
// was an errCredentialsMissingServerURL. // was an errCredentialsMissingServerURL.
func IsCredentialsMissingServerURL(err error) bool { func IsCredentialsMissingServerURL(err error) bool {

View File

@ -0,0 +1,4 @@
package credentials
// Version holds a string describing the current version
const Version = "0.5.2"

View File

@ -135,30 +135,27 @@ func (h Osxkeychain) List() (map[string]string, error) {
} }
func splitServer(serverURL string) (*C.struct_Server, error) { func splitServer(serverURL string) (*C.struct_Server, error) {
u, err := url.Parse(serverURL) u, err := parseURL(serverURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hostAndPort := strings.Split(u.Host, ":") proto := C.kSecProtocolTypeHTTPS
host := hostAndPort[0] if u.Scheme == "http" {
proto = C.kSecProtocolTypeHTTP
}
var port int var port int
if len(hostAndPort) == 2 { p := getPort(u)
p, err := strconv.Atoi(hostAndPort[1]) if p != "" {
port, err = strconv.Atoi(p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
port = p
}
proto := C.kSecProtocolTypeHTTPS
if u.Scheme != "https" {
proto = C.kSecProtocolTypeHTTP
} }
return &C.struct_Server{ return &C.struct_Server{
proto: C.SecProtocolType(proto), proto: C.SecProtocolType(proto),
host: C.CString(host), host: C.CString(getHostname(u)),
port: C.uint(port), port: C.uint(port),
path: C.CString(u.Path), path: C.CString(u.Path),
}, nil }, nil
@ -168,3 +165,32 @@ func freeServer(s *C.struct_Server) {
C.free(unsafe.Pointer(s.host)) C.free(unsafe.Pointer(s.host))
C.free(unsafe.Pointer(s.path)) C.free(unsafe.Pointer(s.path))
} }
// parseURL parses and validates a given serverURL to an url.URL, and
// returns an error if validation failed. Querystring parameters are
// omitted in the resulting URL, because they are not used in the helper.
//
// If serverURL does not have a valid scheme, `//` is used as scheme
// before parsing. This prevents the hostname being used as path,
// and the credentials being stored without host.
func parseURL(serverURL string) (*url.URL, error) {
// Check if serverURL has a scheme, otherwise add `//` as scheme.
if !strings.Contains(serverURL, "://") && !strings.HasPrefix(serverURL, "//") {
serverURL = "//" + serverURL
}
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
}
if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
return nil, errors.New("unsupported scheme: " + u.Scheme)
}
if getHostname(u) == "" {
return nil, errors.New("no hostname in URL")
}
u.RawQuery = ""
return u, nil
}

View File

@ -0,0 +1,13 @@
//+build go1.8
package osxkeychain
import "net/url"
func getHostname(u *url.URL) string {
return u.Hostname()
}
func getPort(u *url.URL) string {
return u.Port()
}

View File

@ -0,0 +1,41 @@
//+build !go1.8
package osxkeychain
import (
"net/url"
"strings"
)
func getHostname(u *url.URL) string {
return stripPort(u.Host)
}
func getPort(u *url.URL) string {
return portOnly(u.Host)
}
func stripPort(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return hostport
}
if i := strings.IndexByte(hostport, ']'); i != -1 {
return strings.TrimPrefix(hostport[:i], "[")
}
return hostport[:colon]
}
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return ""
}
if i := strings.Index(hostport, "]:"); i != -1 {
return hostport[i+len("]:"):]
}
if strings.Contains(hostport, "]") {
return ""
}
return hostport[colon+len(":"):]
}

View File

@ -0,0 +1,208 @@
// A `pass` based credential helper. Passwords are stored as arguments to pass
// of the form: "$PASS_FOLDER/base64-url(serverURL)/username". We base64-url
// encode the serverURL, because under the hood pass uses files and folders, so
// /s will get translated into additional folders.
package pass
import (
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"github.com/docker/docker-credential-helpers/credentials"
)
const PASS_FOLDER = "docker-credential-helpers"
var (
PassInitialized bool
)
func init() {
PassInitialized = exec.Command("pass").Run() == nil
}
func runPass(stdinContent string, args ...string) (string, error) {
cmd := exec.Command("pass", args...)
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
defer stdin.Close()
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
defer stderr.Close()
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
defer stdout.Close()
err = cmd.Start()
if err != nil {
return "", err
}
_, err = stdin.Write([]byte(stdinContent))
if err != nil {
return "", err
}
stdin.Close()
errContent, err := ioutil.ReadAll(stderr)
if err != nil {
return "", fmt.Errorf("error reading stderr: %s", err)
}
result, err := ioutil.ReadAll(stdout)
if err != nil {
return "", fmt.Errorf("Error reading stdout: %s", err)
}
cmdErr := cmd.Wait()
if cmdErr != nil {
return "", fmt.Errorf("%s: %s", cmdErr, errContent)
}
return string(result), nil
}
// Pass handles secrets using Linux secret-service as a store.
type Pass struct{}
// Add adds new credentials to the keychain.
func (h Pass) Add(creds *credentials.Credentials) error {
if !PassInitialized {
return errors.New("pass store is uninitialized")
}
if creds == nil {
return errors.New("missing credentials")
}
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
_, err := runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
return err
}
// Delete removes credentials from the store.
func (h Pass) Delete(serverURL string) error {
if !PassInitialized {
return errors.New("pass store is uninitialized")
}
if serverURL == "" {
return errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
_, err := runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
return err
}
// listPassDir lists all the contents of a directory in the password store.
// Pass uses fancy unicode to emit stuff to stdout, so rather than try
// and parse this, let's just look at the directory structure instead.
func listPassDir(args ...string) ([]os.FileInfo, error) {
passDir := os.ExpandEnv("$HOME/.password-store")
for _, e := range os.Environ() {
parts := strings.SplitN(e, "=", 2)
if len(parts) < 2 {
continue
}
if parts[0] != "PASSWORD_STORE_DIR" {
continue
}
passDir = parts[1]
break
}
p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...)
contents, err := ioutil.ReadDir(p)
if err != nil {
if os.IsNotExist(err) {
return []os.FileInfo{}, nil
}
return nil, err
}
return contents, nil
}
// Get returns the username and secret to use for a given registry server URL.
func (h Pass) Get(serverURL string) (string, string, error) {
if !PassInitialized {
return "", "", errors.New("pass store is uninitialized")
}
if serverURL == "" {
return "", "", errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
usernames, err := listPassDir(encoded)
if err != nil {
return "", "", err
}
if len(usernames) < 1 {
return "", "", fmt.Errorf("no usernames for %s", serverURL)
}
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
secret, err := runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
return actual, secret, err
}
// List returns the stored URLs and corresponding usernames for a given credentials label
func (h Pass) List() (map[string]string, error) {
if !PassInitialized {
return nil, errors.New("pass store is uninitialized")
}
servers, err := listPassDir()
if err != nil {
return nil, err
}
resp := map[string]string{}
for _, server := range servers {
if !server.IsDir() {
continue
}
serverURL, err := base64.URLEncoding.DecodeString(server.Name())
if err != nil {
return nil, err
}
usernames, err := listPassDir(server.Name())
if err != nil {
return nil, err
}
if len(usernames) < 1 {
return nil, fmt.Errorf("no usernames for %s", serverURL)
}
resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg")
}
return resp, nil
}

View File

@ -105,8 +105,11 @@ func (h Secretservice) List() (map[string]string, error) {
if listLen == 0 { if listLen == 0 {
return resp, nil return resp, nil
} }
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen] // The maximum capacity of the following two slices is limited to (2^29)-1 to remain compatible
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen] // with 32-bit platforms. The size of a `*C.char` (a pointer) is 4 Byte on a 32-bit system
// and (2^29)*4 == math.MaxInt32 + 1. -- See issue golang/go#13656
pathTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
acctTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
for i := 0; i < listLen; i++ { for i := 0; i < listLen; i++ {
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i]) resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
} }