diff --git a/api/docs/BUILD b/api/docs/BUILD index 7f0a1de708c9..448534f9e8f6 100644 --- a/api/docs/BUILD +++ b/api/docs/BUILD @@ -47,6 +47,7 @@ proto_library( "//envoy/config/filter/network/http_connection_manager/v2:http_connection_manager", "//envoy/config/filter/network/mongo_proxy/v2:mongo_proxy", "//envoy/config/filter/network/rate_limit/v2:rate_limit", + "//envoy/config/filter/network/rbac/v2:rbac", "//envoy/config/filter/network/redis_proxy/v2:redis_proxy", "//envoy/config/filter/network/tcp_proxy/v2:tcp_proxy", "//envoy/config/grpc_credential/v2alpha:file_based_metadata", diff --git a/api/envoy/config/filter/http/rbac/v2/rbac.proto b/api/envoy/config/filter/http/rbac/v2/rbac.proto index 6292a829ba40..2947c8b69a09 100644 --- a/api/envoy/config/filter/http/rbac/v2/rbac.proto +++ b/api/envoy/config/filter/http/rbac/v2/rbac.proto @@ -19,7 +19,7 @@ message RBAC { // Shadow rules are not enforced by the filter (i.e., returning a 403) // but will emit stats and logs and can be used for rule testing. - // If absent, no shadow RBAC policy with be applied. + // If absent, no shadow RBAC policy will be applied. config.rbac.v2alpha.RBAC shadow_rules = 2; } diff --git a/api/envoy/config/filter/network/rbac/v2/BUILD b/api/envoy/config/filter/network/rbac/v2/BUILD new file mode 100644 index 000000000000..d325e3bcde2d --- /dev/null +++ b/api/envoy/config/filter/network/rbac/v2/BUILD @@ -0,0 +1,9 @@ +load("//bazel:api_build_system.bzl", "api_proto_library_internal") + +licenses(["notice"]) # Apache 2 + +api_proto_library_internal( + name = "rbac", + srcs = ["rbac.proto"], + deps = ["//envoy/config/rbac/v2alpha:rbac"], +) diff --git a/api/envoy/config/filter/network/rbac/v2/rbac.proto b/api/envoy/config/filter/network/rbac/v2/rbac.proto new file mode 100644 index 000000000000..c16ac6838e16 --- /dev/null +++ b/api/envoy/config/filter/network/rbac/v2/rbac.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package envoy.config.filter.network.rbac.v2; +option go_package = "v2"; + +import "envoy/config/rbac/v2alpha/rbac.proto"; + +import "validate/validate.proto"; +import "gogoproto/gogo.proto"; + +// [#protodoc-title: RBAC] +// Role-Based Access Control :ref:`configuration overview `. + +// RBAC network filter config. +// +// Header and Metadata should not be used in rules/shadow_rules in RBAC network filter as +// this information is only available in :ref:`RBAC http filter `. +message RBAC { + // Specify the RBAC rules to be applied globally. + // If absent, no enforcing RBAC policy will be applied. + config.rbac.v2alpha.RBAC rules = 1; + + // Shadow rules are not enforced by the filter but will emit stats and logs + // and can be used for rule testing. + // If absent, no shadow RBAC policy will be applied. + config.rbac.v2alpha.RBAC shadow_rules = 2; + + // The prefix to use when emitting statistics. + string stat_prefix = 3 [(validate.rules).string.min_bytes = 1]; +} diff --git a/api/envoy/config/rbac/v2alpha/rbac.proto b/api/envoy/config/rbac/v2alpha/rbac.proto index ab32aaf475fd..3f1f3eadbcc4 100644 --- a/api/envoy/config/rbac/v2alpha/rbac.proto +++ b/api/envoy/config/rbac/v2alpha/rbac.proto @@ -103,7 +103,8 @@ message Permission { // When any is set, it matches any action. bool any = 3 [(validate.rules).bool.const = true]; - // A header (or psuedo-header such as :path or :method) on the incoming HTTP request. + // A header (or psuedo-header such as :path or :method) on the incoming HTTP request. Only + // available for HTTP request. envoy.api.v2.route.HeaderMatcher header = 4; // A CIDR block that describes the destination IP. @@ -112,7 +113,8 @@ 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. + // Metadata that describes additional information about the action. Only available for HTTP + // request. envoy.type.matcher.MetadataMatcher metadata = 7; // Negates matching the provided permission. For instance, if the value of `not_rule` would @@ -156,10 +158,12 @@ message Principal { // A CIDR block that describes the downstream IP. envoy.api.v2.core.CidrRange source_ip = 5; - // A header (or psuedo-header such as :path or :method) on the incoming HTTP request. + // A header (or psuedo-header such as :path or :method) on the incoming HTTP request. Only + // available for HTTP request. envoy.api.v2.route.HeaderMatcher header = 6; - // Metadata that describes additional information about the principal. + // Metadata that describes additional information about the principal. Only available for HTTP + // request. envoy.type.matcher.MetadataMatcher metadata = 7; // Negates matching the provided principal. For instance, if the value of `not_id` would match, diff --git a/docs/build.sh b/docs/build.sh index d4800c8d345b..c2dd478e8b13 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -98,6 +98,7 @@ PROTO_RST=" /envoy/config/filter/network/http_connection_manager/v2/http_connection_manager/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto.rst /envoy/config/filter/network/mongo_proxy/v2/mongo_proxy/envoy/config/filter/network/mongo_proxy/v2/mongo_proxy.proto.rst /envoy/config/filter/network/rate_limit/v2/rate_limit/envoy/config/filter/network/rate_limit/v2/rate_limit.proto.rst + /envoy/config/filter/network/rbac/v2/rbac/envoy/config/filter/network/rbac/v2/rbac.proto.rst /envoy/config/filter/network/redis_proxy/v2/redis_proxy/envoy/config/filter/network/redis_proxy/v2/redis_proxy.proto.rst /envoy/config/filter/network/tcp_proxy/v2/tcp_proxy/envoy/config/filter/network/tcp_proxy/v2/tcp_proxy.proto.rst /envoy/config/health_checker/redis/v2/redis/envoy/config/health_checker/redis/v2/redis.proto.rst diff --git a/docs/root/configuration/network_filters/network_filters.rst b/docs/root/configuration/network_filters/network_filters.rst index 4edc6c8379c6..55fa77b33350 100644 --- a/docs/root/configuration/network_filters/network_filters.rst +++ b/docs/root/configuration/network_filters/network_filters.rst @@ -15,5 +15,6 @@ filters. ext_authz_filter mongo_proxy_filter rate_limit_filter + rbac_filter redis_proxy_filter tcp_proxy_filter diff --git a/docs/root/configuration/network_filters/rbac_filter.rst b/docs/root/configuration/network_filters/rbac_filter.rst new file mode 100644 index 000000000000..d38dd6f07030 --- /dev/null +++ b/docs/root/configuration/network_filters/rbac_filter.rst @@ -0,0 +1,27 @@ +.. _config_network_filters_rbac: + +Role Based Access Control (RBAC) Network Filter +=============================================== + +The RBAC network filter is used to authorize actions (permissions) by identified downstream clients +(principals). This is useful to explicitly manage callers to an application and protect it from +unexpected or forbidden agents. The filter supports configuration with either a safe-list (ALLOW) or +block-list (DENY) set of policies based on properties of the connection (IPs, ports, SSL subject). +This filter also supports policy in both enforcement and shadow modes. Shadow mode won't effect real +users, it is used to test that a new set of policies work before rolling out to production. + +* :ref:`v2 API reference ` + +Statistics +---------- + +The RBAC network filter outputs statistics in the *.rbac.* namespace. + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + allowed, Counter, Total requests that were allowed access + denied, Counter, Total requests that were denied access + shadow_allowed, Counter, Total requests that would be allowed access by the filter's shadow rules + shadow_denied, Counter, Total requests that would be denied access by the filter's shadow rules diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 5050721d5c85..d574493ff0c7 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -52,6 +52,7 @@ Version history :ref:`use_data_plane_proto` boolean flag in the ratelimit configuration. Support for the legacy proto :repo:`source/common/ratelimit/ratelimit.proto` is deprecated and will be removed at the start of the 1.9.0 release cycle. +* rbac network filter: a :ref:`role-based access control network filter ` has been added. * rest-api: added ability to set the :ref:`request timeout ` for REST API requests. * router: added ability to set request/response headers at the :ref:`envoy_api_msg_route.Route` level. * tracing: added support for configuration of :ref:`tracing sampling diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index a77203f1c51c..555da8826383 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -32,8 +32,8 @@ EXTENSIONS = { "envoy.filters.http.grpc_json_transcoder": "//source/extensions/filters/http/grpc_json_transcoder:config", "envoy.filters.http.grpc_web": "//source/extensions/filters/http/grpc_web:config", "envoy.filters.http.gzip": "//source/extensions/filters/http/gzip:config", - "envoy.filters.http.health_check": "//source/extensions/filters/http/health_check:config", "envoy.filters.http.header_to_metadata": "//source/extensions/filters/http/header_to_metadata:config", + "envoy.filters.http.health_check": "//source/extensions/filters/http/health_check:config", "envoy.filters.http.ip_tagging": "//source/extensions/filters/http/ip_tagging:config", "envoy.filters.http.jwt_authn": "//source/extensions/filters/http/jwt_authn:config", "envoy.filters.http.lua": "//source/extensions/filters/http/lua:config", @@ -65,8 +65,9 @@ EXTENSIONS = { "envoy.filters.network.ext_authz": "//source/extensions/filters/network/ext_authz:config", "envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config", "envoy.filters.network.mongo_proxy": "//source/extensions/filters/network/mongo_proxy:config", - "envoy.filters.network.redis_proxy": "//source/extensions/filters/network/redis_proxy:config", "envoy.filters.network.ratelimit": "//source/extensions/filters/network/ratelimit:config", + "envoy.filters.network.rbac": "//source/extensions/filters/network/rbac:config", + "envoy.filters.network.redis_proxy": "//source/extensions/filters/network/redis_proxy:config", "envoy.filters.network.tcp_proxy": "//source/extensions/filters/network/tcp_proxy:config", "envoy.filters.network.thrift_proxy": "//source/extensions/filters/network/thrift_proxy:config", diff --git a/source/extensions/filters/common/rbac/BUILD b/source/extensions/filters/common/rbac/BUILD index 0170fc2d7b8f..5e5ee7db77de 100644 --- a/source/extensions/filters/common/rbac/BUILD +++ b/source/extensions/filters/common/rbac/BUILD @@ -8,6 +8,18 @@ load( envoy_package() +envoy_cc_library( + name = "utility_lib", + srcs = ["utility.cc"], + hdrs = ["utility.h"], + deps = [ + ":engine_lib", + "//include/envoy/stats:stats_macros", + "@envoy_api//envoy/config/filter/http/rbac/v2:rbac_cc", + "@envoy_api//envoy/config/filter/network/rbac/v2:rbac_cc", + ], +) + envoy_cc_library( name = "matchers_lib", srcs = ["matchers.cc"], diff --git a/source/extensions/filters/common/rbac/engine.h b/source/extensions/filters/common/rbac/engine.h index e966fc3e5489..05478ece09ef 100644 --- a/source/extensions/filters/common/rbac/engine.h +++ b/source/extensions/filters/common/rbac/engine.h @@ -31,6 +31,13 @@ class RoleBasedAccessControlEngine { virtual bool allowed(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, const envoy::api::v2::core::Metadata& metadata, std::string* effective_policy_id) const PURE; + + /** + * Returns whether or not the current action is permitted. + * + * @param connection the downstream connection used to identify the action/principal. + */ + virtual bool allowed(const Network::Connection& connection) const PURE; }; } // namespace RBAC diff --git a/source/extensions/filters/common/rbac/engine_impl.cc b/source/extensions/filters/common/rbac/engine_impl.cc index 0d3a01fdde89..404dadd1e7a7 100644 --- a/source/extensions/filters/common/rbac/engine_impl.cc +++ b/source/extensions/filters/common/rbac/engine_impl.cc @@ -1,11 +1,18 @@ #include "extensions/filters/common/rbac/engine_impl.h" +#include "common/http/header_map_impl.h" + namespace Envoy { namespace Extensions { namespace Filters { namespace Common { namespace RBAC { +namespace { +const Envoy::Http::HeaderMapImpl empty_header = Envoy::Http::HeaderMapImpl(); +const envoy::api::v2::core::Metadata empty_metadata = envoy::api::v2::core::Metadata(); +} // namespace + RoleBasedAccessControlEngineImpl::RoleBasedAccessControlEngineImpl( const envoy::config::rbac::v2alpha::RBAC& rules) : allowed_if_matched_(rules.action() == @@ -37,6 +44,10 @@ bool RoleBasedAccessControlEngineImpl::allowed(const Network::Connection& connec return matched == allowed_if_matched_; } +bool RoleBasedAccessControlEngineImpl::allowed(const Network::Connection& connection) const { + return allowed(connection, empty_header, empty_metadata, nullptr); +} + } // namespace RBAC } // namespace Common } // namespace Filters diff --git a/source/extensions/filters/common/rbac/engine_impl.h b/source/extensions/filters/common/rbac/engine_impl.h index e8343d737d94..9868325f3b93 100644 --- a/source/extensions/filters/common/rbac/engine_impl.h +++ b/source/extensions/filters/common/rbac/engine_impl.h @@ -19,6 +19,8 @@ class RoleBasedAccessControlEngineImpl : public RoleBasedAccessControlEngine { const envoy::api::v2::core::Metadata& metadata, std::string* effective_policy_id) const override; + bool allowed(const Network::Connection& connection) const override; + private: const bool allowed_if_matched_; diff --git a/source/extensions/filters/common/rbac/utility.cc b/source/extensions/filters/common/rbac/utility.cc new file mode 100644 index 000000000000..0b96bd785947 --- /dev/null +++ b/source/extensions/filters/common/rbac/utility.cc @@ -0,0 +1,18 @@ +#include "extensions/filters/common/rbac/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +RoleBasedAccessControlFilterStats generateStats(const std::string& prefix, Stats::Scope& scope) { + const std::string final_prefix = prefix + "rbac."; + return {ALL_RBAC_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; +} + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/rbac/utility.h b/source/extensions/filters/common/rbac/utility.h new file mode 100644 index 000000000000..0b40e74dc017 --- /dev/null +++ b/source/extensions/filters/common/rbac/utility.h @@ -0,0 +1,54 @@ +#pragma once + +#include "envoy/config/filter/http/rbac/v2/rbac.pb.h" +#include "envoy/config/filter/network/rbac/v2/rbac.pb.h" +#include "envoy/stats/stats_macros.h" + +#include "extensions/filters/common/rbac/engine_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +/** + * All stats for the RBAC filter. @see stats_macros.h + */ +// clang-format off +#define ALL_RBAC_FILTER_STATS(COUNTER) \ + COUNTER(allowed) \ + COUNTER(denied) \ + COUNTER(shadow_allowed) \ + COUNTER(shadow_denied) +// clang-format on + +/** + * Wrapper struct for RBAC filter stats. @see stats_macros.h + */ +struct RoleBasedAccessControlFilterStats { + ALL_RBAC_FILTER_STATS(GENERATE_COUNTER_STRUCT) +}; + +RoleBasedAccessControlFilterStats generateStats(const std::string& prefix, Stats::Scope& scope); + +enum class EnforcementMode { Enforced, Shadow }; + +template +absl::optional createEngine(const ConfigType& config) { + return config.has_rules() ? absl::make_optional(config.rules()) + : absl::nullopt; +} + +template +absl::optional createShadowEngine(const ConfigType& config) { + return config.has_shadow_rules() + ? absl::make_optional(config.shadow_rules()) + : absl::nullopt; +} + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/rbac/BUILD b/source/extensions/filters/http/rbac/BUILD index 0dfa0574e1a1..f7adc41aa850 100644 --- a/source/extensions/filters/http/rbac/BUILD +++ b/source/extensions/filters/http/rbac/BUILD @@ -29,6 +29,7 @@ envoy_cc_library( "//include/envoy/stats:stats_macros", "//source/common/http:utility_lib", "//source/extensions/filters/common/rbac:engine_lib", + "//source/extensions/filters/common/rbac:utility_lib", "//source/extensions/filters/http:well_known_names", "@envoy_api//envoy/config/filter/http/rbac/v2:rbac_cc", ], diff --git a/source/extensions/filters/http/rbac/rbac_filter.cc b/source/extensions/filters/http/rbac/rbac_filter.cc index 1f9eede9b1b2..fd82b6fe4c7a 100644 --- a/source/extensions/filters/http/rbac/rbac_filter.cc +++ b/source/extensions/filters/http/rbac/rbac_filter.cc @@ -19,33 +19,19 @@ static const std::string shadow_resp_code_field = "shadow_response_code"; RoleBasedAccessControlFilterConfig::RoleBasedAccessControlFilterConfig( const envoy::config::filter::http::rbac::v2::RBAC& proto_config, const std::string& stats_prefix, Stats::Scope& scope) - : stats_(RoleBasedAccessControlFilter::generateStats(stats_prefix, scope)), - engine_(proto_config.has_rules() - ? absl::make_optional( - proto_config.rules()) - : absl::nullopt), - shadow_engine_( - proto_config.has_shadow_rules() - ? absl::make_optional( - proto_config.shadow_rules()) - : absl::nullopt) {} - -RoleBasedAccessControlFilterStats -RoleBasedAccessControlFilter::generateStats(const std::string& prefix, Stats::Scope& scope) { - const std::string final_prefix = prefix + "rbac."; - return {ALL_RBAC_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; -} + : stats_(Filters::Common::RBAC::generateStats(stats_prefix, scope)), + engine_(Filters::Common::RBAC::createEngine(proto_config)), + shadow_engine_(Filters::Common::RBAC::createShadowEngine(proto_config)) {} const absl::optional& RoleBasedAccessControlFilterConfig::engine(const Router::RouteConstSharedPtr route, - EnforcementMode mode) const { + Filters::Common::RBAC::EnforcementMode mode) const { if (!route || !route->routeEntry()) { return engine(mode); } const std::string& name = HttpFilterNames::get().Rbac; const auto* entry = route->routeEntry(); - const auto* route_local = entry->perFilterConfigTyped(name) ?: entry->virtualHost() @@ -60,15 +46,8 @@ RoleBasedAccessControlFilterConfig::engine(const Router::RouteConstSharedPtr rou RoleBasedAccessControlRouteSpecificFilterConfig::RoleBasedAccessControlRouteSpecificFilterConfig( const envoy::config::filter::http::rbac::v2::RBACPerRoute& per_route_config) - : engine_(per_route_config.rbac().has_rules() - ? absl::make_optional( - per_route_config.rbac().rules()) - : absl::nullopt), - shadow_engine_( - per_route_config.rbac().has_shadow_rules() - ? absl::make_optional( - per_route_config.rbac().shadow_rules()) - : absl::nullopt) {} + : engine_(Filters::Common::RBAC::createEngine(per_route_config.rbac())), + shadow_engine_(Filters::Common::RBAC::createShadowEngine(per_route_config.rbac())) {} Http::FilterHeadersStatus RoleBasedAccessControlFilter::decodeHeaders(Http::HeaderMap& headers, bool) { @@ -84,9 +63,11 @@ Http::FilterHeadersStatus RoleBasedAccessControlFilter::decodeHeaders(Http::Head callbacks_->connection()->ssl()->subjectPeerCertificate() : "none", headers, callbacks_->requestInfo().dynamicMetadata().DebugString()); + std::string effective_policy_id; - const absl::optional& shadow_engine = - config_->engine(callbacks_->route(), EnforcementMode::Shadow); + const auto& shadow_engine = + config_->engine(callbacks_->route(), Filters::Common::RBAC::EnforcementMode::Shadow); + if (shadow_engine.has_value()) { std::string shadow_resp_code = resp_code_200; if (shadow_engine->allowed(*callbacks_->connection(), headers, @@ -119,8 +100,8 @@ Http::FilterHeadersStatus RoleBasedAccessControlFilter::decodeHeaders(Http::Head } } - const absl::optional& engine = - config_->engine(callbacks_->route(), EnforcementMode::Enforced); + const auto& engine = + config_->engine(callbacks_->route(), Filters::Common::RBAC::EnforcementMode::Enforced); if (engine.has_value()) { if (engine->allowed(*callbacks_->connection(), headers, callbacks_->requestInfo().dynamicMetadata(), nullptr)) { diff --git a/source/extensions/filters/http/rbac/rbac_filter.h b/source/extensions/filters/http/rbac/rbac_filter.h index eef9e61c2100..50d916814749 100644 --- a/source/extensions/filters/http/rbac/rbac_filter.h +++ b/source/extensions/filters/http/rbac/rbac_filter.h @@ -10,40 +10,21 @@ #include "common/common/logger.h" #include "extensions/filters/common/rbac/engine_impl.h" +#include "extensions/filters/common/rbac/utility.h" namespace Envoy { namespace Extensions { namespace HttpFilters { namespace RBACFilter { -/** - * All stats for the RBAC filter. @see stats_macros.h - */ -// clang-format off -#define ALL_RBAC_FILTER_STATS(COUNTER) \ - COUNTER(allowed) \ - COUNTER(denied) \ - COUNTER(shadow_allowed) \ - COUNTER(shadow_denied) -// clang-format on - -/** - * Wrapper struct for RBAC filter stats. @see stats_macros.h - */ -struct RoleBasedAccessControlFilterStats { - ALL_RBAC_FILTER_STATS(GENERATE_COUNTER_STRUCT) -}; - -enum class EnforcementMode { Enforced, Shadow }; - class RoleBasedAccessControlRouteSpecificFilterConfig : public Router::RouteSpecificFilterConfig { public: RoleBasedAccessControlRouteSpecificFilterConfig( const envoy::config::filter::http::rbac::v2::RBACPerRoute& per_route_config); const absl::optional& - engine(EnforcementMode mode) const { - return mode == EnforcementMode::Enforced ? engine_ : shadow_engine_; + engine(Filters::Common::RBAC::EnforcementMode mode) const { + return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_ : shadow_engine_; } private: @@ -60,18 +41,19 @@ class RoleBasedAccessControlFilterConfig { const envoy::config::filter::http::rbac::v2::RBAC& proto_config, const std::string& stats_prefix, Stats::Scope& scope); - RoleBasedAccessControlFilterStats& stats() { return stats_; } + Filters::Common::RBAC::RoleBasedAccessControlFilterStats& stats() { return stats_; } const absl::optional& - engine(const Router::RouteConstSharedPtr route, EnforcementMode mode) const; + engine(const Router::RouteConstSharedPtr route, + Filters::Common::RBAC::EnforcementMode mode) const; private: const absl::optional& - engine(EnforcementMode mode) const { - return mode == EnforcementMode::Enforced ? engine_ : shadow_engine_; + engine(Filters::Common::RBAC::EnforcementMode mode) const { + return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_ : shadow_engine_; } - RoleBasedAccessControlFilterStats stats_; + Filters::Common::RBAC::RoleBasedAccessControlFilterStats stats_; const absl::optional engine_; const absl::optional shadow_engine_; @@ -89,9 +71,6 @@ class RoleBasedAccessControlFilter : public Http::StreamDecoderFilter, RoleBasedAccessControlFilter(RoleBasedAccessControlFilterConfigSharedPtr config) : config_(config) {} - static RoleBasedAccessControlFilterStats generateStats(const std::string& prefix, - Stats::Scope& scope); - // Http::StreamDecoderFilter Http::FilterHeadersStatus decodeHeaders(Http::HeaderMap& headers, bool end_stream) override; diff --git a/source/extensions/filters/network/rbac/BUILD b/source/extensions/filters/network/rbac/BUILD new file mode 100644 index 000000000000..9124710a48a7 --- /dev/null +++ b/source/extensions/filters/network/rbac/BUILD @@ -0,0 +1,38 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":rbac_filter", + "//include/envoy/registry", + "//include/envoy/server:filter_config_interface", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/common:factory_base_lib", + "@envoy_api//envoy/config/filter/network/rbac/v2:rbac_cc", + ], +) + +envoy_cc_library( + name = "rbac_filter", + srcs = ["rbac_filter.cc"], + hdrs = ["rbac_filter.h"], + deps = [ + "//include/envoy/buffer:buffer_interface", + "//include/envoy/network:connection_interface", + "//include/envoy/network:filter_interface", + "//source/common/common:minimal_logger_lib", + "//source/extensions/filters/common/rbac:engine_lib", + "//source/extensions/filters/common/rbac:utility_lib", + "//source/extensions/filters/network:well_known_names", + ], +) diff --git a/source/extensions/filters/network/rbac/config.cc b/source/extensions/filters/network/rbac/config.cc new file mode 100644 index 000000000000..35396f6907a1 --- /dev/null +++ b/source/extensions/filters/network/rbac/config.cc @@ -0,0 +1,95 @@ +#include "extensions/filters/network/rbac/config.h" + +#include "envoy/network/connection.h" +#include "envoy/registry/registry.h" + +#include "extensions/filters/network/rbac/rbac_filter.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +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," + "not supported by RBAC network filter", + header, metadata)); +} + +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_and_rules()) { + for (const auto& r : permission.and_rules().rules()) { + validatePermission(r); + } + } + if (permission.has_or_rules()) { + for (const auto& r : permission.or_rules().rules()) { + validatePermission(r); + } + } + if (permission.has_not_rule()) { + validatePermission(permission.not_rule()); + } +} + +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_and_ids()) { + for (const auto& r : principal.and_ids().ids()) { + validatePrincipal(r); + } + } + if (principal.has_or_ids()) { + for (const auto& r : principal.or_ids().ids()) { + validatePrincipal(r); + } + } + if (principal.has_not_id()) { + validatePrincipal(principal.not_id()); + } +} + +/** + * Validate the RBAC rules doesn't include any header or metadata rule. + */ +static void validateRbacRules(const envoy::config::rbac::v2alpha::RBAC& rules) { + for (const auto& policy : rules.policies()) { + for (const auto& permission : policy.second.permissions()) { + validatePermission(permission); + } + for (const auto& principal : policy.second.principals()) { + validatePrincipal(principal); + } + } +} + +Network::FilterFactoryCb +RoleBasedAccessControlNetworkFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::config::filter::network::rbac::v2::RBAC& proto_config, + Server::Configuration::FactoryContext& context) { + validateRbacRules(proto_config.rules()); + validateRbacRules(proto_config.shadow_rules()); + RoleBasedAccessControlFilterConfigSharedPtr config( + std::make_shared(proto_config, context.scope())); + return [config](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter(std::make_shared(config)); + }; +} + +/** + * Static registration for the RBAC network filter. @see RegisterFactory. + */ +static Registry::RegisterFactory + registered_; + +} // namespace RBACFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/rbac/config.h b/source/extensions/filters/network/rbac/config.h new file mode 100644 index 000000000000..9b7cc34ac25d --- /dev/null +++ b/source/extensions/filters/network/rbac/config.h @@ -0,0 +1,33 @@ +#pragma once + +#include "envoy/config/filter/network/rbac/v2/rbac.pb.h" +#include "envoy/config/filter/network/rbac/v2/rbac.pb.validate.h" + +#include "extensions/filters/network/common/factory_base.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RBACFilter { + +/** + * Config registration for the RBAC network filter. @see NamedNetworkFilterConfigFactory. + */ +class RoleBasedAccessControlNetworkFilterConfigFactory + : public Common::FactoryBase { + +public: + RoleBasedAccessControlNetworkFilterConfigFactory() + : FactoryBase(NetworkFilterNames::get().Rbac) {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::config::filter::network::rbac::v2::RBAC& proto_config, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace RBACFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/rbac/rbac_filter.cc b/source/extensions/filters/network/rbac/rbac_filter.cc new file mode 100644 index 000000000000..3709d8a7647c --- /dev/null +++ b/source/extensions/filters/network/rbac/rbac_filter.cc @@ -0,0 +1,82 @@ +#include "extensions/filters/network/rbac/rbac_filter.h" + +#include "envoy/buffer/buffer.h" +#include "envoy/network/connection.h" + +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RBACFilter { + +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)) {} + +Network::FilterStatus RoleBasedAccessControlFilter::onData(Buffer::Instance&, bool) { + ENVOY_LOG( + debug, "checking connection: remoteAddress: {}, localAddress: {}, ssl: {}", + callbacks_->connection().remoteAddress()->asString(), + callbacks_->connection().localAddress()->asString(), + callbacks_->connection().ssl() + ? "uriSanPeerCertificate: " + callbacks_->connection().ssl()->uriSanPeerCertificate() + + ", subjectPeerCertificate: " + + callbacks_->connection().ssl()->subjectPeerCertificate() + : "none"); + + 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. + 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) { + return Network::FilterStatus::Continue; + } else if (engine_result_ == Deny) { + callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + return Network::FilterStatus::StopIteration; + } + + ENVOY_LOG(debug, "no engine, allowed by default"); + return Network::FilterStatus::Continue; +} + +EngineResult +RoleBasedAccessControlFilter::checkEngine(Filters::Common::RBAC::EnforcementMode mode) { + const auto& engine = config_->engine(mode); + if (engine.has_value()) { + if (engine->allowed(callbacks_->connection())) { + if (mode == Filters::Common::RBAC::EnforcementMode::Shadow) { + ENVOY_LOG(debug, "shadow allowed"); + config_->stats().shadow_allowed_.inc(); + } else if (mode == Filters::Common::RBAC::EnforcementMode::Enforced) { + ENVOY_LOG(debug, "enforced allowed"); + config_->stats().allowed_.inc(); + } + return Allow; + } else { + if (mode == Filters::Common::RBAC::EnforcementMode::Shadow) { + ENVOY_LOG(debug, "shadow denied"); + config_->stats().shadow_denied_.inc(); + } else if (mode == Filters::Common::RBAC::EnforcementMode::Enforced) { + ENVOY_LOG(debug, "enforced denied"); + config_->stats().denied_.inc(); + } + return Deny; + } + } + return None; +} + +} // namespace RBACFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/rbac/rbac_filter.h b/source/extensions/filters/network/rbac/rbac_filter.h new file mode 100644 index 000000000000..e492a4bb4800 --- /dev/null +++ b/source/extensions/filters/network/rbac/rbac_filter.h @@ -0,0 +1,73 @@ +#pragma once + +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/stats/stats_macros.h" + +#include "common/common/logger.h" + +#include "extensions/filters/common/rbac/engine_impl.h" +#include "extensions/filters/common/rbac/utility.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RBACFilter { + +enum EngineResult { Unknown, None, Allow, Deny }; + +/** + * Configuration for the RBAC network filter. + */ +class RoleBasedAccessControlFilterConfig { +public: + RoleBasedAccessControlFilterConfig( + const envoy::config::filter::network::rbac::v2::RBAC& proto_config, Stats::Scope& scope); + + Filters::Common::RBAC::RoleBasedAccessControlFilterStats& stats() { return stats_; } + + const absl::optional& + engine(Filters::Common::RBAC::EnforcementMode mode) const { + return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_ : shadow_engine_; + } + +private: + Filters::Common::RBAC::RoleBasedAccessControlFilterStats stats_; + + const absl::optional engine_; + const absl::optional shadow_engine_; +}; + +typedef std::shared_ptr + RoleBasedAccessControlFilterConfigSharedPtr; + +/** + * Implementation of a basic RBAC network filter. + */ +class RoleBasedAccessControlFilter : public Network::ReadFilter, + public Logger::Loggable { +public: + RoleBasedAccessControlFilter(RoleBasedAccessControlFilterConfigSharedPtr config) + : config_(config) {} + ~RoleBasedAccessControlFilter() {} + + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; + Network::FilterStatus onNewConnection() override { return Network::FilterStatus::Continue; }; + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) override { + callbacks_ = &callbacks; + } + +private: + RoleBasedAccessControlFilterConfigSharedPtr config_; + Network::ReadFilterCallbacks* callbacks_{}; + EngineResult engine_result_{Unknown}; + EngineResult shadow_engine_result_{Unknown}; + + EngineResult checkEngine(Filters::Common::RBAC::EnforcementMode mode); +}; + +} // namespace RBACFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index f9014eb0de7c..06123fd6df51 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -30,6 +30,8 @@ class NetworkFilterNameValues { const std::string ExtAuthorization = "envoy.ext_authz"; // Thrift proxy filter const std::string ThriftProxy = "envoy.filters.network.thrift_proxy"; + // Role based access control filter + const std::string Rbac = "envoy.filters.network.rbac"; // Converts names from v1 to v2 const Config::V1Converter v1_converter_; diff --git a/test/extensions/filters/common/rbac/mocks.h b/test/extensions/filters/common/rbac/mocks.h index b0e0a4de82a1..072fc7634c25 100644 --- a/test/extensions/filters/common/rbac/mocks.h +++ b/test/extensions/filters/common/rbac/mocks.h @@ -18,6 +18,8 @@ class MockEngine : public RoleBasedAccessControlEngineImpl { MOCK_CONST_METHOD4(allowed, bool(const Envoy::Network::Connection&, const Envoy::Http::HeaderMap&, const envoy::api::v2::core::Metadata&, std::string* effective_policy_id)); + + MOCK_CONST_METHOD1(allowed, bool(const Envoy::Network::Connection&)); }; } // namespace RBAC diff --git a/test/extensions/filters/http/rbac/BUILD b/test/extensions/filters/http/rbac/BUILD index 4652df246dc2..c1dd764461ea 100644 --- a/test/extensions/filters/http/rbac/BUILD +++ b/test/extensions/filters/http/rbac/BUILD @@ -27,6 +27,7 @@ envoy_extension_cc_test( srcs = ["rbac_filter_test.cc"], extension_name = "envoy.filters.http.rbac", deps = [ + "//source/extensions/filters/common/rbac:utility_lib", "//source/extensions/filters/http/rbac:rbac_filter_lib", "//test/extensions/filters/common/rbac:engine_mocks", "//test/extensions/filters/http/rbac:route_config_mocks", @@ -51,6 +52,6 @@ envoy_extension_cc_mock( hdrs = ["mocks.h"], extension_name = "envoy.filters.http.rbac", deps = [ - "//source/extensions/filters/http/rbac:rbac_filter_lib", + "//source/extensions/filters/common/rbac:utility_lib", ], ) diff --git a/test/extensions/filters/http/rbac/mocks.h b/test/extensions/filters/http/rbac/mocks.h index 0c9cc42cb0d2..b288f1438f13 100644 --- a/test/extensions/filters/http/rbac/mocks.h +++ b/test/extensions/filters/http/rbac/mocks.h @@ -1,6 +1,6 @@ #pragma once -#include "extensions/filters/http/rbac/rbac_filter.h" +#include "extensions/filters/common/rbac/utility.h" #include "gmock/gmock.h" diff --git a/test/extensions/filters/http/rbac/rbac_filter_test.cc b/test/extensions/filters/http/rbac/rbac_filter_test.cc index 4febdda18835..1b597ea7afb7 100644 --- a/test/extensions/filters/http/rbac/rbac_filter_test.cc +++ b/test/extensions/filters/http/rbac/rbac_filter_test.cc @@ -1,6 +1,7 @@ #include "common/config/metadata.h" #include "common/network/utility.h" +#include "extensions/filters/common/rbac/utility.h" #include "extensions/filters/http/rbac/rbac_filter.h" #include "extensions/filters/http/well_known_names.h" diff --git a/test/extensions/filters/network/rbac/BUILD b/test/extensions/filters/network/rbac/BUILD new file mode 100644 index 000000000000..cd43b2613472 --- /dev/null +++ b/test/extensions/filters/network/rbac/BUILD @@ -0,0 +1,44 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_name = "envoy.filters.network.rbac", + deps = [ + "//source/extensions/filters/network/rbac:config", + "//test/mocks/server:server_mocks", + ], +) + +envoy_extension_cc_test( + name = "filter_test", + srcs = ["filter_test.cc"], + extension_name = "envoy.filters.network.rbac", + deps = [ + "//source/extensions/filters/common/rbac:utility_lib", + "//source/extensions/filters/network/rbac:rbac_filter", + "//test/mocks/network:network_mocks", + ], +) + +envoy_extension_cc_test( + name = "integration_test", + srcs = ["integration_test.cc"], + extension_name = "envoy.filters.network.rbac", + deps = [ + "//source/extensions/filters/network/rbac:config", + "//test/integration:integration_lib", + "//test/test_common:environment_lib", + ], +) diff --git a/test/extensions/filters/network/rbac/config_test.cc b/test/extensions/filters/network/rbac/config_test.cc new file mode 100644 index 000000000000..b573dbf9f62b --- /dev/null +++ b/test/extensions/filters/network/rbac/config_test.cc @@ -0,0 +1,145 @@ +#include "envoy/config/filter/network/rbac/v2/rbac.pb.validate.h" + +#include "extensions/filters/network/rbac/config.h" + +#include "test/mocks/server/mocks.h" + +#include "fmt/printf.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::_; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RBACFilter { + +namespace { +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: + void checkRule(const std::string& policy_json) { + envoy::config::rbac::v2alpha::Policy policy_proto{}; + MessageUtil::loadFromJson(policy_json, policy_proto); + + envoy::config::filter::network::rbac::v2::RBAC config{}; + config.set_stat_prefix("test"); + (*config.mutable_rules()->mutable_policies())["foo"] = policy_proto; + + NiceMock context; + RoleBasedAccessControlNetworkFilterConfigFactory factory; + EXPECT_THROW(factory.createFilterFactoryFromProto(config, context), Envoy::EnvoyException); + + config.clear_rules(); + (*config.mutable_shadow_rules()->mutable_policies())["foo"] = policy_proto; + EXPECT_THROW(factory.createFilterFactoryFromProto(config, context), Envoy::EnvoyException); + } +}; + +TEST_F(RoleBasedAccessControlNetworkFilterConfigFactoryTest, ValidProto) { + envoy::config::rbac::v2alpha::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + envoy::config::filter::network::rbac::v2::RBAC config; + config.set_stat_prefix("stats"); + (*config.mutable_rules()->mutable_policies())["foo"] = policy; + + NiceMock context; + RoleBasedAccessControlNetworkFilterConfigFactory factory; + Network::FilterFactoryCb cb = factory.createFilterFactoryFromProto(config, context); + Network::MockConnection connection; + EXPECT_CALL(connection, addReadFilter(_)); + cb(connection); +} + +TEST_F(RoleBasedAccessControlNetworkFilterConfigFactoryTest, EmptyProto) { + RoleBasedAccessControlNetworkFilterConfigFactory factory; + auto* config = dynamic_cast( + factory.createEmptyConfigProto().get()); + EXPECT_NE(nullptr, config); +} + +TEST_F(RoleBasedAccessControlNetworkFilterConfigFactoryTest, InvalidPermission) { + validateRule(R"EOF( +{ + "permissions": [ { "any": true }, { "and_rules": { "rules": [ { "any": true }, %s ] } } ], + "principals": [ { "any": true } ] +} +)EOF"); + + validateRule(R"EOF( +{ + "permissions": [ { "any": true }, { "or_rules": { "rules": [ { "any": true }, %s ] } } ], + "principals": [ { "any": true } ] +} +)EOF"); + + validateRule(R"EOF( +{ + "permissions": [ { "any": true }, { "not_rule": %s } ], + "principals": [ { "any": true } ] +} +)EOF"); + + validateRule(R"EOF( +{ + "permissions": [ { "any": true }, %s ], + "principals": [ { "any": true } ] +} +)EOF"); +} + +TEST_F(RoleBasedAccessControlNetworkFilterConfigFactoryTest, InvalidPrincipal) { + validateRule(R"EOF( +{ + "principals": [ { "any": true }, { "and_ids": { "ids": [ { "any": true }, %s ] } } ], + "permissions": [ { "any": true } ] +} +)EOF"); + + validateRule(R"EOF( +{ + "principals": [ { "any": true }, { "or_ids": { "ids": [ { "any": true }, %s ] } } ], + "permissions": [ { "any": true } ] +} +)EOF"); + + validateRule(R"EOF( +{ + "principals": [ { "any": true }, { "not_id": %s } ], + "permissions": [ { "any": true } ] +} +)EOF"); + + validateRule(R"EOF( +{ + "principals": [ { "any": true }, %s ], + "permissions": [ { "any": true } ] +} +)EOF"); +} + +} // namespace RBACFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/rbac/filter_test.cc b/test/extensions/filters/network/rbac/filter_test.cc new file mode 100644 index 000000000000..0433b711d3a3 --- /dev/null +++ b/test/extensions/filters/network/rbac/filter_test.cc @@ -0,0 +1,104 @@ +#include "common/network/utility.h" + +#include "extensions/filters/common/rbac/utility.h" +#include "extensions/filters/network/rbac/rbac_filter.h" + +#include "test/mocks/network/mocks.h" + +using testing::NiceMock; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RBACFilter { + +class RoleBasedAccessControlNetworkFilterTest : public testing::Test { +public: + RoleBasedAccessControlFilterConfigSharedPtr setupConfig(bool with_policy = true) { + envoy::config::filter::network::rbac::v2::RBAC config; + config.set_stat_prefix("tcp."); + + if (with_policy) { + envoy::config::rbac::v2alpha::Policy policy; + policy.add_permissions()->set_destination_port(123); + policy.add_principals()->set_any(true); + config.mutable_rules()->set_action(envoy::config::rbac::v2alpha::RBAC::ALLOW); + (*config.mutable_rules()->mutable_policies())["foo"] = policy; + + envoy::config::rbac::v2alpha::Policy shadow_policy; + shadow_policy.add_permissions()->set_destination_port(456); + shadow_policy.add_principals()->set_any(true); + config.mutable_shadow_rules()->set_action(envoy::config::rbac::v2alpha::RBAC::ALLOW); + (*config.mutable_shadow_rules()->mutable_policies())["bar"] = shadow_policy; + } + + return std::make_shared(config, store_); + } + + RoleBasedAccessControlNetworkFilterTest() : config_(setupConfig()) { + filter_.reset(new RoleBasedAccessControlFilter(config_)); + filter_->initializeReadFilterCallbacks(callbacks_); + } + + void setDestinationPort(uint16_t port) { + address_ = Envoy::Network::Utility::parseInternetAddress("1.2.3.4", port, false); + EXPECT_CALL(callbacks_.connection_, localAddress()).WillRepeatedly(ReturnRef(address_)); + } + + NiceMock callbacks_; + Stats::IsolatedStoreImpl store_; + Buffer::OwnedImpl data_; + RoleBasedAccessControlFilterConfigSharedPtr config_; + + std::unique_ptr filter_; + Network::Address::InstanceConstSharedPtr address_; +}; + +TEST_F(RoleBasedAccessControlNetworkFilterTest, Allowed) { + setDestinationPort(123); + + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + + // Call onData() twice, should only increase stats once. + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(1U, config_->stats().allowed_.value()); + EXPECT_EQ(0U, config_->stats().denied_.value()); + EXPECT_EQ(0U, config_->stats().shadow_allowed_.value()); + EXPECT_EQ(1U, config_->stats().shadow_denied_.value()); +} + +TEST_F(RoleBasedAccessControlNetworkFilterTest, AllowedWithNoPolicy) { + config_ = setupConfig(false /* with_policy */); + filter_.reset(new RoleBasedAccessControlFilter(config_)); + filter_->initializeReadFilterCallbacks(callbacks_); + setDestinationPort(0); + + // Allow access and no metric change when there is no policy. + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onNewConnection()); + EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data_, false)); + EXPECT_EQ(0U, config_->stats().allowed_.value()); + EXPECT_EQ(0U, config_->stats().denied_.value()); + EXPECT_EQ(0U, config_->stats().shadow_allowed_.value()); + EXPECT_EQ(0U, config_->stats().shadow_denied_.value()); +} + +TEST_F(RoleBasedAccessControlNetworkFilterTest, Denied) { + setDestinationPort(456); + + EXPECT_CALL(callbacks_.connection_, close(Network::ConnectionCloseType::NoFlush)).Times(2); + + // Call onData() twice, should only increase stats once. + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data_, false)); + EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data_, false)); + EXPECT_EQ(0U, config_->stats().allowed_.value()); + EXPECT_EQ(1U, config_->stats().denied_.value()); + EXPECT_EQ(1U, config_->stats().shadow_allowed_.value()); + EXPECT_EQ(0U, config_->stats().shadow_denied_.value()); +} + +} // namespace RBACFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/rbac/integration_test.cc b/test/extensions/filters/network/rbac/integration_test.cc new file mode 100644 index 000000000000..aa4d2482a6dd --- /dev/null +++ b/test/extensions/filters/network/rbac/integration_test.cc @@ -0,0 +1,133 @@ +#include "extensions/filters/network/rbac/config.h" + +#include "test/integration/integration.h" +#include "test/test_common/environment.h" + +#include "fmt/printf.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace RBAC { + +namespace { +std::string rbac_config; +} // namespace + +class RoleBasedAccessControlNetworkFilterIntegrationTest + : public BaseIntegrationTest, + public testing::TestWithParam { +public: + RoleBasedAccessControlNetworkFilterIntegrationTest() + : BaseIntegrationTest(GetParam(), rbac_config) {} + + static void SetUpTestCase() { + rbac_config = ConfigHelper::BASE_CONFIG + R"EOF( + filter_chains: + filters: + - name: envoy.filters.network.rbac + config: + stat_prefix: tcp. + rules: + policies: + "foo": + permissions: + - any: true + principals: + - not_id: + any: true +)EOF"; + } + + void initializeFilter(const std::string& config) { + config_helper_.addConfigModifier([config](envoy::config::bootstrap::v2::Bootstrap& bootstrap) { + envoy::api::v2::listener::Filter filter; + MessageUtil::loadFromYaml(config, filter); + ASSERT_GT(bootstrap.mutable_static_resources()->listeners_size(), 0); + auto l = bootstrap.mutable_static_resources()->mutable_listeners(0); + ASSERT_GT(l->filter_chains_size(), 0); + ASSERT_GT(l->filter_chains(0).filters_size(), 0); + l->mutable_filter_chains(0)->mutable_filters(0)->Swap(&filter); + }); + + BaseIntegrationTest::initialize(); + } + + void TearDown() override { + test_server_.reset(); + fake_upstreams_.clear(); + } +}; + +INSTANTIATE_TEST_CASE_P(IpVersions, RoleBasedAccessControlNetworkFilterIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(RoleBasedAccessControlNetworkFilterIntegrationTest, Allowed) { + initializeFilter(R"EOF( +name: envoy.filters.network.rbac +config: + stat_prefix: tcp. + rules: + policies: + "allow_all": + permissions: + - any: true + principals: + - any: true + shadow_rules: + policies: + "deny_all": + permissions: + - any: true + principals: + - not_id: + any: true +)EOF"); + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + tcp_client->write("hello"); + ASSERT_TRUE(tcp_client->connected()); + tcp_client->close(); + + EXPECT_EQ(1U, test_server_->counter("tcp.rbac.allowed")->value()); + EXPECT_EQ(0U, test_server_->counter("tcp.rbac.denied")->value()); + EXPECT_EQ(0U, test_server_->counter("tcp.rbac.shadow_allowed")->value()); + EXPECT_EQ(1U, test_server_->counter("tcp.rbac.shadow_denied")->value()); +} + +TEST_P(RoleBasedAccessControlNetworkFilterIntegrationTest, Denied) { + initializeFilter(R"EOF( +name: envoy.filters.network.rbac +config: + stat_prefix: tcp. + rules: + policies: + "deny_all": + permissions: + - any: true + principals: + - not_id: + any: true + shadow_rules: + policies: + "allow_all": + permissions: + - any: true + principals: + - any: true +)EOF"); + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + tcp_client->write("hello"); + ASSERT_TRUE(tcp_client->connected()); + tcp_client->waitForDisconnect(); + + EXPECT_EQ(0U, test_server_->counter("tcp.rbac.allowed")->value()); + EXPECT_EQ(1U, test_server_->counter("tcp.rbac.denied")->value()); + EXPECT_EQ(1U, test_server_->counter("tcp.rbac.shadow_allowed")->value()); + EXPECT_EQ(0U, test_server_->counter("tcp.rbac.shadow_denied")->value()); +} + +} // namespace RBAC +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy