Skip to content

Commit 6e16e63

Browse files
authored
enhance: provide contexts to credential helpers when listing credentials (#979)
Signed-off-by: Grant Linville <[email protected]>
1 parent 118e3ae commit 6e16e63

File tree

4 files changed

+195
-6
lines changed

4 files changed

+195
-6
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ init-docs:
5353

5454
# Ensure docs build without errors. Makes sure generated docs are in-sync with CLI.
5555
validate-docs: gen-docs
56-
docker run --rm --workdir=/docs -v $${PWD}/docs:/docs node:18-buster yarn build
56+
docker run --rm --workdir=/docs -v $${PWD}/docs:/docs node:18-buster npm run build
5757
if [ -n "$$(git status --porcelain --untracked-files=no)" ]; then \
5858
git status --porcelain --untracked-files=no; \
5959
echo "Encountered dirty repo!"; \

pkg/credentials/store.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,9 @@ func (s *Store) recreateCredential(store credentials.Store, serverAddress string
269269
func (s *Store) getStore() (credentials.Store, error) {
270270
if s.program != nil {
271271
return &toolCredentialStore{
272-
file: credentials.NewFileStore(s.cfg),
273-
program: s.program,
272+
file: credentials.NewFileStore(s.cfg),
273+
program: s.program,
274+
contexts: s.credCtxs,
274275
}, nil
275276
}
276277
return credentials.NewFileStore(s.cfg), nil

pkg/credentials/toolstore.go

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package credentials
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"errors"
7+
"fmt"
58
"net/url"
69
"regexp"
710
"strings"
@@ -13,8 +16,9 @@ import (
1316
)
1417

1518
type toolCredentialStore struct {
16-
file credentials.Store
17-
program client.ProgramFunc
19+
file credentials.Store
20+
program client.ProgramFunc
21+
contexts []string
1822
}
1923

2024
func (h *toolCredentialStore) Erase(serverAddress string) error {
@@ -42,8 +46,21 @@ func (h *toolCredentialStore) Get(serverAddress string) (types.AuthConfig, error
4246
}, nil
4347
}
4448

49+
// GetAll will list all credentials in the credential store.
50+
// It MAY (but is not required to) filter the credentials based on the contexts provided.
51+
// This is only supported by some credential stores, while others will ignore it and return all credentials.
52+
// The caller of this function is still required to filter the output to only include the contexts requested.
4553
func (h *toolCredentialStore) GetAll() (map[string]types.AuthConfig, error) {
46-
serverAddresses, err := client.List(h.program)
54+
var (
55+
serverAddresses map[string]string
56+
err error
57+
)
58+
if len(h.contexts) == 0 {
59+
serverAddresses, err = client.List(h.program)
60+
} else {
61+
serverAddresses, err = listWithContexts(h.program, h.contexts)
62+
}
63+
4764
if err != nil {
4865
return nil, err
4966
}
@@ -94,3 +111,44 @@ func (h *toolCredentialStore) Store(authConfig types.AuthConfig) error {
94111
Secret: authConfig.Password,
95112
})
96113
}
114+
115+
// listWithContexts is almost an exact copy of the List function in Docker's libraries,
116+
// the only difference being that we pass the context through as input to the program.
117+
// This will allow some credential stores, like Postgres, to do an optimized list.
118+
func listWithContexts(program client.ProgramFunc, contexts []string) (map[string]string, error) {
119+
cmd := program(credentials2.ActionList)
120+
121+
contextsJSON, err := json.Marshal(contexts)
122+
if err != nil {
123+
return nil, err
124+
}
125+
126+
cmd.Input(bytes.NewReader(contextsJSON))
127+
out, err := cmd.Output()
128+
if err != nil {
129+
t := strings.TrimSpace(string(out))
130+
131+
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
132+
err = isValidErr
133+
}
134+
135+
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
136+
}
137+
138+
var resp map[string]string
139+
if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
140+
return nil, err
141+
}
142+
143+
return resp, nil
144+
}
145+
146+
func isValidCredsMessage(msg string) error {
147+
if credentials2.IsCredentialsMissingServerURLMessage(msg) {
148+
return credentials2.NewErrCredentialsMissingServerURL()
149+
}
150+
if credentials2.IsCredentialsMissingUsernameMessage(msg) {
151+
return credentials2.NewErrCredentialsMissingUsername()
152+
}
153+
return nil
154+
}

