mirror of
https://github.com/docker/docker-credential-helpers.git
synced 2026-06-13 16:01:28 +05:30
pass: only init on run, and do so lazily
This also fixes the following issues: 1. Safe for concurrent initialization still (it was before in 'init', but the alternative to this PR is not) 2. Uses the same password directory during init as it does during runtime (the change to getPassDir in initialization logic. 3. Prints significantly better errors if initialization fails 4. Has slightly cleaner abstractions by hiding the initialization check in 'runPass' The 4th item there does mean there are a few cases where more work is done before erroring, but that amount of work is trivial and my manual audit didn't reveal anything that seemed worrying. Fixes #96, alternative to #106 Signed-off-by: Euan Kemp <euank@euank.com>
This commit is contained in:
committed by
Vincent Demeester
parent
26deb2937d
commit
5da09fd251
+40
-37
@@ -5,7 +5,6 @@
|
|||||||
package pass
|
package pass
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,40 +13,63 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/docker/docker-credential-helpers/credentials"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PASS_FOLDER = "docker-credential-helpers"
|
const PASS_FOLDER = "docker-credential-helpers"
|
||||||
|
|
||||||
var (
|
// Pass handles secrets using Linux secret-service as a store.
|
||||||
PassInitialized bool
|
type Pass struct{}
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
// Ideally these would be stored as members of Pass, but since all of Pass's
|
||||||
|
// methods have value receivers, not pointer receivers, and changing that is
|
||||||
|
// backwards incompatible, we assume that all Pass instances share the same configuration
|
||||||
|
|
||||||
|
// initializationMutex is held while initializing so that only one 'pass'
|
||||||
|
// round-tripping is done to check pass is functioning.
|
||||||
|
var initializationMutex sync.Mutex
|
||||||
|
var passInitialized bool
|
||||||
|
|
||||||
|
func (p Pass) checkInitialized() error {
|
||||||
|
initializationMutex.Lock()
|
||||||
|
defer initializationMutex.Unlock()
|
||||||
|
if passInitialized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
// In principle, we could just run `pass init`. However, pass has a bug
|
// In principle, we could just run `pass init`. However, pass has a bug
|
||||||
// where if gpg fails, it doesn't always exit 1. Additionally, pass
|
// where if gpg fails, it doesn't always exit 1. Additionally, pass
|
||||||
// uses gpg2, but gpg is the default, which may be confusing. So let's
|
// uses gpg2, but gpg is the default, which may be confusing. So let's
|
||||||
// just explictily check that pass actually can store and retreive a
|
// just explictily check that pass actually can store and retreive a
|
||||||
// password.
|
// password.
|
||||||
password := "pass is initialized"
|
password := "pass is initialized"
|
||||||
name := path.Join(PASS_FOLDER, "docker-pass-initialized-check")
|
name := path.Join(getPassDir(), "docker-pass-initialized-check")
|
||||||
|
|
||||||
_, err := runPass(password, "insert", "-f", "-m", name)
|
_, err := p.runPassHelper(password, "insert", "-f", "-m", name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return fmt.Errorf("error initializing pass: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stored, err := runPass("", "show", name)
|
stored, err := p.runPassHelper("", "show", name)
|
||||||
PassInitialized = err == nil && stored == password
|
if err != nil {
|
||||||
|
return fmt.Errorf("error fetching password during initialization: %v", err)
|
||||||
if PassInitialized {
|
|
||||||
runPass("", "rm", "-rf", name)
|
|
||||||
}
|
}
|
||||||
|
if stored != password {
|
||||||
|
return fmt.Errorf("error round-tripping password during initialization: %q != %q", password, stored)
|
||||||
|
}
|
||||||
|
passInitialized = true
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPass(stdin string, args ...string) (string, error) {
|
func (p Pass) runPass(stdinContent string, args ...string) (string, error) {
|
||||||
var stdout, stderr bytes.Buffer
|
if err := p.checkInitialized(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return p.runPassHelper(stdinContent, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Pass) runPassHelper(stdinContent string, args ...string) (string, error) {
|
||||||
cmd := exec.Command("pass", args...)
|
cmd := exec.Command("pass", args...)
|
||||||
cmd.Stdin = strings.NewReader(stdin)
|
cmd.Stdin = strings.NewReader(stdin)
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
@@ -61,37 +83,26 @@ func runPass(stdin string, args ...string) (string, error) {
|
|||||||
return stdout.String(), nil
|
return stdout.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass handles secrets using Linux secret-service as a store.
|
|
||||||
type Pass struct{}
|
|
||||||
|
|
||||||
// Add adds new credentials to the keychain.
|
// Add adds new credentials to the keychain.
|
||||||
func (h Pass) Add(creds *credentials.Credentials) error {
|
func (h Pass) Add(creds *credentials.Credentials) error {
|
||||||
if !PassInitialized {
|
|
||||||
return errors.New("pass store is uninitialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if creds == nil {
|
if creds == nil {
|
||||||
return errors.New("missing credentials")
|
return errors.New("missing credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
|
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
|
||||||
|
|
||||||
_, err := runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
|
_, err := h.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes credentials from the store.
|
// Delete removes credentials from the store.
|
||||||
func (h Pass) Delete(serverURL string) error {
|
func (h Pass) Delete(serverURL string) error {
|
||||||
if !PassInitialized {
|
|
||||||
return errors.New("pass store is uninitialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
return errors.New("missing server url")
|
return errors.New("missing server url")
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
|
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
|
||||||
_, err := runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
|
_, err := h.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +134,6 @@ func listPassDir(args ...string) ([]os.FileInfo, error) {
|
|||||||
|
|
||||||
// Get returns the username and secret to use for a given registry server URL.
|
// Get returns the username and secret to use for a given registry server URL.
|
||||||
func (h Pass) Get(serverURL string) (string, string, error) {
|
func (h Pass) Get(serverURL string) (string, string, error) {
|
||||||
if !PassInitialized {
|
|
||||||
return "", "", errors.New("pass store is uninitialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
return "", "", errors.New("missing server url")
|
return "", "", errors.New("missing server url")
|
||||||
}
|
}
|
||||||
@@ -151,16 +158,12 @@ func (h Pass) Get(serverURL string) (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
|
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
|
||||||
secret, err := runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
|
secret, err := h.runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
|
||||||
return actual, secret, err
|
return actual, secret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns the stored URLs and corresponding usernames for a given credentials label
|
// List returns the stored URLs and corresponding usernames for a given credentials label
|
||||||
func (h Pass) List() (map[string]string, error) {
|
func (h Pass) List() (map[string]string, error) {
|
||||||
if !PassInitialized {
|
|
||||||
return nil, errors.New("pass store is uninitialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
servers, err := listPassDir()
|
servers, err := listPassDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Reference in New Issue
Block a user