diff --git a/internal/types/subtypes/attribute_transform.go b/internal/types/subtypes/attribute_transform.go index 9c59509dd..ab4793047 100644 --- a/internal/types/subtypes/attribute_transform.go +++ b/internal/types/subtypes/attribute_transform.go @@ -130,3 +130,59 @@ func protoToStruct(p proto.Message) (*structpb.Struct, error) { } return st, nil } + +// Filterable converts a proto.Message so any subtype attributes fields are +// structed like the API so filter strings will be correctly applied. If the +// given proto.Message does not have any subtype attributes, the original +// proto.Message is returned. To determine if the message needs +// transformation, it looks for a oneof field named "attrs". It also expects +// that there is a structpb.Struct field named "attributes" as part of the +// oneof. Thus the message must be like: +// +// message Foo { +// // other fields +// oneof attrs { +// google.protobuf.Struct attributes = 100; +// // other attribute fields +// } +// } +// +// If the message does not conform to this structure, +// the original message is returned. +func Filterable(item proto.Message) (proto.Message, error) { + clone := proto.Clone(item) + r := clone.ProtoReflect() + + attrsField := r.Descriptor().Oneofs().ByName("attrs") + if attrsField == nil { + return item, nil + } + + defaultAttrField := attrsField.Fields().ByName("attributes") + if defaultAttrField == nil { + return item, nil + } + + var attr proto.Message + var pbAttrs proto.Message + var err error + + oneofField := r.WhichOneof(attrsField) + if oneofField == nil { + // attrs field is not set, nothing to do + return item, nil + } + + attr = r.Get(oneofField).Message().Interface() + pbAttrs, err = protoToStruct(attr) + if err != nil { + return nil, err + } + + r.Set(defaultAttrField, protoreflect.ValueOfMessage(pbAttrs.ProtoReflect())) + f, err := protoToStruct(r.Interface()) + if err != nil { + return nil, err + } + return f, nil +} diff --git a/internal/types/subtypes/attribute_transform_test.go b/internal/types/subtypes/attribute_transform_test.go new file mode 100644 index 000000000..b9aa5dafa --- /dev/null +++ b/internal/types/subtypes/attribute_transform_test.go @@ -0,0 +1,104 @@ +package subtypes + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/gen/testing/attribute" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestFilterable(t *testing.T) { + cases := []struct { + name string + item proto.Message + want proto.Message + }{ + { + "TestResource/sub_resource", + &attribute.TestResource{ + Type: "sub_resource", + Attrs: &attribute.TestResource_SubResourceAttributes{ + SubResourceAttributes: &attribute.TestSubResourceAttributes{ + Name: "test", + }, + }, + }, + func() *structpb.Struct { + w, _ := structpb.NewStruct(map[string]interface{}{ + "type": "sub_resource", + "attributes": map[string]interface{}{ + "name": "test", + }, + }) + return w + }(), + }, + { + "TestResource/default", + &attribute.TestResource{ + Type: "default", + Attrs: &attribute.TestResource_Attributes{ + Attributes: func() *structpb.Struct { + attrs, _ := structpb.NewStruct(map[string]interface{}{ + "name": "test", + }) + return attrs + }(), + }, + }, + func() *structpb.Struct { + w, _ := structpb.NewStruct(map[string]interface{}{ + "type": "default", + "attributes": map[string]interface{}{ + "name": "test", + }, + }) + return w + }(), + }, + { + "TestNoOneOf", + &attribute.TestNoOneOf{ + Type: "sub_resource", + Attributes: func() *structpb.Struct { + attrs, _ := structpb.NewStruct(map[string]interface{}{ + "name": "test", + }) + return attrs + }(), + }, + &attribute.TestNoOneOf{ + Type: "sub_resource", + Attributes: func() *structpb.Struct { + attrs, _ := structpb.NewStruct(map[string]interface{}{ + "name": "test", + }) + return attrs + }(), + }, + }, + { + "TestNoAttributes", + &attribute.TestNoAttributes{ + Id: "test", + Type: "no_attr", + }, + &attribute.TestNoAttributes{ + Id: "test", + Type: "no_attr", + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := Filterable(tc.item) + require.NoError(t, err) + require.Empty(t, cmp.Diff(got, tc.want, protocmp.Transform())) + }) + } +} diff --git a/internal/types/subtypes/attributes.go b/internal/types/subtypes/attributes.go index 7b3ad12d0..135d7c373 100644 --- a/internal/types/subtypes/attributes.go +++ b/internal/types/subtypes/attributes.go @@ -46,7 +46,7 @@ var globalAttributeRegistry attributeRegistry // to be called on any protobuf message. However, if a message has subtypes but // does not provide a field with a subtype of "default", or if the default // field is not a *structpb.Struct type, an error is returned. -func (ak attributeRegistry) register(d protoreflect.MessageDescriptor) error { +func (ak *attributeRegistry) register(d protoreflect.MessageDescriptor) error { ak.Lock() defer ak.Unlock() @@ -96,7 +96,7 @@ func (ak attributeRegistry) register(d protoreflect.MessageDescriptor) error { // attributeField retrieves the FieldDescriptor for a given subtype's // attribute fields. If the corresponding protobuf message has not been // registered it will return an error. -func (ak attributeRegistry) attributeField(d protoreflect.MessageDescriptor, t Subtype) (protoreflect.FieldDescriptor, error) { +func (ak *attributeRegistry) attributeField(d protoreflect.MessageDescriptor, t Subtype) (protoreflect.FieldDescriptor, error) { ak.RLock() defer ak.RUnlock() diff --git a/internal/types/subtypes/source_registry.go b/internal/types/subtypes/source_registry.go index d0bfb5f1e..dc3e0575f 100644 --- a/internal/types/subtypes/source_registry.go +++ b/internal/types/subtypes/source_registry.go @@ -35,7 +35,7 @@ type sourceRegistry struct { m map[protoreflect.FullName]protoreflect.FieldDescriptor } -func (s sourceRegistry) register(d protoreflect.MessageDescriptor) error { +func (s *sourceRegistry) register(d protoreflect.MessageDescriptor) error { s.Lock() defer s.Unlock() @@ -58,7 +58,7 @@ func (s sourceRegistry) register(d protoreflect.MessageDescriptor) error { return nil } -func (s sourceRegistry) get(d protoreflect.MessageDescriptor) protoreflect.FieldDescriptor { +func (s *sourceRegistry) get(d protoreflect.MessageDescriptor) protoreflect.FieldDescriptor { s.RLock() defer s.RUnlock()