Skip to content
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

WIP: http_filter: REST-based external authentication filter #2621

Closed
wants to merge 18 commits into from
Closed

WIP: http_filter: REST-based external authentication filter #2621

wants to merge 18 commits into from

Conversation

kflynn
Copy link
Contributor

@kflynn kflynn commented Feb 15, 2018

Description:
This is the external authentication filter that's been powering Ambassador's authentication mechanism for months now. It's used by many - perhaps most - Ambassador installations, including production use at Datawire and elsewhere.

So clearly this PR is a bit overdue, for which I apologize. [ ;) ] That's doubly true in light of Tigera's recent work: Ambassador's auth mechanism is very different from the mechanism that external_authz.proto embodies, and it seems like a discussion is warranted.

Distinctions Between Methods

The big distinctions between Ambassador's auth and the external_authz.proto mechanism are:

  • The obvious: Ambassador's auth is currently REST-based rather than gRPC-based.

However, we have had requests for gRPC as well as REST; supporting both would probably be a good thing.

  • When an Ambassador auth service indicates a failure, the response from the auth service is handed back to the client.

"The response" includes HTTP status code, headers, body, the works. This has turned out to be very important for our users, as it provides a lot of flexibility and control.

For example, at Datawire, Ambassador fronts a dashboard service, which is to be accessed by a web browser, and we currently use Auth0 for the heavy lifting of authentication. Giving the auth service control over the response to the client allows us to respond to an unauthenticated request with a 301 to redirect the browser to Auth0's login widget, which makes for a much cleaner user experience... but we can just hand a 401 to a program that would be confused by the redirect.

  • When the external auth service indicates a success, the filter can copy headers from the auth service response into the client's request before passing it upstream.

Which headers are copied can be configured; only headers specifically permitted are ever copied. This has also turned out to offer a lot of flexibility: for example, the auth service can insert a header indicating what level of access a the authenticated user has, and that header can trigger a routing decision. Or, as we do at Datawire, the extauth service can inject a token into the request after a successful first-time authentication.

Protocol Details

The protocol between the extauth filter and the extauth service is very simple:

  • Any request that arrives at the filter is sent on to the auth service.

    • The HTTP method, URL, and headers other than Content-Length are forwarded unchanged.
    • The Content-Length header is given a value of 0.
    • No body is sent.
  • If the auth service responds with an HTTP status of 200, the request is allowed to continue.

    • Any allowed_headers listed in the extauth filter config are copied from the auth service's response into the request before it continues upstream.
    • Note well that only 200 will approve the request; any other 2yz response is not an approval.
  • If the auth service responds with an HTTP status other than 200 (including other 2yz responses), the request is not allowed to continue.

    • The full response from the auth service - headers, body, everything - is sent as the response to the client.

It's worth noting, given the above, that:

  • The auth service must be prepared to handle any HTTP method that the client will use.
  • The auth service can, and is expected to, immediately return a 200 for requests that don't require authentication, but
  • At present, all requests will be checked with the auth service.

Future Work

Under consideration but not part of this PR:

  • Performance work. The most obvious thing here is to cache results for a configurable time to cut down on traffic to the auth service; we've prioritized correctness ahead of that, though.
  • Support for multiple auth services. We've had requests for this, too.

Risk Level: Medium

Testing:
This is WIP because we need to add coverage testing! CI tests should all be passing, though.

Docs Changes: needed but not written until we sort out the path forward [ :) ]
Release Notes: N/A

Flynn added 9 commits February 13, 2018 12:01
Signed-off-by: Flynn <flynn@datawire.io>
Signed-off-by: Flynn <flynn@datawire.io>
…fying headers OK to be added.

Signed-off-by: Flynn <flynn@datawire.io>
…hat's what.

Signed-off-by: Flynn <flynn@datawire.io>
…d not the map. Sigh.

Signed-off-by: Flynn <flynn@datawire.io>
Signed-off-by: Flynn <flynn@datawire.io>
SIgned-off-by: Flynn <flynn@datawire.io>
@dnoe
Copy link
Contributor

dnoe commented Feb 15, 2018

Thanks for getting this PR up for discussion, and the super detailed PR description. Tagging @saumoh here, hopefully we can kick off some discussion on the various authentication filter approaches here.

It looks like some CI tests are failing although I didn't look very deeply at them. If you can investigate those in the meantime that'd be great.

@htuch
Copy link
Member

htuch commented Feb 15, 2018

@louiscryan @wora @mandarjog @kyessenov @spikecurtis @rshriram Looping in folks who were involved in the earlier ext_auth PRs. I think we should have folks take a pass over the design here and offer up thoughts on how we can address the use case you describe with ideally a single ext auth approach.

@spikecurtis
Copy link
Contributor

Thanks @kflynn !

