Skip to content

Commit 80833ad

Browse files
authored
Merge pull request #18 from avaid96/listing
Implementing listing functionality across OSX, Linux and Windows
2 parents 5128fa1 + f1498a0 commit 80833ad

18 files changed

+372
-14
lines changed

client/client.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,10 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error
5555
return resp, nil
5656
}
5757

58-
// Erase executes a program to remove the server credentails from the native store.
58+
// Erase executes a program to remove the server credentials from the native store.
5959
func Erase(program ProgramFunc, serverURL string) error {
6060
cmd := program("erase")
6161
cmd.Input(strings.NewReader(serverURL))
62-
6362
out, err := cmd.Output()
6463
if err != nil {
6564
t := strings.TrimSpace(string(out))
@@ -68,3 +67,15 @@ func Erase(program ProgramFunc, serverURL string) error {
6867

6968
return nil
7069
}
70+
71+
// List executes a program to remove the server credentials from the native store.
72+
func List(program ProgramFunc) error {
73+
cmd := program("list")
74+
cmd.Input(strings.NewReader("garbage"))
75+
out, err := cmd.Output()
76+
if err != nil {
77+
t := strings.TrimSpace(string(out))
78+
return fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
79+
}
80+
return nil
81+
}

client/client_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ func (m *mockProgram) Output() ([]byte, error) {
7070
default:
7171
return []byte("error storing credentials"), errProgramExited
7272
}
73+
case "list":
74+
return []byte(`{"Path":"e237574ae22fd53ddb9490dc1f72139946fd5372d42ba54d1eeb3ae5068fd22b","Username":"http://example.com/collections\u003cnotary_key\u003eSnapshot"}`), nil
75+
7376
}
7477

7578
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
@@ -190,3 +193,9 @@ func TestErase(t *testing.T) {
190193
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
191194
}
192195
}
196+
197+
func TestList(t *testing.T) {
198+
if err := List(mockProgramFn); err != nil {
199+
t.Fatal(err)
200+
}
201+
}

credentials/credentials.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ type Credentials struct {
1717
Secret string
1818
}
1919

20+
type KeyData struct {
21+
Path string
22+
Username string
23+
}
24+
2025
// Serve initializes the credentials helper and parses the action argument.
2126
// This function is designed to be called from a command line interface.
2227
// It uses os.Args[1] as the key for the action.
@@ -25,7 +30,7 @@ type Credentials struct {
2530
func Serve(helper Helper) {
2631
var err error
2732
if len(os.Args) != 2 {
28-
err = fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
33+
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0])
2934
}
3035

3136
if err == nil {
@@ -47,6 +52,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error
4752
return Get(helper, in, out)
4853
case "erase":
4954
return Erase(helper, in)
55+
case "list":
56+
return List(helper, out)
5057
}
5158
return fmt.Errorf("Unknown credential action `%s`", key)
5259
}
@@ -127,3 +134,26 @@ func Erase(helper Helper, reader io.Reader) error {
127134

128135
return helper.Delete(serverURL)
129136
}
137+
138+
//List returns all the serverURLs of keys in
139+
//the OS store as a list of strings
140+
func List(helper Helper, writer io.Writer) error {
141+
paths, accts, err := helper.List()
142+
if err != nil {
143+
return err
144+
}
145+
keyDataList := []KeyData{}
146+
for index := 0; index < len(paths); index++ {
147+
keyDataObj := KeyData{
148+
Path: paths[index],
149+
Username: accts[index],
150+
}
151+
keyDataList = append([]KeyData{keyDataObj}, keyDataList...)
152+
}
153+
buffer := new(bytes.Buffer)
154+
if err := json.NewEncoder(buffer).Encode(keyDataList); err != nil {
155+
return err
156+
}
157+
fmt.Fprint(writer, buffer.String())
158+
return nil
159+
}

credentials/credentials_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ func (m *memoryStore) Get(serverURL string) (string, string, error) {
3636
return c.Username, c.Secret, nil
3737
}
3838

39+
func (m *memoryStore) List() ([]string, []string, error) {
40+
//Simply a placeholder to let memoryStore be a valid implementation of Helper interface
41+
return nil, nil, nil
42+
}
43+
3944
func TestStore(t *testing.T) {
4045
serverURL := "https://index.docker.io/v1/"
4146
creds := &Credentials{
@@ -138,3 +143,17 @@ func TestErase(t *testing.T) {
138143
t.Fatal("expected error getting missing creds, got empty")
139144
}
140145
}
146+
147+
func TestList(t *testing.T) {
148+
//This tests that there is proper input an output into the byte stream
149+
//Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
150+
out := new(bytes.Buffer)
151+
h := newMemoryStore()
152+
if err := List(h, out); err != nil {
153+
t.Fatal(err)
154+
}
155+
//testing that there is an output
156+
if out.Len() == 0 {
157+
t.Fatalf("expected output in the writer, got %d", 0)
158+
}
159+
}

credentials/helper.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ type Helper interface {
99
// Get retrieves credentials from the store.
1010
// It returns username and secret as strings.
1111
Get(serverURL string) (string, string, error)
12+
// List returns the serverURLs of keys and their
13+
// associated usernames from the OS store as a
14+
// list of strings
15+
List() ([]string, []string, error)
1216
}

osxkeychain/osxkeychain_darwin.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#include "osxkeychain_darwin.h"
2+
#include <CoreFoundation/CoreFoundation.h>
3+
#include <stdio.h>
4+
#include <string.h>
25

36
char *get_error(OSStatus status) {
47
char *buf = malloc(128);
@@ -96,3 +99,77 @@ char *keychain_delete(struct Server *server) {
9699
}
97100
return NULL;
98101
}
102+
103+
char * CFStringToCharArr(CFStringRef aString) {
104+
if (aString == NULL) {
105+
return NULL;
106+
}
107+
CFIndex length = CFStringGetLength(aString);
108+
CFIndex maxSize =
109+
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
110+
char *buffer = (char *)malloc(maxSize);
111+
if (CFStringGetCString(aString, buffer, maxSize,
112+
kCFStringEncodingUTF8)) {
113+
return buffer;
114+
}
115+
return NULL;
116+
}
117+
118+
char *keychain_list(char *** paths, char *** accts, unsigned int *list_l) {
119+
CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
120+
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
121+
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
122+
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
123+
//Use this query dictionary
124+
CFTypeRef result= NULL;
125+
OSStatus status = SecItemCopyMatching(
126+
query,
127+
&result);
128+
//Ran a search and store the results in result
129+
if (status) {
130+
return get_error(status);
131+
}
132+
int numKeys = CFArrayGetCount(result);
133+
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
134+
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
135+
//result is of type CFArray
136+
for(int i=0; i<numKeys; i++) {
137+
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
138+
if (CFDictionaryContainsKey(currKey, CFSTR("path"))) {
139+
//Even if a key is stored without an account, Apple defaults it to null so these arrays will be of the same length
140+
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path"));
141+
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct"));
142+
if (acctTmp == NULL) {
143+
acctTmp = CFSTR("account not defined");
144+
}
145+
char * path = (char *) malloc(CFStringGetLength(pathTmp)+1);
146+
path = CFStringToCharArr(pathTmp);
147+
path[strlen(path)] = '\0';
148+
char * acct = (char *) malloc(CFStringGetLength(acctTmp)+1);
149+
acct = CFStringToCharArr(acctTmp);
150+
acct[strlen(acct)] = '\0';
151+
//We now have all we need, username and servername. Now export this to .go
152+
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)+1));
153+
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)+1));
154+
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)+1));
155+
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)+1));
156+
}
157+
else {
158+
char * path = "0";
159+
char * acct = "0";
160+
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)));
161+
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)));
162+
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)));
163+
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)));
164+
}
165+
}
166+
*list_l = numKeys;
167+
return NULL;
168+
}
169+
170+
void freeListData(char *** data, unsigned int length) {
171+
for(int i=0; i<length; i++) {
172+
free((*data)[i]);
173+
}
174+
free(*data);
175+
}

osxkeychain/osxkeychain_darwin.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@ package osxkeychain
1010
import "C"
1111
import (
1212
"errors"
13+
"github.com/docker/docker-credential-helpers/credentials"
1314
"net/url"
1415
"strconv"
1516
"strings"
1617
"unsafe"
17-
18-
"github.com/docker/docker-credential-helpers/credentials"
1918
)
2019

2120
// errCredentialsNotFound is the specific error message returned by OS X
@@ -83,7 +82,6 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
8382
if errMsg != nil {
8483
defer C.free(unsafe.Pointer(errMsg))
8584
goMsg := C.GoString(errMsg)
86-
8785
if goMsg == errCredentialsNotFound {
8886
return "", "", credentials.NewErrCredentialsNotFound()
8987
}
@@ -96,6 +94,41 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
9694
return user, pass, nil
9795
}
9896

97+
func (h Osxkeychain) List() ([]string, []string, error) {
98+
var pathsC **C.char
99+
defer C.free(unsafe.Pointer(pathsC))
100+
var acctsC **C.char
101+
defer C.free(unsafe.Pointer(acctsC))
102+
var listLenC C.uint
103+
errMsg := C.keychain_list(&pathsC, &acctsC, &listLenC)
104+
if errMsg != nil {
105+
defer C.free(unsafe.Pointer(errMsg))
106+
goMsg := C.GoString(errMsg)
107+
return nil, nil, errors.New(goMsg)
108+
}
109+
var listLen int
110+
listLen = int(listLenC)
111+
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
112+
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
113+
//taking the array of c strings into go while ignoring all the stuff irrelevant to credentials-helper
114+
paths := make([]string, listLen)
115+
accts := make([]string, listLen)
116+
at := 0
117+
for i := 0; i < listLen; i++ {
118+
if C.GoString(pathTmp[i]) == "0" {
119+
continue
120+
}
121+
paths[at] = C.GoString(pathTmp[i])
122+
accts[at] = C.GoString(acctTmp[i])
123+
at = at + 1
124+
}
125+
paths = paths[:at]
126+
accts = accts[:at]
127+
C.freeListData(&pathsC, listLenC)
128+
C.freeListData(&acctsC, listLenC)
129+
return paths, accts, nil
130+
}
131+
99132
func splitServer(serverURL string) (*C.struct_Server, error) {
100133
u, err := url.Parse(serverURL)
101134
if err != nil {

osxkeychain/osxkeychain_darwin.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,5 @@ struct Server {
1010
char *keychain_add(struct Server *server, char *username, char *secret);
1111
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret);
1212
char *keychain_delete(struct Server *server);
13+
char *keychain_list(char *** data, char *** accts, unsigned int *list_l);
14+
void freeListData(char *** data, unsigned int length);

osxkeychain/osxkeychain_darwin_test.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package osxkeychain
22

33
import (
4-
"testing"
5-
64
"github.com/docker/docker-credential-helpers/credentials"
5+
"testing"
76
)
87

98
func TestOSXKeychainHelper(t *testing.T) {
@@ -12,7 +11,11 @@ func TestOSXKeychainHelper(t *testing.T) {
1211
Username: "foobar",
1312
Secret: "foobarbaz",
1413
}
15-
14+
creds1 := &credentials.Credentials{
15+
ServerURL: "https://foobar.docker.io:2376/v2",
16+
Username: "foobarbaz",
17+
Secret: "foobar",
18+
}
1619
helper := Osxkeychain{}
1720
if err := helper.Add(creds); err != nil {
1821
t.Fatal(err)
@@ -31,6 +34,21 @@ func TestOSXKeychainHelper(t *testing.T) {
3134
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
3235
}
3336

37+
paths, accts, err := helper.List()
38+
if err != nil || len(paths) == 0 || len(accts) == 0 {
39+
t.Fatal(err)
40+
}
41+
42+
helper.Add(creds1)
43+
defer helper.Delete(creds1.ServerURL)
44+
newpaths, newaccts, err := helper.List()
45+
if len(newpaths)-len(paths) != 1 || len(newaccts)-len(accts) != 1 {
46+
if err == nil {
47+
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), "")
48+
}
49+
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())
50+
}
51+
3452
if err := helper.Delete(creds.ServerURL); err != nil {
3553
t.Fatal(err)
3654
}

0 commit comments

Comments
 (0)