Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/envoy/http/authn/authenticator_base_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ class ValidateX509Test : public testing::TestWithParam<iaapi::MutualTls::Mode>,

NiceMock<Envoy::Network::MockConnection> connection_{};
NiceMock<Envoy::Ssl::MockConnection> ssl_{};
Envoy::Http::HeaderMapImpl header_{};
FilterConfig filter_config_{};
FilterContext filter_context_{
envoy::api::v2::core::Metadata::default_instance(), &connection_,
envoy::api::v2::core::Metadata::default_instance(), header_, &connection_,
istio::envoy::config::filter::http::authn::v2alpha1::FilterConfig::
default_instance()};

Expand Down Expand Up @@ -168,8 +169,9 @@ class ValidateJwtTest : public testing::Test,
envoy::api::v2::core::Metadata dynamic_metadata_;
NiceMock<Envoy::Network::MockConnection> connection_{};
// NiceMock<Envoy::Ssl::MockConnection> ssl_{};
Envoy::Http::HeaderMapImpl header_{};
FilterConfig filter_config_{};
FilterContext filter_context_{dynamic_metadata_, &connection_,
FilterContext filter_context_{dynamic_metadata_, header_, &connection_,
filter_config_};
MockAuthenticatorBase authenticator_{&filter_context_};

Expand Down
68 changes: 68 additions & 0 deletions src/envoy/http/authn/authn_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* limitations under the License.
*/

#include <regex>

#include "authn_utils.h"
#include "common/json/json_loader.h"
#include "google/protobuf/struct.pb.h"
Expand Down Expand Up @@ -98,6 +100,72 @@ bool AuthnUtils::ProcessJwtPayload(const std::string& payload_str,
return true;
}

bool AuthnUtils::MatchString(const char* const str,
const iaapi::StringMatch& match) {
if (str == nullptr) {
return false;
}
switch (match.match_type_case()) {
case iaapi::StringMatch::kExact: {
return match.exact().compare(str) == 0;
}
case iaapi::StringMatch::kPrefix: {
return StringUtil::startsWith(str, match.prefix());
}
case iaapi::StringMatch::kSuffix: {
return StringUtil::endsWith(str, match.suffix());
}
case iaapi::StringMatch::kRegex: {
return std::regex_match(str, std::regex(match.regex()));
}
default:
return false;
}
}

static bool matchRule(const char* const path,
const iaapi::Jwt_TriggerRule& rule) {
for (const auto& excluded : rule.excluded_paths()) {
if (AuthnUtils::MatchString(path, excluded)) {
// The rule is not matched if any of excluded_paths matched.
return false;
}
}

if (rule.included_paths_size() > 0) {
for (const auto& included : rule.included_paths()) {
if (AuthnUtils::MatchString(path, included)) {
// The rule is matched if any of included_paths matched.
return true;
}
}

// The rule is not matched if included_paths is not empty and none of them
// matched.
return false;
}

// The rule is matched if none of excluded_paths matched and included_paths is
// empty.
return true;
}

bool AuthnUtils::ShouldValidateJwtPerPath(const char* const path,
const iaapi::Jwt& jwt) {
// If the path is nullptr which shouldn't happen for a HTTP request or if
// there are no trigger rules at all, then simply return true as if there're
// no per-path jwt support.
if (path == nullptr || jwt.trigger_rules_size() == 0) {
return true;
}
for (const auto& rule : jwt.trigger_rules()) {
if (matchRule(path, rule)) {
return true;
}
}
return false;
}

} // namespace AuthN
} // namespace Istio
} // namespace Http
Expand Down
13 changes: 13 additions & 0 deletions src/envoy/http/authn/authn_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@

#pragma once

#include "authentication/v1alpha1/policy.pb.h"
#include "common/common/logger.h"
#include "common/common/utility.h"
#include "envoy/http/header_map.h"
#include "envoy/json/json_object.h"
#include "src/istio/authn/context.pb.h"

namespace iaapi = istio::authentication::v1alpha1;

