Skip to content

Commit 44d4646

Browse files
angrycubEmyrk
andauthored
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>
1 parent 9cbe02e commit 44d4646

File tree

5 files changed

+80
-0
lines changed

5 files changed

+80
-0
lines changed

coderd/idpsync/group.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,17 @@ func (s *GroupSyncSettings) String() string {
274274
return runtimeconfig.JSONString(s)
275275
}
276276

277+
func (s *GroupSyncSettings) MarshalJSON() ([]byte, error) {
278+
if s.Mapping == nil {
279+
s.Mapping = make(map[string][]uuid.UUID)
280+
}
281+
282+
// Aliasing the struct to avoid infinite recursion when calling json.Marshal
283+
// on the struct itself.
284+
type Alias GroupSyncSettings
285+
return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(s)})
286+
}
287+
277288
type ExpectedGroup struct {
278289
OrganizationID uuid.UUID
279290
GroupID *uuid.UUID

coderd/idpsync/idpsync_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,57 @@ package idpsync_test
22

33
import (
44
"encoding/json"
5+
"regexp"
56
"testing"
67

78
"github.com/stretchr/testify/require"
89

910
"github.com/coder/coder/v2/coderd/idpsync"
1011
)
1112

13+
// TestMarshalJSONEmpty ensures no empty maps are marshaled as `null` in JSON.
14+
func TestMarshalJSONEmpty(t *testing.T) {
15+
t.Parallel()
16+
17+
t.Run("Group", func(t *testing.T) {
18+
t.Parallel()
19+
20+
output, err := json.Marshal(&idpsync.GroupSyncSettings{
21+
RegexFilter: regexp.MustCompile(".*"),
22+
})
23+
require.NoError(t, err, "marshal empty group settings")
24+
require.NotContains(t, string(output), "null")
25+
26+
require.JSONEq(t,
27+
`{"field":"","mapping":{},"regex_filter":".*","auto_create_missing_groups":false}`,
28+
string(output))
29+
})
30+
31+
t.Run("Role", func(t *testing.T) {
32+
t.Parallel()
33+
34+
output, err := json.Marshal(&idpsync.RoleSyncSettings{})
35+
require.NoError(t, err, "marshal empty group settings")
36+
require.NotContains(t, string(output), "null")
37+
38+
require.JSONEq(t,
39+
`{"field":"","mapping":{}}`,
40+
string(output))
41+
})
42+
43+
t.Run("Organization", func(t *testing.T) {
44+
t.Parallel()
45+
46+
output, err := json.Marshal(&idpsync.OrganizationSyncSettings{})
47+
require.NoError(t, err, "marshal empty group settings")
48+
require.NotContains(t, string(output), "null")
49+
50+
require.JSONEq(t,
51+
`{"field":"","mapping":{},"assign_default":false}`,
52+
string(output))
53+
})
54+
}
55+
1256
func TestParseStringSliceClaim(t *testing.T) {
1357
t.Parallel()
1458

coderd/idpsync/organization.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,17 @@ func (s *OrganizationSyncSettings) String() string {
234234
return runtimeconfig.JSONString(s)
235235
}
236236

237+
func (s *OrganizationSyncSettings) MarshalJSON() ([]byte, error) {
238+
if s.Mapping == nil {
239+
s.Mapping = make(map[string][]uuid.UUID)
240+
}
241+
242+
// Aliasing the struct to avoid infinite recursion when calling json.Marshal
243+
// on the struct itself.
244+
type Alias OrganizationSyncSettings
245+
return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(s)})
246+
}
247+
237248
// ParseClaims will parse the claims and return the list of organizations the user
238249
// should sync to.
239250
func (s *OrganizationSyncSettings) ParseClaims(ctx context.Context, db database.Store, mergedClaims jwt.MapClaims) ([]uuid.UUID, error) {

coderd/idpsync/role.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,14 @@ func (s *RoleSyncSettings) String() string {
291291
}
292292
return runtimeconfig.JSONString(s)
293293
}
294+
295+
func (s *RoleSyncSettings) MarshalJSON() ([]byte, error) {
296+
if s.Mapping == nil {
297+
s.Mapping = make(map[string][]string)
298+
}
299+
300+
// Aliasing the struct to avoid infinite recursion when calling json.Marshal
301+
// on the struct itself.
302+
type Alias RoleSyncSettings
303+
return json.Marshal(&struct{ *Alias }{Alias: (*Alias)(s)})
304+
}

enterprise/coderd/idpsync.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,9 @@ func (api *API) idpSyncClaimFieldValues(orgID uuid.UUID, rw http.ResponseWriter,
836836
httpapi.InternalServerError(rw, err)
837837
return
838838
}
839+
if fieldValues == nil {
840+
fieldValues = []string{}
841+
}
839842

840843
httpapi.Write(ctx, rw, http.StatusOK, fieldValues)
841844
}

0 commit comments

Comments
 (0)