-
Notifications
You must be signed in to change notification settings - Fork 126
[Protobuf][3/n] Wire an actor UUID validator into generated protobuf server code #2520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mark200
merged 4 commits into
yarpc:main
from
mark200:add-support-for-uuid-annotation-protobuf-wire-validator-in-server
Aug 27, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
19b86d1
Added validator wiring in server
mark200 541ae36
Wire serviceHasActorUUID/methodHasActorUUID into cache
mark200 303fe72
Cache per-message walk results to eliminate redundant traversals
mark200 18502a6
Improved comments and added actor_uuid_test.go
mark200 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
mthakoreuber marked this conversation as resolved.
|
||
|
|
||
| // 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 { | ||
|
mthakoreuber marked this conversation as resolved.
|
||
| 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( | ||
|
mthakoreuber marked this conversation as resolved.
|
||
| "actor UUID validation failed for service %q procedure %q: %w", serviceName, methodName, err) | ||
| } | ||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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") | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.