Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions pkg/secrets/helpers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package secrets

import (
"bytes"
"encoding/json"
"fmt"

corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -79,3 +81,79 @@ func getSecretTypeMap() map[corev1.SecretType]string {
corev1.SecretTypeDockercfg: corev1.DockerConfigKey,
}
}

// Wraps imageRegistrySecretImpl and implements UnmarshalJSON so that
// progressive JSON decoding may be used.
type dockerConfigJSONDecoder struct {
imageRegistrySecretImpl
}

// Custom unmarshal method that supports both old-style and new-style
// DockerConfig unmarshalling.
func (d *dockerConfigJSONDecoder) UnmarshalJSON(in []byte) error {
// Check if we have a nil or zero-length payload.
if in == nil || len(in) == 0 {
return fmt.Errorf("empty dockerconfig bytes")
}

// Check if the input is just the JSON null literal
if bytes.TrimSpace(in) != nil && string(bytes.TrimSpace(in)) == "null" {
return fmt.Errorf("dockerconfig bytes contain JSON null")
}

// Next, determine if the JSON payload is valid, but empty (e.g., `{}`).
empty := map[string]interface{}{}

if err := json.Unmarshal(in, &empty); err != nil {
return err
}

if len(empty) == 0 {
d.cfg.Auths = nil
return nil
}

// The JSON payload is not empty, but we don't know what fields are present.
// So we decode into a private struct with an Auths field.
type cfg struct {
Auths json.RawMessage `json:"auths"`
}

c := &cfg{}
if err := json.Unmarshal(in, c); err != nil {
return fmt.Errorf("could not unmarshal top-level: %w", err)
}

// We determine whether this is a DockerConfigJSON or a DockerConfig by
// looking at the length of the Auths field. These are decoded with extra
// strictness.
if len(c.Auths) > 0 {
// If there is an Auths field, we decode into a DockerConfigJSON instance.
dcJSON := DockerConfigJSON{}
if err := strictJSONDecode(in, &dcJSON); err != nil {
return fmt.Errorf("could not decode DockerConfigJSON: %w", err)
}

d.isLegacyStyle = false
d.cfg.Auths = dcJSON.Auths
return nil
}

// If there is no auths field, we try decoding into a DockerConfig.
dc := DockerConfig{}
if err := strictJSONDecode(in, &dc); err != nil {
return fmt.Errorf("could not decode DockerConfig: %w", err)
}

d.cfg.Auths = dc
d.isLegacyStyle = true
return nil
}

// Adds additional strictness to the unmarshalling process by disallowing
// unknown fields.
func strictJSONDecode(in []byte, target interface{}) error {
decoder := json.NewDecoder(bytes.NewReader(in))
decoder.DisallowUnknownFields()
return decoder.Decode(target)
}
272 changes: 272 additions & 0 deletions pkg/secrets/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,275 @@ func TestNormalizeFuncs(t *testing.T) {
})
}
}

func TestDockerConfigJSONDecoder(t *testing.T) {
legacySecret := `{"registry.hostname.com":{"username":"user","password":"s3kr1t","auth":"s00pers3kr1t","email":"user@hostname.com"}}`
newSecret := `{"auths":` + legacySecret + `}`

testCases := []struct {
name string
input []byte
expected *dockerConfigJSONDecoder
expectError bool
errorContains string
}{
// Nil and Empty Input
{
name: "nil bytes",
input: nil,
expectError: true,
errorContains: "empty dockerconfig bytes",
},
{
name: "zero-length byte slice",
input: []byte{},
expectError: true,
errorContains: "empty dockerconfig bytes",
},
{
name: "JSON null literal",
input: []byte("null"),
expectError: true,
errorContains: "dockerconfig bytes contain JSON null",
},
{
name: "JSON null with whitespace",
input: []byte(" null "),
expectError: true,
errorContains: "dockerconfig bytes contain JSON null",
},

// Valid Empty Object
{
name: "empty JSON object",
input: []byte("{}"),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{Auths: nil},
isLegacyStyle: false,
},
},
},

// Invalid JSON
{
name: "malformed JSON - unclosed brace",
input: []byte(`{"auths":`),
expectError: true,
errorContains: "unexpected end of JSON input",
},
{
name: "malformed JSON - invalid syntax",
input: []byte(`{"auths": invalid}`),
expectError: true,
errorContains: "invalid character",
},
{
name: "invalid JSON - array instead of object",
input: []byte(`["not", "an", "object"]`),
expectError: true,
errorContains: "cannot unmarshal array",
},

