From 77197d82717dc3abfc2e04f1f079f24a9d5d9da8 Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 17 Jul 2024 08:01:26 -0700 Subject: [PATCH] fix: setting secrets and configs error with aliased field names --- backend/controller/ingress/alias.go | 127 ------------------ backend/controller/ingress/request.go | 2 +- backend/controller/ingress/response.go | 2 +- backend/schema/jsonvalidate.go | 121 +++++++++++++++++ .../jsonvalidate_test.go} | 12 +- 5 files changed, 128 insertions(+), 136 deletions(-) delete mode 100644 backend/controller/ingress/alias.go rename backend/{controller/ingress/alias_test.go => schema/jsonvalidate_test.go} (87%) diff --git a/backend/controller/ingress/alias.go b/backend/controller/ingress/alias.go deleted file mode 100644 index 778bf46934..0000000000 --- a/backend/controller/ingress/alias.go +++ /dev/null @@ -1,127 +0,0 @@ -package ingress - -import ( - "fmt" - - "github.com/TBD54566975/ftl/backend/schema" -) - -func transformAliasedFields(sch *schema.Schema, t schema.Type, obj any, aliaser func(obj map[string]any, field *schema.Field) string) error { - if obj == nil { - return nil - } - switch t := t.(type) { - case *schema.Ref: - decl, ok := sch.Resolve(t).Get() - if !ok { - return fmt.Errorf("%s: failed to resolve ref %s", t.Pos, t) - } - switch decl := decl.(type) { - case *schema.Data: - data, err := sch.ResolveMonomorphised(t) - if err != nil { - return fmt.Errorf("%s: failed to resolve data type: %w", t.Pos, err) - } - m, ok := obj.(map[string]any) - if !ok { - return fmt.Errorf("%s: expected map, got %T", t.Pos, obj) - } - for _, field := range data.Fields { - name := aliaser(m, field) - if err := transformAliasedFields(sch, field.Type, m[name], aliaser); err != nil { - return err - } - } - case *schema.Enum: - if decl.IsValueEnum() { - return nil - } - - // type enum - m, ok := obj.(map[string]any) - if !ok { - return fmt.Errorf("%s: expected map, got %T", t.Pos, obj) - } - name, ok := m["name"] - if !ok { - return fmt.Errorf("%s: expected type enum request to have 'name' field", t.Pos) - } - nameStr, ok := name.(string) - if !ok { - return fmt.Errorf("%s: expected 'name' field to be a string, got %T", t.Pos, name) - } - - value, ok := m["value"] - if !ok { - return fmt.Errorf("%s: expected type enum request to have 'value' field", t.Pos) - } - - for _, v := range decl.Variants { - if v.Name == nameStr { - if err := transformAliasedFields(sch, v.Value.(*schema.TypeValue).Value, value, aliaser); err != nil { //nolint:forcetypeassert - return err - } - } - } - case *schema.TypeAlias: - return transformAliasedFields(sch, decl.Type, obj, aliaser) - case *schema.Config, *schema.Database, *schema.FSM, *schema.Secret, *schema.Verb, *schema.Topic, *schema.Subscription: - return fmt.Errorf("%s: unsupported ref type %T", t.Pos, decl) - } - - case *schema.Array: - a, ok := obj.([]any) - if !ok { - return fmt.Errorf("%s: expected array, got %T", t.Pos, obj) - } - for _, elem := range a { - if err := transformAliasedFields(sch, t.Element, elem, aliaser); err != nil { - return err - } - } - - case *schema.Map: - m, ok := obj.(map[string]any) - if !ok { - return fmt.Errorf("%s: expected map, got %T", t.Pos, obj) - } - for key, value := range m { - if err := transformAliasedFields(sch, t.Key, key, aliaser); err != nil { - return err - } - if err := transformAliasedFields(sch, t.Value, value, aliaser); err != nil { - return err - } - } - - case *schema.Optional: - return transformAliasedFields(sch, t.Type, obj, aliaser) - - case *schema.Any, *schema.Bool, *schema.Bytes, *schema.Float, *schema.Int, - *schema.String, *schema.Time, *schema.Unit: - } - return nil -} - -func transformFromAliasedFields(ref *schema.Ref, sch *schema.Schema, request map[string]any) (map[string]any, error) { - return request, transformAliasedFields(sch, ref, request, func(obj map[string]any, field *schema.Field) string { - if jsonAlias, ok := field.Alias(schema.AliasKindJSON).Get(); ok { - if _, ok := obj[field.Name]; !ok && obj[jsonAlias] != nil { - obj[field.Name] = obj[jsonAlias] - delete(obj, jsonAlias) - } - } - return field.Name - }) -} - -func transformToAliasedFields(ref *schema.Ref, sch *schema.Schema, request map[string]any) (map[string]any, error) { - return request, transformAliasedFields(sch, ref, request, func(obj map[string]any, field *schema.Field) string { - if jsonAlias, ok := field.Alias(schema.AliasKindJSON).Get(); ok && field.Name != jsonAlias { - obj[jsonAlias] = obj[field.Name] - delete(obj, field.Name) - return jsonAlias - } - return field.Name - }) -} diff --git a/backend/controller/ingress/request.go b/backend/controller/ingress/request.go index d9bb18a2b4..eee821495e 100644 --- a/backend/controller/ingress/request.go +++ b/backend/controller/ingress/request.go @@ -78,7 +78,7 @@ func BuildRequestBody(route *dal.IngressRoute, r *http.Request, sch *schema.Sche } } - requestMap, err = transformFromAliasedFields(request, sch, requestMap) + requestMap, err = schema.TransformFromAliasedFields(request, sch, requestMap) if err != nil { return nil, err } diff --git a/backend/controller/ingress/response.go b/backend/controller/ingress/response.go index 49b8df1aa4..d4cce6a353 100644 --- a/backend/controller/ingress/response.go +++ b/backend/controller/ingress/response.go @@ -79,7 +79,7 @@ func bodyForType(typ schema.Type, sch *schema.Schema, data []byte) ([]byte, erro return nil, fmt.Errorf("HTTP response body is not valid JSON: %w", err) } - err = transformAliasedFields(sch, t, response, func(obj map[string]any, field *schema.Field) string { + err = schema.TransformAliasedFields(sch, t, response, func(obj map[string]any, field *schema.Field) string { if jsonAlias, ok := field.Alias(schema.AliasKindJSON).Get(); ok && field.Name != jsonAlias { obj[jsonAlias] = obj[field.Name] delete(obj, field.Name) diff --git a/backend/schema/jsonvalidate.go b/backend/schema/jsonvalidate.go index 3e5336b8c2..8cdde5fff5 100644 --- a/backend/schema/jsonvalidate.go +++ b/backend/schema/jsonvalidate.go @@ -211,6 +211,7 @@ func ValidateJSONValue(fieldType Type, path path, value any, sch *Schema) error return nil } +// ValidateRequestMap validates a given JSON map against the provided schema. func ValidateRequestMap(ref *Ref, path path, request map[string]any, sch *Schema) error { data, err := sch.ResolveMonomorphised(ref) if err != nil { @@ -249,3 +250,123 @@ func allowMissingField(field *Field) bool { } return false } + +func TransformAliasedFields(sch *Schema, t Type, obj any, aliaser func(obj map[string]any, field *Field) string) error { + if obj == nil { + return nil + } + switch t := t.(type) { + case *Ref: + decl, ok := sch.Resolve(t).Get() + if !ok { + return fmt.Errorf("%s: failed to resolve ref %s", t.Pos, t) + } + switch decl := decl.(type) { + case *Data: + data, err := sch.ResolveMonomorphised(t) + if err != nil { + return fmt.Errorf("%s: failed to resolve data type: %w", t.Pos, err) + } + m, ok := obj.(map[string]any) + if !ok { + return fmt.Errorf("%s: expected map, got %T", t.Pos, obj) + } + for _, field := range data.Fields { + name := aliaser(m, field) + if err := TransformAliasedFields(sch, field.Type, m[name], aliaser); err != nil { + return err + } + } + case *Enum: + if decl.IsValueEnum() { + return nil + } + + // type enum + m, ok := obj.(map[string]any) + if !ok { + return fmt.Errorf("%s: expected map, got %T", t.Pos, obj) + } + name, ok := m["name"] + if !ok { + return fmt.Errorf("%s: expected type enum request to have 'name' field", t.Pos) + } + nameStr, ok := name.(string) + if !ok { + return fmt.Errorf("%s: expected 'name' field to be a string, got %T", t.Pos, name) + } + + value, ok := m["value"] + if !ok { + return fmt.Errorf("%s: expected type enum request to have 'value' field", t.Pos) + } + + for _, v := range decl.Variants { + if v.Name == nameStr { + if err := TransformAliasedFields(sch, v.Value.(*TypeValue).Value, value, aliaser); err != nil { //nolint:forcetypeassert + return err + } + } + } + case *TypeAlias: + return TransformAliasedFields(sch, decl.Type, obj, aliaser) + case *Config, *Database, *FSM, *Secret, *Verb, *Topic, *Subscription: + return fmt.Errorf("%s: unsupported ref type %T", t.Pos, decl) + } + + case *Array: + a, ok := obj.([]any) + if !ok { + return fmt.Errorf("%s: expected array, got %T", t.Pos, obj) + } + for _, elem := range a { + if err := TransformAliasedFields(sch, t.Element, elem, aliaser); err != nil { + return err + } + } + + case *Map: + m, ok := obj.(map[string]any) + if !ok { + return fmt.Errorf("%s: expected map, got %T", t.Pos, obj) + } + for key, value := range m { + if err := TransformAliasedFields(sch, t.Key, key, aliaser); err != nil { + return err + } + if err := TransformAliasedFields(sch, t.Value, value, aliaser); err != nil { + return err + } + } + + case *Optional: + return TransformAliasedFields(sch, t.Type, obj, aliaser) + + case *Any, *Bool, *Bytes, *Float, *Int, + *String, *Time, *Unit: + } + return nil +} + +func TransformFromAliasedFields(ref *Ref, sch *Schema, request map[string]any) (map[string]any, error) { + return request, TransformAliasedFields(sch, ref, request, func(obj map[string]any, field *Field) string { + if jsonAlias, ok := field.Alias(AliasKindJSON).Get(); ok { + if _, ok := obj[field.Name]; !ok && obj[jsonAlias] != nil { + obj[field.Name] = obj[jsonAlias] + delete(obj, jsonAlias) + } + } + return field.Name + }) +} + +func TransformToAliasedFields(ref *Ref, sch *Schema, request map[string]any) (map[string]any, error) { + return request, TransformAliasedFields(sch, ref, request, func(obj map[string]any, field *Field) string { + if jsonAlias, ok := field.Alias(AliasKindJSON).Get(); ok && field.Name != jsonAlias { + obj[jsonAlias] = obj[field.Name] + delete(obj, field.Name) + return jsonAlias + } + return field.Name + }) +} diff --git a/backend/controller/ingress/alias_test.go b/backend/schema/jsonvalidate_test.go similarity index 87% rename from backend/controller/ingress/alias_test.go rename to backend/schema/jsonvalidate_test.go index 4129e3cbd3..39755f867f 100644 --- a/backend/controller/ingress/alias_test.go +++ b/backend/schema/jsonvalidate_test.go @@ -1,11 +1,9 @@ -package ingress +package schema import ( "testing" "github.com/alecthomas/assert/v2" - - "github.com/TBD54566975/ftl/backend/schema" ) func TestTransformFromAliasedFields(t *testing.T) { @@ -31,9 +29,9 @@ func TestTransformFromAliasedFields(t *testing.T) { } ` - sch, err := schema.ParseString("test", schemaText) + sch, err := ParseString("test", schemaText) assert.NoError(t, err) - actual, err := transformFromAliasedFields(&schema.Ref{Module: "test", Name: "Test"}, sch, map[string]any{ + actual, err := TransformFromAliasedFields(&Ref{Module: "test", Name: "Test"}, sch, map[string]any{ "bar": "value", "inner": map[string]any{ "foo": "value", @@ -106,9 +104,9 @@ func TestTransformToAliasedFields(t *testing.T) { } ` - sch, err := schema.ParseString("test", schemaText) + sch, err := ParseString("test", schemaText) assert.NoError(t, err) - actual, err := transformToAliasedFields(&schema.Ref{Module: "test", Name: "Test"}, sch, map[string]any{ + actual, err := TransformToAliasedFields(&Ref{Module: "test", Name: "Test"}, sch, map[string]any{ "scalar": "value", "inner": map[string]any{ "waz": "value",