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
20 changes: 19 additions & 1 deletion api/envoy/config/filter/network/rbac/v2/rbac.proto
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import "gogoproto/gogo.proto";

// RBAC network filter config.
//
// Header and Metadata should not be used in rules/shadow_rules in RBAC network filter as
// Header should not be used in rules/shadow_rules in RBAC network filter as
// this information is only available in :ref:`RBAC http filter <config_http_filters_rbac>`.
message RBAC {
// Specify the RBAC rules to be applied globally.
Expand All @@ -27,4 +27,22 @@ message RBAC {

// The prefix to use when emitting statistics.
string stat_prefix = 3 [(validate.rules).string.min_bytes = 1];

enum EnforcementType {
// Apply RBAC policies when the first byte of data arrives on the connection.
ONE_TIME_ON_FIRST_BYTE = 0;

// Continuously apply RBAC policies as data arrives. Use this mode when
// using RBAC with message oriented protocols such as Mongo, MySQL, Kafka,
// etc. when the protocol decoders emit dynamic metadata such as the
// resources being accessed and the operations on the resources.
CONTINUOUS = 1;
};

// RBAC enforcement strategy. By default RBAC will be enforced only once
// when the first byte of data arrives from the downstream. When used in
// conjunction with filters that emit dynamic metadata after decoding
// every payload (e.g., Mongo, MySQL, Kafka) set the enforcement type to
// CONTINUOUS to enforce RBAC policies on every message boundary.
EnforcementType enforcement_type = 4;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

A general question: how this work with allow-list type of rules? i.e. during the connection establishment the connection doesn't have enough metadata, but after decoding it will gain the access, won't the connection be dropped during connection establishment? Definitely worth more comment.

}
6 changes: 2 additions & 4 deletions api/envoy/config/rbac/v2alpha/rbac.proto
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ message Permission {
// A port number that describes the destination port connecting to.
uint32 destination_port = 6 [(validate.rules).uint32.lte = 65535];

// Metadata that describes additional information about the action. Only available for HTTP
// request.
// Metadata that describes additional information about the action.
envoy.type.matcher.MetadataMatcher metadata = 7;

// Negates matching the provided permission. For instance, if the value of `not_rule` would
Expand Down Expand Up @@ -191,8 +190,7 @@ message Principal {
// available for HTTP request.
envoy.api.v2.route.HeaderMatcher header = 6;

// Metadata that describes additional information about the principal. Only available for HTTP
// request.
// Metadata that describes additional information about the principal.
envoy.type.matcher.MetadataMatcher metadata = 7;

// Negates matching the provided principal. For instance, if the value of `not_id` would match,
Expand Down
14 changes: 7 additions & 7 deletions source/extensions/filters/network/rbac/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ namespace Extensions {
namespace NetworkFilters {
namespace RBACFilter {

static void validateFail(const std::string& header, const std::string& metadata) {
throw EnvoyException(fmt::format("Found header({}) or metadata({}) rule,"
static void validateFail(const std::string& header) {
throw EnvoyException(fmt::format("Found header({}) rule,"
"not supported by RBAC network filter",
header, metadata));
header));
}

static void validatePermission(const envoy::config::rbac::v2alpha::Permission& permission) {
if (permission.has_header() || permission.has_metadata()) {
validateFail(permission.header().DebugString(), permission.metadata().DebugString());
if (permission.has_header()) {
validateFail(permission.header().DebugString());
}
if (permission.has_and_rules()) {
for (const auto& r : permission.and_rules().rules()) {
Expand All @@ -37,8 +37,8 @@ static void validatePermission(const envoy::config::rbac::v2alpha::Permission& p
}

static void validatePrincipal(const envoy::config::rbac::v2alpha::Principal& principal) {
if (principal.has_header() || principal.has_metadata()) {
validateFail(principal.header().DebugString(), principal.metadata().DebugString());
if (principal.has_header()) {
validateFail(principal.header().DebugString());
}
if (principal.has_and_ids()) {
for (const auto& r : principal.and_ids().ids()) {
Expand Down
22 changes: 14 additions & 8 deletions source/extensions/filters/network/rbac/rbac_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ RoleBasedAccessControlFilterConfig::RoleBasedAccessControlFilterConfig(
const envoy::config::filter::network::rbac::v2::RBAC& proto_config, Stats::Scope& scope)
: stats_(Filters::Common::RBAC::generateStats(proto_config.stat_prefix(), scope)),
engine_(Filters::Common::RBAC::createEngine(proto_config)),
shadow_engine_(Filters::Common::RBAC::createShadowEngine(proto_config)) {}
shadow_engine_(Filters::Common::RBAC::createShadowEngine(proto_config)),
enforcement_type_(proto_config.enforcement_type()) {}

Network::FilterStatus RoleBasedAccessControlFilter::onData(Buffer::Instance&, bool) {
ENVOY_LOG(
Expand All @@ -31,15 +32,20 @@ Network::FilterStatus RoleBasedAccessControlFilter::onData(Buffer::Instance&, bo
: "none",
callbacks_->connection().streamInfo().dynamicMetadata().DebugString());

if (shadow_engine_result_ == Unknown) {
// TODO(quanlin): Pass the shadow engine results to other filters.
// Only check the engine and increase stats for the first time call to onData(), any following
// calls to onData() could just use the cached result and no need to increase the stats anymore.
// When the enforcement type is continuous always do the RBAC checks. If it is a one time check,
// run the check once and skip it for subsequent onData calls.
if (config_->enforcementType() == envoy::config::filter::network::rbac::v2::RBAC::CONTINUOUS) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In the continuous case, shadow rules should stop evaluation if the result is DENY:

if (config_->enforcementType() == envoy::config::filter::network::rbac::v2::RBAC::CONTINUOUS) {
  if (shadow_engine_result_ != DENY) {
     shadow_engine_result_ = checkEngine(Filters::Common::RBAC::EnforcementMode::Shadow);
  }
  engine_result_ = checkEngine(Filters::Common::RBAC::EnforcementMode::Enforced);
}
...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would you mind to clarify more about why should we stop the shadow rule evaluation if the result is DENY?
The metadata generated by other filters could be different in following onData() calls which means the shadow rule evaluation could also have different results for each onData() call.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thats a good point.. Unlike the real rule that terminates the connection, SHADOW rules simply spit out stats about whether each query was allowed or denied..

shadow_engine_result_ = checkEngine(Filters::Common::RBAC::EnforcementMode::Shadow);
}

if (engine_result_ == Unknown) {
engine_result_ = checkEngine(Filters::Common::RBAC::EnforcementMode::Enforced);
} else {
if (shadow_engine_result_ == Unknown) {
// TODO(quanlin): Pass the shadow engine results to other filters.
shadow_engine_result_ = checkEngine(Filters::Common::RBAC::EnforcementMode::Shadow);
}

if (engine_result_ == Unknown) {
engine_result_ = checkEngine(Filters::Common::RBAC::EnforcementMode::Enforced);
}
}

if (engine_result_ == Allow) {
Expand Down
5 changes: 5 additions & 0 deletions source/extensions/filters/network/rbac/rbac_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@ class RoleBasedAccessControlFilterConfig {
return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_ : shadow_engine_;
}

envoy::config::filter::network::rbac::v2::RBAC::EnforcementType enforcementType() const {
return enforcement_type_;
}

private:
Filters::Common::RBAC::RoleBasedAccessControlFilterStats stats_;

const absl::optional<Filters::Common::RBAC::RoleBasedAccessControlEngineImpl> engine_;
const absl::optional<Filters::Common::RBAC::RoleBasedAccessControlEngineImpl> shadow_engine_;
const envoy::config::filter::network::rbac::v2::RBAC::EnforcementType enforcement_type_;
};

typedef std::shared_ptr<RoleBasedAccessControlFilterConfig>
Expand Down
8 changes: 0 additions & 8 deletions test/extensions/filters/network/rbac/config_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,12 @@ const std::string header = R"EOF(
{ "header": {"name": "key", "exact_match": "value"} }
)EOF";

const std::string metadata = R"EOF(
{
"metadata": {
"filter": "t", "path": [ { "key": "a" } ], "value": { "string_match": { "exact": "x" } }
}
}
)EOF";
} // namespace

class RoleBasedAccessControlNetworkFilterConfigFactoryTest : public testing::Test {
public:
void validateRule(const std::string& policy_json) {
checkRule(fmt::sprintf(policy_json, header));
checkRule(fmt::sprintf(policy_json, metadata));
}

private:
Expand Down
26 changes: 24 additions & 2 deletions test/extensions/filters/network/rbac/filter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ namespace RBACFilter {

class RoleBasedAccessControlNetworkFilterTest : public testing::Test {
public:
RoleBasedAccessControlFilterConfigSharedPtr setupConfig(bool with_policy = true) {
RoleBasedAccessControlFilterConfigSharedPtr setupConfig(bool with_policy = true,
bool continuous = false) {
envoy::config::filter::network::rbac::v2::RBAC config;
config.set_stat_prefix("tcp.");

Expand All @@ -41,6 +42,10 @@ class RoleBasedAccessControlNetworkFilterTest : public testing::Test {
(*config.mutable_shadow_rules()->mutable_policies())["bar"] = shadow_policy;
}

if (continuous) {
config.set_enforcement_type(envoy::config::filter::network::rbac::v2::RBAC::CONTINUOUS);
}

return std::make_shared<RoleBasedAccessControlFilterConfig>(config, store_);
}

Expand Down Expand Up @@ -83,7 +88,7 @@ class RoleBasedAccessControlNetworkFilterTest : public testing::Test {
std::string requested_server_name_;
};

TEST_F(RoleBasedAccessControlNetworkFilterTest, Allowed) {
TEST_F(RoleBasedAccessControlNetworkFilterTest, AllowedWithOneTimeEnforcement) {
setDestinationPort(123);

EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection());
Expand All @@ -97,6 +102,23 @@ TEST_F(RoleBasedAccessControlNetworkFilterTest, Allowed) {
EXPECT_EQ(1U, config_->stats().shadow_denied_.value());
}

TEST_F(RoleBasedAccessControlNetworkFilterTest, AllowedWithContinuousEnforcement) {
config_ = setupConfig(true, true /* continuous enforcement */);
filter_ = std::make_unique<RoleBasedAccessControlFilter>(config_);
filter_->initializeReadFilterCallbacks(callbacks_);
setDestinationPort(123);

EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection());

// Call onData() twice, should increase stats twice.
EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false));
EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false));
EXPECT_EQ(2U, config_->stats().allowed_.value());
EXPECT_EQ(0U, config_->stats().denied_.value());
EXPECT_EQ(0U, config_->stats().shadow_allowed_.value());
EXPECT_EQ(2U, config_->stats().shadow_denied_.value());
}

TEST_F(RoleBasedAccessControlNetworkFilterTest, RequestedServerName) {
setDestinationPort(999);
setRequestedServerName("www.cncf.io");
Expand Down