diff --git a/plugins/wasm-cpp/common/route_rule_matcher.h b/plugins/wasm-cpp/common/route_rule_matcher.h index b07c95fd6..cc6c6fd51 100644 --- a/plugins/wasm-cpp/common/route_rule_matcher.h +++ b/plugins/wasm-cpp/common/route_rule_matcher.h @@ -16,6 +16,7 @@ #pragma once +#include #include #include #include @@ -25,6 +26,8 @@ #include "absl/strings/match.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_split.h" #include "common/json_util.h" #include "http_util.h" @@ -58,12 +61,15 @@ using ::Wasm::Common::JsonValueAs; template class RouteRuleMatcher { public: - enum CATEGORY { Route, Host }; + enum CATEGORY { Route, RoutePrefix, Host, Service, RouteAndService }; enum MATCH_TYPE { Prefix, Exact, Suffix }; struct RuleConfig { CATEGORY category; std::unordered_set routes; + std::vector route_prefixs; std::vector> hosts; + std::unordered_set services; + bool disable = false; PluginConfig config; }; struct AuthRuleConfig { @@ -88,6 +94,8 @@ class RouteRuleMatcher { return rules; } + bool globalAuthDisable() { return global_auth_ && !global_auth_.value(); } + FilterHeadersStatus onHeaders( const std::function process) { if (invalid_config_) { @@ -126,10 +134,10 @@ class RouteRuleMatcher { LOG_DEBUG("no match config"); return true; } - if (!config.second && global_auth_ && !global_auth_.value()) { - // No allow set, means no need to check auth if global_auth is false + if (!config.second && globalAuthDisable()) { + // No allow set, means no need to check auth if global auth is disable LOG_DEBUG( - "no allow set found, and global auth is false, no need to auth"); + "no allow set found, and global auth is disable, no need to auth"); return true; } return checkPlugin(config.first.value(), config.second); @@ -154,27 +162,71 @@ class RouteRuleMatcher { auto request_host = request_host_header->view(); std::string route_name; getValue({"route_name"}, &route_name); + std::string service_name; + getValue({"cluster_name"}, &service_name); std::optional> match_config; int rule_id; if (global_config_) { rule_id = 0; match_config = global_config_.value(); } + bool disable_rule = false; for (int i = 0; i < rule_config_.size(); ++i) { auto& rule = rule_config_[i]; if (rule.category == CATEGORY::Host) { if (hostMatch(rule, request_host)) { rule_id = i + 1; match_config = rule.config; + disable_rule = rule.disable; + break; + } + } else if (rule.category == CATEGORY::Route) { + // category == Route + if (rule.routes.find(route_name) != rule.routes.end()) { + rule_id = i + 1; + match_config = rule.config; + disable_rule = rule.disable; + break; + } + } else if (rule.category == CATEGORY::RouteAndService) { + // category == RouteAndService + if (rule.routes.find(route_name) != rule.routes.end()) { + if (serviceMatch(rule, service_name)) { + rule_id = i + 1; + match_config = rule.config; + disable_rule = rule.disable; + break; + } + } + } else if (rule.category == CATEGORY::Service) { + // category == Service + if (serviceMatch(rule, service_name)) { + rule_id = i + 1; + match_config = rule.config; + disable_rule = rule.disable; + break; + } + } else { + // category == RoutePrefix + bool is_matched = false; + for (auto& route_prefix : rule.route_prefixs) { + if (route_name.length() < route_prefix.length() || + route_name.compare(0, route_prefix.length(), route_prefix) != 0) { + continue; + } + is_matched = true; + rule_id = i + 1; + match_config = rule.config; + disable_rule = rule.disable; + break; + } + if (is_matched) { break; } } - // category == Route - if (rule.routes.find(route_name) != rule.routes.end()) { - rule_id = i + 1; - match_config = rule.config; - break; - } + } + if (disable_rule) { + return std::make_pair(-1, std::nullopt); } if (match_config) { return std::make_pair(rule_id, match_config); @@ -190,6 +242,8 @@ class RouteRuleMatcher { auto request_host = request_host_header->view(); std::string route_name; getValue({"route_name"}, &route_name); + std::string service_name; + getValue({"service_name"}, &service_name); std::optional> match_config; std::optional>> allow_set; @@ -200,33 +254,99 @@ class RouteRuleMatcher { return std::make_pair(match_config, std::nullopt); } bool is_matched = false; + bool disable_rule = false; for (auto& auth_rule : auth_rule_config_) { if (auth_rule.rule_config.category == CATEGORY::Host) { if (hostMatch(auth_rule.rule_config, request_host)) { + LOG_DEBUG(absl::StrFormat("host %s is matched for this request", + request_host)); is_matched = true; - if (auth_rule.has_local_config) { - LOG_DEBUG("has local config"); + if (auth_rule.rule_config.disable) { + disable_rule = true; + } else if (auth_rule.has_local_config) { match_config = auth_rule.rule_config.config; } else { - LOG_DEBUG("has not local config"); allow_set = auth_rule.allow_set; } break; } - } - // category == Route - if (auth_rule.rule_config.routes.find(route_name) != - auth_rule.rule_config.routes.end()) { - is_matched = true; - if (auth_rule.has_local_config) { - match_config = auth_rule.rule_config.config; - } else { - allow_set = auth_rule.allow_set; + } else if (auth_rule.rule_config.category == CATEGORY::Route) { + // category == Route + if (auth_rule.rule_config.routes.find(route_name) != + auth_rule.rule_config.routes.end()) { + LOG_DEBUG(absl::StrFormat("route %s is matched for this request", + route_name)); + is_matched = true; + if (auth_rule.rule_config.disable) { + disable_rule = true; + } else if (auth_rule.has_local_config) { + match_config = auth_rule.rule_config.config; + } else { + allow_set = auth_rule.allow_set; + } + break; + } + } else if (auth_rule.rule_config.category == CATEGORY::RouteAndService) { + // category == RouteAndService + if (auth_rule.rule_config.routes.find(route_name) != + auth_rule.rule_config.routes.end()) { + LOG_DEBUG(absl::StrFormat("route %s is matched for this request", + route_name)); + if (serviceMatch(auth_rule.rule_config, service_name)) { + LOG_DEBUG(absl::StrFormat("service %s is matched for this request", + service_name)); + is_matched = true; + if (auth_rule.rule_config.disable) { + disable_rule = true; + } else if (auth_rule.has_local_config) { + match_config = auth_rule.rule_config.config; + } else { + allow_set = auth_rule.allow_set; + } + break; + } + } + } else if (auth_rule.rule_config.category == CATEGORY::Service) { + // category == Service + if (serviceMatch(auth_rule.rule_config, service_name)) { + LOG_DEBUG(absl::StrFormat("service %s is matched for this request", + service_name)); + is_matched = true; + if (auth_rule.rule_config.disable) { + disable_rule = true; + } else if (auth_rule.has_local_config) { + match_config = auth_rule.rule_config.config; + } else { + allow_set = auth_rule.allow_set; + } + break; + } + } else { + // category == RoutePrefix + for (auto& route_prefix : auth_rule.rule_config.route_prefixs) { + if (route_name.length() < route_prefix.length() || + route_name.compare(0, route_prefix.length(), route_prefix) != 0) { + continue; + } + LOG_DEBUG(absl::StrFormat( + "route_prefix %s is matched for this request", route_prefix)); + is_matched = true; + if (auth_rule.rule_config.disable) { + disable_rule = true; + } else if (auth_rule.has_local_config) { + match_config = auth_rule.rule_config.config; + } else { + allow_set = auth_rule.allow_set; + } + break; + } + if (is_matched) { + break; } - break; } } - return is_matched || (global_auth_ && global_auth_.value()) + return !disable_rule && + (is_matched || (global_auth_ && global_auth_.value())) ? std::make_pair(match_config, allow_set) : std::make_pair(std::nullopt, std::nullopt); } @@ -265,23 +385,48 @@ class RouteRuleMatcher { LOG_WARN("failed to parse configuration for _match_route_"); return false; } + if (!parseRoutePrefixMatchConfig(config, rule.route_prefixs)) { + LOG_WARN("failed to parse configuration for _match_route_prefix_"); + return false; + } if (!parseDomainMatchConfig(config, rule.hosts)) { LOG_WARN("failed to parse configuration for _match_domain_"); return false; } - auto no_route = rule.routes.empty(); - auto no_host = rule.hosts.empty(); - if ((no_route && no_host) || (!no_route && !no_host)) { + if (!parseServiceMatchConfig(config, rule.services)) { + LOG_WARN("failed to parse configuration for _match_service_"); + return false; + } + auto has_route = !rule.routes.empty(); + auto has_route_prefix = !rule.route_prefixs.empty(); + auto has_service = !rule.services.empty(); + auto has_host = !rule.hosts.empty(); + if (has_route + has_route_prefix + has_host + has_service == 0) { LOG_WARN( - "there is only one of '_match_route_' and '_match_domain_' can " + "there is at least one of '_match_route_', '_match_domain_', " + "'_match_route_prefix_' and '_match_service_' can " "present in configuration."); return false; } - if (!no_route) { + if (has_route) { rule.category = CATEGORY::Route; + if (has_service) { + rule.category = CATEGORY::RouteAndService; + } + } else if (has_route_prefix) { + rule.category = CATEGORY::RoutePrefix; + } else if (has_service) { + rule.category = CATEGORY::Service; } else { rule.category = CATEGORY::Host; } + auto has_disable = config.find("_disable_"); + if (has_disable != config.end()) { + auto disable = JsonValueAs(has_disable.value()); + if (disable.second == Wasm::Common::JsonParserResultDetail::OK) { + rule.disable = disable.first.value(); + } + } rule_config_.push_back(std::move(rule)); } return true; @@ -323,8 +468,11 @@ class RouteRuleMatcher { for (const auto& item : rules.items()) { AuthRuleConfig auth_rule; auto config = item.value(); + // ignore the '_match_route_' or '_match_domain_' field + auto local_config_size = config.size() - 1; auto has_allow = config.find("allow"); if (has_allow != config.end()) { + local_config_size -= 1; LOG_DEBUG("has allow filed"); if (!JsonArrayIterate(config, "allow", [&](const json& allow) -> bool { auto parse_result = JsonValueAs(allow); @@ -332,10 +480,10 @@ class RouteRuleMatcher { Wasm::Common::JsonParserResultDetail::OK || !parse_result.first) { LOG_WARN( - "failed to parse 'allow' field in filter configuration."); + "failed to parse 'allow' field in filter " + "configuration."); return false; } - LOG_DEBUG(parse_result.first.value()); auth_rule.allow_set.insert(parse_result.first.value()); return true; })) { @@ -343,32 +491,61 @@ class RouteRuleMatcher { return false; } } - if (!parsePluginConfig(config, auth_rule.rule_config.config)) { - if (has_allow == config.end()) { - LOG_WARN("parse rule's config failed"); - return false; + auto has_disable = config.find("_disable_"); + if (has_disable != config.end()) { + local_config_size -= 1; + auto disable = JsonValueAs(has_disable.value()); + if (disable.second == Wasm::Common::JsonParserResultDetail::OK) { + auth_rule.rule_config.disable = disable.first.value(); + } + } + if (local_config_size > 0) { + if (!parsePluginConfig(config, auth_rule.rule_config.config)) { + if (has_allow == config.end()) { + LOG_WARN("parse rule's config failed"); + return false; + } + } else { + auth_rule.has_local_config = true; } - } else { - auth_rule.has_local_config = true; } if (!parseRouteMatchConfig(config, auth_rule.rule_config.routes)) { LOG_WARN("failed to parse configuration for _match_route_"); return false; } + if (!parseRoutePrefixMatchConfig(config, + auth_rule.rule_config.route_prefixs)) { + LOG_WARN("failed to parse configuration for _match_route_prefix_"); + return false; + } + if (!parseServiceMatchConfig(config, auth_rule.rule_config.services)) { + LOG_WARN("failed to parse configuration for _match_service_"); + return false; + } if (!parseDomainMatchConfig(config, auth_rule.rule_config.hosts)) { LOG_WARN("failed to parse configuration for _match_domain_"); return false; } - auto no_route = auth_rule.rule_config.routes.empty(); - auto no_host = auth_rule.rule_config.hosts.empty(); - if ((no_route && no_host) || (!no_route && !no_host)) { + auto has_route = !auth_rule.rule_config.routes.empty(); + auto has_route_prefix = !auth_rule.rule_config.route_prefixs.empty(); + auto has_host = !auth_rule.rule_config.hosts.empty(); + auto has_service = !auth_rule.rule_config.services.empty(); + if (has_route + has_route_prefix + has_host + has_service == 0) { LOG_WARN( - "there is only one of '_match_route_' and '_match_domain_' can " + "there is at least one of '_match_route_', '_match_domain_', " + "'_match_route_prefix_' and '_match_service_' can " "present in configuration."); return false; } - if (!no_route) { + if (has_route) { auth_rule.rule_config.category = CATEGORY::Route; + if (has_service) { + auth_rule.rule_config.category = CATEGORY::RouteAndService; + } + } else if (has_route_prefix) { + auth_rule.rule_config.category = CATEGORY::RoutePrefix; + } else if (has_service) { + auth_rule.rule_config.category = CATEGORY::Service; } else { auth_rule.rule_config.category = CATEGORY::Host; } @@ -419,6 +596,27 @@ class RouteRuleMatcher { return false; } + bool serviceMatch(const RuleConfig& rule, std::string_view request_service) { + if (rule.services.empty()) { + // If no services specified, consider this rule applies to all host. + return true; + } + std::vector result = absl::StrSplit(request_service, '|'); + if (result.size() != 4) { + return false; + } + + std::string port = result[1]; + std::string fqdn = result[3]; + + for (const std::string& service_match : rule.services) { + if (service_match == fqdn || service_match == fqdn + ":" + port) { + return true; + } + } + return false; + } + bool parseRouteMatchConfig(const json& config, std::unordered_set& routes) { return JsonArrayIterate( @@ -436,6 +634,23 @@ class RouteRuleMatcher { }); } + bool parseRoutePrefixMatchConfig(const json& config, + std::vector& route_prefixs) { + return JsonArrayIterate( + config, "_match_route_prefix_", [&](const json& route) -> bool { + auto parse_result = JsonValueAs(route); + if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK || + !parse_result.first) { + LOG_WARN( + "failed to parse '_match_route_prefix_' field in filter " + "configuration."); + return false; + } + route_prefixs.emplace_back(parse_result.first.value()); + return true; + }); + } + bool parseDomainMatchConfig( const json& config, std::vector>& hosts) { @@ -475,6 +690,23 @@ class RouteRuleMatcher { }); } + bool parseServiceMatchConfig(const json& config, + std::unordered_set& services) { + return JsonArrayIterate( + config, "_match_service_", [&](const json& service) -> bool { + auto parse_result = JsonValueAs(service); + if (parse_result.second != Wasm::Common::JsonParserResultDetail::OK || + !parse_result.first) { + LOG_WARN( + "failed to parse '_match_service_' field in filter " + "configuration."); + return false; + } + services.insert(parse_result.first.value()); + return true; + }); + } + bool invalid_config_ = false; std::optional global_auth_ = std::nullopt; std::vector rule_config_; diff --git a/plugins/wasm-cpp/extensions/hmac_auth/plugin.cc b/plugins/wasm-cpp/extensions/hmac_auth/plugin.cc index f3f2ddcb8..f7b774716 100644 --- a/plugins/wasm-cpp/extensions/hmac_auth/plugin.cc +++ b/plugins/wasm-cpp/extensions/hmac_auth/plugin.cc @@ -294,7 +294,12 @@ bool PluginRootContext::checkConsumer( } auto key_to_name_iter = rule.key_to_name.find(std::string(ca_key)); if (key_to_name_iter != rule.key_to_name.end()) { - if (allow_set && !allow_set.value().empty()) { + if (allow_set) { + if (allow_set.value().empty()) { + LOG_DEBUG("allow set is empty, nobody is allowed"); + deniedUnauthorizedConsumer(); + return false; + } if (allow_set.value().find(key_to_name_iter->second) == allow_set.value().end()) { LOG_DEBUG(absl::StrCat("consumer is not allowed: ", @@ -435,6 +440,7 @@ FilterHeadersStatus PluginContext::onRequestHeaders(uint32_t, bool) { auto config = rootCtx->getMatchAuthConfig(); config_ = config.first; if (!config_) { + LOG_DEBUG("no matched config found"); return FilterHeadersStatus::Continue; } allow_set_ = config.second; diff --git a/plugins/wasm-cpp/extensions/hmac_auth/plugin_test.cc b/plugins/wasm-cpp/extensions/hmac_auth/plugin_test.cc index 399a4d1e3..a256aefbe 100644 --- a/plugins/wasm-cpp/extensions/hmac_auth/plugin_test.cc +++ b/plugins/wasm-cpp/extensions/hmac_auth/plugin_test.cc @@ -624,6 +624,41 @@ TEST_F(HmacAuthTest, TimestampSecCheck) { EXPECT_EQ(context_->onRequestBody(0, true), FilterDataStatus::Continue); } +TEST_F(HmacAuthTest, EmptyAllowSet) { + headers_ = { + {":path", "/Third/Tools/checkSign"}, + {":method", "GET"}, + {"accept", "application/json"}, + {"content-type", "application/json"}, + {"x-ca-timestamp", "1646365291734"}, + {"x-ca-nonce", "787dd0c2-7bd8-41cd-9c19-62c05ea524a2"}, + {"x-ca-key", "appKey"}, + {"x-ca-signature-headers", "x-ca-key,x-ca-nonce,x-ca-timestamp"}, + {"x-ca-signature", "EdJSFAMOWyXZOpXhevZnjuS0ZafnwnCqaSk5hz+tXo8="}, + }; + HmacAuthConfigRule rule; + rule.credentials = {{"appKey", "appSecret"}}; + // EXPECT_EQ(root_context_->checkPlugin(rule, std::nullopt), true); + + std::string configuration = R"( +{ + "consumers": [{"key": "appKey", "secret": "appSecret", "name": "consumer"}], + "_rules_": [ + { + "_match_route_prefix_":["test"], + "allow":[] + } + ] +})"; + route_name_ = "test@op1"; + config_.set(configuration); + EXPECT_TRUE(root_context_->configure(configuration.size())); + EXPECT_CALL(*mock_context_, sendLocalResponse(403, testing::_, testing::_, + testing::_, testing::_)); + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::StopAllIterationAndBuffer); +} + } // namespace hmac_auth } // namespace null_plugin } // namespace proxy_wasm diff --git a/plugins/wasm-cpp/extensions/key_auth/plugin.cc b/plugins/wasm-cpp/extensions/key_auth/plugin.cc index ad111760e..b826b5be4 100644 --- a/plugins/wasm-cpp/extensions/key_auth/plugin.cc +++ b/plugins/wasm-cpp/extensions/key_auth/plugin.cc @@ -45,15 +45,13 @@ namespace { const std::string OriginalAuthKey("X-HI-ORIGINAL-AUTH"); void deniedInvalidCredentials(const std::string& realm) { - sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key", - "", + sendLocalResponse(401, "Request denied by Key Auth check. Invalid API key", "", {{"WWW-Authenticate", absl::StrCat("Key realm=", realm)}}); } void deniedUnauthorizedConsumer(const std::string& realm) { - sendLocalResponse( - 403, "Request denied by Key Auth check. Unauthorized consumer", "", - {{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}}); + sendLocalResponse(403, "Request denied by Key Auth check. Unauthorized consumer", "", + {{"WWW-Authenticate", absl::StrCat("Basic realm=", realm)}}); } } // namespace @@ -62,12 +60,16 @@ bool PluginRootContext::parsePluginConfig(const json& configuration, KeyAuthConfigRule& rule) { if ((configuration.find("consumers") != configuration.end()) && (configuration.find("credentials") != configuration.end())) { - LOG_WARN( - "The consumers field and the credentials field cannot appear at the " - "same level"); + LOG_WARN("The consumers field and the credentials field cannot appear at the same level"); return false; } - if (!JsonArrayIterate( + if ((configuration.find("consumers") == configuration.end()) && + (configuration.find("credentials") == configuration.end())) { + LOG_WARN("No consumers and no credentials"); + return false; + } + if (configuration.find("credentials") != configuration.end()) { + if (!JsonArrayIterate( configuration, "credentials", [&](const json& credentials) -> bool { auto credential = JsonValueAs(credentials); if (credential.second != Wasm::Common::JsonParserResultDetail::OK) { @@ -76,34 +78,93 @@ bool PluginRootContext::parsePluginConfig(const json& configuration, rule.credentials.insert(credential.first.value()); return true; })) { - LOG_WARN("failed to parse configuration for credentials."); - return false; + LOG_WARN("failed to parse configuration for credentials."); + return false; + } + if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool { + auto key = JsonValueAs(item); + if (key.second != Wasm::Common::JsonParserResultDetail::OK) { + return false; + } + rule.keys.push_back(key.first.value()); + return true; + })) { + LOG_WARN("failed to parse configuration for keys."); + return false; + } + if (rule.keys.empty()) { + LOG_WARN("at least one key has to be configured for a rule."); + return false; + } + rule.keys.push_back(OriginalAuthKey); + auto it = configuration.find("realm"); + if (it != configuration.end()) { + auto realm_string = JsonValueAs(it.value()); + if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) { + return false; + } + rule.realm = realm_string.first.value(); + } + it = configuration.find("in_query"); + if (it != configuration.end()) { + auto in_query = JsonValueAs(it.value()); + if (in_query.second != Wasm::Common::JsonParserResultDetail::OK || + !in_query.first) { + LOG_WARN("failed to parse 'in_query' field in filter configuration."); + return false; + } + rule.in_query = in_query.first.value(); + } + it = configuration.find("in_header"); + if (it != configuration.end()) { + auto in_header = JsonValueAs(it.value()); + if (in_header.second != Wasm::Common::JsonParserResultDetail::OK || + !in_header.first) { + LOG_WARN("failed to parse 'in_header' field in filter configuration."); + return false; + } + rule.in_header = in_header.first.value(); + } + if (!rule.in_query && !rule.in_header) { + LOG_WARN("at least one of 'in_query' and 'in_header' must set to true"); + return false; + } + // LOG_DEBUG(rule.debugString("parse phase, credentials branch")); } - if (!JsonArrayIterate( - configuration, "consumers", [&](const json& consumer) -> bool { - Consumer c; - auto item = consumer.find("name"); - if (item == consumer.end()) { - LOG_WARN("can't find 'name' field in consumer."); - return false; - } - auto name = JsonValueAs(item.value()); - if (name.second != Wasm::Common::JsonParserResultDetail::OK || - !name.first) { - return false; - } - c.name = name.first.value(); - item = consumer.find("credential"); - if (item == consumer.end()) { - LOG_WARN("can't find 'credential' field in consumer."); - return false; - } + if (configuration.find("consumers") != configuration.end()) { + bool need_global_keys = false; + if (!JsonArrayIterate( + configuration, "consumers", [&](const json& consumer) -> bool { + Consumer c; + auto item = consumer.find("name"); + if (item == consumer.end()) { + LOG_WARN("can't find 'name' field in consumer."); + return false; + } + auto name = JsonValueAs(item.value()); + if (name.second != Wasm::Common::JsonParserResultDetail::OK || + !name.first) { + return false; + } + c.name = name.first.value(); + if (consumer.find("credential") != consumer.end() && + consumer.find("credentials") != consumer.end()) { + LOG_WARN("'credential' and 'credentials' can't appear at the same time."); + return false; + } + if (consumer.find("credential") == consumer.end() && + consumer.find("credentials") == consumer.end()) { + LOG_WARN("at least one of 'credential' and 'credentials' should be set."); + return false; + } + item = consumer.find("credential"); + if (item != consumer.end()) { auto credential = JsonValueAs(item.value()); if (credential.second != Wasm::Common::JsonParserResultDetail::OK || !credential.first) { return false; } - c.credential = credential.first.value(); + c.credentials.insert(credential.first.value()); if (rule.credential_to_name.find(credential.first.value()) != rule.credential_to_name.end()) { LOG_WARN(absl::StrCat("duplicate consumer credential: ", @@ -113,106 +174,118 @@ bool PluginRootContext::parsePluginConfig(const json& configuration, rule.credentials.insert(credential.first.value()); rule.credential_to_name.emplace( std::make_pair(credential.first.value(), name.first.value())); - item = consumer.find("keys"); - if (item != consumer.end()) { - c.keys = std::vector{OriginalAuthKey}; + } + item = consumer.find("credentials"); + if (item != consumer.end()) { if (!JsonArrayIterate( - consumer, "keys", [&](const json& key_json) -> bool { - auto key = JsonValueAs(key_json); - if (key.second != - Wasm::Common::JsonParserResultDetail::OK) { - return false; - } - c.keys->push_back(key.first.value()); - return true; - })) { - LOG_WARN("failed to parse configuration for consumer keys."); + consumer, "credentials", [&](const json& credential_json) -> bool { + auto credential = JsonValueAs(credential_json); + if (credential.second != Wasm::Common::JsonParserResultDetail::OK || + !credential.first) { + return false; + } + c.credentials.insert(credential.first.value()); + if (rule.credential_to_name.find(credential.first.value()) != + rule.credential_to_name.end()) { + LOG_WARN(absl::StrCat("duplicate consumer credential: ", + credential.first.value())); + return false; + } + rule.credentials.insert(credential.first.value()); + rule.credential_to_name.emplace( + std::make_pair(credential.first.value(), name.first.value())); + return true; + })) { + LOG_WARN(absl::StrCat("failed to parse credentials for consumer: ", c.name)); return false; } - item = consumer.find("in_query"); - if (item != consumer.end()) { - auto in_query = JsonValueAs(item.value()); - if (in_query.second != - Wasm::Common::JsonParserResultDetail::OK || - !in_query.first) { - LOG_WARN( - "failed to parse 'in_query' field in consumer " - "configuration."); - return false; - } - c.in_query = in_query.first; - } - item = consumer.find("in_header"); - if (item != consumer.end()) { - auto in_header = JsonValueAs(item.value()); - if (in_header.second != - Wasm::Common::JsonParserResultDetail::OK || - !in_header.first) { - LOG_WARN( - "failed to parse 'in_header' field in consumer " - "configuration."); - return false; - } - c.in_header = in_header.first; - } + } + item = consumer.find("keys"); + if (item == consumer.end()) { + LOG_WARN("not found keys configuration for consumer " + c.name + ", will use global configuration to extract keys"); + need_global_keys = true; + } else { + c.keys = std::vector{OriginalAuthKey}; + if (!JsonArrayIterate( + consumer, "keys", [&](const json& key_json) -> bool { + auto key = JsonValueAs(key_json); + if (key.second != + Wasm::Common::JsonParserResultDetail::OK) { + return false; + } + c.keys->push_back(key.first.value()); + return true; + })) { + LOG_WARN("failed to parse configuration for consumer keys."); + return false; } - rule.consumers.push_back(std::move(c)); + item = consumer.find("in_query"); + if (item != consumer.end()) { + auto in_query = JsonValueAs(item.value()); + if (in_query.second != Wasm::Common::JsonParserResultDetail::OK || !in_query.first) { + LOG_WARN("failed to parse 'in_query' field in consumer configuration."); + return false; + } + c.in_query = in_query.first; + } + item = consumer.find("in_header"); + if (item != consumer.end()) { + auto in_header = JsonValueAs(item.value()); + if (in_header.second != + Wasm::Common::JsonParserResultDetail::OK || + !in_header.first) { + LOG_WARN( + "failed to parse 'in_header' field in consumer " + "configuration."); + return false; + } + c.in_header = in_header.first; + } + } + rule.consumers.push_back(std::move(c)); + return true; + })) { + LOG_WARN("failed to parse configuration for credentials."); + return false; + } + if (need_global_keys) { + if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool { + auto key = JsonValueAs(item); + if (key.second != Wasm::Common::JsonParserResultDetail::OK) { + return false; + } + rule.keys.push_back(key.first.value()); return true; })) { - LOG_WARN("failed to parse configuration for credentials."); - return false; - } - // if (rule.credentials.empty()) { - // LOG_INFO("at least one credential has to be configured for a rule."); - // return false; - // } - if (!JsonArrayIterate(configuration, "keys", [&](const json& item) -> bool { - auto key = JsonValueAs(item); - if (key.second != Wasm::Common::JsonParserResultDetail::OK) { + LOG_WARN("failed to parse configuration for keys."); + return false; + } + auto it = configuration.find("in_query"); + if (it != configuration.end()) { + auto in_query = JsonValueAs(it.value()); + if (in_query.second != Wasm::Common::JsonParserResultDetail::OK || + !in_query.first) { + LOG_WARN("failed to parse 'in_query' field in filter configuration."); return false; } - rule.keys.push_back(key.first.value()); - return true; - })) { - LOG_WARN("failed to parse configuration for keys."); - return false; - } - if (rule.keys.empty()) { - LOG_WARN("at least one key has to be configured for a rule."); - return false; - } - rule.keys.push_back(OriginalAuthKey); - auto it = configuration.find("realm"); - if (it != configuration.end()) { - auto realm_string = JsonValueAs(it.value()); - if (realm_string.second != Wasm::Common::JsonParserResultDetail::OK) { - return false; + rule.in_query = in_query.first.value(); + } + it = configuration.find("in_header"); + if (it != configuration.end()) { + auto in_header = JsonValueAs(it.value()); + if (in_header.second != Wasm::Common::JsonParserResultDetail::OK || + !in_header.first) { + LOG_WARN("failed to parse 'in_header' field in filter configuration."); + return false; + } + rule.in_header = in_header.first.value(); + } + if (!rule.in_query && !rule.in_header) { + LOG_WARN("at least one of 'in_query' and 'in_header' must set to true"); + return false; + } } - rule.realm = realm_string.first.value(); - } - it = configuration.find("in_query"); - if (it != configuration.end()) { - auto in_query = JsonValueAs(it.value()); - if (in_query.second != Wasm::Common::JsonParserResultDetail::OK || - !in_query.first) { - LOG_WARN("failed to parse 'in_query' field in filter configuration."); - return false; - } - rule.in_query = in_query.first.value(); - } - it = configuration.find("in_header"); - if (it != configuration.end()) { - auto in_header = JsonValueAs(it.value()); - if (in_header.second != Wasm::Common::JsonParserResultDetail::OK || - !in_header.first) { - LOG_WARN("failed to parse 'in_header' field in filter configuration."); - return false; - } - rule.in_header = in_header.first.value(); - } - if (!rule.in_query && !rule.in_header) { - LOG_WARN("at least one of 'in_query' and 'in_header' must set to true"); - return false; + // LOG_DEBUG(rule.debugString("parse phase, consumers branch")); } return true; } @@ -220,6 +293,7 @@ bool PluginRootContext::parsePluginConfig(const json& configuration, bool PluginRootContext::checkPlugin( const KeyAuthConfigRule& rule, const std::optional>& allow_set) { + // LOG_DEBUG(rule.debugString("check phase")); if (rule.consumers.empty()) { for (const auto& key : rule.keys) { auto credential = extractCredential(rule.in_header, rule.in_query, key); @@ -263,9 +337,8 @@ bool PluginRootContext::checkPlugin( continue; } - if (credential != consumer.credential) { - LOG_DEBUG("credential does not match the consumer's credential: " + - credential); + if (consumer.credentials.find(credential) == consumer.credentials.end()) { + LOG_DEBUG("credential " + credential + " does not match the consumer " + consumer.name); continue; } @@ -277,9 +350,13 @@ bool PluginRootContext::checkPlugin( auto credential_to_name_iter = rule.credential_to_name.find(credential); if (credential_to_name_iter != rule.credential_to_name.end()) { - if (allow_set && !allow_set->empty()) { - if (allow_set->find(credential_to_name_iter->second) == - allow_set->end()) { + if (allow_set) { + if (allow_set->empty()) { + LOG_DEBUG("allow set is empty, nobody is allowed"); + deniedUnauthorizedConsumer(rule.realm); + return false; + } + if (allow_set->find(credential_to_name_iter->second) == allow_set->end()) { deniedUnauthorizedConsumer(rule.realm); LOG_DEBUG("unauthorized consumer: " + credential_to_name_iter->second); diff --git a/plugins/wasm-cpp/extensions/key_auth/plugin.h b/plugins/wasm-cpp/extensions/key_auth/plugin.h index 4cf329d90..df4062cd1 100644 --- a/plugins/wasm-cpp/extensions/key_auth/plugin.h +++ b/plugins/wasm-cpp/extensions/key_auth/plugin.h @@ -38,10 +38,26 @@ namespace key_auth { struct Consumer { std::string name; - std::string credential; + std::unordered_set credentials; std::optional> keys; std::optional in_query = std::nullopt; std::optional in_header = std::nullopt; + + // std::string debugString() const { + // std::string msg; + // msg += "name: " + name + "\n"; + // msg += " keys: \n"; + // if (keys.has_value()) { + // for (const auto& item : keys.value()) { + // msg += " - " + item + "\n"; + // } + // } + // msg += " credentials: \n"; + // for (const auto& item : credentials) { + // msg += " - " + item + "\n"; + // } + // return msg; + // } }; struct KeyAuthConfigRule { @@ -52,6 +68,30 @@ struct KeyAuthConfigRule { std::vector keys; bool in_query = true; bool in_header = true; + + // std::string debugString(std::string prompt="") const { + // std::string msg; + // msg += prompt + "\n"; + // msg += "realm: " + realm + "\n"; + // msg += "keys: \n"; + // for (const auto& item : keys) { + // msg += "- " + item + "\n"; + // } + // msg += "credentials: \n"; + // for (const auto& item : credentials) { + // msg += "- " + item + "\n"; + // } + // msg += "credential_to_name: \n"; + // for (const auto& item : credential_to_name) { + // msg += "- " + item.first + ": " + item.second + "\n"; + // } + // msg += "consumers: \n"; + // for (const auto& item : consumers) { + // msg += "- " + item.debugString(); + // } + + // return msg; + // } }; // PluginRootContext is the root context for all streams processed by the diff --git a/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc b/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc index 2f60811e0..d399be4e1 100644 --- a/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc +++ b/plugins/wasm-cpp/extensions/key_auth/plugin_test.cc @@ -148,6 +148,11 @@ TEST_F(KeyAuthTest, InQuery) { path_ = "/test?hello=123&apiKey=123&x-api-key=def"; EXPECT_EQ(context_->onRequestHeaders(0, false), FilterHeadersStatus::Continue); + + route_name_ = "pass"; + path_ = "/pass?hello=123&apiKey=123"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::Continue); } TEST_F(KeyAuthTest, InQueryWithConsumer) { @@ -177,6 +182,35 @@ TEST_F(KeyAuthTest, InQueryWithConsumer) { FilterHeadersStatus::StopIteration); } +TEST_F(KeyAuthTest, EmptyAllowSet) { + std::string configuration = R"( +{ + "consumers" : [{"credential" : "abc", "name" : "consumer1"}], + "keys" : [ "apiKey", "x-api-key" ], + "_rules_" : [ {"_match_route_" : ["test"], "allow" : []}, {"_match_route_prefix_" : ["prefix"], "allow" : []} ] +})"; + BufferBase buffer; + buffer.set(configuration); + EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration)) + .WillOnce([&buffer](WasmBufferType) { return &buffer; }); + EXPECT_TRUE(root_context_->configure(configuration.size())); + + route_name_ = "test"; + path_ = "/test?hello=1&apiKey=abc"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::StopIteration); + + route_name_ = "noauth"; + path_ = "/test?hello=1&apiKey=abc"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::Continue); + + route_name_ = "prefix@operation"; + path_ = "/test?hello=1"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::StopIteration); +} + TEST_F(KeyAuthTest, EmptyConsumer) { std::string configuration = R"( { @@ -301,6 +335,131 @@ TEST_F(KeyAuthTest, ConsumerDifferentKey) { FilterHeadersStatus::Continue); } +TEST_F(KeyAuthTest, ConsumerMultiCredentials) { + std::string configuration = R"( +{ + "global_auth": false, + "consumers": [ + { + "name": "c1", + "credentials":["123","345"], + "keys": ["c1key"], + "in_header": false, + "in_query": true + }, + { + "name": "c2", + "credentials":["abc","def"], + "keys": ["c2key"], + "in_header": false, + "in_query": true + } + ], + "_rules_": [ + { + "_match_route_": ["test"], + "allow": ["c1"] + } + ] +})"; + BufferBase buffer; + buffer.set(configuration); + EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration)) + .WillOnce([&buffer](WasmBufferType) { return &buffer; }); + EXPECT_TRUE(root_context_->configure(configuration.size())); + + route_name_ = "test"; + path_ = "/test?c1key=123"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::Continue); + + path_ = "/test?c2key=adc"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::StopIteration); +} + +TEST_F(KeyAuthTest, ConsumerDefaultKey) { + std::string configuration = R"( +{ + "global_auth": false, + "consumers": [ + { + "name": "c1", + "credentials":["123","345"], + "keys": ["c1key"], + "in_header": false, + "in_query": true + }, + { + "name": "c2", + "credentials":["abc","def"] + } + ], + "_rules_": [ + { + "_match_route_": ["test"], + "allow": ["c2"] + } + ], + "keys": ["defaultkey"] +})"; + BufferBase buffer; + buffer.set(configuration); + EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration)) + .WillOnce([&buffer](WasmBufferType) { return &buffer; }); + EXPECT_TRUE(root_context_->configure(configuration.size())); + + route_name_ = "test"; + path_ = "/test?c1key=123"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::StopIteration); + + path_ = "/test?defaultkey=def"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::Continue); +} + +TEST_F(KeyAuthTest, NoGlobalKeySetting) { + std::string configuration = R"( +{ + "global_auth": false, + "consumers": [ + { + "name": "c1", + "credentials":["123","345"], + "keys": ["c1key"], + "in_header": false, + "in_query": true + }, + { + "name": "c2", + "credentials":["abc","def"], + "keys": ["c2key"] + } + ], + "_rules_": [ + { + "_match_route_": ["test"], + "allow": ["c2"] + } + ] +})"; + BufferBase buffer; + buffer.set(configuration); + EXPECT_CALL(*mock_context_, getBuffer(WasmBufferType::PluginConfiguration)) + .WillOnce([&buffer](WasmBufferType) { return &buffer; }); + EXPECT_TRUE(root_context_->configure(configuration.size())); + + route_name_ = "test"; + path_ = "/test?c1key=123"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::StopIteration); + + path_ = "/test?c2key=def"; + EXPECT_EQ(context_->onRequestHeaders(0, false), + FilterHeadersStatus::Continue); +} + } // namespace key_auth } // namespace null_plugin } // namespace proxy_wasm