package authconfig import ( "encoding/base64" "strings" "testing" "github.com/moby/moby/api/types/registry" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestDecodeAuthConfig(t *testing.T) { tests := []struct { doc string input string inputBase64 string expected registry.AuthConfig expectedErr string }{ { doc: "empty", input: ``, inputBase64: ``, expected: registry.AuthConfig{}, }, { doc: "empty JSON", input: `{}`, inputBase64: `e30=`, expected: registry.AuthConfig{}, }, { doc: "malformed JSON", input: `{`, inputBase64: `ew==`, expected: registry.AuthConfig{}, expectedErr: `invalid X-Registry-Auth header: invalid JSON: unexpected EOF`, }, { doc: "test authConfig", input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`, inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`, expected: registry.AuthConfig{ Username: "testuser", Password: "testpassword", ServerAddress: "example.com", }, }, { doc: "multiple authConfig (should be rejected)", input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}{"username":"testuser2","password":"testpassword2","serveraddress":"example.org"}`, inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifXsidXNlcm5hbWUiOiJ0ZXN0dXNlcjIiLCJwYXNzd29yZCI6InRlc3RwYXNzd29yZDIiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5vcmcifQ==`, expected: registry.AuthConfig{}, expectedErr: `invalid X-Registry-Auth header: multiple JSON documents not allowed`, }, // We currently only support base64url encoding with padding, so // un-padded should produce an error. // // RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5 // RFC4648, section 3.2: https://tools.ietf.org/html/rfc4648#section-3.2 { doc: "empty JSON no padding", input: `{}`, inputBase64: `e30`, expected: registry.AuthConfig{}, expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`, }, { doc: "test authConfig", input: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`, inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ`, expected: registry.AuthConfig{}, expectedErr: `invalid X-Registry-Auth header: must be a valid base64url-encoded string`, }, { doc: "JSON with trailing whitespace (should accept)", input: `{"username":"testuser","password":"testpassword"} `, inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQifSAgIA==`, expected: registry.AuthConfig{ Username: "testuser", Password: "testpassword", }, }, { doc: "JSON with trailing invalid data", input: `{"username":"testuser"}invalid`, inputBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIn1pbnZhbGlk`, expected: registry.AuthConfig{}, expectedErr: `invalid X-Registry-Auth header: multiple JSON documents not allowed`, }, { doc: "multiple empty JSON documents", input: `{}{}`, inputBase64: `e317fQ==`, expected: registry.AuthConfig{}, expectedErr: `invalid X-Registry-Auth header: multiple JSON documents not allowed`, }, } for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { if tc.inputBase64 != "" { // Sanity check to make sure our fixtures are correct. b64 := base64.URLEncoding.EncodeToString([]byte(tc.input)) if !strings.HasSuffix(tc.inputBase64, "=") { b64 = strings.TrimRight(b64, "=") } assert.Check(t, is.Equal(b64, tc.inputBase64)) } out, err := Decode(tc.inputBase64) if tc.expectedErr != "" { assert.Check(t, is.ErrorType(err, errInvalidParameter{})) assert.Check(t, is.Error(err, tc.expectedErr)) } else { assert.NilError(t, err) assert.Equal(t, *out, tc.expected) } }) } } func TestEncodeAuthConfig(t *testing.T) { tests := []struct { doc string input registry.AuthConfig outBase64 string outPlain string }{ { // Older daemons (or registries) may not handle an empty string, // which resulted in an "io.EOF" when unmarshaling or decoding. // // FIXME(thaJeztah): find exactly what code-paths are impacted by this. doc: "empty", input: registry.AuthConfig{}, outBase64: `e30=`, outPlain: `{}`, }, { doc: "test authConfig", input: registry.AuthConfig{ Username: "testuser", Password: "testpassword", ServerAddress: "example.com", }, outBase64: `eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwicGFzc3dvcmQiOiJ0ZXN0cGFzc3dvcmQiLCJzZXJ2ZXJhZGRyZXNzIjoiZXhhbXBsZS5jb20ifQ==`, outPlain: `{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`, }, } for _, tc := range tests { // Sanity check to make sure our fixtures are correct. b64 := base64.URLEncoding.EncodeToString([]byte(tc.outPlain)) assert.Check(t, is.Equal(b64, tc.outBase64)) t.Run(tc.doc, func(t *testing.T) { out, err := Encode(tc.input) assert.NilError(t, err) assert.Equal(t, out, tc.outBase64) authJSON, err := base64.URLEncoding.DecodeString(out) assert.NilError(t, err) assert.Equal(t, string(authJSON), tc.outPlain) }) } } func BenchmarkDecodeAuthConfig(b *testing.B) { cases := []struct { doc string inputBase64 string invalid bool }{ { doc: "empty", inputBase64: ``, }, { doc: "empty JSON", inputBase64: `e30=`, }, { doc: "valid", inputBase64: base64.URLEncoding.EncodeToString([]byte(`{"username":"testuser","password":"testpassword","serveraddress":"example.com"}`)), }, { doc: "invalid base64", inputBase64: "not-base64", invalid: true, }, { doc: "malformed JSON", inputBase64: `ew==`, invalid: true, }, } for _, tc := range cases { b.Run(tc.doc, func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _, err := Decode(tc.inputBase64) if !tc.invalid && err != nil { b.Fatal(err) } } }) } }