Another way to slice the big distinctions between Ambassador's auth and the external_authz.proto mechanism is that Ambassador's auth is handling both authentication (authn) and authorization (authz), whereas the attempt with external_authz.proto is only to handle the latter.

Many of the use cases you describe that Ambassador's filter supports are around sophisticated authentication flows: redirect unauthenticated users, attach authentication tokens, attach security claims, etc. This stuff is super useful!

Moving forward I think it is important for us to take a modular approach and keep authentication and authorization separate. On the Istio project we are prototyping authentication filters and associated policies---I would love to see those efforts converged with the use cases described here.

@kflynn
Copy link
Contributor Author

kflynn commented Feb 15, 2018

@dnoe Yup, looking at CI failures now. I'm actually a bit confused by the Clang build failures -- I ran the tests with --config=clang-asan; should that not have forced a rebuild with Clang?

In any case, I can definitely reproduce the failure here (with bazel build -c dbg --config=clang --verbose_failures //source/exe:envoy-static) and am testing the fix now. Sigh. :)

Signed-off-by: Flynn <flynn@datawire.io>
@kflynn
Copy link
Contributor Author

kflynn commented Feb 16, 2018

@spikecurtis Hearing more about what Istio's been exploring sounds great -- having Istio and Ambassador play nicely on this would be a really good thing. If realtime seems easiest there, happy to jump on a call or set up a face visit.

And yes, it was a deliberate decision to allow Ambassador's auth to span both authn and authz -- while separating them certainly has advantages (especially in composability), allowing a single filter to span both avoids a fair amount of complexity, both in the code and in the user experience. It'll be very interesting to see if there's a good way to get the best of both worlds there.

@spikecurtis
Copy link
Contributor

@kflynn Some Istio docs about authn (note you might need to join Istio Dev Group to view)

@louiscryan
Copy link

louiscryan commented Feb 16, 2018 via email

Copy link

@jsedgwick jsedgwick left a comment

Choose a reason for hiding this comment

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

First pass on C++ nits. Won't speak with authority as to the non-language-related content of the diff as I'm new to the repo (this is my first review), but it seems peculiar to have to do an auth RPC on every request.

namespace Http {

/**
* A pass-through filter that talks to an external authn/authz service.

Choose a reason for hiding this comment

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

Nit: Copy-pasting these (excellent) comments in header and source is destined for mismatch. Perhaps refer to one from the other?

Copy link
Member

Choose a reason for hiding this comment

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

👍

*/

static LowerCaseString header_to_add(std::string("x-ambassador-calltype"));
static LowerCaseString value_to_add(std::string("extauth-request"));

Choose a reason for hiding this comment

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

Doesn't trigger here because of the literal, but be careful of most vexing parse. Prefer uniform initialization.
https://en.wikipedia.org/wiki/Most_vexing_parse#Uniform_initialization_syntax
static LowerCaseString header_to_add{std::string{"x-ambassador-calltype"}};
Applies elsewhere in this diff

Copy link
Member

Choose a reason for hiding this comment

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

👍

// ...and hang onto a pointer to the original request headers.
//
// Note that using a reference here won't work. Move semantics are the
// root of this issue, I think.

Choose a reason for hiding this comment

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

Yes, you generally want to avoid reference members, it's easier to have dangling references when you can't explicitly null/null check them.

Choose a reason for hiding this comment

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

Oh, I see reference members are pretty common in envoy. I stand by my comment but convention is convention...

// RequestMessageImpl to hold all the details, and start it off as a
// copy of the incoming request's headers.

MessagePtr reqmsg(new RequestMessageImpl(HeaderMapPtr{new HeaderMapImpl(headers)}));

Choose a reason for hiding this comment

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

Uniform initialization

Copy link
Member

Choose a reason for hiding this comment

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

👍

/**
* ExtAuth filter itself.
*/
class ExtAuth : Logger::Loggable<Logger::Id::filter>,

Choose a reason for hiding this comment

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

If you do want private inheritance, do so explicitly

if (!config_->allowed_headers_.empty()) {
// Yup. Let's see if any of them are present.

for (std::string allowed_header : config_->allowed_headers_) {

Choose a reason for hiding this comment

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

const ref? you're copying it into key, this copy is unnecessary

if (!value.empty()) {
// Not empty! Copy it into our request_headers_.

std::string valstr(value.c_str());

Choose a reason for hiding this comment

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

uniform init

static LowerCaseString header_to_add(std::string("x-ambassador-calltype"));
static LowerCaseString value_to_add(std::string("extauth-request"));

ExtAuth::ExtAuth(ExtAuthConfigConstSharedPtr config) : config_(config) {}

Choose a reason for hiding this comment

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

std::move the pointer in, especially if this filter is constructed per-request

Copy link
Member

@gsagula gsagula Feb 20, 2018

Choose a reason for hiding this comment

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

How about ExtAuth::ExtAuth(const ExtAuthConfigConstSharedPtr& config) : config_(config) {} ?

Choose a reason for hiding this comment

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

That still copies the shared pointer. Passing by value and moving avoids the copy in this ctor. The caller can move their copy into the ctor if possible.


auth_request_ =
config_->cm_.httpAsyncClientForCluster(config_->cluster_)
.send(std::move(reqmsg), *this, Optional<std::chrono::milliseconds>(config_->timeout_));

Choose a reason for hiding this comment

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

Hmm, our Optional should be able to infer the type here. I'll look into either adding that or ideally switching to abseil/folly/whatever

Copy link
Member

Choose a reason for hiding this comment

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

Very good point. It looks like that there is already a TODO for replacing optional. Would be possible to address this in a separated PR?

* TODO: Replace with https://github.com/abseil/abseil-cpp/blob/master/absl/types/optional.h

Choose a reason for hiding this comment

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

Yeah I'll find/create an issue and get to it eventually..

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

👍

// What did we get back from the auth server?

uint64_t response_code = Http::Utility::getResponseStatus(response->headers());
std::string response_body(response->bodyAsString());

Choose a reason for hiding this comment

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

move body? generally fix extra copies of response body/headers in this method as you shouldn't need any

Copy link
Member

Choose a reason for hiding this comment

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

👍

@rshriram
Copy link
Member

"The response" includes HTTP status code, headers, body, the works. This has turned out to be very important for our users, as it provides a lot of flexibility and control.

This is the part I like. Though I am not sure shipping an entire html body as part of the response is helpful all the time. One option might be to indicate a template file to be used as a body.

I haven’t read the code yet, but for something like this, it might be useful to have a route level opaque metadata that triggers the filter, such that people can enable this for a few URIs (eg /login) out of all the routes. (Might alleviate the need for caching in short term).

@kflynn
Copy link
Contributor Author

kflynn commented Feb 18, 2018

@jsedgwick Thanks for checking over the code! More comments inline as we dig into those points.

@louiscryan, @rshriram Yeah -- in practice, the flexibility has proven a huge win, and we haven't seen much evidence that shipping the HTML body for the error case is a big problem yet. Admittedly we tend not to send large HTML bodies: mostly for us it's a 401 with a WWW-Authenticate header, or a 301.

Finally, @spikecurtis, thanks for the pointers! I'm already a member of the istio-dev group, but I need to reread those and think more about abstracting beyond Istio...

@wora
Copy link

wora commented Feb 18, 2018 via email

Signed-off-by: Gabriel <gsagula@gmail.com>
@gsagula
Copy link
Member

gsagula commented Feb 20, 2018

@jsedgwick Thank you for the code review. I will be working on this PR from now on. Fixes will be pushed later this evening (PST).

Gabriel added 5 commits February 21, 2018 20:44
Signed-off-by: Gabriel <gsagula@gmail.com>
Signed-off-by: Gabriel <gsagula@gmail.com>
Signed-off-by: Gabriel <gsagula@gmail.com>
Signed-off-by: Gabriel <gsagula@gmail.com>
Signed-off-by: Gabriel <gsagula@gmail.com>

ENVOY_STREAM_LOG(trace, "ExtAuth Auth responded with code {}", *callbacks_, response_code);

if (!response_body.empty()) {
if (!response->body()->length()) {
Copy link
Member

@gsagula gsagula Feb 22, 2018

Choose a reason for hiding this comment

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

I forgot to roll back this. I will do it on the next pass.

@gsagula
Copy link
Member

gsagula commented Feb 22, 2018

@jsedgwick Feel free to take another look. Thanks!

Copy link

@jsedgwick jsedgwick left a comment

Choose a reason for hiding this comment

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

LGTM except one thing, code wise. I will try to learn the context and give semantic feedback as well, though it looks like you're getting that from a few angles already :)

json_config.getStringArray("allowed_headers", true), prefix});

return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void {
callbacks.addStreamDecoderFilter(std::make_shared<Http::ExtAuth>(config));

Choose a reason for hiding this comment

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

so after the change to ExtAuth ctor, this is where you can std::move config around to have zero-copy behavior

return [config = std::move(config)](Http::FilterChainFactoryCallbacks& callbacks) mutable -> void {
    callbacks.addStreamDecoderFilter(std::make_shared<Http::ExtAuth>(std::move(config))));
}

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, @jsedgwick. I was a bit confused about where I should do the move, but that makes perfect sense.

@htuch
Copy link
Member

htuch commented Feb 22, 2018

Met with @kflynn and @dpn today, the plan to move forward with this is to get some agreement at the design level on how this and ext_authz relate. @kflynn volunteered to write up the use cases that inspired this approach to auth filtering in a Google Doc; hopefully the Istio folks can extend and we can agree on the core use cases that need to be addressed. Ideally we can converge solutions and be able to cover all use cases.

@wora
Copy link

wora commented Feb 22, 2018 via email

@kflynn
Copy link
Contributor Author

kflynn commented Feb 22, 2018

I'll post a link here when the doc ready, yeah.

Also, in case there's any confusion: @gsagula is working with me on code cleanup. :)

@mattklein123
Copy link
Member

mattklein123 commented Feb 23, 2018

@louiscryan @wora @mandarjog @kyessenov @spikecurtis @rshriram @gsagula @kflynn @htuch @dnoe @saumoh et al:

My general feeling on this PR and functionality in general is that we should merge into the existing extauth HTTP filter that @saumoh has been working on. This would involve:

  • Enhancing the existing gRPC API to send/receive the needed data to cover what is done here.
  • Also enabling the extauth filter to optionally speak REST/JSON instead of binary gRPC, all driven by the same proto. (Basically how we do xDS).

I think this is the best way forward as I really don't want to end up with 2 auth filters and I see no real reason we can't make everyone happy with the right configuration and API.

So along those lines my recommendation is to:

  • Close this PR for now.
  • Do the design doc and send to envoy-dev@.
  • We can flesh out everything there and then make the final call on whether to merge or not.

Anyhow feel free to leave this open if it helps but that's my suggestion on how to proceed FWIW. Thank you very much for working on this @kflynn @gsagula I think this is going to be great for the community.

@kflynn
Copy link
Contributor Author

kflynn commented Feb 23, 2018

@mattklein123, thanks for chiming in! Let's leave this open while I get the draft of the design doc done? There are a couple of points I'd like to make sure are clear before we close this one.

For the record, we're definitely not opposed to merging filters. :) We do want to make sure that the folks already using Ambassador's extauth have a smooth path forward, though. Thanks!

@gsagula
Copy link
Member

gsagula commented Feb 23, 2018

@kflynn I'll let you guys decide the design. Meanwhile, If you would like to share the doc with me, it helps in case I need to jump in the code. Thanks!

@spikecurtis
Copy link
Contributor

I'm with @mattklein123 and don't see a reason we can't make everyone happy with the right design.

What I care about with the separation between authN and authZ is primarily about being able to compose them.

@kflynn happy to help where I can with the design. Ping me if you want to have any design discussions or get in front of a virtual whiteboard.

@louiscryan
Copy link

louiscryan commented Feb 24, 2018 via email

@kflynn
Copy link
Contributor Author

kflynn commented Mar 5, 2018

@kflynn
Copy link
Contributor Author

kflynn commented Mar 5, 2018

(Looks like the sharing settings weren't correct earlier -- fixed now, I think.)

@spikecurtis
Copy link
Contributor

@kflynn Thanks!

I've made a couple comments inline on the doc where differences in design goals jumped out at me.

It feels like the next step is to propose something specific for how the existing ext_authz API might be extended for these use cases (or, conclude that it makes more sense to have an entirely separate API). Again, happy to jump on a call and spitball some ideas.

@kflynn
Copy link
Contributor Author

kflynn commented Mar 7, 2018

@spikecurtis How about send me email at flynn@datawire.io and we'll see about getting something scheduled? Thanks!

@louiscryan
Copy link

louiscryan commented Mar 7, 2018 via email

@htuch
Copy link
Member

htuch commented Mar 7, 2018

It would be good to provide a synopsis of how this conversation goes at the next Envoy community call (next Tue). Alternatively, we can have the discussion there, but I have a feeling this is > 20 minutes in scope.

@kflynn
Copy link
Contributor Author

kflynn commented Mar 7, 2018

I'll coordinate, yeah. @htuch -- definitely agreed! Even if we haven't been able to have the call by Tuesday ( ;) ) I can talk about the situation and where we are.

@kflynn
Copy link
Contributor Author

kflynn commented Mar 7, 2018

In other news: we have the design doc open, and it's pretty clear that we have both a desire to work out a way to meet everyone's goals with one mechanism, and an appreciation that we have some work to do to get there. So I'm going to close this PR tomorrow. Further discussion can center around the doc for the moment, and the meeting to come.

If anyone thinks that this'll make it harder for them to be heard, or for us to get to a good outcome, sing out!

@junr03
Copy link
Member

junr03 commented Mar 12, 2018

@kflynn you mentioned closing this PR in your last comment, so I will go ahead and close. If please comment if you would like to re open.

@junr03 junr03 closed this Mar 12, 2018
Shikugawa pushed a commit to Shikugawa/envoy that referenced this pull request Mar 28, 2020
* wasm file gen

* build and push

* clean up

* add check option

* update prow script mode

* update prow script

* update

* add a make target

* fix

* update wasm gen script

* generate stats plugin wasm

* clean up

* fix log
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.

None yet