Skip to content

Commit ed11c58

Browse files
authored
Prevent invalid credentials: no missing server URL or username (#62)
* Prevent invalid credentials: no missing server URL or username Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]> * Add missing username/serverURL error checks in credstore client Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]> * Clean up doc on invalid creds errors and client's error checks Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]> * Add tests for missing ServerURL/Username Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]> * Clean isValidCredsMessage prototype Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]> * Add test for missing server URL and more detailed error msg Signed-off-by: Nassim 'Nass' Eddequiouaq <[email protected]>
1 parent 79ab705 commit ed11c58

File tree

5 files changed

+230
-5
lines changed

5 files changed

+230
-5
lines changed

client/client.go

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,39 @@ import (
99
"github.com/docker/docker-credential-helpers/credentials"
1010
)
1111

12+
// isValidCredsMessage checks if 'msg' contains invalid credentials error message.
13+
// It returns whether the logs are free of invalid credentials errors and the error if it isn't.
14+
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername.
15+
func isValidCredsMessage(msg string) error {
16+
if credentials.IsCredentialsMissingServerURLMessage(msg) {
17+
return credentials.NewErrCredentialsMissingServerURL()
18+
}
19+
20+
if credentials.IsCredentialsMissingUsernameMessage(msg) {
21+
return credentials.NewErrCredentialsMissingUsername()
22+
}
23+
24+
return nil
25+
}
26+
1227
// Store uses an external program to save credentials.
13-
func Store(program ProgramFunc, credentials *credentials.Credentials) error {
28+
func Store(program ProgramFunc, creds *credentials.Credentials) error {
1429
cmd := program("store")
1530

1631
buffer := new(bytes.Buffer)
17-
if err := json.NewEncoder(buffer).Encode(credentials); err != nil {
32+
if err := json.NewEncoder(buffer).Encode(creds); err != nil {
1833
return err
1934
}
2035
cmd.Input(buffer)
2136

2237
out, err := cmd.Output()
2338
if err != nil {
2439
t := strings.TrimSpace(string(out))
40+
41+
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
42+
err = isValidErr
43+
}
44+
2545
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
2646
}
2747

@@ -41,6 +61,10 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error
4161
return nil, credentials.NewErrCredentialsNotFound()
4262
}
4363

64+
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
65+
err = isValidErr
66+
}
67+
4468
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
4569
}
4670

@@ -62,6 +86,11 @@ func Erase(program ProgramFunc, serverURL string) error {
6286
out, err := cmd.Output()
6387
if err != nil {
6488
t := strings.TrimSpace(string(out))
89+
90+
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
91+
err = isValidErr
92+
}
93+
6594
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
6695
}
6796

@@ -75,6 +104,11 @@ func List(program ProgramFunc) (map[string]string, error) {
75104
out, err := cmd.Output()
76105
if err != nil {
77106
t := strings.TrimSpace(string(out))
107+
108+
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
109+
err = isValidErr
110+
}
111+
78112
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
79113
}
80114

client/client_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ func (m *mockProgram) Output() ([]byte, error) {
5656
return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited
5757
case invalidServerAddress:
5858
return []byte("program failed"), errProgramExited
59+
case "":
60+
return []byte(credentials.NewErrCredentialsMissingServerURL().Error()), errProgramExited
5961
}
6062
case "store":
6163
var c credentials.Credentials
@@ -158,12 +160,16 @@ func TestGet(t *testing.T) {
158160
}
159161
}
160162

