-
Notifications
You must be signed in to change notification settings - Fork 116
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
support basic gRPC payload decoding #202
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏 🎉 Glad to see the PR opened! Added a few comments inline 👇
internal/internal_test.go
Outdated
tests := map[string]struct { | ||
input *ext_authz.CheckRequest | ||
want interface{} | ||
isBodyTruncated bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we currently support proper detection of a truncated protobuf body, do we? This could be a follow-up, I suppose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
currently i dont think so
test/files/GenerateProto.md
Outdated
@@ -0,0 +1,3 @@ | |||
Folder: TestGrpc | |||
|
|||
protoc --proto_path=TestGrpc --go_out=testGrpc --go_opt=paths=source_relative TestGrpc/Example.proto --descriptor_set_out=TestGrpc/example.pb --include_imports |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this a README or a Makefile? Also I believe the paths aren't correct at the moment. 🤔
internal/internal_test.go
Outdated
@@ -1062,7 +1062,7 @@ func TestGetResponseHttpStatus(t *testing.T) { | |||
t.Fatal("Expected error but got nil") | |||
} | |||
|
|||
input["http_status"] = json.Number(301) | |||
input["http_status"] = json.Number("1") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that this can disappear when rebasing this branch. It had been dealt with in #201.
Thanks for creating the PR @pnnferreira ! The new Envoy functionality to send raw bytes to the external authz server is great and we sure would want to incorporate that in the plugin. The next release of the envoyproxy/go-control-plane with this new feature included is expected soon and I think it would be better to depend on an official release of the library. |
@ashutosh-narkar Do you have any idea when be the feature will be added on a release? |
Couldn't find the commit @ashutosh-narkar has been referring to (or having in mind), but 0.9.7 was released half a day ago (https://github.com/envoyproxy/go-control-plane/releases/tag/v0.9.7) and since it corresponds to what's in |
|
i have updated the control plane version |
@ashutosh-narkar any obstacle to move forward with this PR? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pnnferreira this seems like a good start. I had some comments about the implementation and some nits. Also we should look into Go protocol buffers APIv2. It may cut down on the dependencies.
internal/internal.go
Outdated
func getGRPCBody(in []byte, parsedPath []interface{}, data interface{}, protoDescriptor string) error { | ||
|
||
// the first 5 bytes are part of gRPC framing. We need to remove them to be able to parse | ||
//https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you describe this in more detail. I'm not able to locate the relevant section in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be able to use this instead: https://pkg.go.dev/github.com/matttproud/golang_protobuf_extensions/pbutil#ReadDelimited
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 I was wrong! 5 is alright, it's documented here:
The repeated sequence of Length-Prefixed-Message items is delivered in DATA frames
Length-Prefixed-Message → Compressed-Flag Message-Length Message
Compressed-Flag → 0 / 1 # encoded as 1 byte unsigned integer
Message-Length → {length of Message} # encoded as 4 byte unsigned integer (big endian)
Message → *{binary octet}
So 1 byte is Compressed-Flag, the next 4 are the Message-Length.
(The ReadDelimited method from pbutil is for situations when the message is prefixed with a varint size, which isn't the case of gRPC's mapping to HTTP2.)
internal/internal.go
Outdated
inputType := "" | ||
packageName := fd.GetPackage() | ||
pathServiceRaw := parsedPath[0].(string) | ||
pathService := strings.Replace(pathServiceRaw, packageName+".", "", 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear to me what's happening here. Some comments may help.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//To get the service.
//The package and service are in the parsedpath[0] (Example.Test.GRPC.ProtoServiceIExampleApplication), we need to get the service (ProtoServiceIExampleApplication) to find the methods available
internal/internal.go
Outdated
jsonBody, err := json.Marshal(message) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err := util.Unmarshal([]byte(jsonBody), &data); err != nil { | ||
return err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be avoided ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how?
internal/internal.go
Outdated
|
||
message := dynamic.NewMessage(msgDesc) | ||
|
||
if err := proto.Unmarshal(in, message); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe instead of skipping the first few bytes (above), we might be able to do something like this here:
if err := proto.Unmarshal(in, message); err != nil { | |
if _, err := pbutil.ReadDelimited(bytes.NewReader(in), message); err != nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was wrong here, see #202 (comment)
@ashutosh-narkar I agree, and I volunteer dealing with this in a follow-up PR. Let's get this in first. 😃 |
I think it would be good if could get this merged and go on from there. Right now, what's missing, I believe, is:
In follow-up PRs, we can deal with
|
We've discussed this PR in slack and @pnnferreira thought it would be a good idea if took over. So, I'm going to pick this up soon. @pnnferreira I could re-use this PR if you made me a collaborator on your fork, https://github.com/pnnferreira/opa-envoy-plugin, please 😃 |
@srenatus as we discussed on slack i agree. |
Alright, I've started working on this. It looks like I cannot switch it back to draft, but I'll let y'all know when to have another look. 😎 |
internal/internal.go
Outdated
cfg: *cfg, | ||
server: grpc.NewServer(), | ||
preparedQueryDoOnce: new(sync.Once), | ||
interQueryBuiltinCache: iCache.NewInterQueryCache(m.InterQueryBuiltinCacheConfig()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO merge/rebase messup by me
@srenatus I've moved the PR to draft mode. Feel free to switch it back. Thanks ! |
I've cleaned up a few things here:
Remaining TODOs:
|
Now, we're dealing properly with edge cases (empty messages, client compression, truncated payloads). Next, I'll add an 💭 Thinking about it, going to v3 might make more sense in a separate PR, even if it blocks this one. I'd like to avoid this one becoming a hairy mud ball mega-PR. |
Are there backwards compatibility considerations here? What is the minimum version of Envoy that will work? We don't necessarily have to be too strict here but it would be good to know. If we break compatibility with older versions of Envoy, I'm sure there will be users who run into this. |
From the discussion of open-policy-agent/opa#2798, it looks like the support for v2 will be dropped next month. But I can follow-up on it. We could support both gRPC APIs (v2 and v3) in one service, if there's a considerable need for it. I'm afraid this is not 100% accurate,
...after switching to v3, the service will just not recognize v2 requests anymore. All the more reason to detangle this from the gRPC body feature. |
internal/internal.go
Outdated
return false, false, fmt.Errorf("less than 5 bytes") | ||
} | ||
|
||
// Can be 0 or 1, 1 indicates thet the payload is compressed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: thet
// Can be 0 or 1, 1 indicates thet the payload is compressed. | ||
// The method could be looked up in the request headers, and the | ||
// request decompressed; but for now, let's skip it. | ||
if in[0] != 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are simply checking here if the payload is compressed and for now we won't support that.
if int(size) > len(in)-5 { | ||
return false, true, nil // truncated body | ||
} | ||
in = in[5 : size+5] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is everything from the 6th byte the actual input ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The structure is <size><payload><size><payload>...
if there are multiple messages (i.e. a streaming gRPC method). So, we only support one message for now, and only read that one (if we've been given enough bytes).
svc, err := findService(parsedPath[0].(string), files) | ||
if err != nil { | ||
return false, false, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't we return the error ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we don't bail on these, the policy code can decide what to do with a payload that OPA didn't know how to parse.
They could still forbid any request that doesn't have a input.parsed_body
; but we'd allow them to do other things.
I think it's conceivable, for example, that some services (like health checks) aren't included in the proto descriptor set, and the input.parsed_path
is used to allow them. If we returned the error here, that wouldn't be possible, every gRPC request ever made would have to be part of our proto descriptor set.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ultimately, I'd love some user feedback on this. 😃
87fa3ac
to
2ae78cd
Compare
working-directory: examples/grpc | ||
|
||
- name: Run test | ||
run: make test-setup test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this could be flakey. test-setup
run docker-compose up -d
, and the tests start calling the service; there's a chance that envoy isn't ready yet.
However, I haven't been able to trigger this so far locally,
$ docker-compose up -d; grpcurl -plaintext -protoset testsrv.pb 127.0.0.1:51051 test.KitchenSink/Ping
Creating network "grpc_default" with the default driver
Creating grpc_envoy_1 ... done
Creating grpc_testsrv_1 ... done
Creating grpc_opa-envoy_1 ... done
{
}
$
and on github, we're more than likely to pull down the grpcurl image anyways, given envoy plenty of time. While I'm not happy with this arrangement, let's try to roll with it. We'll deal with it if it becomes a problem.
ℹ️ We're rebuilding the testsrv image although it's very unlikely to ever change. Since it's below one minute, I've decided that it might be good enough for now. Future alternatives: push the image to dockerhub, or use grpcbin instead (but they're lacking some WKT, https://github.com/moul/pb/blob/master/grpcbin/grpcbin.proto#L67) |
.github/workflows/pull-request.yaml
Outdated
- name: Run test log dump and cleanup | ||
run: make test-teardown | ||
if: ${{ always() }} | ||
working-directory: examples/grpc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if we could find a way to avoid all the duplication. We could create a make target but probably won't look that great in the UI.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally agreed! I think github actions have a feature for that, the one or two steps that are specific to each variant (PR check, post-merge) can probably be if:
'ed accordingly.
README.md
Outdated
The protoset can be generated using `protoc`, e.g. `protoc --descriptor_set_out=protoset.pb --include_imports`. | ||
|
||
Note that gRPC message payload decoding is only available [using the v3 API](#envoy-xds-v2-and-v2). | ||
See [`examples/grpc`](examples/grpc) an example setup using Envoy, a gRPC service, and opa-envoy-plugin examining the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: for
an example setup using Envoy ...
examples/grpc/docker-compose.yaml
Outdated
command: | ||
- run | ||
- --server | ||
# - --log-level=debug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: remove
examples/grpc/docker-compose.yaml
Outdated
volumes: | ||
- ./envoy.yaml:/etc/envoy/envoy.yaml | ||
opa-envoy: | ||
image: openpolicyagent/opa:latest-istio |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: latest-envoy
ℹ️ This would be the kind of (debug) log we'd get when a gRPC request was passed to opa-envoy-plugin that we couldn't make sense of given the provided descriptor set:
|
Looks like this has bitrot again. I'll rebase. |
7d0c3dc
to
2dcf056
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.
@srenatus can you please fix the conflict and then we can merge ! |
With the new functionality from envoy to "pack_as_bytes", opa can decode the raw body into the parsed body to make policy decisions. Co-authored-by: Stephan Renatus <stephan@styra.com> Signed-off-by: Pedro Ferreira <pnlferreira@gmail.com>
- only read one message, balk on compressed payload - move proto parsing into plugin initialization, adapt/add tests - use protobuf APIv2, replace jhump/protoreflect - don't error our on unknown service/method What shall we do if the gRPC service/method isn't in our protoset? We could fail, or we could skip unmarshaling the body (since we cannot). This commit makes the plugin skip unmarshaling the body, since it's then up to the policy code to determine if an unknown request payload is OK or not OK. - cover empty message - skip compressed payloads - deal with truncated gRPC bodies - examples/grpc: add testsrv Imported from https://github.com/fullstorydev/grpcui/tree/47d4d718028c86c/testing/cmd/testsvr The MIT License (MIT) Copyright (c) 2017 FullStory, Inc The only change is the listening address (127.0.0.1 -> 0.0.0.0), and adding a Dockerfile. - examples/grpc: add docker-compose setup with envoy, testsrv, opa-envoy - workflow: add Envoy e2e to post-merge workflow - workflow: combine post-merge and pull-request - add debug logs for gRPC payload processing Also slightly refactors the other callsites of logrus.Debug() -- the loglevel check was redundant. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
@ashutosh-narkar done! :) |
@pnnferreira Look at that! Finally 🎊 Thanks for all your help and effort getting this off the ground. |
Great news 😃 Thanks for all the help and for finishing this 👏 |
with the new functionality from envoy to "pack_as_bytes", opa can decode the raw body into the parsed body to make policy decisions.