feat(subtypes): Add grpc interceptor for transforming attributes

This interceptor is responsible for maintaining compatability with the
JSON API with the change to the protobuf messages. The messages are
changing from a single attribue field to a oneof to explicitly associate
subtype attribute messages with the resource message. The interceptor
transforms the request prior to the auditReqeust interceptor, and
transforms the response after the auditResponse interceptor. This allows
the audit interceptors to use all the necessary classification
information when creating the audit events.
pull/2031/head
Timothy Messier 4 years ago committed by Johan Brandhorst-Satzkorn
parent af93f7fccb
commit 198a39ef1e

File diff suppressed because it is too large Load Diff

@ -0,0 +1,513 @@
// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT.
// source: testing/attribute/v1/attribute.proto
/*
Package attribute is a reverse proxy.
It translates gRPC into RESTful JSON APIs.
*/
package attribute
import (
"context"
"io"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/grpc-ecosystem/grpc-gateway/v2/utilities"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/grpclog"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
// Suppress "imported and not used" errors
var _ codes.Code
var _ io.Reader
var _ status.Status
var _ = runtime.String
var _ = utilities.NewDoubleArray
var _ = metadata.Join
func request_TestResourceService_TestListResource_0(ctx context.Context, marshaler runtime.Marshaler, client TestResourceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestListResourceRequest
var metadata runtime.ServerMetadata
msg, err := client.TestListResource(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_TestResourceService_TestListResource_0(ctx context.Context, marshaler runtime.Marshaler, server TestResourceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestListResourceRequest
var metadata runtime.ServerMetadata
msg, err := server.TestListResource(ctx, &protoReq)
return msg, metadata, err
}
func request_TestResourceService_TestCreateResource_0(ctx context.Context, marshaler runtime.Marshaler, client TestResourceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestCreateResourceRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Item); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.TestCreateResource(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_TestResourceService_TestCreateResource_0(ctx context.Context, marshaler runtime.Marshaler, server TestResourceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestCreateResourceRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Item); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.TestCreateResource(ctx, &protoReq)
return msg, metadata, err
}
var (
filter_TestResourceService_TestUpdateResource_0 = &utilities.DoubleArray{Encoding: map[string]int{"item": 0, "id": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}}
)
func request_TestResourceService_TestUpdateResource_0(ctx context.Context, marshaler runtime.Marshaler, client TestResourceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestUpdateResourceRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Item); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {
if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Item); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
} else {
protoReq.UpdateMask = fieldMask
}
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_TestResourceService_TestUpdateResource_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.TestUpdateResource(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_TestResourceService_TestUpdateResource_0(ctx context.Context, marshaler runtime.Marshaler, server TestResourceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestUpdateResourceRequest
var metadata runtime.ServerMetadata
newReader, berr := utilities.IOReaderFactory(req.Body)
if berr != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
}
if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Item); err != nil && err != io.EOF {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 {
if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Item); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
} else {
protoReq.UpdateMask = fieldMask
}
}
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
if err := req.ParseForm(); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_TestResourceService_TestUpdateResource_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.TestUpdateResource(ctx, &protoReq)
return msg, metadata, err
}
func request_TestResourceService_TestGetResource_0(ctx context.Context, marshaler runtime.Marshaler, client TestResourceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestGetResourceRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
msg, err := client.TestGetResource(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_TestResourceService_TestGetResource_0(ctx context.Context, marshaler runtime.Marshaler, server TestResourceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq TestGetResourceRequest
var metadata runtime.ServerMetadata
var (
val string
ok bool
err error
_ = err
)
val, ok = pathParams["id"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id")
}
protoReq.Id, err = runtime.String(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err)
}
msg, err := server.TestGetResource(ctx, &protoReq)
return msg, metadata, err
}
// RegisterTestResourceServiceHandlerServer registers the http handlers for service TestResourceService to "mux".
// UnaryRPC :call TestResourceServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterTestResourceServiceHandlerFromEndpoint instead.
func RegisterTestResourceServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server TestResourceServiceServer) error {
mux.Handle("GET", pattern_TestResourceService_TestListResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestListResource", runtime.WithHTTPPathPattern("/v1/test-resources"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_TestResourceService_TestListResource_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestListResource_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_TestResourceService_TestCreateResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestCreateResource", runtime.WithHTTPPathPattern("/v1/test-resources"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_TestResourceService_TestCreateResource_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestCreateResource_0(ctx, mux, outboundMarshaler, w, req, response_TestResourceService_TestCreateResource_0{resp}, mux.GetForwardResponseOptions()...)
})
mux.Handle("PATCH", pattern_TestResourceService_TestUpdateResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestUpdateResource", runtime.WithHTTPPathPattern("/v1/test-resources/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_TestResourceService_TestUpdateResource_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestUpdateResource_0(ctx, mux, outboundMarshaler, w, req, response_TestResourceService_TestUpdateResource_0{resp}, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_TestResourceService_TestGetResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestGetResource", runtime.WithHTTPPathPattern("/v1/test-resources/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_TestResourceService_TestGetResource_0(ctx, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestGetResource_0(ctx, mux, outboundMarshaler, w, req, response_TestResourceService_TestGetResource_0{resp}, mux.GetForwardResponseOptions()...)
})
return nil
}
// RegisterTestResourceServiceHandlerFromEndpoint is same as RegisterTestResourceServiceHandler but
// automatically dials to "endpoint" and closes the connection when "ctx" gets done.
func RegisterTestResourceServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) {
conn, err := grpc.Dial(endpoint, opts...)
if err != nil {
return err
}
defer func() {
if err != nil {
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
return
}
go func() {
<-ctx.Done()
if cerr := conn.Close(); cerr != nil {
grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr)
}
}()
}()
return RegisterTestResourceServiceHandler(ctx, mux, conn)
}
// RegisterTestResourceServiceHandler registers the http handlers for service TestResourceService to "mux".
// The handlers forward requests to the grpc endpoint over "conn".
func RegisterTestResourceServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error {
return RegisterTestResourceServiceHandlerClient(ctx, mux, NewTestResourceServiceClient(conn))
}
// RegisterTestResourceServiceHandlerClient registers the http handlers for service TestResourceService
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "TestResourceServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "TestResourceServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "TestResourceServiceClient" to call the correct interceptors.
func RegisterTestResourceServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client TestResourceServiceClient) error {
mux.Handle("GET", pattern_TestResourceService_TestListResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestListResource", runtime.WithHTTPPathPattern("/v1/test-resources"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_TestResourceService_TestListResource_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestListResource_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("POST", pattern_TestResourceService_TestCreateResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestCreateResource", runtime.WithHTTPPathPattern("/v1/test-resources"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_TestResourceService_TestCreateResource_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestCreateResource_0(ctx, mux, outboundMarshaler, w, req, response_TestResourceService_TestCreateResource_0{resp}, mux.GetForwardResponseOptions()...)
})
mux.Handle("PATCH", pattern_TestResourceService_TestUpdateResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestUpdateResource", runtime.WithHTTPPathPattern("/v1/test-resources/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_TestResourceService_TestUpdateResource_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestUpdateResource_0(ctx, mux, outboundMarshaler, w, req, response_TestResourceService_TestUpdateResource_0{resp}, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_TestResourceService_TestGetResource_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
var err error
ctx, err = runtime.AnnotateContext(ctx, mux, req, "/testing.attribute.v1.TestResourceService/TestGetResource", runtime.WithHTTPPathPattern("/v1/test-resources/{id}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_TestResourceService_TestGetResource_0(ctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_TestResourceService_TestGetResource_0(ctx, mux, outboundMarshaler, w, req, response_TestResourceService_TestGetResource_0{resp}, mux.GetForwardResponseOptions()...)
})
return nil
}
type response_TestResourceService_TestCreateResource_0 struct {
proto.Message
}
func (m response_TestResourceService_TestCreateResource_0) XXX_ResponseBody() interface{} {
response := m.Message.(*TestCreateResourceResponse)
return response.Item
}
type response_TestResourceService_TestUpdateResource_0 struct {
proto.Message
}
func (m response_TestResourceService_TestUpdateResource_0) XXX_ResponseBody() interface{} {
response := m.Message.(*TestUpdateResourceResponse)
return response.Item
}
type response_TestResourceService_TestGetResource_0 struct {
proto.Message
}
func (m response_TestResourceService_TestGetResource_0) XXX_ResponseBody() interface{} {
response := m.Message.(*TestGetResourceResponse)
return response.Item
}
var (
pattern_TestResourceService_TestListResource_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "test-resources"}, ""))
pattern_TestResourceService_TestCreateResource_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "test-resources"}, ""))
pattern_TestResourceService_TestUpdateResource_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "test-resources", "id"}, ""))
pattern_TestResourceService_TestGetResource_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "test-resources", "id"}, ""))
)
var (
forward_TestResourceService_TestListResource_0 = runtime.ForwardResponseMessage
forward_TestResourceService_TestCreateResource_0 = runtime.ForwardResponseMessage
forward_TestResourceService_TestUpdateResource_0 = runtime.ForwardResponseMessage
forward_TestResourceService_TestGetResource_0 = runtime.ForwardResponseMessage
)

