1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-13 16:01:28 +05:30

Merge pull request #18 from avaid96/listing

Implementing listing functionality across OSX, Linux and Windows
This commit is contained in:
David Calavera
2016-07-20 13:55:38 -07:00
committed by GitHub
18 changed files with 372 additions and 14 deletions
+13 -2
View File
@@ -55,11 +55,10 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error
return resp, nil
}
// Erase executes a program to remove the server credentails from the native store.
// Erase executes a program to remove the server credentials from the native store.
func Erase(program ProgramFunc, serverURL string) error {
cmd := program("erase")
cmd.Input(strings.NewReader(serverURL))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
@@ -68,3 +67,15 @@ func Erase(program ProgramFunc, serverURL string) error {
return nil
}
// List executes a program to remove the server credentials from the native store.
func List(program ProgramFunc) error {
cmd := program("list")
cmd.Input(strings.NewReader("garbage"))
out, err := cmd.Output()
if err != nil {
t := strings.TrimSpace(string(out))
return fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
}
return nil
}
+9
View File
@@ -70,6 +70,9 @@ func (m *mockProgram) Output() ([]byte, error) {
default:
return []byte("error storing credentials"), errProgramExited
}
case "list":
return []byte(`{"Path":"e237574ae22fd53ddb9490dc1f72139946fd5372d42ba54d1eeb3ae5068fd22b","Username":"http://example.com/collections\u003cnotary_key\u003eSnapshot"}`), nil
}
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
@@ -190,3 +193,9 @@ func TestErase(t *testing.T) {
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
}
}
func TestList(t *testing.T) {
if err := List(mockProgramFn); err != nil {
t.Fatal(err)
}
}
+31 -1
View File
@@ -17,6 +17,11 @@ type Credentials struct {
Secret string
}
type KeyData struct {
Path string
Username string
}
// Serve initializes the credentials helper and parses the action argument.
// This function is designed to be called from a command line interface.
// It uses os.Args[1] as the key for the action.
@@ -25,7 +30,7 @@ type Credentials struct {
func Serve(helper Helper) {
var err error
if len(os.Args) != 2 {
err = fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0])
}
if err == nil {
@@ -47,6 +52,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error
return Get(helper, in, out)
case "erase":
return Erase(helper, in)
case "list":
return List(helper, out)
}
return fmt.Errorf("Unknown credential action `%s`", key)
}
@@ -127,3 +134,26 @@ func Erase(helper Helper, reader io.Reader) error {
return helper.Delete(serverURL)
}
//List returns all the serverURLs of keys in
//the OS store as a list of strings
func List(helper Helper, writer io.Writer) error {
paths, accts, err := helper.List()
if err != nil {
return err
}
keyDataList := []KeyData{}
for index := 0; index < len(paths); index++ {
keyDataObj := KeyData{
Path: paths[index],
Username: accts[index],
}
keyDataList = append([]KeyData{keyDataObj}, keyDataList...)
}
buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(keyDataList); err != nil {
return err
}
fmt.Fprint(writer, buffer.String())
return nil
}
+19
View File
@@ -36,6 +36,11 @@ func (m *memoryStore) Get(serverURL string) (string, string, error) {
return c.Username, c.Secret, nil
}
func (m *memoryStore) List() ([]string, []string, error) {
//Simply a placeholder to let memoryStore be a valid implementation of Helper interface
return nil, nil, nil
}
func TestStore(t *testing.T) {
serverURL := "https://index.docker.io/v1/"
creds := &Credentials{
@@ -138,3 +143,17 @@ func TestErase(t *testing.T) {
t.Fatal("expected error getting missing creds, got empty")
}
}
func TestList(t *testing.T) {
//This tests that there is proper input an output into the byte stream
//Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
out := new(bytes.Buffer)
h := newMemoryStore()
if err := List(h, out); err != nil {
t.Fatal(err)
}
//testing that there is an output
if out.Len() == 0 {
t.Fatalf("expected output in the writer, got %d", 0)
}
}
+4
View File
@@ -9,4 +9,8 @@ type Helper interface {
// Get retrieves credentials from the store.
// It returns username and secret as strings.
Get(serverURL string) (string, string, error)
// List returns the serverURLs of keys and their
// associated usernames from the OS store as a
// list of strings
List() ([]string, []string, error)
}
+77
View File
@@ -1,4 +1,7 @@
#include "osxkeychain_darwin.h"
#include <CoreFoundation/CoreFoundation.h>
#include <stdio.h>
#include <string.h>
char *get_error(OSStatus status) {
char *buf = malloc(128);
@@ -96,3 +99,77 @@ char *keychain_delete(struct Server *server) {
}
return NULL;
}
char * CFStringToCharArr(CFStringRef aString) {
if (aString == NULL) {
return NULL;
}
CFIndex length = CFStringGetLength(aString);
CFIndex maxSize =
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
char *buffer = (char *)malloc(maxSize);
if (CFStringGetCString(aString, buffer, maxSize,
kCFStringEncodingUTF8)) {
return buffer;
}
return NULL;
}
char *keychain_list(char *** paths, char *** accts, unsigned int *list_l) {
CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
//Use this query dictionary
CFTypeRef result= NULL;
OSStatus status = SecItemCopyMatching(
query,
&result);
//Ran a search and store the results in result
if (status) {
return get_error(status);
}
int numKeys = CFArrayGetCount(result);
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
//result is of type CFArray
for(int i=0; i<numKeys; i++) {
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
if (CFDictionaryContainsKey(currKey, CFSTR("path"))) {
//Even if a key is stored without an account, Apple defaults it to null so these arrays will be of the same length
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path"));
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct"));
if (acctTmp == NULL) {
acctTmp = CFSTR("account not defined");
}
char * path = (char *) malloc(CFStringGetLength(pathTmp)+1);
path = CFStringToCharArr(pathTmp);
path[strlen(path)] = '\0';
char * acct = (char *) malloc(CFStringGetLength(acctTmp)+1);
acct = CFStringToCharArr(acctTmp);
acct[strlen(acct)] = '\0';
//We now have all we need, username and servername. Now export this to .go
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)+1));
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)+1));
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)+1));
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)+1));
}
else {
char * path = "0";
char * acct = "0";
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)));
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)));
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)));
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)));
}
}
*list_l = numKeys;
return NULL;
}
void freeListData(char *** data, unsigned int length) {
for(int i=0; i<length; i++) {
free((*data)[i]);
}
free(*data);
}
+36 -3
View File
@@ -10,12 +10,11 @@ package osxkeychain
import "C"
import (
"errors"
"github.com/docker/docker-credential-helpers/credentials"
"net/url"
"strconv"
"strings"
"unsafe"
"github.com/docker/docker-credential-helpers/credentials"
)
// errCredentialsNotFound is the specific error message returned by OS X
@@ -83,7 +82,6 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
if errMsg != nil {
defer C.free(unsafe.Pointer(errMsg))
goMsg := C.GoString(errMsg)
if goMsg == errCredentialsNotFound {
return "", "", credentials.NewErrCredentialsNotFound()
}
@@ -96,6 +94,41 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
return user, pass, nil
}
func (h Osxkeychain) List() ([]string, []string, error) {
var pathsC **C.char
defer C.free(unsafe.Pointer(pathsC))
var acctsC **C.char
defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint
errMsg := C.keychain_list(&pathsC, &acctsC, &listLenC)
if errMsg != nil {
defer C.free(unsafe.Pointer(errMsg))
goMsg := C.GoString(errMsg)
return nil, nil, errors.New(goMsg)
}
var listLen int
listLen = int(listLenC)
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
//taking the array of c strings into go while ignoring all the stuff irrelevant to credentials-helper
paths := make([]string, listLen)
accts := make([]string, listLen)
at := 0
for i := 0; i < listLen; i++ {
if C.GoString(pathTmp[i]) == "0" {
continue
}
paths[at] = C.GoString(pathTmp[i])
accts[at] = C.GoString(acctTmp[i])
at = at + 1
}
paths = paths[:at]
accts = accts[:at]
C.freeListData(&pathsC, listLenC)
C.freeListData(&acctsC, listLenC)
return paths, accts, nil
}
func splitServer(serverURL string) (*C.struct_Server, error) {
u, err := url.Parse(serverURL)
if err != nil {
+2
View File
@@ -10,3 +10,5 @@ struct Server {
char *keychain_add(struct Server *server, char *username, char *secret);
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret);
char *keychain_delete(struct Server *server);
char *keychain_list(char *** data, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length);
+21 -3
View File
@@ -1,9 +1,8 @@
package osxkeychain
import (
"testing"
"github.com/docker/docker-credential-helpers/credentials"
"testing"
)
func TestOSXKeychainHelper(t *testing.T) {
@@ -12,7 +11,11 @@ func TestOSXKeychainHelper(t *testing.T) {
Username: "foobar",
Secret: "foobarbaz",
}
creds1 := &credentials.Credentials{
ServerURL: "https://foobar.docker.io:2376/v2",
Username: "foobarbaz",
Secret: "foobar",
}
helper := Osxkeychain{}
if err := helper.Add(creds); err != nil {
t.Fatal(err)
@@ -31,6 +34,21 @@ func TestOSXKeychainHelper(t *testing.T) {
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
}
paths, accts, err := helper.List()
if err != nil || len(paths) == 0 || len(accts) == 0 {
t.Fatal(err)
}
helper.Add(creds1)
defer helper.Delete(creds1.ServerURL)
newpaths, newaccts, err := helper.List()
if len(newpaths)-len(paths) != 1 || len(newaccts)-len(accts) != 1 {
if err == nil {
t.Fatalf("Error: len(newpaths): %d, len(paths): %d\n len(newaccts): %d, len(accts): %d\n Error= %s", len(newpaths), len(paths), len(newaccts), len(accts), "")
}
t.Fatalf("Error: len(newpaths): %d, len(paths): %d\n len(newaccts): %d, len(accts): %d\n Error= %s", len(newpaths), len(paths), len(newaccts), len(accts), err.Error())
}
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}
+50
View File
@@ -1,4 +1,5 @@
#include <string.h>
#include <stdlib.h>
#include "secretservice_linux.h"
const SecretSchema *docker_get_schema(void)
@@ -96,3 +97,52 @@ GError *get(char *server, char **username, char **secret) {
}
return NULL;
}
GError *list(char *** paths, char *** accts, unsigned int *list_l) {
GList *items;
GError *err = NULL;
SecretService *service;
SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK;
GHashTable *attributes;
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
int numKeys = g_list_length(items);
if (err != NULL) {
return err;
}
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
// items now contains our keys from the gnome keyring
// we will now put it in our two lists to return it to go
GList *current;
int listNumber = 0;
for(current = items; current!=NULL; current = current->next) {
char *pathTmp = secret_item_get_label(current->data);
// you cannot have a key without a label in the gnome keyring
char *acctTmp = get_username(current->data);
if (acctTmp==NULL) {
acctTmp = "account not defined";
}
char *path = (char *) malloc(strlen(pathTmp));
char *acct = (char *) malloc(strlen(acctTmp));
path = pathTmp;
acct = acctTmp;
(*paths)[listNumber] = (char *) malloc(sizeof(char)*(strlen(path)));
memcpy((*paths)[listNumber], path, sizeof(char)*(strlen(path)));
(*accts)[listNumber] = (char *) malloc(sizeof(char)*(strlen(acct)));
memcpy((*accts)[listNumber], acct, sizeof(char)*(strlen(acct)));
listNumber = listNumber + 1;
}
*list_l = numKeys;
return NULL;
}
void freeListData(char *** data, unsigned int length) {
int i;
for(i=0; i<length; i++) {
free((*data)[i]);
}
free(*data);
}
+25
View File
@@ -78,3 +78,28 @@ func (h Secretservice) Get(serverURL string) (string, string, error) {
}
return user, pass, nil
}
func (h Secretservice) List() ([]string, []string, error) {
var pathsC **C.char
defer C.free(unsafe.Pointer(pathsC))
var acctsC **C.char
defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint
err := C.list(&pathsC, &acctsC, &listLenC)
if err != nil {
defer C.free(unsafe.Pointer(err))
return nil, nil, errors.New("Error from list function in secretservice_linux.c likely due to error in secretservice library")
}
listLen := int(listLenC)
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
paths := make([]string, listLen)
accts := make([]string, listLen)
for i := 0; i < listLen; i++ {
paths[i] = C.GoString(pathTmp[i])
accts[i] = C.GoString(acctTmp[i])
}
C.freeListData(&pathsC, listLenC)
C.freeListData(&acctsC, listLenC)
return paths, accts, nil
}
+2
View File
@@ -9,3 +9,5 @@ const SecretSchema *docker_get_schema(void) G_GNUC_CONST;
GError *add(char *server, char *username, char *secret);
GError *delete(char *server);
GError *get(char *server, char **username, char **secret);
GError *list(char *** paths, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length);
@@ -36,6 +36,14 @@ func TestSecretServiceHelper(t *testing.T) {
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}
paths, accts, err := helper.List()
if err != nil || len(paths) == 0 || len(accts) == 0 {
t.Fatal(err)
}
helper.Add(creds)
if newpaths, newaccts, err := helper.List(); (len(newpaths)-len(paths)) != 1 || (len(newaccts)-len(accts)) != 1 {
t.Fatal(err)
}
}
func TestMissingCredentials(t *testing.T) {
-1
View File
@@ -58,7 +58,6 @@ func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
resultAttr.Keyword = utf16PtrToString(attr.Keyword)
resultAttr.Value = C.GoBytes(unsafe.Pointer(attr.Value), C.int(attr.ValueSize))
}
return result
}
+36 -4
View File
@@ -1,6 +1,7 @@
package wincred
import (
"C"
"syscall"
"unsafe"
)
@@ -8,10 +9,11 @@ import (
var (
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
procCredRead = modadvapi32.NewProc("CredReadW")
procCredWrite = modadvapi32.NewProc("CredWriteW")
procCredDelete = modadvapi32.NewProc("CredDeleteW")
procCredFree = modadvapi32.NewProc("CredFree")
procCredRead = modadvapi32.NewProc("CredReadW")
procCredWrite = modadvapi32.NewProc("CredWriteW")
procCredDelete = modadvapi32.NewProc("CredDeleteW")
procCredFree = modadvapi32.NewProc("CredFree")
procCredEnumerate = modadvapi32.NewProc("CredEnumerateW")
)
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
@@ -97,3 +99,33 @@ func nativeCredDelete(cred *Credential, typ nativeCRED_TYPE) error {
return nil
}
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374794(v=vs.85).aspx
func nativeCredEnumerate(filter string, all bool) ([]*Credential, error) {
var count int
var pcreds uintptr
var filterPtr uintptr
if !all {
filterUtf16Ptr, _ := syscall.UTF16PtrFromString(filter)
filterPtr = uintptr(unsafe.Pointer(filterUtf16Ptr))
} else {
filterPtr = 0
}
ret, _, err := procCredEnumerate.Call(
filterPtr,
0,
uintptr(unsafe.Pointer(&count)),
uintptr(unsafe.Pointer(&pcreds)),
)
if ret == 0 {
return nil, err
}
defer procCredFree.Call(pcreds)
pcredsSlice := (*[1 << 30]uintptr)(unsafe.Pointer(pcreds))[:count:count]
creds := make([]*Credential, count)
for i := range creds {
creds[i] = nativeToCredential((*nativeCREDENTIAL)(unsafe.Pointer(pcredsSlice[i])))
}
return creds, nil
}
+5
View File
@@ -67,3 +67,8 @@ func (t *DomainPassword) Delete() (err error) {
func (t *DomainPassword) SetPassword(pw string) {
t.CredentialBlob = utf16ToByte(syscall.StringToUTF16(pw))
}
// List the contents of the Credentials store
func List() ([]*Credential, error) {
return nativeCredEnumerate("", true)
}
+14
View File
@@ -37,3 +37,17 @@ func (h Wincred) Get(serverURL string) (string, string, error) {
}
return g.UserName, string(g.CredentialBlob), nil
}
func (h Wincred) List() ([]string, []string, error) {
creds, err := winc.List()
paths := make([]string, len(creds))
accts := make([]string, len(creds))
if err != nil {
return nil, nil, err
}
for i := range creds {
paths[i] = creds[i].TargetName
accts[i] = creds[i].UserName
}
return paths, accts, nil
}
+20
View File
@@ -12,6 +12,11 @@ func TestWinCredHelper(t *testing.T) {
Username: "foobar",
Secret: "foobarbaz",
}
creds1 := &credentials.Credentials{
ServerURL: "https://foobar.docker.io:2376/v2",
Username: "foobarbaz",
Secret: "foobar",
}
helper := Wincred{}
if err := helper.Add(creds); err != nil {
@@ -31,6 +36,21 @@ func TestWinCredHelper(t *testing.T) {
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
}
paths, accts, err := helper.List()
if err != nil || len(paths) == 0 || len(accts) == 0 {
t.Fatal(err)
}
helper.Add(creds1)
defer helper.Delete(creds1.ServerURL)
newpaths, newaccts, err := helper.List()
if len(newpaths)-len(paths) != 1 || len(newaccts)-len(accts) != 1 {
if err == nil {
t.Fatalf("Error: len(newpaths): %d, len(paths): %d\n len(newaccts): %d, len(accts): %d\n Error= %s", len(newpaths), len(paths), len(newaccts), len(accts), "")
}
t.Fatalf("Error: len(newpaths): %d, len(paths): %d\n len(newaccts): %d, len(accts): %d\n Error= %s", len(newpaths), len(paths), len(newaccts), len(accts), err.Error())
}
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}