diff --git a/api/docs/BUILD b/api/docs/BUILD index d5db36d10e1c0..56e03ee82899d 100644 --- a/api/docs/BUILD +++ b/api/docs/BUILD @@ -34,6 +34,7 @@ proto_library( "//envoy/config/filter/http/ip_tagging/v2:ip_tagging", "//envoy/config/filter/http/lua/v2:lua", "//envoy/config/filter/http/rate_limit/v2:rate_limit", + "//envoy/config/filter/http/rbac/v2:rbac", "//envoy/config/filter/http/router/v2:router", "//envoy/config/filter/http/squash/v2:squash", "//envoy/config/filter/http/transcoder/v2:transcoder", @@ -48,6 +49,7 @@ proto_library( "//envoy/config/metrics/v2:metrics_service", "//envoy/config/metrics/v2:stats", "//envoy/config/ratelimit/v2:rls", + "//envoy/config/rbac/v2alpha:rbac", "//envoy/config/trace/v2:trace", "//envoy/config/transport_socket/capture/v2alpha:capture", "//envoy/extensions/common/tap/v2alpha:capture", diff --git a/api/envoy/config/rbac/v2alpha/rbac.proto b/api/envoy/config/rbac/v2alpha/rbac.proto index 5d003922417dd..818054ada04e7 100644 --- a/api/envoy/config/rbac/v2alpha/rbac.proto +++ b/api/envoy/config/rbac/v2alpha/rbac.proto @@ -8,12 +8,8 @@ package envoy.config.rbac.v2alpha; option go_package = "v2alpha"; // Role Based Access Control (RBAC) provides service-level and method-level access control for a -// service. The RBAC engine authorizes a request by evaluating the request context (expressed in the -// form of :ref: `AttributeContext `) against -// the RBAC policies. -// -// RBAC policies are additive. The policies are examined in order. A request is allowed once a -// matching policy is found (suppose the `action` is ALLOW). +// service. RBAC policies are additive. The policies are examined in order. A request is allowed +// once a matching policy is found (suppose the `action` is ALLOW). // // Here is an example of RBAC configuration. It has two policies: // @@ -48,13 +44,13 @@ option go_package = "v2alpha"; // - any: true // message RBAC { - // Should we do white-list or black-list style access control? + // Should we do safe-list or block-list style access control? enum Action { - // The policies grant access to principals. The rest is denied. This is white-list style + // The policies grant access to principals. The rest is denied. This is safe-list style // access control. This is the default type. ALLOW = 0; - // The policies deny access to principals. The rest is allowed. This is black-list style + // The policies deny access to principals. The rest is allowed. This is block-list style // access control. DENY = 1; } diff --git a/docs/build.sh b/docs/build.sh index 05897e995bf19..9b4b951c7b7d9 100755 --- a/docs/build.sh +++ b/docs/build.sh @@ -82,6 +82,7 @@ PROTO_RST=" /envoy/config/filter/http/ip_tagging/v2/ip_tagging/envoy/config/filter/http/ip_tagging/v2/ip_tagging.proto.rst /envoy/config/filter/http/lua/v2/lua/envoy/config/filter/http/lua/v2/lua.proto.rst /envoy/config/filter/http/rate_limit/v2/rate_limit/envoy/config/filter/http/rate_limit/v2/rate_limit.proto.rst + /envoy/config/filter/http/rbac/v2/rbac/envoy/config/filter/http/rbac/v2/rbac.proto.rst /envoy/config/filter/http/router/v2/router/envoy/config/filter/http/router/v2/router.proto.rst /envoy/config/filter/http/squash/v2/squash/envoy/config/filter/http/squash/v2/squash.proto.rst /envoy/config/filter/http/transcoder/v2/transcoder/envoy/config/filter/http/transcoder/v2/transcoder.proto.rst @@ -92,6 +93,7 @@ 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 + /envoy/config/rbac/v2alpha/rbac/envoy/config/rbac/v2alpha/rbac.proto.rst /envoy/config/transport_socket/capture/v2alpha/capture/envoy/config/transport_socket/capture/v2alpha/capture.proto.rst /envoy/extensions/common/tap/v2alpha/capture/envoy/extensions/common/tap/v2alpha/capture.proto.rst /envoy/type/percent/envoy/type/percent.proto.rst diff --git a/docs/root/api-v2/api.rst b/docs/root/api-v2/api.rst index 2ad6fba30b3ea..9ff17e0f82db6 100644 --- a/docs/root/api-v2/api.rst +++ b/docs/root/api-v2/api.rst @@ -13,6 +13,7 @@ v2 API reference http_routes/http_routes config/filter/filter config/health_checker/health_checker + config/rbac/rbac config/transport_socket/transport_socket admin/admin common_messages/common_messages diff --git a/docs/root/api-v2/config/rbac/rbac.rst b/docs/root/api-v2/config/rbac/rbac.rst new file mode 100644 index 0000000000000..ee8d71c7210a0 --- /dev/null +++ b/docs/root/api-v2/config/rbac/rbac.rst @@ -0,0 +1,8 @@ +RBAC +==== + +.. toctree:: + :glob: + :maxdepth: 1 + + v2alpha/* diff --git a/docs/root/configuration/http_filters/http_filters.rst b/docs/root/configuration/http_filters/http_filters.rst index e7a513f7001c9..6a788f3708aec 100644 --- a/docs/root/configuration/http_filters/http_filters.rst +++ b/docs/root/configuration/http_filters/http_filters.rst @@ -18,5 +18,6 @@ HTTP filters ip_tagging_filter lua_filter rate_limit_filter + rbac_filter router_filter squash_filter diff --git a/docs/root/configuration/http_filters/rbac_filter.rst b/docs/root/configuration/http_filters/rbac_filter.rst new file mode 100644 index 0000000000000..36f5c7b0436dd --- /dev/null +++ b/docs/root/configuration/http_filters/rbac_filter.rst @@ -0,0 +1,32 @@ +.. _config_http_filters_rbac: + +Role Based Access Control (RBAC) Filter +======================================= + +The RBAC 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 off properties of the connection (IPs, ports, SSL subject) +as well as the incoming request's HTTP headers. + +* :ref:`v2 API reference ` + +Per-Route Configuration +----------------------- + +The RBAC filter configuration can be overridden or disabled on a per-route basis by providing a +:ref:`RBACPerRoute ` configuration on +the virtual host, route, or weighted cluster. + +Statistics +---------- + +The RBAC filter outputs statistics in the *http..rbac.* namespace. The :ref:`stat +prefix ` comes from the owning HTTP connection manager. + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + allowed, Counter, Total requests that were allowed access by the filter + denied, Counter, Total requests that were denied access by the filter diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index c9260795492db..57d20d6740207 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -70,6 +70,7 @@ Version history * logger: added the ability to optionally set the log format via the :option:`--log-format` option. * logger: all :ref:`logging levels ` can be configured at run-time: trace debug info warning error critical. +* rbac http filter: a :ref:`role-based access control http filter ` has been added. * router: The behavior of per-try timeouts have changed in the case where a portion of the response has already been proxied downstream when the timeout occurs. Previously, the response would be reset leading to either an HTTP/2 reset or an HTTP/1 closed connection and a partial response. Now, the diff --git a/include/envoy/ssl/connection.h b/include/envoy/ssl/connection.h index dc8887c2ec5fe..cd8ebe55cf06a 100644 --- a/include/envoy/ssl/connection.h +++ b/include/envoy/ssl/connection.h @@ -48,7 +48,7 @@ class Connection { * @return std::string the URI in the SAN field of the peer certificate. Returns "" if there is no * peer certificate, or no SAN field, or no URI. **/ - virtual std::string uriSanPeerCertificate() PURE; + virtual std::string uriSanPeerCertificate() const PURE; /** * @return std::string the URL-encoded PEM-encoded representation of the peer certificate. Returns diff --git a/source/common/http/header_utility.cc b/source/common/http/header_utility.cc index 06df30612e093..8df06b42d399a 100644 --- a/source/common/http/header_utility.cc +++ b/source/common/http/header_utility.cc @@ -65,53 +65,49 @@ HeaderUtility::HeaderData::HeaderData(const Json::Object& config) bool HeaderUtility::matchHeaders(const Http::HeaderMap& request_headers, const std::vector& config_headers) { - bool matches = true; + // TODO (rodaine): Should this really allow empty headers to always match? if (!config_headers.empty()) { for (const HeaderData& cfg_header_data : config_headers) { - const Http::HeaderEntry* header = request_headers.get(cfg_header_data.name_); - - if (header == nullptr) { - matches = cfg_header_data.header_match_type_ == HeaderMatchType::Present - ? cfg_header_data.invert_match_ - : false; - break; - } - - switch (cfg_header_data.header_match_type_) { - case HeaderMatchType::Value: - matches &= - (cfg_header_data.value_.empty() || header->value() == cfg_header_data.value_.c_str()); - break; - - case HeaderMatchType::Regex: - matches &= std::regex_match(header->value().c_str(), cfg_header_data.regex_pattern_); - break; - - case HeaderMatchType::Range: { - int64_t header_value = 0; - matches &= StringUtil::atol(header->value().c_str(), header_value, 10) && - header_value >= cfg_header_data.range_.start() && - header_value < cfg_header_data.range_.end(); - break; + if (!matchHeaders(request_headers, cfg_header_data)) { + return false; } + } + } - case HeaderMatchType::Present: - break; + return true; +} - default: - NOT_REACHED; - } +bool HeaderUtility::matchHeaders(const Http::HeaderMap& request_headers, + const HeaderData& header_data) { + const Http::HeaderEntry* header = request_headers.get(header_data.name_); - matches ^= cfg_header_data.invert_match_; + if (header == nullptr) { + return header_data.invert_match_ && header_data.header_match_type_ == HeaderMatchType::Present; + } - if (!matches) { - break; - } - } + bool match; + switch (header_data.header_match_type_) { + case HeaderMatchType::Value: + match = header_data.value_.empty() || header->value() == header_data.value_.c_str(); + break; + case HeaderMatchType::Regex: + match = std::regex_match(header->value().c_str(), header_data.regex_pattern_); + break; + case HeaderMatchType::Range: { + int64_t header_value = 0; + match = StringUtil::atol(header->value().c_str(), header_value, 10) && + header_value >= header_data.range_.start() && header_value < header_data.range_.end(); + break; + } + case HeaderMatchType::Present: + match = true; + break; + default: + NOT_REACHED; } - return matches; + return match != header_data.invert_match_; } } // namespace Http diff --git a/source/common/http/header_utility.h b/source/common/http/header_utility.h index c8a49bb4f1591..450c01f9b081d 100644 --- a/source/common/http/header_utility.h +++ b/source/common/http/header_utility.h @@ -42,6 +42,8 @@ class HeaderUtility { */ static bool matchHeaders(const Http::HeaderMap& request_headers, const std::vector& config_headers); + + static bool matchHeaders(const Http::HeaderMap& request_headers, const HeaderData& config_header); }; } // namespace Http } // namespace Envoy diff --git a/source/common/ssl/ssl_socket.cc b/source/common/ssl/ssl_socket.cc index 7fc08421c1329..b433358f16532 100644 --- a/source/common/ssl/ssl_socket.cc +++ b/source/common/ssl/ssl_socket.cc @@ -273,7 +273,7 @@ const std::string& SslSocket::urlEncodedPemEncodedPeerCertificate() const { return cached_url_encoded_pem_encoded_peer_certificate_; } -std::string SslSocket::uriSanPeerCertificate() { +std::string SslSocket::uriSanPeerCertificate() const { bssl::UniquePtr cert(SSL_get_peer_certificate(ssl_.get())); if (!cert) { return ""; @@ -289,7 +289,7 @@ std::vector SslSocket::dnsSansPeerCertificate() { return getDnsSansFromCertificate(cert.get()); } -std::string SslSocket::getUriSanFromCertificate(X509* cert) { +std::string SslSocket::getUriSanFromCertificate(X509* cert) const { bssl::UniquePtr san_names( static_cast(X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr))); if (san_names == nullptr) { diff --git a/source/common/ssl/ssl_socket.h b/source/common/ssl/ssl_socket.h index d5f39a026b10c..6bb040edcd7ef 100644 --- a/source/common/ssl/ssl_socket.h +++ b/source/common/ssl/ssl_socket.h @@ -28,7 +28,7 @@ class SslSocket : public Network::TransportSocket, const std::string& sha256PeerCertificateDigest() const override; std::string subjectPeerCertificate() const override; std::string subjectLocalCertificate() const override; - std::string uriSanPeerCertificate() override; + std::string uriSanPeerCertificate() const override; const std::string& urlEncodedPemEncodedPeerCertificate() const override; std::vector dnsSansPeerCertificate() override; std::vector dnsSansLocalCertificate() override; @@ -50,7 +50,7 @@ class SslSocket : public Network::TransportSocket, Network::PostIoAction doHandshake(); void drainErrorQueue(); void shutdownSsl(); - std::string getUriSanFromCertificate(X509* cert); + std::string getUriSanFromCertificate(X509* cert) const; std::string getSubjectFromCertificate(X509* cert) const; std::vector getDnsSansFromCertificate(X509* cert); diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 3ee7988e56f5a..eeaca070d577c 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -30,6 +30,7 @@ EXTENSIONS = { "envoy.filters.http.ip_tagging": "//source/extensions/filters/http/ip_tagging:config", "envoy.filters.http.lua": "//source/extensions/filters/http/lua:config", "envoy.filters.http.ratelimit": "//source/extensions/filters/http/ratelimit:config", + "envoy.filters.http.rbac": "//source/extensions/filters/http/rbac:config", "envoy.filters.http.router": "//source/extensions/filters/http/router:config", "envoy.filters.http.squash": "//source/extensions/filters/http/squash:config", diff --git a/source/extensions/filters/common/rbac/BUILD b/source/extensions/filters/common/rbac/BUILD new file mode 100644 index 0000000000000..59c0cf7beb7dd --- /dev/null +++ b/source/extensions/filters/common/rbac/BUILD @@ -0,0 +1,44 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "matchers_lib", + srcs = ["matchers.cc"], + hdrs = ["matchers.h"], + deps = [ + "//include/envoy/http:header_map_interface", + "//include/envoy/network:connection_interface", + "//source/common/common:assert_lib", + "//source/common/http:header_utility_lib", + "//source/common/network:cidr_range_lib", + "@envoy_api//envoy/config/rbac/v2alpha:rbac_cc", + ], +) + +envoy_cc_library( + name = "engine_interface", + hdrs = ["engine.h"], + deps = [ + "//include/envoy/http:filter_interface", + "//include/envoy/http:header_map_interface", + "//include/envoy/network:connection_interface", + ], +) + +envoy_cc_library( + name = "engine_lib", + srcs = ["engine_impl.cc"], + hdrs = ["engine_impl.h"], + deps = [ + "//source/extensions/filters/common/rbac:engine_interface", + "//source/extensions/filters/common/rbac:matchers_lib", + "@envoy_api//envoy/config/filter/http/rbac/v2:rbac_cc", + ], +) diff --git a/source/extensions/filters/common/rbac/engine.h b/source/extensions/filters/common/rbac/engine.h new file mode 100644 index 0000000000000..76db025a44411 --- /dev/null +++ b/source/extensions/filters/common/rbac/engine.h @@ -0,0 +1,35 @@ +#pragma once + +#include "envoy/http/filter.h" +#include "envoy/http/header_map.h" +#include "envoy/network/connection.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +/** + * Shared logic for evaluating RBAC policies. + */ +class RoleBasedAccessControlEngine : public Router::RouteSpecificFilterConfig { +public: + virtual ~RoleBasedAccessControlEngine() {} + + /** + * Returns whether or not the current action is permitted. + * + * @param connection the downstream connection used to identify the action/principal. + * @param headers the headers of the incoming request used to identify the action/principal. An + * empty map should be used if there are no headers available. + */ + virtual bool allowed(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const PURE; +}; + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/rbac/engine_impl.cc b/source/extensions/filters/common/rbac/engine_impl.cc new file mode 100644 index 0000000000000..1ae25f878291a --- /dev/null +++ b/source/extensions/filters/common/rbac/engine_impl.cc @@ -0,0 +1,52 @@ +#include "extensions/filters/common/rbac/engine_impl.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +RoleBasedAccessControlEngineImpl::RoleBasedAccessControlEngineImpl( + const envoy::config::filter::http::rbac::v2::RBAC& config, bool disabled) + : engine_disabled_(disabled), + allowed_if_matched_(disabled || + config.rules().action() == + envoy::config::rbac::v2alpha::RBAC_Action::RBAC_Action_ALLOW) { + if (disabled) { + return; + } + + for (const auto& policy : config.rules().policies()) { + policies_.emplace_back(policy.second); + } +} + +RoleBasedAccessControlEngineImpl::RoleBasedAccessControlEngineImpl( + const envoy::config::filter::http::rbac::v2::RBACPerRoute& per_route_config) + : RoleBasedAccessControlEngineImpl(per_route_config.rbac(), per_route_config.disabled()) {} + +bool RoleBasedAccessControlEngineImpl::allowed(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const { + if (engine_disabled_) { + return true; + } + + bool matched = false; + for (const auto& policy : policies_) { + if (policy.matches(connection, headers)) { + matched = true; + break; + } + } + + // only allowed if: + // - matched and ALLOW action + // - not matched and DENY action + return matched == allowed_if_matched_; +} + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/rbac/engine_impl.h b/source/extensions/filters/common/rbac/engine_impl.h new file mode 100644 index 0000000000000..8a46a6fe4f04d --- /dev/null +++ b/source/extensions/filters/common/rbac/engine_impl.h @@ -0,0 +1,39 @@ +#pragma once + +#include "envoy/config/filter/http/rbac/v2/rbac.pb.h" + +#include "extensions/filters/common/rbac/engine.h" +#include "extensions/filters/common/rbac/matchers.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +class RoleBasedAccessControlEngineImpl : public RoleBasedAccessControlEngine { +public: + RoleBasedAccessControlEngineImpl(const envoy::config::filter::http::rbac::v2::RBAC& config, + bool disabled); + RoleBasedAccessControlEngineImpl( + const envoy::config::filter::http::rbac::v2::RBACPerRoute& per_route_config); + + bool allowed(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + // Indicates that the engine will not evaluate an action and just return true for calls to + // allowed. This value is only set by route-local configuration. + const bool engine_disabled_; + + // Indicates the behavior to take if a policy matches an action. + const bool allowed_if_matched_; + + std::vector policies_; +}; + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/rbac/matchers.cc b/source/extensions/filters/common/rbac/matchers.cc new file mode 100644 index 0000000000000..9cead82116c70 --- /dev/null +++ b/source/extensions/filters/common/rbac/matchers.cc @@ -0,0 +1,140 @@ +#include "extensions/filters/common/rbac/matchers.h" + +#include "common/common/assert.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +MatcherConstSharedPtr Matcher::create(const envoy::config::rbac::v2alpha::Permission& permission) { + switch (permission.rule_case()) { + case envoy::config::rbac::v2alpha::Permission::RuleCase::kAndRules: + return std::make_shared(permission.and_rules()); + case envoy::config::rbac::v2alpha::Permission::RuleCase::kOrRules: + return std::make_shared(permission.or_rules()); + case envoy::config::rbac::v2alpha::Permission::RuleCase::kHeader: + return std::make_shared(permission.header()); + case envoy::config::rbac::v2alpha::Permission::RuleCase::kDestinationIp: + return std::make_shared(permission.destination_ip(), true); + case envoy::config::rbac::v2alpha::Permission::RuleCase::kDestinationPort: + return std::make_shared(permission.destination_port()); + case envoy::config::rbac::v2alpha::Permission::RuleCase::kAny: + return std::make_shared(); + default: + NOT_REACHED; + } +} + +MatcherConstSharedPtr Matcher::create(const envoy::config::rbac::v2alpha::Principal& principal) { + switch (principal.identifier_case()) { + case envoy::config::rbac::v2alpha::Principal::IdentifierCase::kAndIds: + return std::make_shared(principal.and_ids()); + case envoy::config::rbac::v2alpha::Principal::IdentifierCase::kOrIds: + return std::make_shared(principal.or_ids()); + case envoy::config::rbac::v2alpha::Principal::IdentifierCase::kAuthenticated: + return std::make_shared(principal.authenticated()); + case envoy::config::rbac::v2alpha::Principal::IdentifierCase::kSourceIp: + return std::make_shared(principal.source_ip(), false); + case envoy::config::rbac::v2alpha::Principal::IdentifierCase::kHeader: + return std::make_shared(principal.header()); + case envoy::config::rbac::v2alpha::Principal::IdentifierCase::kAny: + return std::make_shared(); + default: + NOT_REACHED; + } +} + +AndMatcher::AndMatcher(const envoy::config::rbac::v2alpha::Permission_Set& set) { + for (const auto& rule : set.rules()) { + matchers_.push_back(Matcher::create(rule)); + } +} + +AndMatcher::AndMatcher(const envoy::config::rbac::v2alpha::Principal_Set& set) { + for (const auto& id : set.ids()) { + matchers_.push_back(Matcher::create(id)); + } +} + +bool AndMatcher::matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const { + for (const auto& matcher : matchers_) { + if (!matcher->matches(connection, headers)) { + return false; + } + } + + return true; +} + +OrMatcher::OrMatcher( + const Protobuf::RepeatedPtrField<::envoy::config::rbac::v2alpha::Permission>& rules) { + for (const auto& rule : rules) { + matchers_.push_back(Matcher::create(rule)); + } +} + +OrMatcher::OrMatcher( + const Protobuf::RepeatedPtrField<::envoy::config::rbac::v2alpha::Principal>& ids) { + for (const auto& id : ids) { + matchers_.push_back(Matcher::create(id)); + } +} + +bool OrMatcher::matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const { + for (const auto& matcher : matchers_) { + if (matcher->matches(connection, headers)) { + return true; + } + } + + return false; +} + +bool HeaderMatcher::matches(const Network::Connection&, + const Envoy::Http::HeaderMap& headers) const { + return Envoy::Http::HeaderUtility::matchHeaders(headers, header_); +} + +bool IPMatcher::matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap&) const { + const Envoy::Network::Address::InstanceConstSharedPtr& ip = + destination_ ? connection.localAddress() : connection.remoteAddress(); + + return range_.isInRange(*ip.get()); +} + +bool PortMatcher::matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap&) const { + const Envoy::Network::Address::Ip* ip = connection.localAddress().get()->ip(); + return ip && ip->port() == port_; +} + +bool AuthenticatedMatcher::matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap&) const { + const auto* ssl = connection.ssl(); + if (!ssl) { // connection was not authenticated + return false; + } else if (name_.empty()) { // matcher allows any subject + return true; + } + + std::string principal = ssl->uriSanPeerCertificate(); + principal = principal.empty() ? ssl->subjectPeerCertificate() : principal; + + return principal == name_; +} + +bool PolicyMatcher::matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const { + return permissions_.matches(connection, headers) && principals_.matches(connection, headers); +} + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/rbac/matchers.h b/source/extensions/filters/common/rbac/matchers.h new file mode 100644 index 0000000000000..2c09288a08354 --- /dev/null +++ b/source/extensions/filters/common/rbac/matchers.h @@ -0,0 +1,178 @@ +#pragma once + +#include + +#include "envoy/config/rbac/v2alpha/rbac.pb.h" +#include "envoy/http/header_map.h" +#include "envoy/network/connection.h" + +#include "common/http/header_utility.h" +#include "common/network/cidr_range.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +class Matcher; +typedef std::shared_ptr MatcherConstSharedPtr; + +/** + * Matchers describe the rules for matching either a permission action or principal. + */ +class Matcher { +public: + virtual ~Matcher() {} + + /** + * Returns whether or not the permission/principal matches the rules of the matcher. + * + * @param connection the downstream connection used to match against. + * @param headers the request headers used to match against. An empty map should be used if + * there are none headers available. + */ + virtual bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const PURE; + + /** + * Creates a shared instance of a matcher based off the rules defined in the Permission config + * proto message. + */ + static MatcherConstSharedPtr create(const envoy::config::rbac::v2alpha::Permission& permission); + + /** + * Creates a shared instance of a matcher based off the rules defined in the Principal config + * proto message. + */ + static MatcherConstSharedPtr create(const envoy::config::rbac::v2alpha::Principal& principal); +}; + +/** + * Always matches, returning true for any input. + */ +class AlwaysMatcher : public Matcher { +public: + bool matches(const Network::Connection&, const Envoy::Http::HeaderMap&) const override { + return true; + } +}; + +/** + * A composite matcher where all sub-matchers must match for this to return true. Evaluation + * short-circuits on the first non-match. + */ +class AndMatcher : public Matcher { +public: + AndMatcher(const envoy::config::rbac::v2alpha::Permission_Set& rules); + AndMatcher(const envoy::config::rbac::v2alpha::Principal_Set& ids); + + bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + std::vector matchers_; +}; + +/** + * A composite matcher where only one sub-matcher must match for this to return true. Evaluation + * short-circuits on the first match. + */ +class OrMatcher : public Matcher { +public: + OrMatcher(const envoy::config::rbac::v2alpha::Permission_Set& set) : OrMatcher(set.rules()) {} + OrMatcher(const envoy::config::rbac::v2alpha::Principal_Set& set) : OrMatcher(set.ids()) {} + OrMatcher(const Protobuf::RepeatedPtrField<::envoy::config::rbac::v2alpha::Permission>& rules); + OrMatcher(const Protobuf::RepeatedPtrField<::envoy::config::rbac::v2alpha::Principal>& ids); + + bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + std::vector matchers_; +}; + +/** + * Perform a match against any HTTP header (or pseudo-header, such as `:path` or `:authority`). Will + * always fail to match on any non-HTTP connection. + */ +class HeaderMatcher : public Matcher { +public: + HeaderMatcher(const envoy::api::v2::route::HeaderMatcher& matcher) : header_(matcher) {} + + bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + const Envoy::Http::HeaderUtility::HeaderData header_; +}; + +/** + * Perform a match against an IP CIDR range. This rule can be applied to either the source + * (remote) or the destination (local) IP. + */ +class IPMatcher : public Matcher { +public: + IPMatcher(const envoy::api::v2::core::CidrRange& range, bool destination) + : range_(Network::Address::CidrRange::create(range)), destination_(destination) {} + + bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + const Network::Address::CidrRange range_; + const bool destination_; +}; + +/** + * Matches the port number of the destination (local) address. + */ +class PortMatcher : public Matcher { +public: + PortMatcher(const uint32_t port) : port_(port) {} + + bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + const uint32_t port_; +}; + +/** + * Matches the principal name as described in the peer certificate. Uses the URI SAN first. If that + * field is not present, uses the subject instead. + */ +class AuthenticatedMatcher : public Matcher { +public: + AuthenticatedMatcher(const envoy::config::rbac::v2alpha::Principal_Authenticated& auth) + : name_(auth.name()) {} + + bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + const std::string name_; +}; + +/** + * Matches a Policy which is a collection of permission and principal matchers. If any action + * matches a permission, the principals are then checked for a match. + */ +class PolicyMatcher : public Matcher { +public: + PolicyMatcher(const envoy::config::rbac::v2alpha::Policy& policy) + : permissions_(policy.permissions()), principals_(policy.principals()) {} + + bool matches(const Network::Connection& connection, + const Envoy::Http::HeaderMap& headers) const override; + +private: + const OrMatcher permissions_; + const OrMatcher principals_; +}; + +} // 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 new file mode 100644 index 0000000000000..0dfa0574e1a1c --- /dev/null +++ b/source/extensions/filters/http/rbac/BUILD @@ -0,0 +1,35 @@ +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 = [ + "//include/envoy/registry", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "//source/extensions/filters/http/rbac:rbac_filter_lib", + ], +) + +envoy_cc_library( + name = "rbac_filter_lib", + srcs = ["rbac_filter.cc"], + hdrs = ["rbac_filter.h"], + deps = [ + "//include/envoy/http:filter_interface", + "//include/envoy/stats:stats_macros", + "//source/common/http:utility_lib", + "//source/extensions/filters/common/rbac:engine_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/config.cc b/source/extensions/filters/http/rbac/config.cc new file mode 100644 index 0000000000000..ef652bc674fcf --- /dev/null +++ b/source/extensions/filters/http/rbac/config.cc @@ -0,0 +1,42 @@ +#include "extensions/filters/http/rbac/config.h" + +#include "envoy/registry/registry.h" + +#include "extensions/filters/http/rbac/rbac_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RBACFilter { + +Http::FilterFactoryCb RoleBasedAccessControlFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::config::filter::http::rbac::v2::RBAC& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) { + + auto config = std::make_shared(proto_config, stats_prefix, + context.scope()); + + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter(std::make_shared(config)); + }; +} + +Router::RouteSpecificFilterConfigConstSharedPtr +RoleBasedAccessControlFilterConfigFactory::createRouteSpecificFilterConfigTyped( + const envoy::config::filter::http::rbac::v2::RBACPerRoute& proto_config, + Server::Configuration::FactoryContext&) { + return std::make_shared( + proto_config); +} + +/** + * Static registration for the RBAC filter. @see RegisterFactory + */ +static Registry::RegisterFactory + register_; + +} // namespace RBACFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/rbac/config.h b/source/extensions/filters/http/rbac/config.h new file mode 100644 index 0000000000000..c6002fb284018 --- /dev/null +++ b/source/extensions/filters/http/rbac/config.h @@ -0,0 +1,37 @@ +#pragma once + +#include "envoy/config/filter/http/rbac/v2/rbac.pb.h" +#include "envoy/config/filter/http/rbac/v2/rbac.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RBACFilter { + +/** + * Config registration for the RBAC filter. @see NamedHttpFilterConfigFactory. + */ +class RoleBasedAccessControlFilterConfigFactory + : public Common::FactoryBase { +public: + RoleBasedAccessControlFilterConfigFactory() : FactoryBase(HttpFilterNames::get().RBAC) {} + +private: + Http::FilterFactoryCb + createFilterFactoryFromProtoTyped(const envoy::config::filter::http::rbac::v2::RBAC& proto_config, + const std::string& stats_prefix, + Server::Configuration::FactoryContext& context) override; + + Router::RouteSpecificFilterConfigConstSharedPtr createRouteSpecificFilterConfigTyped( + const envoy::config::filter::http::rbac::v2::RBACPerRoute& proto_config, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace RBACFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/rbac/rbac_filter.cc b/source/extensions/filters/http/rbac/rbac_filter.cc new file mode 100644 index 0000000000000..cafd0f2d4b6c2 --- /dev/null +++ b/source/extensions/filters/http/rbac/rbac_filter.cc @@ -0,0 +1,60 @@ +#include "extensions/filters/http/rbac/rbac_filter.h" + +#include "common/http/utility.h" + +#include "extensions/filters/common/rbac/engine_impl.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RBACFilter { + +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, false) {} + +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))}; +} + +const Filters::Common::RBAC::RoleBasedAccessControlEngine& +RoleBasedAccessControlFilterConfig::engine(const Router::RouteConstSharedPtr route) const { + if (!route || !route->routeEntry()) { + return engine_; + } + + const std::string& name = HttpFilterNames::get().RBAC; + const auto* entry = route->routeEntry(); + + const auto* route_local = + entry->perFilterConfigTyped(name) + ?: entry->virtualHost() + .perFilterConfigTyped(name); + + return route_local ? *route_local : engine_; +} + +Http::FilterHeadersStatus RoleBasedAccessControlFilter::decodeHeaders(Http::HeaderMap& headers, + bool) { + const Filters::Common::RBAC::RoleBasedAccessControlEngine& engine = + config_->engine(callbacks_->route()); + + if (engine.allowed(*callbacks_->connection(), headers)) { + config_->stats().allowed_.inc(); + return Http::FilterHeadersStatus::Continue; + } + + callbacks_->sendLocalReply(Http::Code::Forbidden, "RBAC: access denied", nullptr); + config_->stats().denied_.inc(); + return Http::FilterHeadersStatus::StopIteration; +} + +} // namespace RBACFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/rbac/rbac_filter.h b/source/extensions/filters/http/rbac/rbac_filter.h new file mode 100644 index 0000000000000..bce03c39fad03 --- /dev/null +++ b/source/extensions/filters/http/rbac/rbac_filter.h @@ -0,0 +1,91 @@ +#pragma once + +#include + +#include "envoy/config/filter/http/rbac/v2/rbac.pb.h" +#include "envoy/http/filter.h" +#include "envoy/stats/stats_macros.h" + +#include "extensions/filters/common/rbac/engine_impl.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) +// clang-format on + +/** + * Wrapper struct for RBAC filter stats. @see stats_macros.h + */ +struct RoleBasedAccessControlFilterStats { + ALL_RBAC_FILTER_STATS(GENERATE_COUNTER_STRUCT) +}; + +/** + * Configuration for the RBAC filter. + */ +class RoleBasedAccessControlFilterConfig { +public: + RoleBasedAccessControlFilterConfig( + const envoy::config::filter::http::rbac::v2::RBAC& proto_config, + const std::string& stats_prefix, Stats::Scope& scope); + + RoleBasedAccessControlFilterStats& stats() { return stats_; } + + const Filters::Common::RBAC::RoleBasedAccessControlEngine& + engine(const Router::RouteConstSharedPtr route) const; + +private: + RoleBasedAccessControlFilterStats stats_; + const Filters::Common::RBAC::RoleBasedAccessControlEngineImpl engine_; +}; + +typedef std::shared_ptr + RoleBasedAccessControlFilterConfigSharedPtr; + +/** + * A filter that provides role-based access control authorization for HTTP requests. + */ +class RoleBasedAccessControlFilter : public Http::StreamDecoderFilter { +public: + 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; + + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override { + return Http::FilterDataStatus::Continue; + } + + Http::FilterTrailersStatus decodeTrailers(Http::HeaderMap&) override { + return Http::FilterTrailersStatus::Continue; + } + + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override { + callbacks_ = &callbacks; + } + + // Http::StreamFilterBase + void onDestroy() override {} + +private: + RoleBasedAccessControlFilterConfigSharedPtr config_; + Http::StreamDecoderFilterCallbacks* callbacks_{}; +}; + +} // namespace RBACFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index e54e226837d03..a2f09b1c53f20 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -42,6 +42,8 @@ class HttpFilterNameValues { const std::string SQUASH = "envoy.squash"; // External Authorization filter const std::string EXT_AUTHORIZATION = "envoy.ext_authz"; + // RBAC HTTP Authorization filter + const std::string RBAC = "envoy.filters.http.rbac"; // Converts names from v1 to v2 const Config::V1Converter v1_converter_; diff --git a/test/extensions/extensions_build_system.bzl b/test/extensions/extensions_build_system.bzl index 2c6cb0e352017..1ea8f3630c1e0 100644 --- a/test/extensions/extensions_build_system.bzl +++ b/test/extensions/extensions_build_system.bzl @@ -1,4 +1,4 @@ -load("//bazel:envoy_build_system.bzl", "envoy_cc_test") +load("//bazel:envoy_build_system.bzl", "envoy_cc_test", "envoy_cc_mock") load("@envoy_build_config//:extensions_build_config.bzl", "EXTENSIONS") # All extension tests should use this version of envoy_cc_test(). It allows compiling out @@ -11,3 +11,11 @@ def envoy_extension_cc_test(name, return envoy_cc_test(name, **kwargs) + +def envoy_extension_cc_mock(name, + extension_name, + **kwargs): + if not extension_name in EXTENSIONS: + return + + envoy_cc_mock(name, **kwargs) \ No newline at end of file diff --git a/test/extensions/filters/common/rbac/BUILD b/test/extensions/filters/common/rbac/BUILD new file mode 100644 index 0000000000000..6e2f134f73247 --- /dev/null +++ b/test/extensions/filters/common/rbac/BUILD @@ -0,0 +1,47 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_mock", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_extension_cc_test( + name = "matchers_test", + srcs = ["matchers_test.cc"], + extension_name = "envoy.filters.http.rbac", + deps = [ + "//source/extensions/filters/common/rbac:matchers_lib", + "//test/mocks/network:network_mocks", + "//test/mocks/ssl:ssl_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "engine_impl_test", + srcs = ["engine_impl_test.cc"], + extension_name = "envoy.filters.http.rbac", + deps = [ + "//source/extensions/filters/common/rbac:engine_lib", + "//test/mocks/network:network_mocks", + "//test/mocks/ssl:ssl_mocks", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_mock( + name = "engine_mocks", + srcs = ["mocks.cc"], + hdrs = ["mocks.h"], + extension_name = "envoy.filters.http.rbac", + deps = [ + "//source/extensions/filters/common/rbac:engine_interface", + ], +) diff --git a/test/extensions/filters/common/rbac/engine_impl_test.cc b/test/extensions/filters/common/rbac/engine_impl_test.cc new file mode 100644 index 0000000000000..d1cc0008d757c --- /dev/null +++ b/test/extensions/filters/common/rbac/engine_impl_test.cc @@ -0,0 +1,89 @@ +#include "common/network/utility.h" + +#include "extensions/filters/common/rbac/engine_impl.h" + +#include "test/mocks/network/mocks.h" +#include "test/mocks/ssl/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::Const; +using testing::Return; +using testing::ReturnRef; +using testing::_; + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { +namespace { + +void checkEngine(const RBAC::RoleBasedAccessControlEngineImpl& engine, bool expected, + const Envoy::Network::Connection& connection = Envoy::Network::MockConnection(), + const Envoy::Http::HeaderMap& headers = Envoy::Http::HeaderMapImpl()) { + EXPECT_EQ(expected, engine.allowed(connection, headers)); +} + +TEST(RoleBasedAccessControlEngineImpl, Disabled) { + envoy::config::filter::http::rbac::v2::RBACPerRoute config; + config.set_disabled(true); + checkEngine(RBAC::RoleBasedAccessControlEngineImpl(config), true); + checkEngine( + RBAC::RoleBasedAccessControlEngineImpl(envoy::config::filter::http::rbac::v2::RBAC(), true), + true); +} + +TEST(RoleBasedAccessControlEngineImpl, AllowedWhitelist) { + envoy::config::rbac::v2alpha::Policy policy; + policy.add_permissions()->set_destination_port(123); + policy.add_principals()->set_any(true); + + envoy::config::filter::http::rbac::v2::RBACPerRoute config; + envoy::config::rbac::v2alpha::RBAC* rbac = config.mutable_rbac()->mutable_rules(); + rbac->set_action(envoy::config::rbac::v2alpha::RBAC_Action::RBAC_Action_ALLOW); + (*rbac->mutable_policies())["foo"] = policy; + + RBAC::RoleBasedAccessControlEngineImpl engine(config); + + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 123, false); + EXPECT_CALL(conn, localAddress()).WillOnce(ReturnRef(addr)); + checkEngine(engine, true, conn); + + addr = Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 456, false); + EXPECT_CALL(conn, localAddress()).WillOnce(ReturnRef(addr)); + checkEngine(engine, false, conn); +} + +TEST(RoleBasedAccessControlEngineImpl, DeniedBlacklist) { + envoy::config::rbac::v2alpha::Policy policy; + policy.add_permissions()->set_destination_port(123); + policy.add_principals()->set_any(true); + + envoy::config::filter::http::rbac::v2::RBACPerRoute config; + envoy::config::rbac::v2alpha::RBAC* rbac = config.mutable_rbac()->mutable_rules(); + rbac->set_action(envoy::config::rbac::v2alpha::RBAC_Action::RBAC_Action_DENY); + (*rbac->mutable_policies())["foo"] = policy; + + RBAC::RoleBasedAccessControlEngineImpl engine(config); + + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 123, false); + EXPECT_CALL(conn, localAddress()).WillOnce(ReturnRef(addr)); + checkEngine(engine, false, conn); + + addr = Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 456, false); + EXPECT_CALL(conn, localAddress()).WillOnce(ReturnRef(addr)); + checkEngine(engine, true, conn); +} + +} // namespace +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/common/rbac/matchers_test.cc b/test/extensions/filters/common/rbac/matchers_test.cc new file mode 100644 index 0000000000000..680293acb8f0c --- /dev/null +++ b/test/extensions/filters/common/rbac/matchers_test.cc @@ -0,0 +1,249 @@ +#include "common/network/utility.h" + +#include "extensions/filters/common/rbac/matchers.h" + +#include "test/mocks/network/mocks.h" +#include "test/mocks/ssl/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::Const; +using testing::Return; +using testing::ReturnRef; +using testing::_; + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { +namespace { + +void checkMatcher(const RBAC::Matcher& matcher, bool expected, + const Envoy::Network::Connection& connection = Envoy::Network::MockConnection(), + const Envoy::Http::HeaderMap& headers = Envoy::Http::HeaderMapImpl()) { + EXPECT_EQ(expected, matcher.matches(connection, headers)); +} + +TEST(AlwaysMatcher, AlwaysMatches) { checkMatcher(RBAC::AlwaysMatcher(), true); } + +TEST(AndMatcher, Permission_Set) { + envoy::config::rbac::v2alpha::Permission_Set set; + envoy::config::rbac::v2alpha::Permission* perm = set.add_rules(); + perm->set_any(true); + + checkMatcher(RBAC::AndMatcher(set), true); + + perm = set.add_rules(); + perm->set_destination_port(123); + + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 123, false); + EXPECT_CALL(conn, localAddress()).WillOnce(ReturnRef(addr)); + + checkMatcher(RBAC::AndMatcher(set), true, conn); + + addr = Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 8080, false); + EXPECT_CALL(conn, localAddress()).WillOnce(ReturnRef(addr)); + + checkMatcher(RBAC::AndMatcher(set), false, conn); +} + +TEST(AndMatcher, Principal_Set) { + envoy::config::rbac::v2alpha::Principal_Set set; + envoy::config::rbac::v2alpha::Principal* principal = set.add_ids(); + principal->set_any(true); + + checkMatcher(RBAC::AndMatcher(set), true); + + principal = set.add_ids(); + auto* cidr = principal->mutable_source_ip(); + cidr->set_address_prefix("1.2.3.0"); + cidr->mutable_prefix_len()->set_value(24); + + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 123, false); + EXPECT_CALL(conn, remoteAddress()).WillOnce(ReturnRef(addr)); + + checkMatcher(RBAC::AndMatcher(set), true, conn); + + addr = Envoy::Network::Utility::parseInternetAddress("1.2.4.6", 123, false); + EXPECT_CALL(conn, remoteAddress()).WillOnce(ReturnRef(addr)); + + checkMatcher(RBAC::AndMatcher(set), false, conn); +} + +TEST(OrMatcher, Permission_Set) { + envoy::config::rbac::v2alpha::Permission_Set set; + envoy::config::rbac::v2alpha::Permission* perm = set.add_rules(); + perm->set_destination_port(123); + + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 456, false); + EXPECT_CALL(conn, localAddress()).Times(2).WillRepeatedly(ReturnRef(addr)); + + checkMatcher(RBAC::OrMatcher(set), false, conn); + + perm = set.add_rules(); + perm->set_any(true); + + checkMatcher(RBAC::OrMatcher(set), true, conn); +} + +TEST(OrMatcher, Principal_Set) { + envoy::config::rbac::v2alpha::Principal_Set set; + envoy::config::rbac::v2alpha::Principal* id = set.add_ids(); + auto* cidr = id->mutable_source_ip(); + cidr->set_address_prefix("1.2.3.0"); + cidr->mutable_prefix_len()->set_value(24); + + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.4.6", 456, false); + EXPECT_CALL(conn, remoteAddress()).Times(2).WillRepeatedly(ReturnRef(addr)); + + checkMatcher(RBAC::OrMatcher(set), false, conn); + + id = set.add_ids(); + id->set_any(true); + + checkMatcher(RBAC::OrMatcher(set), true, conn); +} + +TEST(HeaderMatcher, HeaderMatcher) { + envoy::api::v2::route::HeaderMatcher config; + config.set_name("foo"); + config.set_exact_match("bar"); + + Envoy::Http::HeaderMapImpl headers; + Envoy::Http::LowerCaseString key("foo"); + std::string value = "bar"; + headers.setReference(key, value); + + RBAC::HeaderMatcher matcher(config); + + checkMatcher(matcher, true, Envoy::Network::MockConnection(), headers); + + value = "baz"; + headers.setReference(key, value); + + checkMatcher(matcher, false, Envoy::Network::MockConnection(), headers); + checkMatcher(matcher, false); +} + +TEST(IPMatcher, IPMatcher) { + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr local = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 123, false); + Envoy::Network::Address::InstanceConstSharedPtr remote = + Envoy::Network::Utility::parseInternetAddress("4.5.6.7", 456, false); + EXPECT_CALL(conn, localAddress()).Times(2).WillRepeatedly(ReturnRef(local)); + EXPECT_CALL(conn, remoteAddress()).Times(2).WillRepeatedly(ReturnRef(remote)); + + envoy::api::v2::core::CidrRange local_cidr; + local_cidr.set_address_prefix("1.2.3.0"); + local_cidr.mutable_prefix_len()->set_value(24); + + envoy::api::v2::core::CidrRange remote_cidr; + remote_cidr.set_address_prefix("4.5.6.7"); + remote_cidr.mutable_prefix_len()->set_value(32); + + checkMatcher(IPMatcher(local_cidr, true), true, conn); + checkMatcher(IPMatcher(remote_cidr, false), true, conn); + + local_cidr.set_address_prefix("1.2.4.8"); + remote_cidr.set_address_prefix("4.5.6.0"); + + checkMatcher(IPMatcher(local_cidr, true), false, conn); + checkMatcher(IPMatcher(remote_cidr, false), false, conn); +} + +TEST(PortMatcher, PortMatcher) { + Envoy::Network::MockConnection conn; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 123, false); + EXPECT_CALL(conn, localAddress()).Times(2).WillRepeatedly(ReturnRef(addr)); + + checkMatcher(PortMatcher(123), true, conn); + checkMatcher(PortMatcher(456), false, conn); +} + +TEST(AuthenticatedMatcher, uriSanPeerCertificate) { + Envoy::Network::MockConnection conn; + Envoy::Ssl::MockConnection ssl; + + EXPECT_CALL(ssl, uriSanPeerCertificate()).WillOnce(Return("foo")); + EXPECT_CALL(Const(conn), ssl()).WillOnce(Return(&ssl)); + + envoy::config::rbac::v2alpha::Principal_Authenticated auth; + auth.set_name("foo"); + checkMatcher(AuthenticatedMatcher(auth), true, conn); +} + +TEST(AuthenticatedMatcher, subjectPeerCertificate) { + Envoy::Network::MockConnection conn; + Envoy::Ssl::MockConnection ssl; + + EXPECT_CALL(ssl, uriSanPeerCertificate()).WillOnce(Return("")); + EXPECT_CALL(ssl, subjectPeerCertificate()).WillOnce(Return("bar")); + EXPECT_CALL(Const(conn), ssl()).WillOnce(Return(&ssl)); + + envoy::config::rbac::v2alpha::Principal_Authenticated auth; + auth.set_name("bar"); + checkMatcher(AuthenticatedMatcher(auth), true, conn); +} + +TEST(AuthenticatedMatcher, AnySSLSubject) { + Envoy::Network::MockConnection conn; + Envoy::Ssl::MockConnection ssl; + EXPECT_CALL(Const(conn), ssl()).WillOnce(Return(&ssl)); + checkMatcher(AuthenticatedMatcher({}), true, conn); +} + +TEST(AuthenticatedMatcher, NoSSL) { + Envoy::Network::MockConnection conn; + EXPECT_CALL(Const(conn), ssl()).WillOnce(Return(nullptr)); + checkMatcher(AuthenticatedMatcher({}), false, conn); +} + +TEST(PolicyMatcher, PolicyMatcher) { + envoy::config::rbac::v2alpha::Policy policy; + policy.add_permissions()->set_destination_port(123); + policy.add_permissions()->set_destination_port(456); + policy.add_principals()->mutable_authenticated()->set_name("foo"); + policy.add_principals()->mutable_authenticated()->set_name("bar"); + + RBAC::PolicyMatcher matcher(policy); + + Envoy::Network::MockConnection conn; + Envoy::Ssl::MockConnection ssl; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 456, false); + + EXPECT_CALL(ssl, uriSanPeerCertificate()).Times(2).WillRepeatedly(Return("bar")); + EXPECT_CALL(Const(conn), ssl()).Times(2).WillRepeatedly(Return(&ssl)); + EXPECT_CALL(conn, localAddress()).Times(2).WillRepeatedly(ReturnRef(addr)); + + checkMatcher(matcher, true, conn); + + EXPECT_CALL(Const(conn), ssl()).Times(2).WillRepeatedly(Return(nullptr)); + EXPECT_CALL(conn, localAddress()).Times(2).WillRepeatedly(ReturnRef(addr)); + + checkMatcher(matcher, false, conn); + + addr = Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 789, false); + EXPECT_CALL(conn, localAddress()).Times(2).WillRepeatedly(ReturnRef(addr)); + + checkMatcher(matcher, false, conn); +} + +} // namespace +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/common/rbac/mocks.cc b/test/extensions/filters/common/rbac/mocks.cc new file mode 100644 index 0000000000000..139d9c39309d4 --- /dev/null +++ b/test/extensions/filters/common/rbac/mocks.cc @@ -0,0 +1,16 @@ +#include "test/extensions/filters/common/rbac/mocks.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +MockEngine::MockEngine() {} +MockEngine::~MockEngine() {} + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/common/rbac/mocks.h b/test/extensions/filters/common/rbac/mocks.h new file mode 100644 index 0000000000000..0255aed895358 --- /dev/null +++ b/test/extensions/filters/common/rbac/mocks.h @@ -0,0 +1,26 @@ +#pragma once + +#include "extensions/filters/common/rbac/engine.h" + +#include "gmock/gmock.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace RBAC { + +class MockEngine : public RoleBasedAccessControlEngine { +public: + MockEngine(); + virtual ~MockEngine(); + + MOCK_CONST_METHOD2(allowed, + bool(const Envoy::Network::Connection&, const Envoy::Http::HeaderMap&)); +}; + +} // namespace RBAC +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/rbac/BUILD b/test/extensions/filters/http/rbac/BUILD new file mode 100644 index 0000000000000..cf6a35c1535b2 --- /dev/null +++ b/test/extensions/filters/http/rbac/BUILD @@ -0,0 +1,45 @@ +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.http.rbac", + deps = [ + "//source/extensions/filters/http/rbac:config", + "//test/mocks/server:server_mocks", + ], +) + +envoy_extension_cc_test( + name = "rbac_filter_test", + srcs = ["rbac_filter_test.cc"], + extension_name = "envoy.filters.http.rbac", + deps = [ + "//source/extensions/filters/http/rbac:rbac_filter_lib", + "//test/extensions/filters/common/rbac:engine_mocks", + "//test/mocks/http:http_mocks", + "//test/mocks/network:network_mocks", + ], +) + +envoy_extension_cc_test( + name = "rbac_filter_integration_test", + srcs = ["rbac_filter_integration_test.cc"], + extension_name = "envoy.filters.http.rbac", + deps = [ + "//source/extensions/filters/http/rbac:config", + "//test/config:utility_lib", + "//test/integration:http_protocol_integration_lib", + ], +) diff --git a/test/extensions/filters/http/rbac/config_test.cc b/test/extensions/filters/http/rbac/config_test.cc new file mode 100644 index 0000000000000..2d3e1b3891f56 --- /dev/null +++ b/test/extensions/filters/http/rbac/config_test.cc @@ -0,0 +1,78 @@ +#include "extensions/filters/common/rbac/engine.h" +#include "extensions/filters/http/rbac/config.h" + +#include "test/mocks/server/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::_; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RBACFilter { +namespace { + +TEST(RoleBasedAccessControlFilterConfigFactoryTest, ValidateFail) { + NiceMock context; + EXPECT_THROW(RoleBasedAccessControlFilterConfigFactory().createFilterFactoryFromProto( + envoy::config::filter::http::rbac::v2::RBAC(), "stats", context), + ProtoValidationException); +} + +TEST(RoleBasedAccessControlFilterConfigFactoryTest, ValidProto) { + envoy::config::rbac::v2alpha::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + envoy::config::filter::http::rbac::v2::RBAC config; + (*config.mutable_rules()->mutable_policies())["foo"] = policy; + + NiceMock context; + RoleBasedAccessControlFilterConfigFactory factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(config, "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callbacks; + EXPECT_CALL(filter_callbacks, addStreamDecoderFilter(_)); + cb(filter_callbacks); +} + +TEST(RoleBasedAccessControlFilterConfigFactoryTest, EmptyProto) { + RoleBasedAccessControlFilterConfigFactory factory; + auto* config = dynamic_cast( + factory.createEmptyConfigProto().get()); + EXPECT_NE(nullptr, config); +} + +TEST(RoleBasedAccessControlFilterConfigFactoryTest, EmptyRouteProto) { + RoleBasedAccessControlFilterConfigFactory factory; + auto* config = dynamic_cast( + factory.createEmptyRouteConfigProto().get()); + EXPECT_NE(nullptr, config); +} + +TEST(RoleBasedAccessControlFilterConfigFactoryTest, RouteSpecificConfig) { + RoleBasedAccessControlFilterConfigFactory factory; + NiceMock context; + + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + EXPECT_TRUE(proto_config.get()); + + auto& cfg = + dynamic_cast(*proto_config.get()); + cfg.set_disabled(true); + + Router::RouteSpecificFilterConfigConstSharedPtr route_config = + factory.createRouteSpecificFilterConfig(*proto_config, context); + EXPECT_TRUE(route_config.get()); + + const auto* inflated = + dynamic_cast(route_config.get()); + EXPECT_NE(nullptr, inflated); +} + +} // namespace +} // namespace RBACFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/rbac/rbac_filter_integration_test.cc b/test/extensions/filters/http/rbac/rbac_filter_integration_test.cc new file mode 100644 index 0000000000000..6039c328044ba --- /dev/null +++ b/test/extensions/filters/http/rbac/rbac_filter_integration_test.cc @@ -0,0 +1,106 @@ +#include "extensions/filters/http/well_known_names.h" + +#include "test/integration/http_protocol_integration.h" + +namespace Envoy { +namespace { + +const std::string RBAC_CONFIG = R"EOF( +name: envoy.filters.http.rbac +config: + rules: + policies: + foo: + permissions: + - header: { name: ":method", exact_match: "GET" } + principals: + - any: true +)EOF"; + +typedef HttpProtocolIntegrationTest RBACIntegrationTest; + +INSTANTIATE_TEST_CASE_P(Protocols, RBACIntegrationTest, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +TEST_P(RBACIntegrationTest, Allowed) { + config_helper_.addFilter(RBAC_CONFIG); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = codec_client_->makeRequestWithBody( + Http::TestHeaderMapImpl{ + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + }, + 1024); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, true); + + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); +} + +TEST_P(RBACIntegrationTest, Denied) { + config_helper_.addFilter(RBAC_CONFIG); + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = codec_client_->makeRequestWithBody( + Http::TestHeaderMapImpl{ + {":method", "POST"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + }, + 1024); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("403", response->headers().Status()->value().c_str()); +} + +TEST_P(RBACIntegrationTest, RouteOverride) { + config_helper_.addConfigModifier( + [](envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager& cfg) { + ProtobufWkt::Struct pfc; + Protobuf::util::JsonStringToMessage(R"EOF({"disabled": true})EOF", &pfc); + + auto* config = cfg.mutable_route_config() + ->mutable_virtual_hosts() + ->Mutable(0) + ->mutable_per_filter_config(); + + (*config)[Extensions::HttpFilters::HttpFilterNames::get().RBAC] = pfc; + }); + config_helper_.addFilter(RBAC_CONFIG); + + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody( + Http::TestHeaderMapImpl{ + {":method", "POST"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "10.0.0.1"}, + }, + 1024); + + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(Http::TestHeaderMapImpl{{":status", "200"}}, true); + + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); +} + +} // namespace +} // namespace Envoy diff --git a/test/extensions/filters/http/rbac/rbac_filter_test.cc b/test/extensions/filters/http/rbac/rbac_filter_test.cc new file mode 100644 index 0000000000000..7d123de4eca22 --- /dev/null +++ b/test/extensions/filters/http/rbac/rbac_filter_test.cc @@ -0,0 +1,103 @@ +#include "common/network/utility.h" + +#include "extensions/filters/http/rbac/rbac_filter.h" +#include "extensions/filters/http/well_known_names.h" + +#include "test/extensions/filters/common/rbac/mocks.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/network/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; +using testing::_; + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace RBACFilter { +namespace { + +class RoleBasedAccessControlFilterTest : public testing::Test { +public: + RoleBasedAccessControlFilterConfigSharedPtr setupConfig() { + envoy::config::rbac::v2alpha::Policy policy; + policy.add_permissions()->set_destination_port(123); + policy.add_principals()->set_any(true); + envoy::config::filter::http::rbac::v2::RBAC config; + config.mutable_rules()->set_action(envoy::config::rbac::v2alpha::RBAC::ALLOW); + (*config.mutable_rules()->mutable_policies())["foo"] = policy; + + return std::make_shared(config, "test", store_); + } + + RoleBasedAccessControlFilterTest() : config_(setupConfig()), filter_(config_) {} + + void SetUp() { + EXPECT_CALL(callbacks_, connection()).WillRepeatedly(Return(&connection_)); + filter_.setDecoderFilterCallbacks(callbacks_); + } + + void setDestinationPort(uint16_t port, int times = 1) { + address_ = Envoy::Network::Utility::parseInternetAddress("1.2.3.4", port, false); + auto& expect = EXPECT_CALL(connection_, localAddress()); + if (times > 0) { + expect.Times(times); + } + expect.WillRepeatedly(ReturnRef(address_)); + } + + NiceMock callbacks_; + NiceMock connection_{}; + Stats::IsolatedStoreImpl store_; + RoleBasedAccessControlFilterConfigSharedPtr config_; + RoleBasedAccessControlFilter filter_; + Network::Address::InstanceConstSharedPtr address_; + Http::TestHeaderMapImpl headers_; +}; + +TEST_F(RoleBasedAccessControlFilterTest, Allowed) { + setDestinationPort(123); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers_, false)); + EXPECT_EQ(1U, config_->stats().allowed_.value()); + + Buffer::OwnedImpl data(""); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_.decodeData(data, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(headers_)); +} + +TEST_F(RoleBasedAccessControlFilterTest, Denied) { + setDestinationPort(456); + + Http::TestHeaderMapImpl response_headers{ + {":status", "403"}, + {"content-length", "19"}, + {"content-type", "text/plain"}, + }; + EXPECT_CALL(callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); + EXPECT_CALL(callbacks_, encodeData(_, true)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_.decodeHeaders(headers_, true)); + EXPECT_EQ(1U, config_->stats().denied_.value()); +} + +TEST_F(RoleBasedAccessControlFilterTest, RouteLocalOverride) { + setDestinationPort(456, 0); + + NiceMock engine; + EXPECT_CALL(engine, allowed(_, _)).WillOnce(Return(true)); + EXPECT_CALL(callbacks_.route_->route_entry_, perFilterConfig(HttpFilterNames::get().RBAC)) + .WillRepeatedly(Return(&engine)); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(headers_, true)); +} + +} // namespace +} // namespace RBACFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/mocks/ssl/mocks.h b/test/mocks/ssl/mocks.h index c91bb9349e0b2..d7b6a8705cff6 100644 --- a/test/mocks/ssl/mocks.h +++ b/test/mocks/ssl/mocks.h @@ -47,7 +47,7 @@ class MockConnection : public Connection { MOCK_METHOD0(uriSanLocalCertificate, std::string()); MOCK_CONST_METHOD0(sha256PeerCertificateDigest, std::string&()); MOCK_CONST_METHOD0(subjectPeerCertificate, std::string()); - MOCK_METHOD0(uriSanPeerCertificate, std::string()); + MOCK_CONST_METHOD0(uriSanPeerCertificate, std::string()); MOCK_CONST_METHOD0(subjectLocalCertificate, std::string()); MOCK_CONST_METHOD0(urlEncodedPemEncodedPeerCertificate, std::string&()); MOCK_METHOD0(dnsSansPeerCertificate, std::vector());