163+
missingServerURLErr := credentials.NewErrCredentialsMissingServerURL()
164+
161165
invalid := []struct {
162166
serverURL string
163167
err string
164168
}{
165169
{missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()},
166170
{invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"},
171+
{"", fmt.Sprintf("error getting credentials - err: %s, out: `%s`",
172+
missingServerURLErr.Error(), missingServerURLErr.Error())},
167173
}
168174

169175
for _, v := range invalid {

credentials/credentials.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,22 @@ type Credentials struct {
1717
Secret string
1818
}
1919

20+
// isValid checks the integrity of Credentials object such that no credentials lack
21+
// a server URL or a username.
22+
// It returns whether the credentials are valid and the error if it isn't.
23+
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername
24+
func (c *Credentials) isValid() (bool, error) {
25+
if len(c.ServerURL) == 0 {
26+
return false, NewErrCredentialsMissingServerURL()
27+
}
28+
29+
if len(c.Username) == 0 {
30+
return false, NewErrCredentialsMissingUsername()
31+
}
32+
33+
return true, nil
34+
}
35+
2036
// Docker credentials should be labeled as such in credentials stores that allow labelling.
2137
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
2238
// Windows credentials manager and Linux libsecret. Default value is "Docker Credentials"
@@ -81,6 +97,10 @@ func Store(helper Helper, reader io.Reader) error {
8197
return err
8298
}
8399

100+
if ok, err := creds.isValid(); !ok {
101+
return err
102+
}
103+
84104
return helper.Add(&creds)
85105
}
86106

@@ -100,13 +120,17 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
100120
}
101121

102122
serverURL := strings.TrimSpace(buffer.String())
123+
if len(serverURL) == 0 {
124+
return NewErrCredentialsMissingServerURL()
125+
}
103126

104127
username, secret, err := helper.Get(serverURL)
105128
if err != nil {
106129
return err
107130
}
108131

109132
resp := Credentials{
133+
ServerURL: serverURL,
110134
Username: username,
111135
Secret: secret,
112136
}
@@ -135,6 +159,9 @@ func Erase(helper Helper, reader io.Reader) error {
135159
}
136160

137161
serverURL := strings.TrimSpace(buffer.String())
162+
if len(serverURL) == 0 {
163+
return NewErrCredentialsMissingServerURL()
164+
}
138165

139166
return helper.Delete(serverURL)
140167
}

credentials/credentials_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,46 @@ func TestStore(t *testing.T) {
7373
}
7474
}
7575

