Skip to content

Support for external authorization grpc service.#2415

Merged
htuch merged 22 commits intoenvoyproxy:masterfrom
colabsaumoh:ext-auth-review-common
Feb 7, 2018
Merged

Support for external authorization grpc service.#2415
htuch merged 22 commits intoenvoyproxy:masterfrom
colabsaumoh:ext-auth-review-common

Conversation

@saumoh
Copy link
Copy Markdown
Contributor

@saumoh saumoh commented Jan 19, 2018

Title
Authorization TCP and HTTP filters using an external gRPC service.
Patch 1 of 3: gRPC client and common framework.

Description
As per the feedback on #2359 I am breaking it up into three PR's.

The patch in this PR adds support for gRPC client that will be used by TCP and HTTP filters to make authorization checks with an external service.
This PR implements the discussing we had in #2291.

Testing
I have tested this on my local setup with an external gRPC authorization server. I have also added UT's for the gRPC client side.

Risk
Low: Because only the users of this filter will be impacted. It should not impact general stability of Envoy it self.

Caveats

Signed-off-by: Saurabh Mohan saurabh+github@tigera.io

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@htuch
Copy link
Copy Markdown
Member

htuch commented Jan 23, 2018

Thanks @saumoh for the PR series. Now that #2393 is merged, can you rebase this and use the client factory for gRPC services provided there?

Saurabh Mohan added 2 commits January 23, 2018 14:44
…ommon

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@louiscryan
Copy link
Copy Markdown

louiscryan commented Jan 24, 2018 via email

@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Jan 24, 2018

@htuch I've merged with the new gRPC manager changes.

@htuch
Copy link
Copy Markdown
Member

htuch commented Jan 24, 2018

@saumoh can you get the build to pass? I'll take an in-depth pass tomorrow, but I think it's helpful to see the CI build passing. Thanks.

headers.iterate(fillHttpHeaders, mhdrs);

std::unique_ptr<AttributeContext_Request> req = std::unique_ptr<AttributeContext_Request>{new AttributeContext_Request()};
ASSERT(req);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very brief drive-by nits here:

  1. Don't need to ASSERT on a basic memory allocation, we assume memory allocations always succeed or the world ends in Envoy.
  2. Should write this as auto req = std::make_unique<Attribute_Request>(); for C++14.