// Unknown Fields
{
name: "unknown field in DockerConfigJSON format",
input: []byte(`{"auths":{},"unknownField":"value"}`),
expectError: true,
errorContains: "unknown field",
},
{
name: "unknown field in DockerConfig format",
input: []byte(`{"registry.hostname.com":{"username":"user","unknownField":"value"}}`),
expectError: true,
errorContains: "unknown field",
},
{
name: "unknown field in DockerConfigEntry",
input: []byte(`{"auths":{"registry.hostname.com":{"username":"user","unknownField":"value"}}}`),
expectError: true,
errorContains: "unknown field",
},

// Valid DockerConfigJSON Format
{
name: "DockerConfigJSON with empty auths",
input: []byte(`{"auths":{}}`),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{
Auths: DockerConfig{},
},
isLegacyStyle: false,
},
},
},
{
name: "DockerConfigJSON with single registry",
input: []byte(newSecret),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{
Auths: DockerConfig{
"registry.hostname.com": {
Username: "user",
Password: "s3kr1t",
Auth: "s00pers3kr1t",
Email: "user@hostname.com",
},
},
},
isLegacyStyle: false,
},
},
},
{
name: "DockerConfigJSON with multiple registries",
input: []byte(`{"auths":{
"registry1.com":{"username":"user1","password":"pass1","auth":"auth1","email":"user1@example.com"},
"registry2.com":{"username":"user2","password":"pass2","auth":"auth2","email":"user2@example.com"}
}}`),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{
Auths: DockerConfig{
"registry1.com": {
Username: "user1",
Password: "pass1",
Auth: "auth1",
Email: "user1@example.com",
},
"registry2.com": {
Username: "user2",
Password: "pass2",
Auth: "auth2",
Email: "user2@example.com",
},
},
},
isLegacyStyle: false,
},
},
},
{
name: "DockerConfigJSON with minimal fields",
input: []byte(`{"auths":{"registry.example.com":{"auth":"dXNlcjpwYXNz"}}}`),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{
Auths: DockerConfig{
"registry.example.com": {
Auth: "dXNlcjpwYXNz",
},
},
},
isLegacyStyle: false,
},
},
},

// Valid DockerConfig Format
{
name: "DockerConfig with single registry",
input: []byte(legacySecret),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{
Auths: DockerConfig{
"registry.hostname.com": {
Username: "user",
Password: "s3kr1t",
Auth: "s00pers3kr1t",
Email: "user@hostname.com",
},
},
},
isLegacyStyle: true,
},
},
},
{
name: "DockerConfig with multiple registries",
input: []byte(`{
"registry1.com":{"username":"user1","password":"pass1","auth":"auth1","email":"user1@example.com"},
"registry2.com":{"username":"user2","password":"pass2","auth":"auth2","email":"user2@example.com"}
}`),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{
Auths: DockerConfig{
"registry1.com": {
Username: "user1",
Password: "pass1",
Auth: "auth1",
Email: "user1@example.com",
},
"registry2.com": {
Username: "user2",
Password: "pass2",
Auth: "auth2",
Email: "user2@example.com",
},
},
},
isLegacyStyle: true,
},
},
},
{
name: "DockerConfig with minimal fields",
input: []byte(`{"registry.example.com":{"auth":"dXNlcjpwYXNz"}}`),
expected: &dockerConfigJSONDecoder{
imageRegistrySecretImpl: imageRegistrySecretImpl{
cfg: DockerConfigJSON{
Auths: DockerConfig{
"registry.example.com": {
Auth: "dXNlcjpwYXNz",
},
},
},
isLegacyStyle: true,
},
},
},

// Malformed Structure
{
name: "invalid DockerConfigEntry structure (string instead of object)",
input: []byte(`{"registry.hostname.com":"invalid-string-value"}`),
expectError: true,
errorContains: "cannot unmarshal string",
},
{
name: "invalid auth value type in legacy format",
input: []byte(`{"registry.hostname.com":{"auth":123}}`),
expectError: true,
errorContains: "cannot unmarshal number",
},
{
name: "auths field with invalid value type",
input: []byte(`{"auths":"invalid-string"}`),
expectError: true,
errorContains: "cannot unmarshal string",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
d := &dockerConfigJSONDecoder{}
err := d.UnmarshalJSON(tc.input)

if tc.expectError {
assert.Error(t, err)
if tc.errorContains != "" {
assert.Contains(t, err.Error(), tc.errorContains)
}
return
}

assert.NoError(t, err)
assert.Equal(t, tc.expected.isLegacyStyle, d.isLegacyStyle)
assert.Equal(t, tc.expected.cfg.Auths, d.cfg.Auths)
})
}
}
Loading