76+
func TestStoreMissingServerURL(t *testing.T) {
77+
creds := &Credentials{
78+
ServerURL: "",
79+
Username: "foo",
80+
Secret: "bar",
81+
}
82+
83+
b, err := json.Marshal(creds)
84+
if err != nil {
85+
t.Fatal(err)
86+
}
87+
in := bytes.NewReader(b)
88+
89+
h := newMemoryStore()
90+
91+
if err := Store(h, in); IsCredentialsMissingServerURL(err) == false {
92+
t.Fatal(err)
93+
}
94+
}
95+
96+
func TestStoreMissingUsername(t *testing.T) {
97+
creds := &Credentials{
98+
ServerURL: "https://index.docker.io/v1/",
99+
Username: "",
100+
Secret: "bar",
101+
}
102+
103+
b, err := json.Marshal(creds)
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
in := bytes.NewReader(b)
108+
109+
h := newMemoryStore()
110+
111+
if err := Store(h, in); IsCredentialsMissingUsername(err) == false {
112+
t.Fatal(err)
113+
}
114+
}
115+
76116
func TestGet(t *testing.T) {
77117
serverURL := "https://index.docker.io/v1/"
78118
creds := &Credentials{
@@ -115,6 +155,32 @@ func TestGet(t *testing.T) {
115155
}
116156
}
117157

158+
func TestGetMissingServerURL(t *testing.T) {
159+
serverURL := "https://index.docker.io/v1/"
160+
creds := &Credentials{
161+
ServerURL: serverURL,
162+
Username: "foo",
163+
Secret: "bar",
164+
}
165+
b, err := json.Marshal(creds)
166+
if err != nil {
167+
t.Fatal(err)
168+
}
169+
in := bytes.NewReader(b)
170+
171+
h := newMemoryStore()
172+
if err := Store(h, in); err != nil {
173+
t.Fatal(err)
174+
}
175+
176+
buf := strings.NewReader("")
177+
w := new(bytes.Buffer)
178+
179+
if err := Get(h, buf, w); IsCredentialsMissingServerURL(err) == false {
180+
t.Fatal(err)
181+
}
182+
}
183+
118184
func TestErase(t *testing.T) {
119185
serverURL := "https://index.docker.io/v1/"
120186
creds := &Credentials{
@@ -144,6 +210,30 @@ func TestErase(t *testing.T) {
144210
}
145211
}
146212

213+
func TestEraseMissingServerURL(t *testing.T) {
214+
serverURL := "https://index.docker.io/v1/"
215+
creds := &Credentials{
216+
ServerURL: serverURL,
217+
Username: "foo",
218+
Secret: "bar",
219+
}
220+
b, err := json.Marshal(creds)
221+
if err != nil {
222+
t.Fatal(err)
223+
}
224+
in := bytes.NewReader(b)
225+
226+
h := newMemoryStore()
227+
if err := Store(h, in); err != nil {
228+
t.Fatal(err)
229+
}
230+
231+
buf := strings.NewReader("")
232+
if err := Erase(h, buf); IsCredentialsMissingServerURL(err) == false {
233+
t.Fatal(err)
234+
}
235+
}
236+
147237
func TestList(t *testing.T) {
148238
//This tests that there is proper input an output into the byte stream
149239
//Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively

credentials/error.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package credentials
22

3-
// ErrCredentialsNotFound standarizes the not found error, so every helper returns
4-
// the same message and docker can handle it properly.
5-
const errCredentialsNotFoundMessage = "credentials not found in native keychain"
3+
const (
4+
// ErrCredentialsNotFound standardizes the not found error, so every helper returns
5+
// the same message and docker can handle it properly.
6+
errCredentialsNotFoundMessage = "credentials not found in native keychain"
7+
8+
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
9+
// invalid credentials or credentials management operations
10+
errCredentialsMissingServerURLMessage = "no credentials server URL"
11+
errCredentialsMissingUsernameMessage = "no credentials username"
12+
)
613

714
// errCredentialsNotFound represents an error
815
// raised when credentials are not in the store.
@@ -35,3 +42,64 @@ func IsErrCredentialsNotFound(err error) bool {
3542
func IsErrCredentialsNotFoundMessage(err string) bool {
3643
return err == errCredentialsNotFoundMessage
3744
}
45+
46+
47+
// errCredentialsMissingServerURL represents an error raised
48+
// when the credentials object has no server URL or when no
49+
// server URL is provided to a credentials operation requiring
50+
// one.
51+
type errCredentialsMissingServerURL struct{}
52+
53+
func (errCredentialsMissingServerURL) Error() string {
54+
return errCredentialsMissingServerURLMessage
55+
}
56+
57+
// errCredentialsMissingUsername represents an error raised
58+
// when the credentials object has no username or when no
59+
// username is provided to a credentials operation requiring
60+
// one.
61+
type errCredentialsMissingUsername struct{}
62+
63+
func (errCredentialsMissingUsername) Error() string {
64+
return errCredentialsMissingUsernameMessage
65+
}
66+
67+
68+
// NewErrCredentialsMissingServerURL creates a new error for
69+
// errCredentialsMissingServerURL.
70+
func NewErrCredentialsMissingServerURL() error {
71+
return errCredentialsMissingServerURL{}
72+
}
73+
74+
// NewErrCredentialsMissingUsername creates a new error for
75+
// errCredentialsMissingUsername.
76+
func NewErrCredentialsMissingUsername() error {
77+
return errCredentialsMissingUsername{}
78+
}
79+
80+
81+
// IsCredentialsMissingServerURL returns true if the error
82+
// was an errCredentialsMissingServerURL.
83+
func IsCredentialsMissingServerURL(err error) bool {
84+
_, ok := err.(errCredentialsMissingServerURL)
85+
return ok
86+
}
87+
88+
// IsCredentialsMissingServerURLMessage checks for an
89+
// errCredentialsMissingServerURL in the error message.
90+
func IsCredentialsMissingServerURLMessage(err string) bool {
91+
return err == errCredentialsMissingServerURLMessage
92+
}
93+
94+
// IsCredentialsMissingUsername returns true if the error
95+
// was an errCredentialsMissingUsername.
96+
func IsCredentialsMissingUsername(err error) bool {
97+
_, ok := err.(errCredentialsMissingUsername)
98+
return ok
99+
}
100+
101+
// IsCredentialsMissingUsernameMessage checks for an
102+
// errCredentialsMissingUsername in the error message.
103+
func IsCredentialsMissingUsernameMessage(err string) bool {
104+
return err == errCredentialsMissingUsernameMessage
105+
}

0 commit comments

Comments
 (0)