class ExtAuthzCheckRequestGenerator: public CheckRequestGenerator {
public:
ExtAuthzCheckRequestGenerator() {}
~ExtAuthzCheckRequestGenerator() {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick nit: the default constructor/destructors are provided by the compiler, no need to add them.

Saurabh Mohan added 5 commits January 24, 2018 16:27
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Jan 25, 2018

@htuch Fixed the build and addressed your last set of comments. If you could please have a look. I am working on fixing coverage in the mean while. thanks.

@htuch
Copy link
Copy Markdown
Member

htuch commented Jan 25, 2018

@saumoh there is coverage regression here, see Code coverage 97.7 is lower than limit of 98.0 in CI. Have you generated a full coverage report and looked to see where you are missing tests?

@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Jan 25, 2018

@htuch I am adding more tests for some of the newly added code. That should help creep the numbers closer to 100%.

Copy link
Copy Markdown
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK great, this is looking like it's heading in the right direction, thanks for taking this on!

auth_bind_targets = [
"auth"
]
for t in auth_bind_targets:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, the recent merges of envoyproxy/data-plane-api#421 and #2430 will break this (and other parts of your PR). Be prepared to update when you merge master or pull in an updated data-plane-api SHA.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads up. I'll do another merge with master to pull in the latest data-plane-api and rework the impacted code.

/**
* Called when a check request is complete. The resulting status is supplied.
*/
virtual void complete(CheckStatus status) PURE;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think onComplete is a clearer name.

virtual ~ClientFactory() {}

/**
* Return a new authz client.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@param docs for all parameters on include/envoy interface headers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also @return for the return type & text.

* and fill out the details in the authorization protobuf that is sent to authorization
* service.
*/
class CheckRequestGenerator {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really a helper utility class. Generally we don't put these in include/envoy. A litmus test is "will I need to mock this class?" and "will this class have potentially more than one implementation?". If no to both, put it in the source/common subtree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I had it in the souce/common subtree. When I started writing the UT's for the filters I had to mock it. That's when I moved it here.
The usage in http filter is here

I think that the mock is needed. @htuch Should I just keep it here or move to source/common and have the mock reference it from there? please confirm. thx.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need to mock the proto creation? These are basically straight line pure functions. Personally, I would mock the input parameters, since it's just as easy to do and results in easier to understand code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think i know what you mean here. Let me try to redo the ut's without using the mock.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@htuch last commit removes the mock for proto creation.


// TODO(saumoh): Uncomment once unauthorization has been added to accesslog.proto
//
// if (request_info.getResponseFlag(RequestInfo::ResponseFlag::Unauthorized)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove all of this for now and keep it locally. It will bit rot otherwise.

async_client_factory_ = async_client_manager.factoryForGrpcService(grpc_service, scope);
}

ClientPtr GrpcFactoryImpl::create(const Optional<std::chrono::milliseconds>& timeout) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the GrpcFactoryImpl if all you're doing is wrapping the Grpc::AsyncClientManager/Factory calls, just use those classes directly.

std::unique_ptr<::envoy::api::v2::Address> ExtAuthzCheckRequestGenerator::getProtobufAddress(
const Network::Address::InstanceConstSharedPtr& instance) {

auto addr = std::make_unique<::envoy::api::v2::Address>();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's more natural in protobuf to invert this construction. What you have is:

  1. getConnectionPeer calls getProtobufAddress to get some remote address PB.
  2. getConnectionPeer then does a set_allocated_address with the result.

A much more common pattern is.

  1. getConnectionPeer calls mutable_allocated_addresss() to allocate memory and obtain a pointer.
  2. getProtobufAddress takes this pointer and then writes directly into the memory.

Of course, you would probably rename the methods. See https://github.com/envoyproxy/envoy/blob/master/source/common/config/address_json.cc#L10 and its callers for an example.

What you have is correct, I just find it noisier, since we have to deal with smart pointer creation and release, rather than idiomatically having the protobuf library mutable_foo() allocators do their work.

req->set_allocated_http(httpreq);

return req;
}
Copy link
Copy Markdown
Member

@htuch htuch Jan 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be instructive to compare with https://github.com/envoyproxy/envoy/blob/master/source/common/access_log/grpc_access_log_impl.cc#L251. There is a lot of overlap between the access log proto data and construction and the auth request.

As a non-actionable reflection, it would have been great if we could have converged the common objects for both access logs and auth, but I don't think this is going to happen (@mattklein123 @wora).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@htuch This really depends on how big the scale we are targeting. If we target millions of services, billions of requests per second, we can surely refactor the code, but it will take quite a bit work.

The general approach would be something like this:

  • Reach an agreement on the standard Envoy attributes, and define them as a proto like AttributeContext.
  • Envoy natively generate the standard attributes.
  • Design the standard logs, metrics, events, traces , and other telemetry schemas.
  • Generate standard telemetry data based on the standard attributes.
  • Optional. Generate legacy telemetry data, such as access logs, from the standard attributes.

New users of Envoy won't need the legacy telemetry data, so it would just be disabled by a flag. We can delete the legacy code after a year, or just keep them on the side.

This refactoring would only be useful if we can agree on a rich set of standard telemetry data that appeal to wide range of users, so a customer can correlate telemetry data across services written by different vendors.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@htuch Ref to the first paragraph, are you suggesting that I write this in a way that it's done in https://github.com/envoyproxy/envoy/blob/master/source/common/access_log/grpc_access_log_impl.cc#L251.
I am happy to do that but just want to confirm.

Separately, thanks for all the other review comments. I am working on making those changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wora agreed, I'm just waxing philosophical.

@saumoh just suggesting you give that code a pass and see if there's anything in there that your code would benefit from following.

ClientPtr create(const Optional<std::chrono::milliseconds>&) override {
return ClientPtr{new NullClientImpl()};
}
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing you modeled this on the RateLimiterImpl, do you really need a null implementation?

Saurabh Mohan and others added 7 commits January 26, 2018 11:15
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
…ommon

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Jan 30, 2018

@htuch Thanks for working with me on this PR. I've updated with the last set of pr comments.

  • Merged with latest data-plane-api re-org Split base API package into sub-packages data-plane-api#421
  • Moved the CheckRequestGenerator out of the include/ to source/common
  • Added more UT's
  • Rewrote the proto buf creation to avoid creating/destroying smart pointers (thanks for the reference code)
  • Addressed rest of the comments from the previous reviews.

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@htuch
Copy link
Copy Markdown
Member

htuch commented Feb 1, 2018

I think if you can do a few small PRs for extra utilities for header strings and network address proto that would be good to do now; the more general separation of attribute context stuff I'll leave for you and @wora to discuss.

Copy link
Copy Markdown

@wora wora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separation of attribute context is done, you can sync up to it. I think it is easier to do the split before it is being used.

}

// Fill in the headers
auto mhdrs = httpreq.mutable_headers();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a way to control which headers to send.

}
}
}
peer.set_principal(principal);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need some format normalization here. At least document what is the format used here?

