-
Notifications
You must be signed in to change notification settings - Fork 182
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add a
pass
credential helper backend
Signed-off-by: Tycho Andersen <tycho@docker.com>
- Loading branch information
Tycho Andersen
committed
Aug 11, 2017
1 parent
4fbc86d
commit 1ab1037
Showing
6 changed files
with
313 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package main | ||
|
||
import ( | ||
"github.com/docker/docker-credential-helpers/credentials" | ||
"github.com/docker/docker-credential-helpers/pass" | ||
) | ||
|
||
func main() { | ||
credentials.Serve(pass.Pass{}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package pass | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"github.com/docker/docker-credential-helpers/credentials" | ||
) | ||
|
||
func TestPassHelper(t *testing.T) { | ||
helper := Pass{} | ||
|
||
creds := &credentials.Credentials{ | ||
ServerURL: "https://foobar.docker.io:2376/v1", | ||
Username: "nothing", | ||
Secret: "isthebestmeshuggahalbum", | ||
} | ||
|
||
helper.Add(creds) | ||
|
||
creds.ServerURL = "https://foobar.docker.io:9999/v2" | ||
helper.Add(creds) | ||
|
||
credsList, err := helper.List() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
for server, username := range credsList { | ||
if !(strings.Contains(server, "2376") || | ||
strings.Contains(server, "9999")) { | ||
t.Fatalf("invalid url: %s", creds.ServerURL) | ||
} | ||
|
||
if username != "nothing" { | ||
t.Fatalf("invalid username: %v", username) | ||
} | ||
|
||
u, s, err := helper.Get(server) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if u != username { | ||
t.Fatalf("invalid username %s", u) | ||
} | ||
|
||
if s != "isthebestmeshuggahalbum" { | ||
t.Fatalf("invalid secret: %s", s) | ||
} | ||
|
||
err = helper.Delete(server) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
_, _, err = helper.Get(server) | ||
if err == nil { | ||
t.Fatalf("%s shuldn't exist any more", server) | ||
} | ||
} | ||
|
||
credsList, err = helper.List() | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
if len(credsList) != 0 { | ||
t.Fatal("didn't delete all creds?") | ||
} | ||
} |