namespace Envoy {
namespace Http {
namespace Istio {
Expand All @@ -33,6 +37,15 @@ class AuthnUtils : public Logger::Loggable<Logger::Id::filter> {
// successfully. Otherwise, return false.
static bool ProcessJwtPayload(const std::string& jwt_payload_str,
istio::authn::JwtPayload* payload);

// Returns true if str is matched to match.
static bool MatchString(const char* const str,
const iaapi::StringMatch& match);

// Returns true if the jwt should be validated. It will check if the request
// path is matched to the trigger rule in the jwt.
static bool ShouldValidateJwtPerPath(const char* const path,
const iaapi::Jwt& jwt);
};

} // namespace AuthN
Expand Down
95 changes: 95 additions & 0 deletions src/envoy/http/authn/authn_utils_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,101 @@ TEST(AuthnUtilsTest, ProcessJwtPayloadWithTwoAudTest) {
EXPECT_TRUE(MessageDifferencer::Equals(expected_payload, payload));
}

TEST(AuthnUtilsTest, MatchString) {
iaapi::StringMatch match;
EXPECT_FALSE(AuthnUtils::MatchString(nullptr, match));
EXPECT_FALSE(AuthnUtils::MatchString("", match));

match.set_exact("exact");
EXPECT_TRUE(AuthnUtils::MatchString("exact", match));
EXPECT_FALSE(AuthnUtils::MatchString("exac", match));
EXPECT_FALSE(AuthnUtils::MatchString("exacy", match));

match.set_prefix("prefix");
EXPECT_TRUE(AuthnUtils::MatchString("prefix-1", match));
EXPECT_TRUE(AuthnUtils::MatchString("prefix", match));
EXPECT_FALSE(AuthnUtils::MatchString("prefi", match));
EXPECT_FALSE(AuthnUtils::MatchString("prefiy", match));

match.set_suffix("suffix");
EXPECT_TRUE(AuthnUtils::MatchString("1-suffix", match));
EXPECT_TRUE(AuthnUtils::MatchString("suffix", match));
EXPECT_FALSE(AuthnUtils::MatchString("suffi", match));
EXPECT_FALSE(AuthnUtils::MatchString("suffiy", match));

match.set_regex(".+abc.+");
EXPECT_TRUE(AuthnUtils::MatchString("1-abc-1", match));
EXPECT_FALSE(AuthnUtils::MatchString("1-abc", match));
EXPECT_FALSE(AuthnUtils::MatchString("abc-1", match));
EXPECT_FALSE(AuthnUtils::MatchString("1-ac-1", match));
}

TEST(AuthnUtilsTest, ShouldValidateJwtPerPathExcluded) {
iaapi::Jwt jwt;

// Create a rule that triggers on everything except /good-x and /allow-x.
auto* rule = jwt.add_trigger_rules();
rule->add_excluded_paths()->set_exact("/good-x");
rule->add_excluded_paths()->set_exact("/allow-x");
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt));
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-1", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt));

// Change the rule to only triggers on prefix /good and /allow.
rule->add_included_paths()->set_prefix("/good");
rule->add_included_paths()->set_prefix("/allow");
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt));
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-1", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt));
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt));
}

TEST(AuthnUtilsTest, ShouldValidateJwtPerPathIncluded) {
iaapi::Jwt jwt;

// Create a rule that triggers on everything with prefix /good and /allow.
auto* rule = jwt.add_trigger_rules();
rule->add_included_paths()->set_prefix("/good");
rule->add_included_paths()->set_prefix("/allow");
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-2", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt));
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt));

// Change the rule to also exclude /allow-x and /good-x.
rule->add_excluded_paths()->set_exact("/good-x");
rule->add_excluded_paths()->set_exact("/allow-x");
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/good-x", jwt));
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/allow-x", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/good-2", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/allow-1", jwt));
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt));
}

TEST(AuthnUtilsTest, ShouldValidateJwtPerPathDefault) {
iaapi::Jwt jwt;

// Always trigger when path is unavailable.
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath(nullptr, jwt));

// Always trigger when there is no rules in jwt.
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/test", jwt));

// Add a rule that triggers on everything except /hello.
jwt.add_trigger_rules()->add_excluded_paths()->set_exact("/hello");
EXPECT_FALSE(AuthnUtils::ShouldValidateJwtPerPath("/hello", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt));

// Add another rule that triggers on path /hello.
jwt.add_trigger_rules()->add_included_paths()->set_exact("/hello");
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/hello", jwt));
EXPECT_TRUE(AuthnUtils::ShouldValidateJwtPerPath("/other", jwt));
}

} // namespace
} // namespace AuthN
} // namespace Istio
Expand Down
10 changes: 9 additions & 1 deletion src/envoy/http/authn/filter_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include "common/common/logger.h"
#include "envoy/api/v2/core/base.pb.h"
#include "envoy/config/filter/http/authn/v2alpha1/config.pb.h"
#include "envoy/http/filter.h"
#include "envoy/network/connection.h"
#include "src/istio/authn/context.pb.h"

