fix: defensively handle nil maps and slices in marshaling (#18418)

Adds a custom marshaler to handle some cases where nils were being
marshaled to nulls, causing the web UI to throw an error.

---------

Co-authored-by: Steven Masley <stevenmasley@gmail.com>
This commit is contained in:
Charlie Voiselle
2025-06-17 17:50:18 -04:00
committed by GitHub
parent 9cbe02e8b7
commit 44d46469e1
5 changed files with 80 additions and 0 deletions

View File

@ -274,6 +274,17 @@ func (s *GroupSyncSettings) String() string {
return runtimeconfig.JSONString(s)
}
func (s *GroupSyncSettings) MarshalJSON() ([]byte, error) {
if s.Mapping == nil {
s.Mapping = make(map[string][]uuid.UUID)
}
// Aliasing the struct to avoid infinite recursion when calling json.Marshal
// on the struct itself.
type Alias GroupSyncSettings
return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(s)})
}
type ExpectedGroup struct {
OrganizationID uuid.UUID
GroupID *uuid.UUID

View File

@ -2,6 +2,7 @@ package idpsync_test
import (
"encoding/json"
"regexp"
"testing"
"github.com/stretchr/testify/require"
@ -9,6 +10,49 @@ import (
"github.com/coder/coder/v2/coderd/idpsync"
)
// TestMarshalJSONEmpty ensures no empty maps are marshaled as `null` in JSON.
func TestMarshalJSONEmpty(t *testing.T) {
t.Parallel()
t.Run("Group", func(t *testing.T) {
t.Parallel()
output, err := json.Marshal(&idpsync.GroupSyncSettings{
RegexFilter: regexp.MustCompile(".*"),
})
require.NoError(t, err, "marshal empty group settings")
require.NotContains(t, string(output), "null")
require.JSONEq(t,
`{"field":"","mapping":{},"regex_filter":".*","auto_create_missing_groups":false}`,
string(output))
})
t.Run("Role", func(t *testing.T) {
t.Parallel()
output, err := json.Marshal(&idpsync.RoleSyncSettings{})
require.NoError(t, err, "marshal empty group settings")
require.NotContains(t, string(output), "null")
require.JSONEq(t,
`{"field":"","mapping":{}}`,
string(output))
})
t.Run("Organization", func(t *testing.T) {
t.Parallel()
output, err := json.Marshal(&idpsync.OrganizationSyncSettings{})
require.NoError(t, err, "marshal empty group settings")
require.NotContains(t, string(output), "null")
require.JSONEq(t,
`{"field":"","mapping":{},"assign_default":false}`,
string(output))
})
}
func TestParseStringSliceClaim(t *testing.T) {
t.Parallel()

View File

@ -234,6 +234,17 @@ func (s *OrganizationSyncSettings) String() string {
return runtimeconfig.JSONString(s)
}
func (s *OrganizationSyncSettings) MarshalJSON() ([]byte, error) {
if s.Mapping == nil {
s.Mapping = make(map[string][]uuid.UUID)
}
// Aliasing the struct to avoid infinite recursion when calling json.Marshal
// on the struct itself.
type Alias OrganizationSyncSettings
return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(s)})
}
// ParseClaims will parse the claims and return the list of organizations the user
// should sync to.
func (s *OrganizationSyncSettings) ParseClaims(ctx context.Context, db database.Store, mergedClaims jwt.MapClaims) ([]uuid.UUID, error) {

View File

@ -291,3 +291,14 @@ func (s *RoleSyncSettings) String() string {
}
return runtimeconfig.JSONString(s)
}
func (s *RoleSyncSettings) MarshalJSON() ([]byte, error) {
if s.Mapping == nil {
s.Mapping = make(map[string][]string)
}
// Aliasing the struct to avoid infinite recursion when calling json.Marshal
// on the struct itself.
type Alias RoleSyncSettings
return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(s)})
}

View File

@ -836,6 +836,9 @@ func (api *API) idpSyncClaimFieldValues(orgID uuid.UUID, rw http.ResponseWriter,
httpapi.InternalServerError(rw, err)
return
}
if fieldValues == nil {
fieldValues = []string{}
}
httpapi.Write(ctx, rw, http.StatusOK, fieldValues)
}