diff --git a/management/connection.go b/management/connection.go index 1492e092..9071a0f8 100644 --- a/management/connection.go +++ b/management/connection.go @@ -1420,16 +1420,24 @@ type ConnectionOptionsOAuth2 struct { // UnmarshalJSON implements the json.Unmarshaler interface for ConnectionOptionsOAuth2. // It is required to handle differences in the scope field, which can -// be an array of strings or a single string. +// be an array of strings or a single string, and to handle the customHeaders +// field, which legacy Auth0 tenants may return as a JSON-encoded string instead +// of a map[string]string object. func (c *ConnectionOptionsOAuth2) UnmarshalJSON(data []byte) error { type connectionOptionsOAuth2 ConnectionOptionsOAuth2 + // RawCustomHeaders shadows the embedded CustomHeaders *map[string]string field + // so that encoding/json places the raw value here (as interface{}) rather than + // attempting to decode it directly into *map[string]string. This prevents a + // "cannot unmarshal string into Go struct field ... of type map[string]string" + // error for connections whose customHeaders were stored in a legacy format. type connectionOptionsOAuth2Wrapper struct { *connectionOptionsOAuth2 - RawScope interface{} `json:"scope,omitempty"` + RawScope interface{} `json:"scope,omitempty"` + RawCustomHeaders interface{} `json:"customHeaders,omitempty"` } - alias := &connectionOptionsOAuth2Wrapper{(*connectionOptionsOAuth2)(c), nil} + alias := &connectionOptionsOAuth2Wrapper{(*connectionOptionsOAuth2)(c), nil, nil} err := json.Unmarshal(data, alias) if err != nil { @@ -1452,6 +1460,29 @@ func (c *ConnectionOptionsOAuth2) UnmarshalJSON(data []byte) error { } } + // Normalise customHeaders: accept both the current map[string]interface{} form + // and the legacy string form (a JSON-encoded value from an older schema version). + // In the legacy string case we discard the value — it cannot be reliably decoded + // without knowing the original structure — so custom_headers will appear empty + // and can be set manually in the Terraform config. + if alias.RawCustomHeaders != nil { + switch v := alias.RawCustomHeaders.(type) { + case map[string]interface{}: + headers := make(map[string]string, len(v)) + for key, val := range v { + if s, ok := val.(string); ok { + headers[key] = s + } + } + if len(headers) > 0 { + c.CustomHeaders = &headers + } + case string: + // Legacy format: discard. The provider will surface custom_headers as + // empty; the operator can restore the values in their Terraform config. + } + } + return nil } diff --git a/management/connection_test.go b/management/connection_test.go index c3f9a616..73d8c147 100644 --- a/management/connection_test.go +++ b/management/connection_test.go @@ -1630,12 +1630,32 @@ func TestOAuth2Connection_UnmarshalJSON(t *testing.T) { `{"scope":null}`: {Scope: nil}, `{}`: {}, `{"scope":[]}`: {Scope: auth0.String("")}, + `{"customHeaders":{"X-Foo":"bar","X-Baz":"qux"}}`: { + CustomHeaders: &map[string]string{"X-Foo": "bar", "X-Baz": "qux"}, + }, } { var actual *ConnectionOptionsOAuth2 err := json.Unmarshal([]byte(expectedAsString), &actual) assert.NoError(t, err) assert.Equal(t, expected, actual) } + + t.Run("It handles a legacy string-encoded customHeaders value without error", func(t *testing.T) { + // Some older Auth0 tenants stored customHeaders as a JSON-encoded string + // rather than a map. The unmarshaler must not return an error in this case; + // instead it discards the value so the caller can restore it via config. + var actual *ConnectionOptionsOAuth2 + err := json.Unmarshal([]byte(`{"customHeaders":"{\"X-Foo\":\"bar\"}"}`), &actual) + assert.NoError(t, err) + assert.Nil(t, actual.CustomHeaders) + }) + + t.Run("It handles a null customHeaders value without error", func(t *testing.T) { + var actual *ConnectionOptionsOAuth2 + err := json.Unmarshal([]byte(`{"customHeaders":null}`), &actual) + assert.NoError(t, err) + assert.Nil(t, actual.CustomHeaders) + }) } func TestGoogleOauth2Connection_MarshalJSON(t *testing.T) {