@ -0,0 +1,209 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
package attribute
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// TestResourceServiceClient is the client API for TestResourceService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type TestResourceServiceClient interface {
TestListResource(ctx context.Context, in *TestListResourceRequest, opts ...grpc.CallOption) (*TestListResourceResponse, error)
TestCreateResource(ctx context.Context, in *TestCreateResourceRequest, opts ...grpc.CallOption) (*TestCreateResourceResponse, error)
TestUpdateResource(ctx context.Context, in *TestUpdateResourceRequest, opts ...grpc.CallOption) (*TestUpdateResourceResponse, error)
TestGetResource(ctx context.Context, in *TestGetResourceRequest, opts ...grpc.CallOption) (*TestGetResourceResponse, error)
}
type testResourceServiceClient struct {
cc grpc.ClientConnInterface
}
func NewTestResourceServiceClient(cc grpc.ClientConnInterface) TestResourceServiceClient {
return &testResourceServiceClient{cc}
}
func (c *testResourceServiceClient) TestListResource(ctx context.Context, in *TestListResourceRequest, opts ...grpc.CallOption) (*TestListResourceResponse, error) {
out := new(TestListResourceResponse)
err := c.cc.Invoke(ctx, "/testing.attribute.v1.TestResourceService/TestListResource", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *testResourceServiceClient) TestCreateResource(ctx context.Context, in *TestCreateResourceRequest, opts ...grpc.CallOption) (*TestCreateResourceResponse, error) {
out := new(TestCreateResourceResponse)
err := c.cc.Invoke(ctx, "/testing.attribute.v1.TestResourceService/TestCreateResource", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *testResourceServiceClient) TestUpdateResource(ctx context.Context, in *TestUpdateResourceRequest, opts ...grpc.CallOption) (*TestUpdateResourceResponse, error) {
out := new(TestUpdateResourceResponse)
err := c.cc.Invoke(ctx, "/testing.attribute.v1.TestResourceService/TestUpdateResource", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *testResourceServiceClient) TestGetResource(ctx context.Context, in *TestGetResourceRequest, opts ...grpc.CallOption) (*TestGetResourceResponse, error) {
out := new(TestGetResourceResponse)
err := c.cc.Invoke(ctx, "/testing.attribute.v1.TestResourceService/TestGetResource", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// TestResourceServiceServer is the server API for TestResourceService service.
// All implementations must embed UnimplementedTestResourceServiceServer
// for forward compatibility
type TestResourceServiceServer interface {
TestListResource(context.Context, *TestListResourceRequest) (*TestListResourceResponse, error)
TestCreateResource(context.Context, *TestCreateResourceRequest) (*TestCreateResourceResponse, error)
TestUpdateResource(context.Context, *TestUpdateResourceRequest) (*TestUpdateResourceResponse, error)
TestGetResource(context.Context, *TestGetResourceRequest) (*TestGetResourceResponse, error)
mustEmbedUnimplementedTestResourceServiceServer()
}
// UnimplementedTestResourceServiceServer must be embedded to have forward compatible implementations.
type UnimplementedTestResourceServiceServer struct {
}
func (UnimplementedTestResourceServiceServer) TestListResource(context.Context, *TestListResourceRequest) (*TestListResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method TestListResource not implemented")
}
func (UnimplementedTestResourceServiceServer) TestCreateResource(context.Context, *TestCreateResourceRequest) (*TestCreateResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method TestCreateResource not implemented")
}
func (UnimplementedTestResourceServiceServer) TestUpdateResource(context.Context, *TestUpdateResourceRequest) (*TestUpdateResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method TestUpdateResource not implemented")
}
func (UnimplementedTestResourceServiceServer) TestGetResource(context.Context, *TestGetResourceRequest) (*TestGetResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method TestGetResource not implemented")
}
func (UnimplementedTestResourceServiceServer) mustEmbedUnimplementedTestResourceServiceServer() {}
// UnsafeTestResourceServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to TestResourceServiceServer will
// result in compilation errors.
type UnsafeTestResourceServiceServer interface {
mustEmbedUnimplementedTestResourceServiceServer()
}
func RegisterTestResourceServiceServer(s grpc.ServiceRegistrar, srv TestResourceServiceServer) {
s.RegisterService(&TestResourceService_ServiceDesc, srv)
}
func _TestResourceService_TestListResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TestListResourceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestResourceServiceServer).TestListResource(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/testing.attribute.v1.TestResourceService/TestListResource",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestResourceServiceServer).TestListResource(ctx, req.(*TestListResourceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _TestResourceService_TestCreateResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TestCreateResourceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestResourceServiceServer).TestCreateResource(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/testing.attribute.v1.TestResourceService/TestCreateResource",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestResourceServiceServer).TestCreateResource(ctx, req.(*TestCreateResourceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _TestResourceService_TestUpdateResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TestUpdateResourceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestResourceServiceServer).TestUpdateResource(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/testing.attribute.v1.TestResourceService/TestUpdateResource",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestResourceServiceServer).TestUpdateResource(ctx, req.(*TestUpdateResourceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _TestResourceService_TestGetResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TestGetResourceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(TestResourceServiceServer).TestGetResource(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/testing.attribute.v1.TestResourceService/TestGetResource",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(TestResourceServiceServer).TestGetResource(ctx, req.(*TestGetResourceRequest))
}
return interceptor(ctx, in, info, handler)
}
// TestResourceService_ServiceDesc is the grpc.ServiceDesc for TestResourceService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var TestResourceService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "testing.attribute.v1.TestResourceService",
HandlerType: (*TestResourceServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "TestListResource",
Handler: _TestResourceService_TestListResource_Handler,
},
{
MethodName: "TestCreateResource",
Handler: _TestResourceService_TestCreateResource_Handler,
},
{
MethodName: "TestUpdateResource",
Handler: _TestResourceService_TestUpdateResource_Handler,
},
{
MethodName: "TestGetResource",
Handler: _TestResourceService_TestGetResource_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "testing/attribute/v1/attribute.proto",
}

@ -4,11 +4,19 @@ package testing.attribute.v1;
option go_package = "github.com/hashicorp/boundary/internal/gen/testing/attribute;attribute";
import "protoc-gen-openapiv2/options/annotations.proto";
import "google/api/annotations.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/field_mask.proto";
import "controller/custom_options/v1/options.proto";
option (controller.custom_options.v1.domain) = "test";
message TestResource {
string id = 1;
string type = 2;
string other_id = 3 [(controller.custom_options.v1.subtype_source_id) = true];
oneof attrs {
google.protobuf.Struct attributes = 10 [(controller.custom_options.v1.subtype) = "default"];
@ -22,4 +30,141 @@ message TestSubResourceAttributes {
message TestNoAttributes {
string id = 1;
string type = 2;
}
message TestNoOneOf {
string id = 1;
string type = 2;
google.protobuf.Struct attributes = 10;
}
service TestResourceService {
rpc TestListResource(TestListResourceRequest) returns (TestListResourceResponse) {
option (google.api.http) = {
get: "/v1/test-resources"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Test list"
};
}
rpc TestCreateResource(TestCreateResourceRequest) returns (TestCreateResourceResponse) {
option (google.api.http) = {
post: "/v1/test-resources"
body: "item"
response_body: "item"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Test create"
};
}
rpc TestUpdateResource(TestUpdateResourceRequest) returns (TestUpdateResourceResponse) {
option (google.api.http) = {
patch: "/v1/test-resources/{id}"
body: "item"
response_body: "item"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Test update"
};
}
rpc TestGetResource(TestGetResourceRequest) returns (TestGetResourceResponse) {
option (google.api.http) = {
get: "/v1/test-resources/{id}"
response_body: "item"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Test get"
};
}
}
message TestListResourceRequest {}
message TestListResourceResponse {
repeated TestResource items = 1;
}
message TestGetResourceRequest {
string id = 1;
}
message TestGetResourceResponse {
TestResource item = 1;
}
message TestCreateResourceRequest {
TestResource item = 2;
}
message TestCreateResourceResponse {
TestResource item = 1;
}
message TestUpdateResourceRequest {
string id = 1;
TestResource item = 2;
google.protobuf.FieldMask update_mask = 3 [json_name="update_mask"];
}
message TestUpdateResourceResponse {
TestResource item = 1;
}
message TestRequestNoItem {
string name = 1;
}
message TestRequestItemNotMessage {
string item = 1;
}
message TestItemNoType {
string id = 1;
}
message TestRequestItemNoType {
TestItemNoType item = 1;
}
message TestNoItemAttributes {
string id = 1;
oneof attrs {
google.protobuf.Struct attributes = 10 [(controller.custom_options.v1.subtype) = "default"];
TestSubResourceAttributes sub_resource_attributes = 20 [(controller.custom_options.v1.subtype) = "sub_resource"];
}
}
message TestCreateNoOneOfRequest {
TestNoOneOf item = 1;
}
message TestUpdateNoOneOfRequest {
string id = 1;
TestNoOneOf item = 2;
}
message TestResponseNoItem {
string name = 1;
}
message TestResponseItemNotMessage {
string item = 1;
}
message TestResponseItemNoType {
TestItemNoType item = 1;
}
message TestCreateNoOneOfResponse {
TestNoOneOf item = 1;
}
message TestUpdateNoOneOfResponse {
TestNoOneOf item = 1;
}

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/servers/controller/common"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/internal/types/subtypes"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
)
@ -73,11 +74,12 @@ func newGrpcServer(
grpc.MaxSendMsgSize(math.MaxInt32),
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
requestCtxInterceptor, // populated requestInfo from headers into the request ctx
auditRequestInterceptor(ctx), // before we get started, audit the request
errorInterceptor(ctx), // convert domain and api errors into headers for the http proxy
statusCodeInterceptor(ctx), // convert grpc codes into http status codes for the http proxy (can modify the resp)
auditResponseInterceptor(ctx), // as we finish, audit the response
requestCtxInterceptor, // populated requestInfo from headers into the request ctx
subtypes.AttributeTransformerInterceptor(ctx), // convert to/from generic attributes from/to subtype specific attributes
auditRequestInterceptor(ctx), // before we get started, audit the request
errorInterceptor(ctx), // convert domain and api errors into headers for the http proxy
statusCodeInterceptor(ctx), // convert grpc codes into http status codes for the http proxy (can modify the resp)
auditResponseInterceptor(ctx), // as we finish, audit the response
grpc_recovery.UnaryServerInterceptor( // recover from panics with a grpc internal error
grpc_recovery.WithRecoveryHandlerContext(recoveryHandler()),
),

@ -0,0 +1,132 @@
package subtypes
import (
"fmt"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/structpb"
)
func convertAttributesToSubtype(msg proto.Message, st Subtype) error {
r := msg.ProtoReflect()
d := r.Descriptor()
defaultAttrField, err := attributeField(d, defaultSubtype)
if err != nil {
// If unable to get a default attribute field, the message is either
// not registered or not in the format needed for conversion, so no
// conversion should be performed. The most likely case here is that
// the message has not been changed to use a oneof for attributes yet.
return nil
}
stAttrField, err := attributeField(d, st)
if err != nil {
// This error should not be possible, since any registration issue
// would trigger the previous call, and if this particular subtype
// is unknown, the default should be returned.
return nil
}
if defaultAttrField == stAttrField {
// no need to convert
return nil
}
defaultAttrs, ok := r.Get(defaultAttrField).Message().Interface().(*structpb.Struct)
if !ok {
// This should not be possible since this is checked in
// (attributeRegistry).register at initialization time and would panic
// if this was the case.
return fmt.Errorf("found default attribute field that is not structpb.Struct: %s %s", d.FullName(), defaultAttrField.FullName())
}
stAttrs := r.Get(stAttrField).Message().New().Interface()
if err := structToProto(defaultAttrs, stAttrs); err != nil {
return err
}
// implicitly clears any previously set oneof value
r.Set(stAttrField, protoreflect.ValueOfMessage(stAttrs.ProtoReflect()))
return nil
}
func convertAttributesToDefault(msg proto.Message, st Subtype) error {
r := msg.ProtoReflect()
d := r.Descriptor()
defaultAttrField, err := attributeField(d, defaultSubtype)
if err != nil {
// If unable to get a default attribute field, the message is either
// not registered or not in the format needed for conversion, so no
// conversion should be performed. The most likely case here is that
// the message has not been changed to use a oneof for attributes yet.
return nil
}
stAttrField, err := attributeField(d, st)
if err != nil {
// This error should not be possible, since any registration issue
// would trigger the previous call, and if this particular subtype
// is unknown, the default should be returned.
return nil
}
if defaultAttrField == stAttrField {
// no need to convert
return nil
}
stAttrs, ok := r.Get(stAttrField).Message().Interface().(proto.Message)
if !ok {
return fmt.Errorf("found subtype attribute field that is not proto.Message: %s %s", d.FullName(), stAttrField.FullName())
}
defaultAttrs, err := protoToStruct(stAttrs)
if err != nil {
return err
}
// implicitly clears any previously set oneof value
r.Set(defaultAttrField, protoreflect.ValueOfMessage(defaultAttrs.ProtoReflect()))
return nil
}
func structToProto(fields *structpb.Struct, p proto.Message) error {
if fields == nil {
// If there is no struct, don't update the default proto message.
return nil
}
js, err := fields.MarshalJSON()
if err != nil {
return err
}
// TODO: replicate this logic but with a proto extension set on the Field
// descriptor There are some attributes where we want to discard unknown
// fields, while others that should error if there are unknown fields
//
// opts := GetOpts(opt...)
// if opts.withDiscardUnknownFields {
// err = (protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(js, p))
// } else {
// err = protojson.Unmarshal(js, p)
// }
err = protojson.Unmarshal(js, p)
if err != nil {
return err
}
return nil
}
func protoToStruct(p proto.Message) (*structpb.Struct, error) {
js, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(p)
if err != nil {
return nil, err
}
st := &structpb.Struct{}
if err := protojson.Unmarshal(js, st); err != nil {
return nil, err
}
return st, nil
}

@ -9,6 +9,7 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/known/structpb"
)
const (
@ -16,13 +17,13 @@ const (
)
func init() {
globalAttributeKeys = attributeKeys{
globalAttributeRegistry = attributeRegistry{
m: make(map[protoreflect.FullName]fieldMap),
}
protoregistry.GlobalTypes.RangeMessages(func(m protoreflect.MessageType) bool {
d := m.Descriptor()
if err := globalAttributeKeys.register(d); err != nil {
if err := globalAttributeRegistry.register(d); err != nil {
panic(err)
}
return true
@ -31,20 +32,21 @@ func init() {
type fieldMap map[Subtype]protoreflect.FieldDescriptor
type attributeKeys struct {
type attributeRegistry struct {
sync.RWMutex
m map[protoreflect.FullName]fieldMap
}
var globalAttributeKeys attributeKeys
var globalAttributeRegistry attributeRegistry
// register examines the given protobuf MessageDescriptor for fields that have
// the Subtype protobuf extension. It uses these to build a fieldMap of subtype
// strings to the protobuf FieldDescriptors. If the message does not have any
// subtypes it will not be registered, but no error is returned, allowing this
// to be called on any protobuf message. However, if a message has subtypes but
// does not provide a field with a subtype of "default" an error is returned.
func (ak attributeKeys) register(d protoreflect.MessageDescriptor) error {
// 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 {
ak.Lock()
defer ak.Unlock()
@ -77,19 +79,24 @@ func (ak attributeKeys) register(d protoreflect.MessageDescriptor) error {
}
// If a message has subtypes, it must provide a "default" to support plugins.
if _, ok := km[defaultSubtype]; !ok {
defSubtype, ok := km[defaultSubtype]
if !ok {
return fmt.Errorf("proto message %s with subtype attributes but no 'default'", fn)
}
if defSubtype.Message().FullName() != (&structpb.Struct{}).ProtoReflect().Descriptor().FullName() {
return fmt.Errorf("proto message %s with 'default' subtype attributes that is not structpb.Struct", fn)
}
ak.m[fn] = km
return nil
}
// protoAttributeField retrieves the FieldDescriptor for a given subtype's
// 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 attributeKeys) protoAttributeField(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()
@ -112,13 +119,8 @@ func (ak attributeKeys) protoAttributeField(d protoreflect.MessageDescriptor, t
return tt, nil
}
// TODO: fix doc
// protoAttributeField is used by the attrMarshaler to translate between JSON
// formats for the API and for the protobuf messages. It expects a
// proto.Message with a OneOf field for the subtype attributes and the subtype
// string. It returns the string for the JSON key that that should be used for
// the subtype's attributes fields.
func protoAttributeField(msg proto.Message, t Subtype) (protoreflect.FieldDescriptor, error) {
d := msg.ProtoReflect().Descriptor()
return globalAttributeKeys.protoAttributeField(d, t)
// attributeField is used by the AttributeTransformInterceptor to retrieve
// the proto FieldDescriptor for a given subtype.
func attributeField(d protoreflect.MessageDescriptor, t Subtype) (protoreflect.FieldDescriptor, error) {
return globalAttributeRegistry.attributeField(d, t)
}

@ -39,7 +39,7 @@ func TestProtoAttributeKey(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
k, err := protoAttributeField(tc.msg, tc.subtype)
k, err := attributeField(tc.msg.ProtoReflect().Descriptor(), tc.subtype)
require.NoError(t, err)
assert.Equal(t, tc.expected, k.FullName())
})
@ -76,7 +76,7 @@ func TestProtoAttributeKeyErrors(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := protoAttributeField(tc.msg, tc.subtype)
_, err := attributeField(tc.msg.ProtoReflect().Descriptor(), tc.subtype)
require.EqualError(t, err, tc.expectedErr)
})
}
@ -97,14 +97,14 @@ func TestRegisterErrors(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := globalAttributeKeys.register(tc.msg.ProtoReflect().Descriptor())
err := globalAttributeRegistry.register(tc.msg.ProtoReflect().Descriptor())
require.EqualError(t, err, tc.expectedErr)
})
}
}
func TestRegisterNoSubtypes(t *testing.T) {
ak := attributeKeys{
ak := attributeRegistry{
m: make(map[protoreflect.FullName]fieldMap),
}

@ -0,0 +1,308 @@
package subtypes
import (
"context"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/sdk/pbs/controller/protooptions"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
)
func messageDomain(m proto.Message) string {
r := m.ProtoReflect()
fd := r.Descriptor().ParentFile()
if fd == nil {
return ""
}
opts, ok := fd.Options().(*descriptorpb.FileOptions)
if !ok {
return ""
}
domain := proto.GetExtension(opts, protooptions.E_Domain).(string)
return domain
}
// transformRequestAttributes will modify the request proto.Message, setting
// any subtype attribute fields into the corresponding strongly-typed struct
// for the subtype. It looks for some specific structure in the proto.Message
// to identify the correct subtype and apply the transformation.
//
// The first structure is a message that contains a single "item" that is a
// message that has a "type" and an "attrs" oneof for attributes:
//
// message CreateFooRequest {
// item Foo = 1;
// }
// message Foo {
// string type = 1;
// // other fields
// oneof attrs {
// google.protobuf.Struct attributes = 10 [(custom_options.v1.subtype) = "default"];
// // other subtype attributes types
// }
// }
//
// The second structure is similar to the first, but rather then the type field
// being provided, an id field is set. Note this is a different id from the
// third structure. In this case it is an id for a related resource and the id
// is marked with the "subtype_source_id" custom option:
//
// message CreateFooRequest {
// item Foo = 1;
// }
// message Foo {
// string bar_id = 1 [(custom_options.v1.subtype_source_id) = true];
// // other fields
// oneof attrs {
// google.protobuf.Struct attributes = 10 [(custom_options.v1.subtype) = "default"];
// // other subtype attributes types
// }
// }
//
// The third structure is a message that contains an id string and an "item"
// that is a message that has an "attrs" oneof for attributes:
//
// message UpdateFooRequest {
// string id = 1;
// item Foo = 2;
// }
// message Foo {
// // other fields
// oneof attrs {
// google.protobuf.Struct attributes = 10 [(custom_options.v1.subtype) = "default"];
// // other subtype attributes types
// }
// }
//
// The forth structure is a message that contains an id string and an "attrs" oneof for
// attributes:
//
// message FooActionRequest {
// string id = 1;
// oneof attrs {
// google.protobuf.Struct attributes = 10 [(custom_options.v1.subtype) = "default"];
// // other subtype attributes types
// }
// }
//
// Also note that for any of the id based lookups to function, the file that contains
// the proto.Message definition must set the "domain" custom option.
func transformRequestAttributes(req proto.Message) (proto.Message, error) {
domain := messageDomain(req)
r := req.ProtoReflect()
fields := r.Descriptor().Fields()
itemField := fields.ByName("item")
idField := fields.ByName("id")
attributesField := fields.ByName("attributes")
fieldValue := func(m protoreflect.Message, fd protoreflect.FieldDescriptor) string {
if fd == nil {
return ""
}
return m.Get(fd).String()
}
var st Subtype
switch {
case itemField != nil:
itemR := itemField.Message()
if itemR == nil {
return req, nil
}
id := fieldValue(r, idField)
item := r.Get(itemField).Message().Interface()
itemFields := itemR.Fields()
typeField := itemFields.ByName("type")
t := fieldValue(item.ProtoReflect(), typeField)
sourceIdField := sourceIdFieldDescriptor(item.ProtoReflect().Descriptor())
sourceId := fieldValue(item.ProtoReflect(), sourceIdField)
switch {
case idField != nil && id != "":
st = SubtypeFromId(domain, id)
case sourceIdField != nil && sourceId != "":
st = SubtypeFromId(domain, sourceId)
case typeField != nil && t != "":
st = Subtype(t)
default: // need either type or id
return req, nil
}
if err := convertAttributesToSubtype(item, st); err != nil {
return req, err
}
case idField != nil && attributesField != nil:
id := r.Get(idField).String()
st = SubtypeFromId(domain, id)
if err := convertAttributesToSubtype(req, st); err != nil {
return req, err
}
}
return req, nil
}
func transformResponseItemAttributes(item proto.Message) error {
r := item.ProtoReflect()
desc := r.Descriptor()
typeField := desc.Fields().ByName("type")
if typeField == nil {
// not an item with subtypes
return nil
}
attrsField := desc.Oneofs().ByName("attrs")
if attrsField == nil {
// not an item with attrs oneof
return nil
}
if r.WhichOneof(attrsField) == nil {
// attrs field is not set, nothing to do
return nil
}
st := Subtype(item.ProtoReflect().Get(typeField).String())
return convertAttributesToDefault(item, st)
}
// transformResponseAttributes will modify the response proto.Message, setting
// any subtype attribute fields into the default structpb.Struct field. It looks
// for some specific structure in the proto.Message to identify that it is a
// message that needs transformation.
//
// The first structure is a message that contains a single "item" that is a
// message that has an "attrs" oneof for attributes:
//
// message CreateFooResponse {
// item Foo = 1;
// }
// message Foo {
// oneof attrs {
// google.protobuf.Struct attributes = 10 [(custom_options.v1.subtype) = "default"];
// // other subtype attributes types
// }
// }
//
// The second structure is a message that contains a single "items" that is a
// slice of item messages that have an "attrs" oneof for attributes:
//
// message ListFooResponse {
// items []Foo = 1;
// }
// message Foo {
// oneof attrs {
// google.protobuf.Struct attributes = 10 [(custom_options.v1.subtype) = "default"];
// // other subtype attributes types
// }
// }
func transformResponseAttributes(res proto.Message) (proto.Message, error) {
r := res.ProtoReflect()
fields := r.Descriptor().Fields()
itemField := fields.ByName("item")
itemsField := fields.ByName("items")
switch {
case itemField != nil:
if itemR := itemField.Message(); itemR == nil {
return res, nil
}
item := r.Get(itemField).Message().Interface()
if err := transformResponseItemAttributes(item); err != nil {
return res, err
}
case itemsField != nil:
if !itemsField.IsList() {
return res, nil
}
items := r.Get(itemsField).List()
for i := 0; i < items.Len(); i++ {
item := items.Get(i).Message().Interface()
if err := transformResponseItemAttributes(item); err != nil {
return res, err
}
}
}
return res, nil
}
// AttributeTransformerInterceptor is a grpc server interceptor that will
// transform subtype attributes for requests and responses. This will only
// modify requests and responses that adhere to a specific structure and is done
// to support the use of a oneof for attributes to strongly type the attributes
// while allowing the JSON API to provide attributes via a single key.
//
// For example with a protobuf message definition like:
//
// message Account {
// string id = 1;
// string type = 2;
// oneof attrs {
// google.protobuf.Struct attributes = 10 [(controller.custom_options.v1.subtype) = "default"];
// PasswordAttributes password_attributes = 20 [(controller.custom_options.v1.subtype) = "password"];
// }
// }
//
// message PasswordAttributes {
// string login_name = 1;
// }
//
// message AccountCreateRequest {
// Account item = 1;
// }
//
// message AccountCreateResponse {
// Account item = 1;
// }
//
// And a create request with JSON request body like:
// {
// "type": "password",
// "attributes": {
// "login_name": "tim"
// }
// }
//
// Will result in a proto request like:
//
// type:"password" attributes:{fields:{key:"login_name" value:{string_value:"tim"}}}
//
// This request will be transformed into:
//
// type:"password" password_attributes:{login_name:"tim"}
func AttributeTransformerInterceptor(_ context.Context) grpc.UnaryServerInterceptor {
const op = "subtypes.AttributeTransformInterceptor"
return func(interceptorCtx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
var err error
if reqMsg, ok := req.(proto.Message); ok {
req, err = transformRequestAttributes(reqMsg)
if err != nil {
return nil, handlers.InvalidArgumentErrorf("Error in provided request.",
map[string]string{"attributes": "Attribute fields do not match the expected format."})
}
}
res, handlerErr := handler(interceptorCtx, req)
newRes := res
if res, ok := res.(proto.Message); ok {
newRes, err = transformResponseAttributes(res)
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building attribute struct: %v", err)
}
}
return newRes, handlerErr
}
}

@ -0,0 +1,355 @@
package subtypes_test
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/gen/testing/attribute"
"github.com/hashicorp/boundary/internal/servers/controller/handlers"
"github.com/hashicorp/boundary/internal/types/subtypes"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
)
func TestAttributeTransformerInterceptor(t *testing.T) {
cases := []struct {
name string
req interface{}
handlerResp interface{}
expectHandlerReq interface{}
excpetResp interface{}
}{
{
"TestCreateResource/SubResourceRequest",
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestCreateResource/SubResourceRequest/OtherId",
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
OtherId: "trsr_parent",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
OtherId: "trsr_parent",
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
OtherId: "trsr_parent",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
OtherId: "trsr_parent",
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestUpdateResource/SubResourceRequest",
&attribute.TestUpdateResourceRequest{
Id: "trsr_one",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestUpdateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestUpdateResourceRequest{
Id: "trsr_one",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestUpdateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestNoItemAttributes/SubResourceRequest",
&attribute.TestNoItemAttributes{
Id: "trsr_one",
Attrs: &attribute.TestNoItemAttributes_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
&attribute.TestNoItemAttributes{
Id: "trsr_one",
Attrs: &attribute.TestNoItemAttributes_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
&attribute.TestNoItemAttributes{
Id: "trsr_one",
Attrs: &attribute.TestNoItemAttributes_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
&attribute.TestNoItemAttributes{
Id: "trsr_one",
Attrs: &attribute.TestNoItemAttributes_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
{
"TestListResourceResponse",
nil,
&attribute.TestListResourceResponse{
Items: []*attribute.TestResource{
{
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
{
Type: "default",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
{
Type: "unknown",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
nil,
&attribute.TestListResourceResponse{
Items: []*attribute.TestResource{
{
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
{
Type: "default",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
{
Type: "unknown",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
require.Empty(t, cmp.Diff(req, tc.expectHandlerReq, protocmp.Transform()))
return tc.handlerResp, nil
}
got, err := subtypes.AttributeTransformerInterceptor(ctx)(ctx, tc.req, nil, handler)
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tc.excpetResp, protocmp.Transform()))
})
}
}
func TestAttributeTransformerInterceptorRequestErrors(t *testing.T) {
cases := []struct {
name string
req interface{}
want error
}{
{
"InvalidAttributes",
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"foo": "test",
})
return attrs
}(),
},
},
},
handlers.InvalidArgumentErrorf("Error in provided request.",
map[string]string{"attributes": "Attribute fields do not match the expected format."}),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
t.Fatalf("handler should not be called")
return nil, nil
}
_, err := subtypes.AttributeTransformerInterceptor(ctx)(ctx, tc.req, nil, handler)
require.Error(t, err)
require.ErrorIs(t, err, tc.want)
})
}
}

@ -0,0 +1,590 @@
package subtypes
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/gen/testing/attribute"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
)
func init() {
Register("test", Subtype("sub_resource"), "trsr")
Register("test", Subtype("resource_plugin"), "trrp")
}
func TestTransformRequestAttributes(t *testing.T) {
cases := []struct {
name string
req proto.Message
expected proto.Message
}{
{
"TestCreateResource/SubResourceRequest",
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
},
{
"TestCreateResource/DefaultResourceRequest",
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "default",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "default",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestCreateResource/UnknownResourceRequest",
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "unknown",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestCreateResourceRequest{
Item: &attribute.TestResource{
Type: "unknown",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestUpdateResource/SubResourceRequest",
&attribute.TestUpdateResourceRequest{
Id: "trsr_one",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestUpdateResourceRequest{
Id: "trsr_one",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
},
{
"TestUpdateResource/PluginResourceRequest",
&attribute.TestUpdateResourceRequest{
Id: "trdp_one",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestUpdateResourceRequest{
Id: "trdp_one",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestUpdateResource/UnknownResourceRequest",
&attribute.TestUpdateResourceRequest{
Id: "unknown",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestUpdateResourceRequest{
Id: "unknown",
Item: &attribute.TestResource{
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestRequestNoItem",
&attribute.TestRequestNoItem{
Name: "test",
},
&attribute.TestRequestNoItem{
Name: "test",
},
},
{
"TestRequestItemNotMessage",
&attribute.TestRequestItemNotMessage{
Item: "test",
},
&attribute.TestRequestItemNotMessage{
Item: "test",
},
},
{
"TestRequestItemNoType",
&attribute.TestRequestItemNoType{
Item: &attribute.TestItemNoType{
Id: "test",
},
},
&attribute.TestRequestItemNoType{
Item: &attribute.TestItemNoType{
Id: "test",
},
},
},
{
"TestNoItemAttributes/SubResourceRequest",
&attribute.TestNoItemAttributes{
Id: "trsr_one",
Attrs: &attribute.TestNoItemAttributes_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
&attribute.TestNoItemAttributes{
Id: "trsr_one",
Attrs: &attribute.TestNoItemAttributes_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
{
"TestCreateNoOneOfRequest",
&attribute.TestCreateNoOneOfRequest{
Item: &attribute.TestNoOneOf{
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
&attribute.TestCreateNoOneOfRequest{
Item: &attribute.TestNoOneOf{
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
{
"TestUpdateNoOneOfRequest",
&attribute.TestUpdateNoOneOfRequest{
Id: "trsr_one",
Item: &attribute.TestNoOneOf{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
&attribute.TestUpdateNoOneOfRequest{
Id: "trsr_one",
Item: &attribute.TestNoOneOf{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := transformRequestAttributes(tc.req)
require.NoError(t, err)
assert.Empty(t, cmp.Diff(got, tc.expected, protocmp.Transform()))
})
}
}
func TestTransformResponseAttributes(t *testing.T) {
cases := []struct {
name string
resp proto.Message
expected proto.Message
}{
{
"TestListResourceResponse",
&attribute.TestListResourceResponse{
Items: []*attribute.TestResource{
{
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
{
Type: "default",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
&attribute.TestListResourceResponse{
Items: []*attribute.TestResource{
{
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
{
Type: "default",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
},
{
"TestGetResourceResponse/SubResourceAttributes",
&attribute.TestGetResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestGetResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestGetResourceResponse/PluginAttributes",
&attribute.TestGetResourceResponse{
Item: &attribute.TestResource{
Id: "trrp_one",
Type: "plugin",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
&attribute.TestGetResourceResponse{
Item: &attribute.TestResource{
Id: "trrp_one",
Type: "plugin",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestCreateResourceResponse",
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestUpdateResourceResponse",
&attribute.TestUpdateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
},
&attribute.TestUpdateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
},
{
"TestResponseNoItem",
&attribute.TestResponseNoItem{
Name: "test",
},
&attribute.TestResponseNoItem{
Name: "test",
},
},
{
"TestResponseItemNotMessage",
&attribute.TestResponseItemNotMessage{
Item: "test",
},
&attribute.TestResponseItemNotMessage{
Item: "test",
},
},
{
"TestResponseItemNoType",
&attribute.TestResponseItemNoType{
Item: &attribute.TestItemNoType{
Id: "test",
},
},
&attribute.TestResponseItemNoType{
Item: &attribute.TestItemNoType{
Id: "test",
},
},
},
{
"TestCreateNoOneOfResponse",
&attribute.TestCreateNoOneOfResponse{
Item: &attribute.TestNoOneOf{
Id: "trsr_one",
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
&attribute.TestCreateNoOneOfResponse{
Item: &attribute.TestNoOneOf{
Id: "trsr_one",
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
{
"TestUpdateNoOneOfResponse",
&attribute.TestUpdateNoOneOfResponse{
Item: &attribute.TestNoOneOf{
Id: "trsr_one",
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
&attribute.TestUpdateNoOneOfResponse{
Item: &attribute.TestNoOneOf{
Id: "trsr_one",
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
},
{
"TestCreateResourceOneofUnset",
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
},
},
&attribute.TestCreateResourceResponse{
Item: &attribute.TestResource{
Id: "trsr_one",
Type: "sub_resource",
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := transformResponseAttributes(tc.resp)
require.NoError(t, err)
assert.Empty(t, cmp.Diff(got, tc.expected, protocmp.Transform()))
})
}
}

@ -0,0 +1,73 @@
package subtypes
import (
"fmt"
"sync"
"github.com/hashicorp/boundary/sdk/pbs/controller/protooptions"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/reflect/protoregistry"
"google.golang.org/protobuf/types/descriptorpb"
)
func init() {
globalSourceRegistry = sourceRegistry{
m: make(map[protoreflect.FullName]protoreflect.FieldDescriptor),
}
protoregistry.GlobalTypes.RangeMessages(func(m protoreflect.MessageType) bool {
d := m.Descriptor()
if err := globalSourceRegistry.register(d); err != nil {
panic(err)
}
return true
})
}
var globalSourceRegistry sourceRegistry
// sourceRegistry is collection of proto messages with a corresponding
// field descriptor that can be used to determine the Subtype for the message.
type sourceRegistry struct {
sync.RWMutex
m map[protoreflect.FullName]protoreflect.FieldDescriptor
}
func (s sourceRegistry) register(d protoreflect.MessageDescriptor) error {
s.Lock()
defer s.Unlock()
fn := d.FullName()
if _, present := s.m[fn]; present {
return fmt.Errorf("proto message %s already registered", fn)
}
fields := d.Fields()
for i := 0; i < fields.Len(); i++ {
f := fields.Get(i)
opts := f.Options().(*descriptorpb.FieldOptions)
isSourceId := proto.GetExtension(opts, protooptions.E_SubtypeSourceId).(bool)
if isSourceId {
s.m[fn] = f
}
}
return nil
}
func (s sourceRegistry) get(d protoreflect.MessageDescriptor) protoreflect.FieldDescriptor {
s.RLock()
defer s.RUnlock()
return s.m[d.FullName()]
}
// sourceIdFieldDescriptor is used by the AttributeTransformInterceptor to retrieve
// a proto FieldDescriptor of an id field that can be used to determine
// the given Message's subtype.
func sourceIdFieldDescriptor(d protoreflect.MessageDescriptor) protoreflect.FieldDescriptor {
return globalSourceRegistry.get(d)
}
Loading…
Cancel
Save