Expand All @@ -33,10 +34,11 @@ class FilterContext : public Logger::Loggable<Logger::Id::filter> {
public:
FilterContext(
const envoy::api::v2::core::Metadata& dynamic_metadata,
const Network::Connection* connection,
const HeaderMap& header_map, const Network::Connection* connection,
const istio::envoy::config::filter::http::authn::v2alpha1::FilterConfig&
filter_config)
: dynamic_metadata_(dynamic_metadata),
header_map_(header_map),
connection_(connection),
filter_config_(filter_config) {}
virtual ~FilterContext() {}
Expand Down Expand Up @@ -70,11 +72,17 @@ class FilterContext : public Logger::Loggable<Logger::Id::filter> {
// returns false.
bool getJwtPayload(const std::string& issuer, std::string* payload) const;

const HeaderMap& headerMap() const { return header_map_; }

private:
// Const reference to request info dynamic metadata. This provides data that
// output from other filters, e.g JWT.
const envoy::api::v2::core::Metadata& dynamic_metadata_;

// Const reference to header map of the request. This provides request path
// that could be used to decide if a JWT should be used for validation.
const HeaderMap& header_map_;

// Pointer to network connection of the request.
const Network::Connection* connection_;

Expand Down
3 changes: 2 additions & 1 deletion src/envoy/http/authn/filter_context_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ class FilterContextTest : public testing::Test {
virtual ~FilterContextTest() {}

envoy::api::v2::core::Metadata metadata_;
Envoy::Http::TestHeaderMapImpl header_{};
// This test suit does not use connection, so ok to use null for it.
FilterContext filter_context_{metadata_, nullptr,
FilterContext filter_context_{metadata_, header_, nullptr,
istio::envoy::config::filter::http::authn::
v2alpha1::FilterConfig::default_instance()};

Expand Down
5 changes: 3 additions & 2 deletions src/envoy/http/authn/http_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ void AuthenticationFilter::onDestroy() {
ENVOY_LOG(debug, "Called AuthenticationFilter : {}", __func__);
}

FilterHeadersStatus AuthenticationFilter::decodeHeaders(HeaderMap&, bool) {
FilterHeadersStatus AuthenticationFilter::decodeHeaders(HeaderMap& headers,
bool) {
ENVOY_LOG(debug, "AuthenticationFilter::decodeHeaders with config\n{}",
filter_config_.DebugString());
state_ = State::PROCESSING;

filter_context_.reset(new Istio::AuthN::FilterContext(
decoder_callbacks_->requestInfo().dynamicMetadata(),
decoder_callbacks_->requestInfo().dynamicMetadata(), headers,
decoder_callbacks_->connection(), filter_config_));

Payload payload;
Expand Down
31 changes: 27 additions & 4 deletions src/envoy/http/authn/origin_authenticator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

#include "src/envoy/http/authn/origin_authenticator.h"
#include "authentication/v1alpha1/policy.pb.h"
#include "src/envoy/http/authn/authn_utils.h"

using istio::authn::Payload;

Expand Down Expand Up @@ -57,10 +58,30 @@ bool OriginAuthenticator::run(Payload* payload) {
}
}

bool triggered = false;
const char* request_path = nullptr;
if (filter_context()->headerMap().Path() != nullptr) {
request_path = filter_context()->headerMap().Path()->value().c_str();
ENVOY_LOG(debug, "Got request path {}", request_path);
} else {
ENVOY_LOG(error,
"Failed to get request path, JWT will always be used for "
"validation");
}

for (const auto& method : policy_.origins()) {
if (validateJwt(method.jwt(), payload)) {
success = true;
break;
const auto& jwt = method.jwt();

if (AuthnUtils::ShouldValidateJwtPerPath(request_path, jwt)) {
ENVOY_LOG(debug, "Validating request path {} for jwt {}", request_path,
jwt.DebugString());
// set triggered to true if any of the jwt trigger rule matched.
triggered = true;
if (validateJwt(jwt, payload)) {
ENVOY_LOG(debug, "JWT validation succeeded");
success = true;
break;
}
}
}

Expand All @@ -69,7 +90,9 @@ bool OriginAuthenticator::run(Payload* payload) {
filter_context()->setPrincipal(policy_.principal_binding());
}

return success;
// If none of the JWT triggered, origin authentication will be ignored, as if
// it is not defined.
return !triggered || success;
}

} // namespace AuthN
Expand Down
Loading