peer.set_principal(principal);

if (!service.empty()) {
peer.set_service(service);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also document the format?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wora: The fulfillment of the AttributeContext is as-per the documentation in the data-plane-api specification 1. The code here is adhering to that interface agreement.
I can make a note of that at the top of this class.

@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Feb 1, 2018

@htuch added a new pr #2504 for the changes for address and http changes.

@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Feb 1, 2018

@htuch @wora For the separation of the AttributeContext in the data-plane-api 1
Should I create a utility.[cc|h] in a new directory source/common/auth which will have a static class CreateProtoAttributeContext that will fill up the protobuf.
How do u suggest this be structured in the code? Thanks.

@htuch
Copy link
Copy Markdown
Member

htuch commented Feb 2, 2018

I'm not sure if we need to refactor it today; it's done at the API level already, we don't have any extra consumers yet.

@wora if you feel strongly about pulling the AttributeContext code out of the ext_authz impl, can you enumerate some other potential users in the Envoy code base? That will allow us to see how this is going to be used.

We know it's not going to be used for logging, so I'm guessing it's for other auth filters?

htuch pushed a commit that referenced this pull request Feb 2, 2018
Title
Add a few utility functions to the

http/protocol and http_header_map.h files
Another utility function to copy Address::Instance into it's protobuf representation
Testing
I have tested and used these new functions in the PR #2415
added a ut for the http changes.

Risk
Low: like really low.

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Saurabh Mohan added 3 commits February 2, 2018 15:26
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@htuch
Copy link
Copy Markdown
Member

htuch commented Feb 5, 2018

Please merge master following #2495. @saumoh is this ready for another review pass?

@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Feb 5, 2018

@htuch Will merge #2495. thanks for the heads up.
The current branch has all the changes except for per-thread grpc client. I am working on a prototype for it. Do you think I should do the per-thread grpc client post-merge of this PR? I feel that would be a big enough change that it merits a more focused pr. wdyt?

@htuch
Copy link
Copy Markdown
Member

htuch commented Feb 5, 2018

@saumoh Yeah, you can do that as a followup PR (I did something similar with the Google gRPC client library PRs, it makes it easier to separate out reasoning about thread stuff from the semantics of the changes).

…ommon

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@saumoh
Copy link
Copy Markdown
Contributor Author

saumoh commented Feb 5, 2018

@htuch I merged with #2495 and think i've addressed all the review comments.

Copy link
Copy Markdown
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saumoh This is ready to ship once comments are addressed. They are mostly nits. Thanks!

/**
* Cancel an inflight Check request.
*/
virtual void cancel() PURE;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this a bit more, maybe we don't need to do the TLS thing at all here. This is unary RPC, so we don't have a significant cost in rebuilding the client each time, it should be a fairly small construction/destruction cost. I think what you have in this PR is fine, I would be more concerned if we had a bidi streaming auth check.


// Set the address
auto addr = peer.mutable_address();
if (!local) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can you invert this such that it reads if (local) {, same below, it's a bit cleaner.

Envoy::Http::StreamDecoderFilterCallbacks* cb =
const_cast<Envoy::Http::StreamDecoderFilterCallbacks*>(callbacks);

std::string service = getHeaderStr(headers.EnvoyDownstreamServiceCluster());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: const std::string

typedef ConstSingleton<ConstantValues> Constants;

// TODO(htuch): We should have only one client per thread, but today we create one per filter stack.
// This will require support for more than one outstanding request per client.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, I think it's different for authz, since you are unary RPC, so you can probably put this aside for now.

* createHttpCheck is used to extract the attributes from the stream and the http headers
* and fill them up in the CheckRequest proto message.
* @param callbacks supplies the Http stream context from which data can be extracted.
* @param headers supplies the heeader map with http headers that will be used to create the
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: s/heeader/header/

client_.onSuccess(std::move(response), span_);
}

{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you split this into multiple tests? The scoped-grouping-of-a-bunch-of-related-tests is fine for small simple tests, but I think here it would be clearer to have them as distinct tests.


client_.onCreateInitialMetadata(headers);

response.reset(new envoy::service::auth::v2::CheckResponse());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: prefer std::make_unique, here and elsewhere there is a reset(new ...) pattern.

response.reset(new envoy::service::auth::v2::CheckResponse());
::google::rpc::Status* status = new ::google::rpc::Status();
status->set_code(Grpc::Status::GrpcStatus::Ok);
response->set_allocated_status(status);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer the mutable_foo pattern to raw allocations and set. Sorry if I'm being a pain here, it's just a reflection of the Envoy style where we prefer to always have smart pointers to simplify reasoning about correctness. This can be relaxed in tests when really needed, but I don't think this is one of those places.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are fine. It is more a reflection of my old-school C heritage/archaic behavior. The mutable_foo pattern and the smart pointers are much better.

CreateCheckRequest::createTcpCheck(&net_callbacks_, request);
}

TEST_F(CreateCheckRequestTest, BasicHttp) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://23972-65214191-gh.circle-artifacts.com/0/build/envoy/generated/coverage/coverage.source_common_ext_authz_ext_authz_impl.cc.html tells me you have full coverage here, but I'm a bit surprised looking at just these tests, since they don't seem to be driving all the paths (at least directly). Could you say a few words on why we get full coverage here in this thread?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@htuch
The underlying structures like Ssl::Connection context are mocks hence they are there but empty. The calls from it here thus invoks all the conditional code.
The other connditional is around local boolean which invokes with both true and false within the createTcpCheck and createHttpCheck it self.
The remaining conditional checks like protocol and service is invoked because those structures are getting initialized in the tests.

I've addressed the rest of the feedback as well. Thanks for the review...it has made the code much better.

}
}
}
peer.set_principal(principal);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you're actually doing an additional string copy here that could be skipped if you did the set_principal directly in the if cases.

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
/**
* Cancel an inflight Check request.
*/
virtual void cancel() PURE;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, for Envoy gRPC, this will come for free from the cluster pool management. In theory, Google gRPC also does this, based on good authority, but I haven't validated. Let's work on your other PRs first and we can circle back to this later.

