Skip to content

Commit 46b7535

Browse files
authored
DecodeMetadata: add support for aliases via "mapstructurealiases" struct tag (#3034)
Signed-off-by: ItalyPaleAle <[email protected]>
1 parent 4a84a01 commit 46b7535

File tree

3 files changed

+264
-22
lines changed

3 files changed

+264
-22
lines changed

metadata/utils.go

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"time"
2424

2525
"github.com/mitchellh/mapstructure"
26+
"github.com/spf13/cast"
2627

2728
"github.com/dapr/components-contrib/internal/utils"
2829
"github.com/dapr/kit/ptr"
@@ -142,16 +143,27 @@ func GetMetadataProperty(props map[string]string, keys ...string) (val string, o
142143
// This is an extension of mitchellh/mapstructure which also supports decoding durations
143144
func DecodeMetadata(input any, result any) error {
144145
// avoids a common mistake of passing the metadata struct, instead of the properties map
145-
// if input is of type struct, case it to metadata.Base and access the Properties instead
146+
// if input is of type struct, cast it to metadata.Base and access the Properties instead
146147
v := reflect.ValueOf(input)
147148
if v.Kind() == reflect.Struct {
148149
f := v.FieldByName("Properties")
149150
if f.IsValid() && f.Kind() == reflect.Map {
150-
properties := f.Interface().(map[string]string)
151-
input = properties
151+
input = f.Interface().(map[string]string)
152152
}
153153
}
154154

155+
inputMap, err := cast.ToStringMapStringE(input)
156+
if err != nil {
157+
return fmt.Errorf("input object cannot be cast to map[string]string: %w", err)
158+
}
159+
160+
// Handle aliases
161+
err = resolveAliases(inputMap, result)
162+
if err != nil {
163+
return fmt.Errorf("failed to resolve aliases: %w", err)
164+
}
165+
166+
// Finally, decode the metadata using mapstructure
155167
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
156168
DecodeHook: mapstructure.ComposeDecodeHookFunc(
157169
toTimeDurationArrayHookFunc(),
@@ -166,10 +178,77 @@ func DecodeMetadata(input any, result any) error {
166178
if err != nil {
167179
return err
168180
}
169-
err = decoder.Decode(input)
181+
err = decoder.Decode(inputMap)
170182
return err
171183
}
172184

185+
func resolveAliases(md map[string]string, result any) error {
186+
// Get the list of all keys in the map
187+
keys := make(map[string]string, len(md))
188+
for k := range md {
189+
lk := strings.ToLower(k)
190+
191+
// Check if there are duplicate keys after lowercasing
192+
_, ok := keys[lk]
193+
if ok {
194+
return fmt.Errorf("key %s is duplicate in the metadata", lk)
195+
}
196+
197+
keys[lk] = k
198+
}
199+
200+
// Error if result is not pointer to struct, or pointer to pointer to struct
201+
t := reflect.TypeOf(result)
202+
if t.Kind() != reflect.Pointer {
203+
return fmt.Errorf("not a pointer: %s", t.Kind().String())
204+
}
205+
t = t.Elem()
206+
if t.Kind() == reflect.Pointer {
207+
t = t.Elem()
208+
}
209+
if t.Kind() != reflect.Struct {
210+
return fmt.Errorf("not a struct: %s", t.Kind().String())
211+
}
212+
213+
// Iterate through all the properties of result to see if anyone has the "mapstructurealiases" property
214+
for i := 0; i < t.NumField(); i++ {
215+
currentField := t.Field(i)
216+
217+
// Ignored fields that are not exported or that don't have a "mapstructure" tag
218+
mapstructureTag := currentField.Tag.Get("mapstructure")
219+
if !currentField.IsExported() || mapstructureTag == "" {
220+
continue
221+
}
222+
223+
// If the current property has a value in the metadata, then we don't need to handle aliases
224+
_, ok := keys[strings.ToLower(mapstructureTag)]
225+
if ok {
226+
continue
227+
}
228+
229+
// Check if there's a "mapstructurealiases" tag
230+
aliasesTag := strings.ToLower(currentField.Tag.Get("mapstructurealiases"))
231+
if aliasesTag == "" {
232+
continue
233+
}
234+
235+
// Look for the first alias that has a value
236+
var mdKey string
237+
for _, alias := range strings.Split(aliasesTag, ",") {
238+
mdKey, ok = keys[alias]
239+
if !ok {
240+
continue
241+
}
242+
243+
// We found an alias
244+
md[mapstructureTag] = md[mdKey]
245+
break
246+
}
247+
}
248+
249+
return nil
250+
}
251+
173252
func toTruthyBoolHookFunc() mapstructure.DecodeHookFunc {
174253
return func(
175254
f reflect.Type,

metadata/utils_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"time"
2020

2121
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
"golang.org/x/exp/maps"
2224
)
2325

2426
func TestIsRawPayload(t *testing.T) {
@@ -111,6 +113,9 @@ func TestMetadataDecode(t *testing.T) {
111113

112114
MyRegularDurationDefaultValueUnset time.Duration `mapstructure:"myregulardurationdefaultvalueunset"`
113115
MyRegularDurationDefaultValueEmpty time.Duration `mapstructure:"myregulardurationdefaultvalueempty"`
116+
117+
AliasedFieldA string `mapstructure:"aliasA1" mapstructurealiases:"aliasA2"`
118+
AliasedFieldB string `mapstructure:"aliasB1" mapstructurealiases:"aliasB2"`
114119
}
115120

116121
var m testMetadata
@@ -131,6 +136,9 @@ func TestMetadataDecode(t *testing.T) {
131136
"mydurationarray": "1s,2s,3s,10",
132137
"mydurationarraypointer": "1s,10,2s,20,3s,30",
133138
"mydurationarraypointerempty": ",",
139+
"aliasA2": "hello",
140+
"aliasB1": "ciao",
141+
"aliasB2": "bonjour",
134142
}
135143

136144
err := DecodeMetadata(testData, &m)
@@ -149,6 +157,8 @@ func TestMetadataDecode(t *testing.T) {
149157
assert.Equal(t, []time.Duration{time.Second, time.Second * 2, time.Second * 3, time.Second * 10}, m.MyDurationArray)
150158
assert.Equal(t, []time.Duration{time.Second, time.Second * 10, time.Second * 2, time.Second * 20, time.Second * 3, time.Second * 30}, *m.MyDurationArrayPointer)
151159
assert.Equal(t, []time.Duration{}, *m.MyDurationArrayPointerEmpty)
160+
assert.Equal(t, "hello", m.AliasedFieldA)
161+
assert.Equal(t, "ciao", m.AliasedFieldB)
152162
})
153163

154164
t.Run("Test metadata decode hook for truthy values", func(t *testing.T) {
@@ -303,3 +313,172 @@ func TestMetadataStructToStringMap(t *testing.T) {
303313
assert.Empty(t, metadatainfo["ignored"].Aliases)
304314
})
305315
}
316+
317+
func TestResolveAliases(t *testing.T) {
318+
tests := []struct {
319+
name string
320+
md map[string]string
321+
result any
322+
wantErr bool
323+
wantMd map[string]string
324+
}{
325+
{
326+
name: "no aliases",
327+
md: map[string]string{
328+
"hello": "world",
329+
"ciao": "mondo",
330+
},
331+
result: &struct {
332+
Hello string `mapstructure:"hello"`
333+
Ciao string `mapstructure:"ciao"`
334+
Bonjour string `mapstructure:"bonjour"`
335+
}{},
336+
wantMd: map[string]string{
337+
"hello": "world",
338+
"ciao": "mondo",
339+
},
340+
},
341+
{
342+
name: "set with aliased field",
343+
md: map[string]string{
344+
"ciao": "mondo",
345+
},
346+
result: &struct {
347+
Hello string `mapstructure:"hello" mapstructurealiases:"ciao"`
348+
Bonjour string `mapstructure:"bonjour"`
349+
}{},
350+
wantMd: map[string]string{
351+
"hello": "mondo",
352+
"ciao": "mondo",
353+
},
354+
},
355+
{
356+
name: "do not overwrite existing fields with aliases",
357+
md: map[string]string{
358+
"hello": "world",
359+
"ciao": "mondo",
360+
},
361+
result: &struct {
362+
Hello string `mapstructure:"hello" mapstructurealiases:"ciao"`
363+
Bonjour string `mapstructure:"bonjour"`
364+
}{},
365+
wantMd: map[string]string{
366+
"hello": "world",
367+
"ciao": "mondo",
368+
},
369+
},
370+
{
371+
name: "no fields with aliased value",
372+
md: map[string]string{
373+
"bonjour": "monde",
374+
},
375+
result: &struct {
376+
Hello string `mapstructure:"hello" mapstructurealiases:"ciao"`
377+
Bonjour string `mapstructure:"bonjour"`
378+
}{},
379+
wantMd: map[string]string{
380+
"bonjour": "monde",
381+
},
382+
},
383+
{
384+
name: "multiple aliases",
385+
md: map[string]string{
386+
"bonjour": "monde",
387+
},
388+
result: &struct {
389+
Hello string `mapstructure:"hello" mapstructurealiases:"ciao,bonjour"`
390+
}{},
391+
wantMd: map[string]string{
392+
"hello": "monde",
393+
"bonjour": "monde",
394+
},
395+
},
396+
{
397+
name: "first alias wins",
398+
md: map[string]string{
399+
"ciao": "mondo",
400+
"bonjour": "monde",
401+
},
402+
result: &struct {
403+
Hello string `mapstructure:"hello" mapstructurealiases:"ciao,bonjour"`
404+
}{},
405+
wantMd: map[string]string{
406+
"hello": "mondo",
407+
"ciao": "mondo",
408+
"bonjour": "monde",
409+
},
410+
},
411+
{
412+
name: "no aliases with mixed case",
413+
md: map[string]string{
414+
"hello": "world",
415+
"CIAO": "mondo",
416+
},
417+
result: &struct {
418+
Hello string `mapstructure:"Hello"`
419+
Ciao string `mapstructure:"ciao"`
420+
Bonjour string `mapstructure:"bonjour"`
421+
}{},
422+
wantMd: map[string]string{
423+
"hello": "world",
424+
"CIAO": "mondo",
425+
},
426+
},
427+
{
428+
name: "set with aliased field with mixed case",
429+
md: map[string]string{
430+
"ciao": "mondo",
431+
},
432+
result: &struct {
433+
Hello string `mapstructure:"Hello" mapstructurealiases:"CIAO"`
434+
Bonjour string `mapstructure:"bonjour"`
435+
}{},
436+
wantMd: map[string]string{
437+
"Hello": "mondo",
438+
"ciao": "mondo",
439+
},
440+
},
441+
{
442+
name: "do not overwrite existing fields with aliases with mixed cases",
443+
md: map[string]string{
444+
"HELLO": "world",
445+
"CIAO": "mondo",
446+
},
447+
result: &struct {
448+
Hello string `mapstructure:"hELLo" mapstructurealiases:"cIAo"`
449+
Bonjour string `mapstructure:"bonjour"`
450+
}{},
451+
wantMd: map[string]string{
452+
"HELLO": "world",
453+
"CIAO": "mondo",
454+
},
455+
},
456+
{
457+
name: "multiple aliases with mixed cases",
458+
md: map[string]string{
459+
"bonjour": "monde",
460+
},
461+
result: &struct {
462+
Hello string `mapstructure:"HELLO" mapstructurealiases:"CIAO,BONJOUR"`
463+
}{},
464+
wantMd: map[string]string{
465+
"HELLO": "monde",
466+
"bonjour": "monde",
467+
},
468+
},
469+
}
470+
for _, tt := range tests {
471+
t.Run(tt.name, func(t *testing.T) {
472+
md := maps.Clone(tt.md)
473+
err := resolveAliases(md, tt.result)
474+
475+
if tt.wantErr {
476+
require.Error(t, err)
477+
return
478+
}
479+
480+
require.NoError(t, err)
481+
require.Equal(t, tt.wantMd, md)
482+
})
483+
}
484+
}

middleware/http/bearer/metadata.go

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,12 @@ import (
2929

3030
type bearerMiddlewareMetadata struct {
3131
// Issuer authority.
32-
Issuer string `json:"issuer" mapstructure:"issuer"`
32+
Issuer string `json:"issuer" mapstructure:"issuer" mapstructurealiases:"issuerURL"`
3333
// Audience to expect in the token (usually, a client ID).
34-
Audience string `json:"audience" mapstructure:"audience"`
34+
Audience string `json:"audience" mapstructure:"audience" mapstructurealiases:"clientID"`
3535
// Optional address of the JKWS file.
3636
// If missing, will try to fetch the URL set in the OpenID Configuration document `<issuer>/.well-known/openid-configuration`.
3737
JWKSURL string `json:"jwksURL" mapstructure:"jwksURL"`
38-
// Deprecated - use "issuer" instead.
39-
IssuerURL string `json:"issuerURL" mapstructure:"issuerURL"`
40-
// Deprecated - use "audience" instead.
41-
ClientID string `json:"clientID" mapstructure:"clientID"`
4238

4339
// Internal properties
4440
logger logger.Logger `json:"-" mapstructure:"-"`
@@ -52,18 +48,6 @@ func (md *bearerMiddlewareMetadata) fromMetadata(metadata middleware.Metadata) e
5248
return err
5349
}
5450

55-
// Support IssuerURL as deprecated alias for Issuer
56-
if md.Issuer == "" && md.IssuerURL != "" {
57-
md.Issuer = md.IssuerURL
58-
md.logger.Warnf("Metadata property 'issuerURL' is deprecated and will be removed in the future. Please use 'issuer' instead.")
59-
}
60-
61-
// Support ClientID as deprecated alias for Audience
62-
if md.Audience == "" && md.ClientID != "" {
63-
md.Audience = md.ClientID
64-
md.logger.Warnf("Metadata property 'clientID' is deprecated and will be removed in the future. Please use 'audience' instead.")
65-
}
66-
6751
// Validate properties
6852
if md.Issuer == "" {
6953
return errors.New("metadata property 'issuer' is required")

0 commit comments

Comments
 (0)