pkg/credentials/toolstore_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package credentials
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"testing"
8+
9+
"github.com/docker/cli/cli/config/types"
10+
"github.com/docker/docker-credential-helpers/client"
11+
"github.com/docker/docker-credential-helpers/credentials"
12+
)
13+
14+
type mockProgram struct {
15+
// mode is either "db" or "normal"
16+
// db mode will honor contexts, normal mode will not
17+
mode string
18+
action string
19+
contexts []string
20+
}
21+
22+
func (m *mockProgram) Input(in io.Reader) {
23+
switch m.action {
24+
case credentials.ActionList:
25+
var contexts []string
26+
if err := json.NewDecoder(in).Decode(&contexts); err == nil && len(contexts) > 0 {
27+
m.contexts = contexts
28+
}
29+
}
30+
// TODO: add other cases here as needed
31+
}
32+
33+
func (m *mockProgram) Output() ([]byte, error) {
34+
switch m.action {
35+
case credentials.ActionList:
36+
switch m.mode {
37+
case "db":
38+
// Return only credentials that are in the list of contexts.
39+
creds := make(map[string]string)
40+
for _, context := range m.contexts {
41+
creds[fmt.Sprintf("https://example///%s", context)] = "username"
42+
}
43+
return json.Marshal(creds)
44+
case "normal":
45+
// Return credentials in the list of contexts, plus some made up extras.
46+
creds := make(map[string]string)
47+
for _, context := range m.contexts {
48+
creds[fmt.Sprintf("https://example///%s", context)] = "username"
49+
}
50+
creds[fmt.Sprintf("https://example///%s", "otherContext1")] = "username"
51+
creds[fmt.Sprintf("https://example///%s", "otherContext2")] = "username"
52+
return json.Marshal(creds)
53+
}
54+
}
55+
return nil, nil
56+
}
57+
58+
func newMockProgram(t *testing.T, mode string) client.ProgramFunc {
59+
t.Helper()
60+
return func(args ...string) client.Program {
61+
p := &mockProgram{
62+
mode: mode,
63+
}
64+
if len(args) > 0 {
65+
p.action = args[0]
66+
}
67+
return p
68+
}
69+
}
70+
71+
func TestGetAll(t *testing.T) {
72+
dbProgram := newMockProgram(t, "db")
73+
normalProgram := newMockProgram(t, "normal")
74+
75+
tests := []struct {
76+
name string
77+
program client.ProgramFunc
78+
wantErr bool
79+
contexts []string
80+
expected map[string]types.AuthConfig
81+
}{
82+
{name: "db", program: dbProgram, wantErr: false, contexts: []string{"credctx"}, expected: map[string]types.AuthConfig{
83+
"https://example///credctx": {
84+
Username: "username",
85+
ServerAddress: "https://example///credctx",
86+
},
87+
}},
88+
{name: "normal", program: normalProgram, wantErr: false, contexts: []string{"credctx"}, expected: map[string]types.AuthConfig{
89+
"https://example///credctx": {
90+
Username: "username",
91+
ServerAddress: "https://example///credctx",
92+
},
93+
"https://example///otherContext1": {
94+
Username: "username",
95+
ServerAddress: "https://example///otherContext1",
96+
},
97+
"https://example///otherContext2": {
98+
Username: "username",
99+
ServerAddress: "https://example///otherContext2",
100+
},
101+
}},
102+
}
103+
104+
for _, test := range tests {
105+
t.Run(test.name, func(t *testing.T) {
106+
store := &toolCredentialStore{
107+
program: test.program,
108+
contexts: test.contexts,
109+
}
110+
got, err := store.GetAll()
111+
if (err != nil) != test.wantErr {
112+
t.Errorf("GetAll() error = %v, wantErr %v", err, test.wantErr)
113+
}
114+
if len(got) != len(test.expected) {
115+
t.Errorf("GetAll() got %d credentials, want %d", len(got), len(test.expected))
116+
}
117+
for name, cred := range got {
118+
if _, ok := test.expected[name]; !ok {
119+
t.Errorf("GetAll() got unexpected credential: %s", name)
120+
}
121+
if got[name].Username != test.expected[name].Username {
122+
t.Errorf("GetAll() got unexpected username for %s", cred.ServerAddress)
123+
}
124+
if got[name].Username != test.expected[name].Username {
125+
t.Errorf("GetAll() got unexpected username for %s", name)
126+
}
127+
}
128+
})
129+
}
130+
}

0 commit comments

Comments
 (0)