@htuch htuch merged commit 4bd671d into envoyproxy:master Feb 7, 2018
@htuch
Copy link
Copy Markdown
Member

htuch commented Feb 7, 2018

@saumoh Thanks for patience during the review process, this is a nice contribution, can you rebase your other changes and get them passing tests, I'll take them one at a time? Ta!

const RequestInfo::RequestInfo& request_info) {

static_assert(RequestInfo::ResponseFlag::LastFlag == 0x800,
static_assert(RequestInfo::ResponseFlag::LastFlag == 0x1000,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like gRPC access logging protos should be enhanced for unauthorized? Can you add a TODO and take care of this in one of your follow ups?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. I'll do it in the follow up pr; thanks.

htuch pushed a commit to envoyproxy/data-plane-api that referenced this pull request Mar 1, 2018
In the proxy we've added an Unauthorized response flag. This PR adds the same to filter access logs.
Once this PR is merged it will be possible to set the flag in source/common/access_log/grpc_access_log_impl.cc
See also, comment in envoyproxy/envoy#2415

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
@saumoh saumoh deleted the ext-auth-review-common branch May 4, 2018 17:28
jpsim added a commit that referenced this pull request Nov 28, 2022
And properly clean up reachability ref.

Signed-off-by: JP Simard <jp@jpsim.com>
jpsim added a commit that referenced this pull request Nov 29, 2022
And properly clean up reachability ref.

Signed-off-by: JP Simard <jp@jpsim.com>
Elite1015 pushed a commit to Elite1015/data-plane-api that referenced this pull request Feb 23, 2025
In the proxy we've added an Unauthorized response flag. This PR adds the same to filter access logs.
Once this PR is merged it will be possible to set the flag in source/common/access_log/grpc_access_log_impl.cc
See also, comment in envoyproxy/envoy#2415

Signed-off-by: Saurabh Mohan <saurabh+github@tigera.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants