diff --git a/coderd/idpsync/group.go b/coderd/idpsync/group.go index b85ce1b749e28..b5d8003165665 100644 --- a/coderd/idpsync/group.go +++ b/coderd/idpsync/group.go @@ -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 diff --git a/coderd/idpsync/idpsync_test.go b/coderd/idpsync/idpsync_test.go index 317122ddc6092..0db5c66bed174 100644 --- a/coderd/idpsync/idpsync_test.go +++ b/coderd/idpsync/idpsync_test.go @@ -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() diff --git a/coderd/idpsync/organization.go b/coderd/idpsync/organization.go index f0736e1ea7559..cfc6e819d7ae5 100644 --- a/coderd/idpsync/organization.go +++ b/coderd/idpsync/organization.go @@ -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) { diff --git a/coderd/idpsync/role.go b/coderd/idpsync/role.go index c21e7c99c4614..b6f555dc1e1e8 100644 --- a/coderd/idpsync/role.go +++ b/coderd/idpsync/role.go @@ -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)}) +} diff --git a/enterprise/coderd/idpsync.go b/enterprise/coderd/idpsync.go index 2dcee572eb692..416acc7ee070f 100644 --- a/enterprise/coderd/idpsync.go +++ b/enterprise/coderd/idpsync.go @@ -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) }