diff --git a/encoding/protobuf/actor_uuid.go b/encoding/protobuf/actor_uuid.go new file mode 100644 index 000000000..6c2d754da --- /dev/null +++ b/encoding/protobuf/actor_uuid.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package protobuf + +import ( + "context" + + "go.uber.org/yarpc/yarpcerrors" +) + +// ActorUUIDValidator validates the actor UUIDs extracted from a protobuf +// request's uber.auth.annotations.actor_uuid-annotated fields. It is +// invoked by generated server code after the request is decoded but +// before the user's handler runs. A non-nil error short-circuits the +// request and is returned to the caller as the handler's response error. +// +// The slice mirrors what the generated ActorUUID() accessor returns: one +// entry per annotated field reachable from the request, in declaration +// order, with container fields (repeated / map) contributing every +// element or value they hold. Callers that want to support unannotated +// handlers should treat an empty slice (or empty strings) as "not +// provided" rather than as a failure. +// +// Mirrors the ActorUUIDValidator handled by thriftrw-plugin-yarpc (see +// encoding/thrift/options.go), adapted to protobuf's []string accessor. +type ActorUUIDValidator func(ctx context.Context, actorUUIDs []string) error + +// registerConfig holds the settings collected from RegisterOptions. +type registerConfig struct { + ActorUUIDValidator ActorUUIDValidator +} + +// RegisterOption customizes the behavior of a protobuf handler during +// registration. Unlike ClientOption it applies to the server side only. +type RegisterOption interface { + applyRegisterOption(*registerConfig) +} + +type actorUUIDValidatorOption struct{ validator ActorUUIDValidator } + +func (a actorUUIDValidatorOption) applyRegisterOption(c *registerConfig) { + c.ActorUUIDValidator = a.validator +} + +// WithActorUUIDValidator returns a RegisterOption that installs the given +// ActorUUIDValidator on the generated server. When unset, generated +// handlers skip the validator call entirely, so existing services keep +// their pre-validator behaviour. +// +// procedures := examplepb.BuildExampleYARPCProcedures( +// handler, protobuf.WithActorUUIDValidator(validator)) +func WithActorUUIDValidator(v ActorUUIDValidator) RegisterOption { + return actorUUIDValidatorOption{validator: v} +} + +// ActorUUIDValidatorFromOptions returns the ActorUUIDValidator installed +// by WithActorUUIDValidator in the given slice of RegisterOptions, or nil +// if none was installed. It exists so code generated by +// protoc-gen-yarpc-go can pull the validator out of the user-supplied +// options; user code should not need to call it directly. +func ActorUUIDValidatorFromOptions(opts []RegisterOption) ActorUUIDValidator { + var c registerConfig + for _, o := range opts { + o.applyRegisterOption(&c) + } + return c.ActorUUIDValidator +} + +// ValidateActorUUID runs validator against actorUUIDs when validator is +// non-nil, wrapping a rejection in a PermissionDenied YARPC error that +// names the service and procedure while preserving the original error in +// the errors.Is chain (via %w). A nil validator is a no-op, keeping +// services registered without WithActorUUIDValidator backwards +// compatible. +// +// It is called by code generated by protoc-gen-yarpc-go; user code should +// not need to call it directly. +func ValidateActorUUID(ctx context.Context, validator ActorUUIDValidator, actorUUIDs []string, serviceName string, methodName string) error { + if validator == nil { + return nil + } + if err := validator(ctx, actorUUIDs); err != nil { + return yarpcerrors.PermissionDeniedErrorf( + "actor UUID validation failed for service %q procedure %q: %w", serviceName, methodName, err) + } + return nil +} diff --git a/encoding/protobuf/actor_uuid_test.go b/encoding/protobuf/actor_uuid_test.go new file mode 100644 index 000000000..adc113a0e --- /dev/null +++ b/encoding/protobuf/actor_uuid_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2026 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package protobuf + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/yarpc/yarpcerrors" +) + +func TestActorUUIDValidatorFromOptions(t *testing.T) { + t.Run("none installed returns nil", func(t *testing.T) { + assert.Nil(t, ActorUUIDValidatorFromOptions(nil)) + assert.Nil(t, ActorUUIDValidatorFromOptions([]RegisterOption{})) + }) + + t.Run("returns the installed validator", func(t *testing.T) { + var got []string + validator := func(_ context.Context, uuids []string) error { + got = uuids + return nil + } + + out := ActorUUIDValidatorFromOptions([]RegisterOption{WithActorUUIDValidator(validator)}) + require.NotNil(t, out, "the installed validator must be returned") + + require.NoError(t, out(context.Background(), []string{"alice"})) + assert.Equal(t, []string{"alice"}, got, "the returned func must be the one we installed") + }) + + t.Run("last option wins", func(t *testing.T) { + first := func(context.Context, []string) error { return errors.New("first") } + second := func(context.Context, []string) error { return errors.New("second") } + + out := ActorUUIDValidatorFromOptions([]RegisterOption{ + WithActorUUIDValidator(first), + WithActorUUIDValidator(second), + }) + require.NotNil(t, out) + assert.EqualError(t, out(context.Background(), nil), "second") + }) +} + +func TestValidateActorUUID(t *testing.T) { + t.Run("nil validator is a no-op", func(t *testing.T) { + assert.NoError(t, ValidateActorUUID(context.Background(), nil, []string{"alice"}, "svc", "Method")) + }) + + t.Run("validator receives ctx and uuids and its nil result passes", func(t *testing.T) { + type ctxKey struct{} + ctx := context.WithValue(context.Background(), ctxKey{}, "v") + + var gotUUIDs []string + var gotCtxValue interface{} + validator := func(c context.Context, uuids []string) error { + gotCtxValue = c.Value(ctxKey{}) + gotUUIDs = uuids + return nil + } + + require.NoError(t, ValidateActorUUID(ctx, validator, []string{"alice", "bob"}, "svc", "Method")) + assert.Equal(t, []string{"alice", "bob"}, gotUUIDs) + assert.Equal(t, "v", gotCtxValue, "the caller's context must be threaded through") + }) + + t.Run("rejection is wrapped as PermissionDenied naming service and procedure", func(t *testing.T) { + denied := errors.New("validator denied") + validator := func(context.Context, []string) error { return denied } + + err := ValidateActorUUID(context.Background(), validator, []string{"alice"}, "uber.example.UserService", "DeleteUser") + require.Error(t, err) + + assert.Equal(t, yarpcerrors.CodePermissionDenied, yarpcerrors.FromError(err).Code()) + assert.ErrorIs(t, err, denied, "the original error must stay in the errors.Is chain") + msg := err.Error() + assert.Contains(t, msg, "uber.example.UserService") + assert.Contains(t, msg, "DeleteUser") + }) +} diff --git a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/lib.go b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/lib.go index 9bdfb138f..a7fab7eb3 100644 --- a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/lib.go +++ b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/lib.go @@ -35,7 +35,9 @@ import ( "go.uber.org/yarpc/internal/protoplugin" ) -const tmpl = `{{$packagePath := .GoPackage.Path}}{{$packageName := .GoPackage.Name}} +// tmpl is the text/template that renders a .proto file's generated +// .pb.yarpc.go: its clients, server handlers, and Fx providers. +const tmpl = `{{$packagePath := .GoPackage.Path}}{{$packageName := .GoPackage.Name}}{{$info := .}} // Code generated by protoc-gen-yarpc-go. DO NOT EDIT. // source: {{.GetName}} @@ -142,10 +144,11 @@ type {{$service.GetName}}Service{{$method.GetName}}YARPCServer interface { type build{{$service.GetName}}YARPCProceduresParams struct { Server {{$service.GetName}}YARPCServer AnyResolver jsonpb.AnyResolver -} +{{if serviceHasActorUUID $info $service}} ActorUUIDValidator protobuf.ActorUUIDValidator +{{end}}} func build{{$service.GetName}}YARPCProcedures(params build{{$service.GetName}}YARPCProceduresParams) []transport.Procedure { - handler := &_{{$service.GetName}}YARPCHandler{params.Server} + {{if serviceHasActorUUID $info $service}}handler := &_{{$service.GetName}}YARPCHandler{server: params.Server, actorUUIDValidator: params.ActorUUIDValidator}{{else}}handler := &_{{$service.GetName}}YARPCHandler{params.Server}{{end}} return protobuf.BuildProcedures( protobuf.BuildProceduresParams{ ServiceName: "{{trimPrefixPeriod $service.FQSN}}", @@ -208,8 +211,8 @@ func build{{$service.GetName}}YARPCProcedures(params build{{$service.GetName}}YA } // Build{{$service.GetName}}YARPCProcedures prepares an implementation of the {{$service.GetName}} service for YARPC registration. -func Build{{$service.GetName}}YARPCProcedures(server {{$service.GetName}}YARPCServer) []transport.Procedure { - return build{{$service.GetName}}YARPCProcedures(build{{$service.GetName}}YARPCProceduresParams{Server:server}) +func Build{{$service.GetName}}YARPCProcedures(server {{$service.GetName}}YARPCServer{{if serviceHasActorUUID $info $service}}, options ...protobuf.RegisterOption{{end}}) []transport.Procedure { + return build{{$service.GetName}}YARPCProcedures(build{{$service.GetName}}YARPCProceduresParams{Server:server{{if serviceHasActorUUID $info $service}}, ActorUUIDValidator: protobuf.ActorUUIDValidatorFromOptions(options){{end}}}) } // Fx{{$service.GetName}}YARPCClientParams defines the input @@ -271,7 +274,8 @@ type Fx{{$service.GetName}}YARPCProceduresParams struct { Server {{$service.GetName}}YARPCServer AnyResolver jsonpb.AnyResolver ` + "`" + `name:"yarpcfx" optional:"true"` + "`" + ` -} +{{if serviceHasActorUUID $info $service}} ActorUUIDValidator protobuf.ActorUUIDValidator ` + "`" + `optional:"true"` + "`" + ` +{{end}}} // Fx{{$service.GetName}}YARPCProceduresResult defines the output // of NewFx{{$service.GetName}}YARPCProcedures. It provides @@ -299,7 +303,8 @@ func NewFx{{$service.GetName}}YARPCProcedures() interface{} { Procedures: build{{$service.GetName}}YARPCProcedures(build{{$service.GetName}}YARPCProceduresParams{ Server: params.Server, AnyResolver: params.AnyResolver, - }), +{{if serviceHasActorUUID $info $service}} ActorUUIDValidator: params.ActorUUIDValidator, +{{end}} }), ReflectionMeta: {{$service.GetName}}ReflectionMeta, } } @@ -369,7 +374,8 @@ func (c *_{{$service.GetName}}YARPCCaller) {{$method.GetName}}(ctx context.Conte type _{{$service.GetName}}YARPCHandler struct { server {{$service.GetName}}YARPCServer -} +{{if serviceHasActorUUID $info $service}} actorUUIDValidator protobuf.ActorUUIDValidator +{{end}}} {{range $method := unaryMethods $service}} func (h *_{{$service.GetName}}YARPCHandler) {{$method.GetName}}(ctx context.Context, requestMessage proto.Message) (proto.Message, error) { @@ -381,7 +387,10 @@ func (h *_{{$service.GetName}}YARPCHandler) {{$method.GetName}}(ctx context.Cont return nil, protobuf.CastError(empty{{$service.GetName}}Service{{$method.GetName}}YARPCRequest, requestMessage) } } - response, err := h.server.{{$method.GetName}}(ctx, request) +{{if methodHasActorUUID $info $method}} if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "{{trimPrefixPeriod $service.FQSN}}", "{{$method.GetName}}"); err != nil { + return nil, err + } +{{end}} response, err := h.server.{{$method.GetName}}(ctx, request) if response == nil { return nil, err } @@ -398,7 +407,10 @@ func (h *_{{$service.GetName}}YARPCHandler) {{$method.GetName}}(ctx context.Cont return protobuf.CastError(empty{{$service.GetName}}Service{{$method.GetName}}YARPCRequest, requestMessage) } } - return h.server.{{$method.GetName}}(ctx, request) +{{if methodHasActorUUID $info $method}} if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "{{trimPrefixPeriod $service.FQSN}}", "{{$method.GetName}}"); err != nil { + return err + } +{{end}} return h.server.{{$method.GetName}}(ctx, request) } {{end}} {{range $method := clientStreamingMethods $service}} @@ -421,7 +433,10 @@ func (h *_{{$service.GetName}}YARPCHandler) {{$method.GetName}}(serverStream *pr if !ok { return protobuf.CastError(empty{{$service.GetName}}Service{{$method.GetName}}YARPCRequest, requestMessage) } - return h.server.{{$method.GetName}}(request, &_{{$service.GetName}}Service{{$method.GetName}}YARPCServer{serverStream: serverStream}) +{{if methodHasActorUUID $info $method}} if err := protobuf.ValidateActorUUID(serverStream.Context(), h.actorUUIDValidator, request.ActorUUID(), "{{trimPrefixPeriod $service.FQSN}}", "{{$method.GetName}}"); err != nil { + return err + } +{{end}} return h.server.{{$method.GetName}}(request, &_{{$service.GetName}}Service{{$method.GetName}}YARPCServer{serverStream: serverStream}) } {{end}} {{range $method := clientServerStreamingMethods $service}} @@ -652,6 +667,8 @@ var Runner = protoplugin.NewRunner( "fileDescriptorClosureVarName": fileDescriptorClosureVarName, "trimPrefixPeriod": trimPrefixPeriod, "actorUUIDMethods": actorUUIDMethods, + "serviceHasActorUUID": serviceHasActorUUID, + "methodHasActorUUID": methodHasActorUUID, }).Parse(tmpl)), nil, []string{ diff --git a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/runner_test.go b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/runner_test.go index 6379a64e0..562ac3072 100644 --- a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/runner_test.go +++ b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/runner_test.go @@ -63,6 +63,65 @@ func TestRunnerEmitsActorUUIDAccessor(t *testing.T) { "sanity check: existing client/server emission still works") } +// TestRunnerWiresActorUUIDValidator asserts the server template injects the +// ActorUUID validator plumbing for a service with an annotated request +// type: the handler carries a validator field, Build/registration accept +// protobuf.RegisterOptions, and the per-method handler calls +// protobuf.ValidateActorUUID with the request's ActorUUID() accessor. +func TestRunnerWiresActorUUIDValidator(t *testing.T) { + req := &plugin_go.CodeGeneratorRequest{ + FileToGenerate: []string{"svc/foo.proto"}, + ProtoFile: []*descriptor.FileDescriptorProto{ + optionsFileDescriptor(), + targetFileDescriptor(t), + }, + } + + resp := Runner.Run(req) + require.Nil(t, resp.Error, "plugin returned error: %v", resp.GetError()) + require.Len(t, resp.File, 1) + + out := resp.File[0].GetContent() + assert.Contains(t, out, "actorUUIDValidator protobuf.ActorUUIDValidator", + "handler struct must carry a validator field; output was:\n%s", out) + assert.Contains(t, out, "func BuildUserServiceYARPCProcedures(server UserServiceYARPCServer, options ...protobuf.RegisterOption)", + "Build entry point must accept register options; output was:\n%s", out) + assert.Contains(t, out, "protobuf.ActorUUIDValidatorFromOptions(options)", + "Build entry point must extract the validator from options; output was:\n%s", out) + assert.Contains(t, out, `protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "svc.UserService", "DeleteUser")`, + "the annotated method must call the validator with its ActorUUID(); output was:\n%s", out) +} + +// TestRunnerSkipsValidatorForUnannotatedService asserts that a service +// whose request types carry no annotation keeps its original, option-free +// signature and gets no validator plumbing, preserving backwards +// compatibility. +func TestRunnerSkipsValidatorForUnannotatedService(t *testing.T) { + target := targetFileDescriptor(t) + // Drop the annotation from the request's first field. + target.MessageType[0].Field[0].Options = nil + + req := &plugin_go.CodeGeneratorRequest{ + FileToGenerate: []string{"svc/foo.proto"}, + ProtoFile: []*descriptor.FileDescriptorProto{ + optionsFileDescriptor(), + target, + }, + } + + resp := Runner.Run(req) + require.Nil(t, resp.Error, "plugin returned error: %v", resp.GetError()) + require.Len(t, resp.File, 1) + + out := resp.File[0].GetContent() + assert.NotContains(t, out, "actorUUIDValidator", + "an unannotated service must not get validator plumbing; output was:\n%s", out) + assert.NotContains(t, out, "ValidateActorUUID", + "an unannotated service must not call the validator; output was:\n%s", out) + assert.Contains(t, out, "func BuildUserServiceYARPCProcedures(server UserServiceYARPCServer) []transport.Procedure", + "an unannotated service keeps its original option-free signature; output was:\n%s", out) +} + // TestRunnerCollectsEveryAnnotationOnMultiplyAnnotatedRequest asserts the // "collect all" rule: a request type with two actor_uuid-annotated fields // generates exactly one ActorUUID() accessor whose []string body returns diff --git a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid.go b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid.go index d8e83b75a..9abc5d9fd 100644 --- a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid.go +++ b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid.go @@ -81,12 +81,22 @@ type actorUUIDMethod struct { } // uuidFileInfo caches the per-file UUID discovery results so that -// findActorUUIDFieldNumber and newUUIDContext are each computed at most -// once per file generation, regardless of how many times the template -// invokes UUID-related helpers for the same file. +// findActorUUIDFieldNumber, newUUIDContext, and the per-message +// convert-and-walk are each computed at most once per file generation, +// regardless of how many times the template invokes UUID-related +// helpers for the same file. type uuidFileInfo struct { - num int32 // 0 means the annotation is not in scope - ctx *uuidContext // nil when num == 0 + num int32 // 0 means the annotation is not in scope + ctx *uuidContext // nil when num == 0 + methods []*actorUUIDMethod + serviceHasUUID map[*protoplugin.Service]bool + methodHasUUID map[*protoplugin.Method]bool + // err records a failure of the file's UUID analysis - a message + // exceeding _maxActorUUIDPaths, or an annotation-read + // failure (see hasActorUUID) such as an extension registry + // collision; actorUUIDMethods surfaces it to fail the file's + // generation. + err error } var ( @@ -94,25 +104,25 @@ var ( uuidFileInfoCache = map[*protoplugin.File]*uuidFileInfo{} ) +// getUUIDFileInfo returns the cached UUID analysis for file, computing +// it on first access. func getUUIDFileInfo(file *protoplugin.File) *uuidFileInfo { uuidFileInfoMu.Lock() defer uuidFileInfoMu.Unlock() if info, ok := uuidFileInfoCache[file]; ok { return info } - num := findActorUUIDFieldNumber(file) - var ctx *uuidContext - if num != 0 { - ctx = newUUIDContext(file) - } - info := &uuidFileInfo{num: num, ctx: ctx} + info := buildUUIDFileInfo(file) uuidFileInfoCache[file] = info return info } -// actorUUIDMethods returns one ActorUUID() emission per message declared -// in the target file that has at least one path to an actor_uuid-annotated -// string leaf. +// buildUUIDFileInfo runs the file's UUID analysis once: it locates the +// actor_uuid extension, computes one ActorUUID() emission per message +// declared in the file that has at least one path to an annotated string +// leaf, and records which services and methods have a request type that +// reaches such a leaf (the server template gates validator wiring on +// those). // // Emission is keyed on declaration, not on service usage. Go only allows // a method on a type from the type's declaring package, and the declaring @@ -132,21 +142,20 @@ func getUUIDFileInfo(file *protoplugin.File) *uuidFileInfo { // // Synthetic map-entry messages are skipped: they appear in the file's // message list but have no generated Go struct to carry a method. -// -// Returns nil (and no error) when the target file's import graph does not -// reach the option's declaration, which is the common case and means the -// plugin should not emit any accessors. -func actorUUIDMethods(info *protoplugin.TemplateInfo) ([]*actorUUIDMethod, error) { - fi := getUUIDFileInfo(info.File) - if fi.num == 0 { - return nil, nil - } - pkgPath := info.File.GoPackage.Path - conv := newUUIDConverter(fi.num, fi.ctx) - var out []*actorUUIDMethod - for _, msg := range info.File.Messages { - if msg.GetOptions().GetMapEntry() { - continue +func buildUUIDFileInfo(file *protoplugin.File) *uuidFileInfo { + num := findActorUUIDFieldNumber(file) + if num == 0 { + return &uuidFileInfo{} + } + ctx := newUUIDContext(file) + pkgPath := file.GoPackage.Path + serviceHas := make(map[*protoplugin.Service]bool) + methodHas := make(map[*protoplugin.Method]bool) + conv := newUUIDConverter(num, ctx) + seen := map[*protoplugin.Message][]*protogen.Path{} + walk := func(msg *protoplugin.Message) ([]*protogen.Path, error) { + if paths, ok := seen[msg]; ok { + return paths, nil } node, err := conv.convert(msg) if err != nil { @@ -159,12 +168,86 @@ func actorUUIDMethods(info *protoplugin.TemplateInfo) ([]*actorUUIDMethod, error "restructure the message to reduce shared sub-message fan-out", msg.GetName(), _maxActorUUIDPaths) } - if len(paths) == 0 { + seen[msg] = paths + return paths, nil + } + + var methods []*actorUUIDMethod + for _, msg := range file.Messages { + if msg.GetOptions().GetMapEntry() { continue } - out = append(out, newActorUUIDMethod(msg.GoType(pkgPath), paths)) + paths, err := walk(msg) + if err != nil { + return &uuidFileInfo{num: num, ctx: ctx, err: err} + } + if len(paths) > 0 { + methods = append(methods, newActorUUIDMethod(msg.GoType(pkgPath), paths)) + } } - return out, nil + + // A request type declared in another file gets no accessor here (its + // declaring file emits it) but still drives the validator gating for + // this file's services; walk memoizes messages shared with the + // emission loop above. + for _, svc := range file.Services { + for _, m := range svc.Methods { + req := m.RequestType + if req == nil { + continue + } + paths, err := walk(req) + if err != nil { + return &uuidFileInfo{num: num, ctx: ctx, err: err} + } + hasUUID := len(paths) > 0 + methodHas[m] = hasUUID + if hasUUID { + serviceHas[svc] = true + } + } + } + return &uuidFileInfo{ + num: num, + ctx: ctx, + methods: methods, + serviceHasUUID: serviceHas, + methodHasUUID: methodHas, + } +} + +// actorUUIDMethods returns one ActorUUID() emission per message declared +// in the target file that has at least one path to an actor_uuid-annotated +// string leaf; see buildUUIDFileInfo for why emission is keyed on +// declaration rather than service usage. +// +// Returns nil (and no error) when the target file's import graph does not +// reach the option's declaration, which is the common case and means the +// plugin should not emit any accessors. +func actorUUIDMethods(info *protoplugin.TemplateInfo) ([]*actorUUIDMethod, error) { + fi := getUUIDFileInfo(info.File) + if fi.err != nil { + return nil, fi.err + } + return fi.methods, nil +} + +// serviceHasActorUUID reports whether any method on the service takes a request +// type with an actor_uuid annotation (including nested fields). Services that +// do get validator wiring; services where no RPC request is annotated do not. +func serviceHasActorUUID(info *protoplugin.TemplateInfo, service *protoplugin.Service) bool { + fi := getUUIDFileInfo(info.File) + return fi.serviceHasUUID[service] +} + +// methodHasActorUUID reports whether the given method's request type +// reaches at least one actor_uuid-annotated leaf. The server template +// uses it to gate the per-method validator call (which invokes +// request.ActorUUID()), so it must stay in lockstep with the accessor +// emission in actorUUIDMethods. +func methodHasActorUUID(info *protoplugin.TemplateInfo, method *protoplugin.Method) bool { + fi := getUUIDFileInfo(info.File) + return fi.methodHasUUID[method] } // newActorUUIDMethod lowers the given non-empty set of paths into the diff --git a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid_test.go b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid_test.go index 484336831..5e4f77869 100644 --- a/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid_test.go +++ b/encoding/protobuf/protoc-gen-yarpc-go/internal/lib/uuid_test.go @@ -697,6 +697,64 @@ func TestActorUUIDMethods(t *testing.T) { }) } +// TestServiceAndMethodHasActorUUID exercises the server-template gating +// helpers: methodHasActorUUID reports per-method whether the request type +// reaches an annotation, and serviceHasActorUUID is the service-wide OR of +// that over every method. Both must stay in lockstep with actorUUIDMethods +// so the generated validator call (request.ActorUUID()) only appears where +// the accessor exists. +func TestServiceAndMethodHasActorUUID(t *testing.T) { + annotatedReq := newMessage(t, "DeleteUserRequest", stringField(t, "actor", true)) + plainReq := newMessage(t, "PingRequest", stringField(t, "token", false)) + resp := newMessage(t, "Resp", stringField(t, "ok", false)) + + deleteMethod := method(t, "DeleteUser", annotatedReq, resp) + pingMethod := method(t, "Ping", plainReq, resp) + + info := newTemplateInfoWithServices(t, + []*protoplugin.Message{annotatedReq, plainReq, resp}, + svc(t, "UserService", deleteMethod, pingMethod), + ) + + t.Run("methodHasActorUUID_true_for_annotated_request", func(t *testing.T) { + assert.True(t, methodHasActorUUID(info, deleteMethod)) + }) + + t.Run("methodHasActorUUID_false_for_unannotated_request", func(t *testing.T) { + assert.False(t, methodHasActorUUID(info, pingMethod)) + }) + + t.Run("serviceHasActorUUID_true_when_any_method_annotated", func(t *testing.T) { + assert.True(t, serviceHasActorUUID(info, info.File.Services[0])) + }) + + t.Run("serviceHasActorUUID_false_when_no_method_annotated", func(t *testing.T) { + plainOnly := newTemplateInfoWithServices(t, + []*protoplugin.Message{plainReq, resp}, + svc(t, "PingService", method(t, "Ping", plainReq, resp)), + ) + assert.False(t, serviceHasActorUUID(plainOnly, plainOnly.File.Services[0])) + }) + + t.Run("false_when_extension_not_in_scope", func(t *testing.T) { + target := &protoplugin.File{ + FileDescriptorProto: &descriptor.FileDescriptorProto{ + Name: proto.String("svc/foo.proto"), + Package: proto.String("svc"), + }, + GoPackage: &protoplugin.GoPackage{Path: "svc/foopb"}, + } + annotatedReq.File = target + s := svc(t, "S", method(t, "M", annotatedReq, resp)) + s.File = target + target.Services = []*protoplugin.Service{s} + noExt := &protoplugin.TemplateInfo{File: target} + assert.False(t, serviceHasActorUUID(noExt, s), + "without the extension in scope there is nothing to validate") + assert.False(t, methodHasActorUUID(noExt, s.Methods[0])) + }) +} + // --- helpers -------------------------------------------------------------- // newOptionsFile builds a synthetic protoplugin.File that mirrors the shape @@ -909,7 +967,7 @@ func annotatedMessageField(t *testing.T, name string, target *protoplugin.Messag return f } -// messageFQMN returns the FQMN that walkForUUID's lookup expects: +// messageFQMN returns the FQMN that the uuidConverter's lookup expects: // ".pkg.MessageName" for a top-level message in the synthetic test // package "svc". func messageFQMN(m *protoplugin.Message) string { diff --git a/encoding/protobuf/protoc-gen-yarpc-go/internal/tests/withuuid/validator_test.go b/encoding/protobuf/protoc-gen-yarpc-go/internal/tests/withuuid/validator_test.go new file mode 100644 index 000000000..4323819ea --- /dev/null +++ b/encoding/protobuf/protoc-gen-yarpc-go/internal/tests/withuuid/validator_test.go @@ -0,0 +1,230 @@ +// Copyright (c) 2026 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// End-to-end test for the ActorUUID validator wiring protoc-gen-yarpc-go +// injects into generated servers. Mirrors +// encoding/thrift/thriftrw-plugin-yarpc/validator_test.go, adapted to +// protobuf's generated handlers and []string accessor. + +package withuuid + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/yarpc/api/transport" + "go.uber.org/yarpc/api/transport/transporttest" + "go.uber.org/yarpc/encoding/protobuf" + "go.uber.org/yarpc/yarpcerrors" +) + +// recordingServer is a minimal UserServiceYARPCServer that records +// whether the unary handler under test was reached and what actor it +// observed. Only the methods exercised below carry behaviour; the rest +// satisfy the interface. +type recordingServer struct { + called bool + gotActor string + gotCaller string +} + +func (s *recordingServer) DeleteUser(_ context.Context, req *DeleteUserRequest) (*DeleteUserResponse, error) { + s.called = true + s.gotActor = req.GetActor() + return &DeleteUserResponse{Ok: true}, nil +} + +func (s *recordingServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) { + return &GetUserResponse{}, nil +} +func (s *recordingServer) Ping(context.Context, *UnannotatedRequest) (*UnannotatedResponse, error) { + s.called = true + return &UnannotatedResponse{Ok: true}, nil +} +func (s *recordingServer) CredentialedAction(context.Context, *NestedRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) CycleAction(context.Context, *CycleRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) MultipleAction(context.Context, *MultiAnnotatedRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) RepeatedActorsAction(context.Context, *RepeatedActorsRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) MapActorsAction(context.Context, *MapActorsRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) RepeatedMessageAction(context.Context, *RepeatedMessageRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) MapMessageAction(context.Context, *MapMessageRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) IgnoredAction(context.Context, *IgnoredAnnotationsRequest) (*DeleteUserResponse, error) { + return &DeleteUserResponse{}, nil +} +func (s *recordingServer) ListUsers(*ListUsersRequest, UserServiceServiceListUsersYARPCServer) error { + return nil +} + +// unaryProcedure finds the proto-encoding unary procedure for the given +// method name out of the slice BuildUserServiceYARPCProcedures returns +// (each method emits both a proto and a JSON procedure). +func unaryProcedure(t *testing.T, procedures []transport.Procedure, methodName string) transport.Procedure { + t.Helper() + for _, p := range procedures { + if p.Encoding != protobuf.Encoding || p.HandlerSpec.Type() != transport.Unary { + continue + } + if bytes.HasSuffix([]byte(p.Name), []byte(methodName)) { + return p + } + } + t.Fatalf("unary proto procedure for method %q not found", methodName) + return transport.Procedure{} +} + +// driveDeleteUser builds a server with the given options, marshals req, +// and runs the DeleteUser unary handler, returning any handler error. +func driveDeleteUser(t *testing.T, impl UserServiceYARPCServer, req *DeleteUserRequest, opts ...protobuf.RegisterOption) error { + t.Helper() + procedures := BuildUserServiceYARPCProcedures(impl, opts...) + proc := unaryProcedure(t, procedures, "DeleteUser") + + body, err := proto.Marshal(req) + require.NoError(t, err) + + return proc.HandlerSpec.Unary().Handle( + context.Background(), + &transport.Request{ + Caller: "caller-test", + Service: "callee-test", + Procedure: proc.Name, + Encoding: protobuf.Encoding, + Body: bytes.NewReader(body), + }, + &transporttest.FakeResponseWriter{}, + ) +} + +// TestActorUUIDValidator_NoValidatorBackwardCompat proves a server built +// without WithActorUUIDValidator still runs the user handler: the +// generated nil-guard short-circuits when no validator is installed. +func TestActorUUIDValidator_NoValidatorBackwardCompat(t *testing.T) { + impl := &recordingServer{} + err := driveDeleteUser(t, impl, &DeleteUserRequest{Actor: "alice"}) + require.NoError(t, err) + assert.True(t, impl.called, "user handler must run when no validator is installed") +} + +// TestActorUUIDValidator_Allow proves that when the validator returns nil +// the generated code falls through to the user handler and threads the +// decoded actor UUID slice through to the validator. +func TestActorUUIDValidator_Allow(t *testing.T) { + var seen []string + validator := func(_ context.Context, actorUUIDs []string) error { + seen = actorUUIDs + return nil + } + + impl := &recordingServer{} + err := driveDeleteUser(t, impl, &DeleteUserRequest{Actor: "alice"}, + protobuf.WithActorUUIDValidator(validator)) + require.NoError(t, err) + assert.True(t, impl.called, "user handler must run when validator returns nil") + assert.Equal(t, []string{"alice"}, seen, "validator should receive the decoded actor UUIDs") + assert.Equal(t, "alice", impl.gotActor, "user handler should still see the original request") +} + +// TestActorUUIDValidator_Deny proves that when the validator returns an +// error the generated handler short-circuits: the user handler is never +// called and the validator's error reaches the caller wrapped in a +// PermissionDenied YARPC error, with the original error preserved in the +// errors.Is chain (the generator wraps via %w). +func TestActorUUIDValidator_Deny(t *testing.T) { + denied := errors.New("validator denied") + validator := func(context.Context, []string) error { return denied } + + impl := &recordingServer{} + err := driveDeleteUser(t, impl, &DeleteUserRequest{Actor: "alice"}, + protobuf.WithActorUUIDValidator(validator)) + require.Error(t, err) + assert.ErrorIs(t, err, denied, "validator error should remain in the wrapped errors.Is chain") + assert.Equal(t, yarpcerrors.CodePermissionDenied, yarpcerrors.FromError(err).Code(), + "a rejected actor UUID should surface as PermissionDenied") + assert.False(t, impl.called, "user handler must NOT run when validator rejects") +} + +// TestActorUUIDValidator_EmptyActorUUID proves the validator still fires +// when the annotated field is unset: the generated accessor returns +// []string{""} and that is what the validator sees. Policy on whether +// empty is acceptable belongs to the validator, not the generated code. +func TestActorUUIDValidator_EmptyActorUUID(t *testing.T) { + var calls int + var seen []string + validator := func(_ context.Context, actorUUIDs []string) error { + calls++ + seen = actorUUIDs + return nil + } + + impl := &recordingServer{} + err := driveDeleteUser(t, impl, &DeleteUserRequest{}, protobuf.WithActorUUIDValidator(validator)) + require.NoError(t, err) + assert.Equal(t, 1, calls, "validator should still fire even with an empty actor UUID") + assert.Equal(t, []string{""}, seen, "an unset annotated field surfaces as the empty string") + assert.True(t, impl.called, "validator returning nil should let the handler run") +} + +// TestActorUUIDValidator_UnannotatedMethodSkipsValidator proves the +// generated code never calls the validator for a method whose request +// type carries no annotation (Ping / UnannotatedRequest): such handlers +// have no ActorUUID() accessor, so the validator gate is omitted +// entirely and the handler runs even when a validator is installed. +func TestActorUUIDValidator_UnannotatedMethodSkipsValidator(t *testing.T) { + var calls int + validator := func(context.Context, []string) error { calls++; return errors.New("should not run") } + + impl := &recordingServer{} + procedures := BuildUserServiceYARPCProcedures(impl, protobuf.WithActorUUIDValidator(validator)) + proc := unaryProcedure(t, procedures, "Ping") + + body, err := proto.Marshal(&UnannotatedRequest{Token: "t"}) + require.NoError(t, err) + err = proc.HandlerSpec.Unary().Handle( + context.Background(), + &transport.Request{ + Procedure: proc.Name, + Encoding: protobuf.Encoding, + Body: bytes.NewReader(body), + }, + &transporttest.FakeResponseWriter{}, + ) + require.NoError(t, err) + assert.Equal(t, 0, calls, "validator must not fire for an unannotated method") + assert.True(t, impl.called, "the unannotated method's handler must still run") +} diff --git a/encoding/protobuf/protoc-gen-yarpc-go/internal/tests/withuuid/withuuid.pb.yarpc.go b/encoding/protobuf/protoc-gen-yarpc-go/internal/tests/withuuid/withuuid.pb.yarpc.go index 5f8425ef7..6d9f2ab21 100644 --- a/encoding/protobuf/protoc-gen-yarpc-go/internal/tests/withuuid/withuuid.pb.yarpc.go +++ b/encoding/protobuf/protoc-gen-yarpc-go/internal/tests/withuuid/withuuid.pb.yarpc.go @@ -82,12 +82,13 @@ type UserServiceServiceListUsersYARPCServer interface { } type buildUserServiceYARPCProceduresParams struct { - Server UserServiceYARPCServer - AnyResolver jsonpb.AnyResolver + Server UserServiceYARPCServer + AnyResolver jsonpb.AnyResolver + ActorUUIDValidator protobuf.ActorUUIDValidator } func buildUserServiceYARPCProcedures(params buildUserServiceYARPCProceduresParams) []transport.Procedure { - handler := &_UserServiceYARPCHandler{params.Server} + handler := &_UserServiceYARPCHandler{server: params.Server, actorUUIDValidator: params.ActorUUIDValidator} return protobuf.BuildProcedures( protobuf.BuildProceduresParams{ ServiceName: "uber.yarpc.tests.protouuid.UserService", @@ -220,8 +221,8 @@ func buildUserServiceYARPCProcedures(params buildUserServiceYARPCProceduresParam } // BuildUserServiceYARPCProcedures prepares an implementation of the UserService service for YARPC registration. -func BuildUserServiceYARPCProcedures(server UserServiceYARPCServer) []transport.Procedure { - return buildUserServiceYARPCProcedures(buildUserServiceYARPCProceduresParams{Server: server}) +func BuildUserServiceYARPCProcedures(server UserServiceYARPCServer, options ...protobuf.RegisterOption) []transport.Procedure { + return buildUserServiceYARPCProcedures(buildUserServiceYARPCProceduresParams{Server: server, ActorUUIDValidator: protobuf.ActorUUIDValidatorFromOptions(options)}) } // FxUserServiceYARPCClientParams defines the input @@ -281,8 +282,9 @@ func NewFxUserServiceYARPCClient(name string, options ...protobuf.ClientOption) type FxUserServiceYARPCProceduresParams struct { fx.In - Server UserServiceYARPCServer - AnyResolver jsonpb.AnyResolver `name:"yarpcfx" optional:"true"` + Server UserServiceYARPCServer + AnyResolver jsonpb.AnyResolver `name:"yarpcfx" optional:"true"` + ActorUUIDValidator protobuf.ActorUUIDValidator `optional:"true"` } // FxUserServiceYARPCProceduresResult defines the output @@ -309,8 +311,9 @@ func NewFxUserServiceYARPCProcedures() interface{} { return func(params FxUserServiceYARPCProceduresParams) FxUserServiceYARPCProceduresResult { return FxUserServiceYARPCProceduresResult{ Procedures: buildUserServiceYARPCProcedures(buildUserServiceYARPCProceduresParams{ - Server: params.Server, - AnyResolver: params.AnyResolver, + Server: params.Server, + AnyResolver: params.AnyResolver, + ActorUUIDValidator: params.ActorUUIDValidator, }), ReflectionMeta: UserServiceReflectionMeta, } @@ -474,7 +477,8 @@ func (c *_UserServiceYARPCCaller) ListUsers(ctx context.Context, request *ListUs } type _UserServiceYARPCHandler struct { - server UserServiceYARPCServer + server UserServiceYARPCServer + actorUUIDValidator protobuf.ActorUUIDValidator } func (h *_UserServiceYARPCHandler) DeleteUser(ctx context.Context, requestMessage proto.Message) (proto.Message, error) { @@ -486,6 +490,9 @@ func (h *_UserServiceYARPCHandler) DeleteUser(ctx context.Context, requestMessag return nil, protobuf.CastError(emptyUserServiceServiceDeleteUserYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "DeleteUser"); err != nil { + return nil, err + } response, err := h.server.DeleteUser(ctx, request) if response == nil { return nil, err @@ -502,6 +509,9 @@ func (h *_UserServiceYARPCHandler) GetUser(ctx context.Context, requestMessage p return nil, protobuf.CastError(emptyUserServiceServiceGetUserYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "GetUser"); err != nil { + return nil, err + } response, err := h.server.GetUser(ctx, request) if response == nil { return nil, err @@ -534,6 +544,9 @@ func (h *_UserServiceYARPCHandler) CredentialedAction(ctx context.Context, reque return nil, protobuf.CastError(emptyUserServiceServiceCredentialedActionYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "CredentialedAction"); err != nil { + return nil, err + } response, err := h.server.CredentialedAction(ctx, request) if response == nil { return nil, err @@ -550,6 +563,9 @@ func (h *_UserServiceYARPCHandler) CycleAction(ctx context.Context, requestMessa return nil, protobuf.CastError(emptyUserServiceServiceCycleActionYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "CycleAction"); err != nil { + return nil, err + } response, err := h.server.CycleAction(ctx, request) if response == nil { return nil, err @@ -566,6 +582,9 @@ func (h *_UserServiceYARPCHandler) MultipleAction(ctx context.Context, requestMe return nil, protobuf.CastError(emptyUserServiceServiceMultipleActionYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "MultipleAction"); err != nil { + return nil, err + } response, err := h.server.MultipleAction(ctx, request) if response == nil { return nil, err @@ -582,6 +601,9 @@ func (h *_UserServiceYARPCHandler) RepeatedActorsAction(ctx context.Context, req return nil, protobuf.CastError(emptyUserServiceServiceRepeatedActorsActionYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "RepeatedActorsAction"); err != nil { + return nil, err + } response, err := h.server.RepeatedActorsAction(ctx, request) if response == nil { return nil, err @@ -598,6 +620,9 @@ func (h *_UserServiceYARPCHandler) MapActorsAction(ctx context.Context, requestM return nil, protobuf.CastError(emptyUserServiceServiceMapActorsActionYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "MapActorsAction"); err != nil { + return nil, err + } response, err := h.server.MapActorsAction(ctx, request) if response == nil { return nil, err @@ -614,6 +639,9 @@ func (h *_UserServiceYARPCHandler) RepeatedMessageAction(ctx context.Context, re return nil, protobuf.CastError(emptyUserServiceServiceRepeatedMessageActionYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "RepeatedMessageAction"); err != nil { + return nil, err + } response, err := h.server.RepeatedMessageAction(ctx, request) if response == nil { return nil, err @@ -630,6 +658,9 @@ func (h *_UserServiceYARPCHandler) MapMessageAction(ctx context.Context, request return nil, protobuf.CastError(emptyUserServiceServiceMapMessageActionYARPCRequest, requestMessage) } } + if err := protobuf.ValidateActorUUID(ctx, h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "MapMessageAction"); err != nil { + return nil, err + } response, err := h.server.MapMessageAction(ctx, request) if response == nil { return nil, err @@ -663,6 +694,9 @@ func (h *_UserServiceYARPCHandler) ListUsers(serverStream *protobuf.ServerStream if !ok { return protobuf.CastError(emptyUserServiceServiceListUsersYARPCRequest, requestMessage) } + if err := protobuf.ValidateActorUUID(serverStream.Context(), h.actorUUIDValidator, request.ActorUUID(), "uber.yarpc.tests.protouuid.UserService", "ListUsers"); err != nil { + return err + } return h.server.ListUsers(request, &_UserServiceServiceListUsersYARPCServer{serverStream: serverStream}) }