diff --git a/api/envoy/extensions/filters/http/basic_auth/v3/basic_auth.proto b/api/envoy/extensions/filters/http/basic_auth/v3/basic_auth.proto index c99d28162063..995d2c3bca2a 100644 --- a/api/envoy/extensions/filters/http/basic_auth/v3/basic_auth.proto +++ b/api/envoy/extensions/filters/http/basic_auth/v3/basic_auth.proto @@ -42,3 +42,11 @@ message BasicAuth { string forward_username_header = 2 [(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME strict: false}]; } + +// Extra settings that may be added to per-route configuration for +// a virtual host or a cluster. +message BasicAuthPerRoute { + // Username-password pairs for this route. + config.core.v3.DataSource users = 1 + [(validate.rules).message = {required: true}, (udpa.annotations.sensitive) = true]; +} diff --git a/changelogs/current.yaml b/changelogs/current.yaml index a921709119eb..e2fea0736ede 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -463,6 +463,10 @@ new_features: Added maximum gRPC message size that is allowed to be received in Envoy gRPC. If a message over this limit is received, the gRPC stream is terminated with the RESOURCE_EXHAUSTED error. This limit is applied to individual messages in the streaming response and not the total size of streaming response. Defaults to 0, which means unlimited. +- area: filters + change: | + Added :ref:`per-route configuration support to the Basic Auth filter + `. deprecated: - area: listener diff --git a/docs/root/configuration/http/http_filters/basic_auth_filter.rst b/docs/root/configuration/http/http_filters/basic_auth_filter.rst index da8b160fd054..a55ba52db0be 100644 --- a/docs/root/configuration/http/http_filters/basic_auth_filter.rst +++ b/docs/root/configuration/http/http_filters/basic_auth_filter.rst @@ -26,13 +26,49 @@ An example configuration of the filter may look like the following: .. code-block:: yaml - users: - inline_string: |- - user1:{SHA}hashed_user1_password - user2:{SHA}hashed_user2_password + http_filters: + - name: envoy.filters.http.basic_auth + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth + users: + inline_string: |- + user1:{SHA}hashed_user1_password + user2:{SHA}hashed_user2_password Note that only SHA format is currently supported. Other formats may be added in the future. +Per-Route Configuration +----------------------- + +An example configuration of the route filter may look like the following: + +.. code-block:: yaml + + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { path: "/admin" } + route: { cluster: some_service } + typed_per_filter_config: + envoy.filters.http.basic_auth: + "@type": type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute + users: + inline_string: |- + admin:{SHA}hashed_admin_password + - match: { prefix: "/static" } + route: { cluster: some_service } + typed_per_filter_config: + envoy.filters.http.basic_auth: + "@type": type.googleapis.com/envoy.config.route.v3.FilterConfig + disabled: true + - match: { prefix: "/" } + route: { cluster: some_service } + +In this example we customize users for ``/admin`` route, and disable authentication for ``/static`` prefixed routes. + Statistics ---------- diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 88957f155d5e..749c4fc7d856 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -235,6 +235,7 @@ envoy.filters.http.basic_auth: status: alpha type_urls: - envoy.extensions.filters.http.basic_auth.v3.BasicAuth + - envoy.extensions.filters.http.basic_auth.v3.BasicAuthPerRoute envoy.filters.http.buffer: categories: - envoy.filters.http diff --git a/source/extensions/filters/http/basic_auth/BUILD b/source/extensions/filters/http/basic_auth/BUILD index 7f47e0226048..6cc8fdeae047 100644 --- a/source/extensions/filters/http/basic_auth/BUILD +++ b/source/extensions/filters/http/basic_auth/BUILD @@ -22,6 +22,7 @@ envoy_cc_library( "//source/common/http:header_utility_lib", "//source/common/protobuf:utility_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", + "@envoy_api//envoy/extensions/filters/http/basic_auth/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/http/basic_auth/basic_auth_filter.cc b/source/extensions/filters/http/basic_auth/basic_auth_filter.cc index 862470d96f58..1b01b66e23a1 100644 --- a/source/extensions/filters/http/basic_auth/basic_auth_filter.cc +++ b/source/extensions/filters/http/basic_auth/basic_auth_filter.cc @@ -5,6 +5,7 @@ #include "source/common/common/base64.h" #include "source/common/http/header_utility.h" #include "source/common/http/headers.h" +#include "source/common/http/utility.h" namespace Envoy { namespace Extensions { @@ -31,18 +32,16 @@ FilterConfig::FilterConfig(UserMap&& users, const std::string& forward_username_ : users_(std::move(users)), forward_username_header_(forward_username_header), stats_(generateStats(stats_prefix + "basic_auth.", scope)) {} -bool FilterConfig::validateUser(absl::string_view username, absl::string_view password) const { - auto user = users_.find(username); - if (user == users_.end()) { - return false; - } - - return computeSHA1(password) == user->second.hash; -} - BasicAuthFilter::BasicAuthFilter(FilterConfigConstSharedPtr config) : config_(std::move(config)) {} Http::FilterHeadersStatus BasicAuthFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { + const auto* route_specific_settings = + Http::Utility::resolveMostSpecificPerFilterConfig(decoder_callbacks_); + const UserMap* users = &config_->users(); + if (route_specific_settings != nullptr) { + users = &route_specific_settings->users(); + } + auto auth_header = headers.get(Http::CustomHeaders::get().Authorization); if (auth_header.empty()) { @@ -72,7 +71,7 @@ Http::FilterHeadersStatus BasicAuthFilter::decodeHeaders(Http::RequestHeaderMap& absl::string_view username = decoded_view.substr(0, colon_pos); absl::string_view password = decoded_view.substr(colon_pos + 1); - if (!config_->validateUser(username, password)) { + if (!validateUser(*users, username, password)) { return onDenied("User authentication failed. Invalid username/password combination.", "invalid_credential_for_basic_auth"); } @@ -85,6 +84,16 @@ Http::FilterHeadersStatus BasicAuthFilter::decodeHeaders(Http::RequestHeaderMap& return Http::FilterHeadersStatus::Continue; } +bool BasicAuthFilter::validateUser(const UserMap& users, absl::string_view username, + absl::string_view password) const { + auto user = users.find(username); + if (user == users.end()) { + return false; + } + + return computeSHA1(password) == user->second.hash; +} + Http::FilterHeadersStatus BasicAuthFilter::onDenied(absl::string_view body, absl::string_view response_code_details) { config_->stats().denied_.inc(); diff --git a/source/extensions/filters/http/basic_auth/basic_auth_filter.h b/source/extensions/filters/http/basic_auth/basic_auth_filter.h index cfbc178d300d..4d1c9a86210a 100644 --- a/source/extensions/filters/http/basic_auth/basic_auth_filter.h +++ b/source/extensions/filters/http/basic_auth/basic_auth_filter.h @@ -1,5 +1,6 @@ #pragma once +#include "envoy/extensions/filters/http/basic_auth/v3/basic_auth.pb.h" #include "envoy/stats/stats_macros.h" #include "source/common/common/logger.h" @@ -46,8 +47,8 @@ class FilterConfig { FilterConfig(UserMap&& users, const std::string& forward_username_header, const std::string& stats_prefix, Stats::Scope& scope); const BasicAuthStats& stats() const { return stats_; } - bool validateUser(absl::string_view username, absl::string_view password) const; const std::string& forwardUsernameHeader() const { return forward_username_header_; } + const UserMap& users() const { return users_; } private: static BasicAuthStats generateStats(const std::string& prefix, Stats::Scope& scope) { @@ -59,6 +60,20 @@ class FilterConfig { BasicAuthStats stats_; }; using FilterConfigConstSharedPtr = std::shared_ptr; +using FilterConfigSharedPtr = std::shared_ptr; + +/** + * Per route settings for BasicAuth. Allows customizing users on a virtualhost\route\weighted + * cluster level. + */ +class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { +public: + FilterConfigPerRoute(UserMap&& users) : users_(std::move(users)) {} + const UserMap& users() const { return users_; } + +private: + const UserMap users_; +}; // The Envoy filter to process HTTP basic auth. class BasicAuthFilter : public Http::PassThroughDecoderFilter, @@ -68,6 +83,8 @@ class BasicAuthFilter : public Http::PassThroughDecoderFilter, // Http::StreamDecoderFilter Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool) override; + bool validateUser(const UserMap& users, absl::string_view username, + absl::string_view password) const; private: Http::FilterHeadersStatus onDenied(absl::string_view body, diff --git a/source/extensions/filters/http/basic_auth/config.cc b/source/extensions/filters/http/basic_auth/config.cc index 118d6351c246..20e66b767acf 100644 --- a/source/extensions/filters/http/basic_auth/config.cc +++ b/source/extensions/filters/http/basic_auth/config.cc @@ -9,6 +9,7 @@ namespace HttpFilters { namespace BasicAuth { using envoy::extensions::filters::http::basic_auth::v3::BasicAuth; +using envoy::extensions::filters::http::basic_auth::v3::BasicAuthPerRoute; namespace { @@ -73,6 +74,15 @@ Http::FilterFactoryCb BasicAuthFilterFactory::createFilterFactoryFromProtoTyped( }; } +Router::RouteSpecificFilterConfigConstSharedPtr +BasicAuthFilterFactory::createRouteSpecificFilterConfigTyped( + const BasicAuthPerRoute& proto_config, Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor&) { + UserMap users = readHtpasswd(THROW_OR_RETURN_VALUE( + Config::DataSource::read(proto_config.users(), true, context.api()), std::string)); + return std::make_unique(std::move(users)); +} + REGISTER_FACTORY(BasicAuthFilterFactory, Server::Configuration::NamedHttpFilterConfigFactory); } // namespace BasicAuth diff --git a/source/extensions/filters/http/basic_auth/config.h b/source/extensions/filters/http/basic_auth/config.h index 7abebaaa789c..7069537feb7e 100644 --- a/source/extensions/filters/http/basic_auth/config.h +++ b/source/extensions/filters/http/basic_auth/config.h @@ -11,7 +11,9 @@ namespace HttpFilters { namespace BasicAuth { class BasicAuthFilterFactory - : public Common::FactoryBase { + : public Common::FactoryBase< + envoy::extensions::filters::http::basic_auth::v3::BasicAuth, + envoy::extensions::filters::http::basic_auth::v3::BasicAuthPerRoute> { public: BasicAuthFilterFactory() : FactoryBase("envoy.filters.http.basic_auth") {} @@ -19,6 +21,10 @@ class BasicAuthFilterFactory Http::FilterFactoryCb createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::basic_auth::v3::BasicAuth& config, const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; + Router::RouteSpecificFilterConfigConstSharedPtr createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::basic_auth::v3::BasicAuthPerRoute& proto_config, + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor&) override; }; } // namespace BasicAuth diff --git a/test/extensions/filters/http/basic_auth/BUILD b/test/extensions/filters/http/basic_auth/BUILD index 39e573d580cd..56348ac35652 100644 --- a/test/extensions/filters/http/basic_auth/BUILD +++ b/test/extensions/filters/http/basic_auth/BUILD @@ -29,6 +29,7 @@ envoy_extension_cc_test( deps = [ "//source/extensions/filters/http/basic_auth:config", "//test/mocks/server:server_mocks", + "@envoy_api//envoy/extensions/filters/http/basic_auth/v3:pkg_cc_proto", ], ) @@ -41,5 +42,7 @@ envoy_extension_cc_test( "//source/extensions/filters/http/basic_auth:config", "//test/integration:http_protocol_integration_lib", "//test/test_common:utility_lib", + "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/basic_auth/v3:pkg_cc_proto", ], ) diff --git a/test/extensions/filters/http/basic_auth/basic_auth_integration_test.cc b/test/extensions/filters/http/basic_auth/basic_auth_integration_test.cc index fd997b75c00d..147fea35a63a 100644 --- a/test/extensions/filters/http/basic_auth/basic_auth_integration_test.cc +++ b/test/extensions/filters/http/basic_auth/basic_auth_integration_test.cc @@ -1,3 +1,8 @@ +#include + +#include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/extensions/filters/http/basic_auth/v3/basic_auth.pb.h" + #include "test/integration/http_protocol_integration.h" #include "test/test_common/utility.h" @@ -9,13 +14,10 @@ namespace HttpFilters { namespace BasicAuth { namespace { -class BasicAuthIntegrationTest : public HttpProtocolIntegrationTest { -public: - void initializeFilter() { - // user1, test1 - // user2, test2 - const std::string filter_config = - R"EOF( +// user1, test1 +// user2, test2 +const std::string BasicAuthFilterConfig = + R"EOF( name: envoy.filters.http.basic_auth typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.basic_auth.v3.BasicAuth @@ -25,7 +27,56 @@ name: envoy.filters.http.basic_auth user2:{SHA}EJ9LPFDXsN9ynSmbxvjp75Bmlx8= forward_username_header: x-username )EOF"; - config_helper_.prependFilter(filter_config); + +// admin, admin +const std::string AdminUsers = + R"EOF( +users: + inline_string: |- + admin:{SHA}0DPiKuNIrrVmD8IUCuw1hQxNqZc= +)EOF"; + +class BasicAuthIntegrationTest : public HttpProtocolIntegrationTest { +public: + void initializeFilter() { + config_helper_.prependFilter(BasicAuthFilterConfig); + initialize(); + } + + void initializePerRouteFilter(const std::string& yaml_config) { + config_helper_.addConfigModifier( + [&yaml_config]( + envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + cfg) { + envoy::extensions::filters::http::basic_auth::v3::BasicAuthPerRoute per_route_config; + TestUtility::loadFromYaml(yaml_config, per_route_config); + + auto* config = cfg.mutable_route_config() + ->mutable_virtual_hosts() + ->Mutable(0) + ->mutable_typed_per_filter_config(); + + (*config)["envoy.filters.http.basic_auth"].PackFrom(per_route_config); + }); + config_helper_.prependFilter(BasicAuthFilterConfig); + initialize(); + } + + void disablePerRouteFilter() { + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + cfg) { + envoy::config::route::v3::FilterConfig per_route_config; + per_route_config.set_disabled(true); + + auto* config = cfg.mutable_route_config() + ->mutable_virtual_hosts() + ->Mutable(0) + ->mutable_typed_per_filter_config(); + + (*config)["envoy.filters.http.basic_auth"].PackFrom(per_route_config); + }); + config_helper_.prependFilter(BasicAuthFilterConfig); initialize(); } }; @@ -145,6 +196,61 @@ TEST_P(BasicAuthIntegrationTestAllProtocols, ExistingUsernameHeader) { EXPECT_EQ("200", response->headers().getStatusValue()); } +TEST_P(BasicAuthIntegrationTestAllProtocols, BasicAuthPerRouteDisabled) { + disablePerRouteFilter(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest(Http::TestRequestHeaderMapImpl{ + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + }); + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + +TEST_P(BasicAuthIntegrationTestAllProtocols, BasicAuthPerRouteEnabled) { + initializePerRouteFilter(AdminUsers); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest(Http::TestRequestHeaderMapImpl{ + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"Authorization", "Basic YWRtaW46YWRtaW4="}, // admin, admin + }); + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + +TEST_P(BasicAuthIntegrationTestAllProtocols, BasicAuthPerRouteEnabledInvalidCredentials) { + initializePerRouteFilter(AdminUsers); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest(Http::TestRequestHeaderMapImpl{ + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"Authorization", "Basic dXNlcjE6dGVzdDE="}, // user1, test1 + }); + + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("401", response->headers().getStatusValue()); + EXPECT_EQ("User authentication failed. Invalid username/password combination.", response->body()); +} + } // namespace } // namespace BasicAuth } // namespace HttpFilters diff --git a/test/extensions/filters/http/basic_auth/config_test.cc b/test/extensions/filters/http/basic_auth/config_test.cc index 5ed811995e35..4f22da4e8386 100644 --- a/test/extensions/filters/http/basic_auth/config_test.cc +++ b/test/extensions/filters/http/basic_auth/config_test.cc @@ -1,3 +1,5 @@ +#include "envoy/extensions/filters/http/basic_auth/v3/basic_auth.pb.h" + #include "source/extensions/filters/http/basic_auth/basic_auth_filter.h" #include "source/extensions/filters/http/basic_auth/config.h" @@ -11,6 +13,8 @@ namespace Extensions { namespace HttpFilters { namespace BasicAuth { +using envoy::extensions::filters::http::basic_auth::v3::BasicAuth; + TEST(Factory, ValidConfig) { const std::string yaml = R"( users: @@ -21,12 +25,12 @@ TEST(Factory, ValidConfig) { )"; BasicAuthFilterFactory factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); + BasicAuth proto_config; + TestUtility::loadFromYaml(yaml, proto_config); NiceMock context; - auto callback = factory.createFilterFactoryFromProto(*proto_config, "stats", context); + auto callback = factory.createFilterFactoryFromProto(proto_config, "stats", context); Http::MockFilterChainFactoryCallbacks filter_callback; EXPECT_CALL(filter_callback, addStreamDecoderFilter(_)); callback.value()(filter_callback); @@ -41,13 +45,13 @@ TEST(Factory, InvalidConfigNoColon) { )"; BasicAuthFilterFactory factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); + BasicAuth proto_config; + TestUtility::loadFromYaml(yaml, proto_config); NiceMock context; EXPECT_THROW( - factory.createFilterFactoryFromProto(*proto_config, "stats", context).status().IgnoreError(), + factory.createFilterFactoryFromProto(proto_config, "stats", context).status().IgnoreError(), EnvoyException); } @@ -60,13 +64,13 @@ TEST(Factory, InvalidConfigDuplicateUsers) { )"; BasicAuthFilterFactory factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); + BasicAuth proto_config; + TestUtility::loadFromYaml(yaml, proto_config); NiceMock context; EXPECT_THROW_WITH_MESSAGE( - factory.createFilterFactoryFromProto(*proto_config, "stats", context).status().IgnoreError(), + factory.createFilterFactoryFromProto(proto_config, "stats", context).status().IgnoreError(), EnvoyException, "basic auth: duplicate users"); } @@ -79,13 +83,13 @@ TEST(Factory, InvalidConfigNoUser) { )"; BasicAuthFilterFactory factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); + BasicAuth proto_config; + TestUtility::loadFromYaml(yaml, proto_config); NiceMock context; EXPECT_THROW_WITH_MESSAGE( - factory.createFilterFactoryFromProto(*proto_config, "stats", context).status().IgnoreError(), + factory.createFilterFactoryFromProto(proto_config, "stats", context).status().IgnoreError(), EnvoyException, "basic auth: empty user name or password"); } @@ -98,13 +102,13 @@ TEST(Factory, InvalidConfigNoPassword) { )"; BasicAuthFilterFactory factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); + BasicAuth proto_config; + TestUtility::loadFromYaml(yaml, proto_config); NiceMock context; EXPECT_THROW_WITH_MESSAGE( - factory.createFilterFactoryFromProto(*proto_config, "stats", context).status().IgnoreError(), + factory.createFilterFactoryFromProto(proto_config, "stats", context).status().IgnoreError(), EnvoyException, "basic auth: empty user name or password"); } @@ -117,13 +121,13 @@ TEST(Factory, InvalidConfigNoHash) { )"; BasicAuthFilterFactory factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); + BasicAuth proto_config; + TestUtility::loadFromYaml(yaml, proto_config); NiceMock context; EXPECT_THROW_WITH_MESSAGE( - factory.createFilterFactoryFromProto(*proto_config, "stats", context).status().IgnoreError(), + factory.createFilterFactoryFromProto(proto_config, "stats", context).status().IgnoreError(), EnvoyException, "basic auth: invalid htpasswd format, invalid SHA hash length"); } @@ -136,13 +140,13 @@ TEST(Factory, InvalidConfigNotSHA) { )"; BasicAuthFilterFactory factory; - ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); - TestUtility::loadFromYaml(yaml, *proto_config); + BasicAuth proto_config; + TestUtility::loadFromYaml(yaml, proto_config); NiceMock context; EXPECT_THROW_WITH_MESSAGE( - factory.createFilterFactoryFromProto(*proto_config, "stats", context).status().IgnoreError(), + factory.createFilterFactoryFromProto(proto_config, "stats", context).status().IgnoreError(), EnvoyException, "basic auth: unsupported htpasswd format: please use {SHA}"); } diff --git a/test/extensions/filters/http/basic_auth/filter_test.cc b/test/extensions/filters/http/basic_auth/filter_test.cc index 2eb950292f00..ba8a7928be72 100644 --- a/test/extensions/filters/http/basic_auth/filter_test.cc +++ b/test/extensions/filters/http/basic_auth/filter_test.cc @@ -143,6 +143,47 @@ TEST_F(FilterTest, ExistingUsernameHeader) { EXPECT_EQ("user1", request_headers_user1.get_("x-username")); } +TEST_F(FilterTest, BasicAuthPerRouteDefaultSettings) { + Http::TestRequestHeaderMapImpl empty_request_headers; + UserMap empty_users; + FilterConfigPerRoute basic_auth_per_route(std::move(empty_users)); + + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(testing::Return(&basic_auth_per_route)); + + EXPECT_CALL(decoder_filter_callbacks_, sendLocalReply(_, _, _, _, _)) + .WillOnce(Invoke([&](Http::Code code, absl::string_view body, + std::function, + const absl::optional grpc_status, + absl::string_view details) { + EXPECT_EQ(Http::Code::Unauthorized, code); + EXPECT_EQ("User authentication failed. Missing username and password.", body); + EXPECT_EQ(grpc_status, absl::nullopt); + EXPECT_EQ(details, "no_credential_for_basic_auth"); + })); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(empty_request_headers, true)); +} + +TEST_F(FilterTest, BasicAuthPerRouteEnabled) { + UserMap users_for_route; + users_for_route.insert({"admin", {"admin", "0DPiKuNIrrVmD8IUCuw1hQxNqZc="}}); // admin:admin + FilterConfigPerRoute basic_auth_per_route(std::move(users_for_route)); + + ON_CALL(*decoder_filter_callbacks_.route_, mostSpecificPerFilterConfig(_)) + .WillByDefault(testing::Return(&basic_auth_per_route)); + + Http::TestRequestHeaderMapImpl valid_credentials{{"Authorization", "Basic YWRtaW46YWRtaW4="}}; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(valid_credentials, true)); + + Http::TestRequestHeaderMapImpl invalid_credentials{{"Authorization", "Basic dXNlcjE6dGVzdDE="}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(invalid_credentials, true)); +} + } // namespace BasicAuth } // namespace HttpFilters } // namespace Extensions