diff --git a/CODEOWNERS b/CODEOWNERS index a6ef0e4aa0dce..72efd16d0562d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -51,3 +51,5 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/http/adaptive_concurrency @tonya11en @mattklein123 # http inspector /*/extensions/filters/listener/http_inspector @crazyxy @PiotrSikora @lizan +# attribute context +/*/extensions/filters/common/expr @kyessenov @yangminzhu diff --git a/api/bazel/api_build_system.bzl b/api/bazel/api_build_system.bzl index 9eb8e434a5319..d9a3e2d943e63 100644 --- a/api/bazel/api_build_system.bzl +++ b/api/bazel/api_build_system.bzl @@ -23,13 +23,13 @@ def _LibrarySuffix(library_name, suffix): # TODO(htuch): Convert this to native py_proto_library once # https://github.com/bazelbuild/bazel/issues/3935 and/or # https://github.com/bazelbuild/bazel/issues/2626 are resolved. -def api_py_proto_library(name, srcs = [], deps = [], has_services = 0): +def api_py_proto_library(name, srcs = [], deps = [], external_py_proto_deps = [], has_services = 0): _py_proto_library( name = _Suffix(name, _PY_SUFFIX), srcs = srcs, default_runtime = "@com_google_protobuf//:protobuf_python", protoc = "@com_google_protobuf//:protoc", - deps = [_LibrarySuffix(d, _PY_SUFFIX) for d in deps] + [ + deps = [_LibrarySuffix(d, _PY_SUFFIX) for d in deps] + external_py_proto_deps + [ "@com_envoyproxy_protoc_gen_validate//validate:validate_py", "@com_google_googleapis//google/rpc:status_py_proto", "@com_google_googleapis//google/api:annotations_py_proto", @@ -116,6 +116,7 @@ def api_proto_library( deps = [], external_proto_deps = [], external_cc_proto_deps = [], + external_py_proto_deps = [], has_services = 0, linkstatic = None, require_py = 1): @@ -152,7 +153,7 @@ def api_proto_library( ) py_export_suffixes = [] if (require_py == 1): - api_py_proto_library(name, srcs, deps, has_services) + api_py_proto_library(name, srcs, deps, external_py_proto_deps, has_services) py_export_suffixes = ["_py", "_py_genproto"] # Allow unlimited visibility for consumers diff --git a/api/envoy/config/rbac/v2/BUILD b/api/envoy/config/rbac/v2/BUILD index c2059893912c8..fac50eb66f9b0 100644 --- a/api/envoy/config/rbac/v2/BUILD +++ b/api/envoy/config/rbac/v2/BUILD @@ -5,6 +5,15 @@ load("@envoy_api//bazel:api_build_system.bzl", "api_go_proto_library", "api_prot api_proto_library_internal( name = "rbac", srcs = ["rbac.proto"], + external_cc_proto_deps = [ + "@com_google_googleapis//google/api/expr/v1alpha1:syntax_cc_proto", + ], + external_proto_deps = [ + "@com_google_googleapis//google/api/expr/v1alpha1:syntax_proto", + ], + external_py_proto_deps = [ + "@com_google_googleapis//google/api/expr/v1alpha1:syntax_py_proto", + ], visibility = ["//visibility:public"], deps = [ "//envoy/api/v2/core:address", @@ -22,5 +31,6 @@ api_go_proto_library( "//envoy/api/v2/route:route_go_proto", "//envoy/type/matcher:metadata_go_proto", "//envoy/type/matcher:string_go_proto", + "@com_google_googleapis//google/api/expr/v1alpha1:cel_go_proto", ], ) diff --git a/api/envoy/config/rbac/v2/rbac.proto b/api/envoy/config/rbac/v2/rbac.proto index 77e1aa687fa97..15554e561df4e 100644 --- a/api/envoy/config/rbac/v2/rbac.proto +++ b/api/envoy/config/rbac/v2/rbac.proto @@ -7,6 +7,8 @@ import "envoy/api/v2/route/route.proto"; import "envoy/type/matcher/metadata.proto"; import "envoy/type/matcher/string.proto"; +import "google/api/expr/v1alpha1/syntax.proto"; + package envoy.config.rbac.v2; option java_outer_classname = "RbacProto"; @@ -81,7 +83,7 @@ message RBAC { // Policy specifies a role and the principals that are assigned/denied the role. A policy matches if // and only if at least one of its permissions match the action taking place AND at least one of its -// principals match the downstream. +// principals match the downstream AND the condition is true if specified. message Policy { // Required. The set of permissions that define a role. Each permission is matched with OR // semantics. To match all actions for this policy, a single Permission with the `any` field set @@ -92,6 +94,10 @@ message Policy { // principal is matched with OR semantics. To match all downstreams for this policy, a single // Principal with the `any` field set to true should be used. repeated Principal principals = 2 [(validate.rules).repeated .min_items = 1]; + + // An optional symbolic expression specifying an access control condition. + // The condition is combined with AND semantics. + google.api.expr.v1alpha1.Expr condition = 3; } // Permission defines an action (or actions) that a principal can take. diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index d0df39d541bde..319d374286bd4 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -153,6 +153,8 @@ def envoy_dependencies(skip_targets = []): _com_lightstep_tracer_cpp() _io_opentracing_cpp() _net_zlib() + _repository_impl("com_googlesource_code_re2") + _com_google_cel_cpp() _repository_impl("bazel_toolchains") _python_deps() @@ -315,6 +317,9 @@ def _net_zlib(): actual = "@envoy//bazel/foreign_cc:zlib", ) +def _com_google_cel_cpp(): + _repository_impl("com_google_cel_cpp") + def _com_github_nghttp2_nghttp2(): location = REPOSITORY_LOCATIONS["com_github_nghttp2_nghttp2"] http_archive( diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 133e57d5731bd..fcb0e4b79a343 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -248,4 +248,14 @@ REPOSITORY_LOCATIONS = dict( sha256 = "fcdebf54c89d839ffa7eefae166c8e4b551c765559db13ff15bff98047f344fb", urls = ["https://storage.googleapis.com/quiche-envoy-integration/2a930469533c3b541443488a629fe25cd8ff53d0.tar.gz"], ), + com_google_cel_cpp = dict( + sha256 = "f027c551d57d38fb9f0b5e4f21a2b0b8663987119e23b1fd8dfcc7588e9a2350", + strip_prefix = "cel-cpp-d9d02b20ab85da2444dbdd03410bac6822141364", + urls = ["https://github.com/google/cel-cpp/archive/d9d02b20ab85da2444dbdd03410bac6822141364.tar.gz"], + ), + com_googlesource_code_re2 = dict( + sha256 = "f31db9cd224d018a7e4fe88ef84aaa874b0b3ed91d4d98ee5a1531101d3fdc64", + strip_prefix = "re2-87e2ad45e7b18738e1551474f7ee5886ff572059", + urls = ["https://github.com/google/re2/archive/87e2ad45e7b18738e1551474f7ee5886ff572059.tar.gz"], + ), ) diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 0d309f18f022d..b0f6f49275aba 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -24,6 +24,7 @@ Version history * rbac: added support for DNS SAN as :ref:`principal_name `. * lua: extended `httpCall()` and `respond()` APIs to accept headers with entry values that can be a string or table of strings. * performance: new buffer implementation enabled by default (to disable add "--use-libevent-buffers 1" to the command-line arguments when starting Envoy). +* rbac: added conditions to the policy, see :ref:`condition `. * router: added :ref:`rq_retry_skipped_request_not_complete ` counter stat to router stats. * router check tool: add coverage reporting & enforcement. * router check tool: add comprehensive coverage reporting. diff --git a/source/extensions/filters/common/expr/BUILD b/source/extensions/filters/common/expr/BUILD new file mode 100644 index 0000000000000..7b2a6f140792b --- /dev/null +++ b/source/extensions/filters/common/expr/BUILD @@ -0,0 +1,34 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "evaluator_lib", + srcs = ["evaluator.cc"], + hdrs = ["evaluator.h"], + deps = [ + ":context_lib", + "//source/common/http:utility_lib", + "//source/common/protobuf", + "@com_google_cel_cpp//eval/public:builtin_func_registrar", + "@com_google_cel_cpp//eval/public:cel_expr_builder_factory", + "@com_google_cel_cpp//eval/public:cel_expression", + "@com_google_cel_cpp//eval/public:cel_value", + ], +) + +envoy_cc_library( + name = "context_lib", + srcs = ["context.cc"], + hdrs = ["context.h"], + deps = [ + "//source/common/http:utility_lib", + "@com_google_cel_cpp//eval/public:cel_value", + ], +) diff --git a/source/extensions/filters/common/expr/context.cc b/source/extensions/filters/common/expr/context.cc new file mode 100644 index 0000000000000..5ee1e91e69b35 --- /dev/null +++ b/source/extensions/filters/common/expr/context.cc @@ -0,0 +1,164 @@ +#include "extensions/filters/common/expr/context.h" + +#include "absl/strings/numbers.h" +#include "absl/time/time.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace Expr { + +namespace { + +absl::optional convertHeaderEntry(const Http::HeaderEntry* header) { + if (header == nullptr) { + return {}; + } + return CelValue::CreateString(header->value().getStringView()); +} + +} // namespace + +absl::optional HeadersWrapper::operator[](CelValue key) const { + if (value_ == nullptr || !key.IsString()) { + return {}; + } + auto out = value_->get(Http::LowerCaseString(std::string(key.StringOrDie().value()))); + return convertHeaderEntry(out); +} + +absl::optional RequestWrapper::operator[](CelValue key) const { + if (!key.IsString()) { + return {}; + } + auto value = key.StringOrDie().value(); + + if (value == Headers) { + return CelValue::CreateMap(&headers_); + } else if (value == Time) { + return CelValue::CreateTimestamp(absl::FromChrono(info_.startTime())); + } else if (value == Size) { + // it is important to make a choice whether to rely on content-length vs stream info + // (which is not available at the time of the request headers) + if (headers_.value_ != nullptr && headers_.value_->ContentLength() != nullptr) { + int64_t length; + if (absl::SimpleAtoi(headers_.value_->ContentLength()->value().getStringView(), &length)) { + return CelValue::CreateInt64(length); + } + } else { + return CelValue::CreateInt64(info_.bytesReceived()); + } + } else if (value == Duration) { + auto duration = info_.requestComplete(); + if (duration.has_value()) { + return CelValue::CreateDuration(absl::FromChrono(duration.value())); + } + } + + if (headers_.value_ != nullptr) { + if (value == Path) { + return convertHeaderEntry(headers_.value_->Path()); + } else if (value == UrlPath) { + absl::string_view path = headers_.value_->Path()->value().getStringView(); + size_t query_offset = path.find('?'); + if (query_offset == absl::string_view::npos) { + return CelValue::CreateString(path); + } + return CelValue::CreateString(path.substr(0, query_offset)); + } else if (value == Host) { + return convertHeaderEntry(headers_.value_->Host()); + } else if (value == Scheme) { + return convertHeaderEntry(headers_.value_->Scheme()); + } else if (value == Method) { + return convertHeaderEntry(headers_.value_->Method()); + } else if (value == Referer) { + return convertHeaderEntry(headers_.value_->Referer()); + } else if (value == ID) { + return convertHeaderEntry(headers_.value_->RequestId()); + } else if (value == UserAgent) { + return convertHeaderEntry(headers_.value_->UserAgent()); + } else if (value == TotalSize) { + return CelValue::CreateInt64(info_.bytesReceived() + headers_.value_->byteSize()); + } + } + return {}; +} + +absl::optional ResponseWrapper::operator[](CelValue key) const { + if (!key.IsString()) { + return {}; + } + auto value = key.StringOrDie().value(); + if (value == Code) { + auto code = info_.responseCode(); + if (code.has_value()) { + return CelValue::CreateInt64(code.value()); + } + } else if (value == Size) { + return CelValue::CreateInt64(info_.bytesSent()); + } else if (value == Headers) { + return CelValue::CreateMap(&headers_); + } else if (value == Trailers) { + return CelValue::CreateMap(&trailers_); + } + return {}; +} + +absl::optional ConnectionWrapper::operator[](CelValue key) const { + if (!key.IsString()) { + return {}; + } + auto value = key.StringOrDie().value(); + if (value == UpstreamAddress) { + auto upstream_host = info_.upstreamHost(); + if (upstream_host != nullptr && upstream_host->address() != nullptr) { + return CelValue::CreateString(upstream_host->address()->asStringView()); + } + } else if (value == UpstreamPort) { + auto upstream_host = info_.upstreamHost(); + if (upstream_host != nullptr && upstream_host->address() != nullptr && + upstream_host->address()->ip() != nullptr) { + return CelValue::CreateInt64(upstream_host->address()->ip()->port()); + } + } else if (value == MTLS) { + return CelValue::CreateBool(info_.downstreamSslConnection() != nullptr && + info_.downstreamSslConnection()->peerCertificatePresented()); + } else if (value == RequestedServerName) { + return CelValue::CreateString(info_.requestedServerName()); + } + + return {}; +} + +absl::optional PeerWrapper::operator[](CelValue key) const { + if (!key.IsString()) { + return {}; + } + auto value = key.StringOrDie().value(); + if (value == Address) { + if (local_) { + return CelValue::CreateString(info_.downstreamLocalAddress()->asStringView()); + } else { + return CelValue::CreateString(info_.downstreamRemoteAddress()->asStringView()); + } + } else if (value == Port) { + if (local_) { + if (info_.downstreamLocalAddress()->ip() != nullptr) { + return CelValue::CreateInt64(info_.downstreamLocalAddress()->ip()->port()); + } + } else { + if (info_.downstreamRemoteAddress()->ip() != nullptr) { + return CelValue::CreateInt64(info_.downstreamRemoteAddress()->ip()->port()); + } + } + } + + return {}; +} + +} // namespace Expr +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/expr/context.h b/source/extensions/filters/common/expr/context.h new file mode 100644 index 0000000000000..7b59c41a5a101 --- /dev/null +++ b/source/extensions/filters/common/expr/context.h @@ -0,0 +1,129 @@ +#pragma once + +#include "envoy/stream_info/stream_info.h" + +#include "common/http/headers.h" + +#include "eval/public/cel_value.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace Expr { + +using CelValue = google::api::expr::runtime::CelValue; + +// Symbols for traversing the request properties +constexpr absl::string_view Request = "request"; +constexpr absl::string_view Path = "path"; +constexpr absl::string_view UrlPath = "url_path"; +constexpr absl::string_view Host = "host"; +constexpr absl::string_view Scheme = "scheme"; +constexpr absl::string_view Method = "method"; +constexpr absl::string_view Referer = "referer"; +constexpr absl::string_view Headers = "headers"; +constexpr absl::string_view Time = "time"; +constexpr absl::string_view ID = "id"; +constexpr absl::string_view UserAgent = "useragent"; +constexpr absl::string_view Size = "size"; +constexpr absl::string_view TotalSize = "total_size"; +constexpr absl::string_view Duration = "duration"; + +// Symbols for traversing the response properties +constexpr absl::string_view Response = "response"; +constexpr absl::string_view Code = "code"; +constexpr absl::string_view Trailers = "trailers"; + +// Per-request or per-connection metadata +constexpr absl::string_view Metadata = "metadata"; + +// Connection properties +constexpr absl::string_view Connection = "connection"; +constexpr absl::string_view UpstreamAddress = "upstream_address"; +constexpr absl::string_view UpstreamPort = "upstream_port"; +constexpr absl::string_view MTLS = "mtls"; +constexpr absl::string_view RequestedServerName = "requested_server_name"; + +// Source properties +constexpr absl::string_view Source = "source"; +constexpr absl::string_view Address = "address"; +constexpr absl::string_view Port = "port"; + +// Destination properties +constexpr absl::string_view Destination = "destination"; + +class RequestWrapper; + +class HeadersWrapper : public google::api::expr::runtime::CelMap { +public: + HeadersWrapper(const Http::HeaderMap* value) : value_(value) {} + absl::optional operator[](CelValue key) const override; + int size() const override { return value_ == nullptr ? 0 : value_->size(); } + bool empty() const override { return value_ == nullptr ? true : value_->empty(); } + const google::api::expr::runtime::CelList* ListKeys() const override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + +private: + friend class RequestWrapper; + const Http::HeaderMap* value_; +}; + +class BaseWrapper : public google::api::expr::runtime::CelMap { +public: + int size() const override { return 0; } + bool empty() const override { return false; } + const google::api::expr::runtime::CelList* ListKeys() const override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } +}; + +class RequestWrapper : public BaseWrapper { +public: + RequestWrapper(const Http::HeaderMap* headers, const StreamInfo::StreamInfo& info) + : headers_(headers), info_(info) {} + absl::optional operator[](CelValue key) const override; + +private: + const HeadersWrapper headers_; + const StreamInfo::StreamInfo& info_; +}; + +class ResponseWrapper : public BaseWrapper { +public: + ResponseWrapper(const Http::HeaderMap* headers, const Http::HeaderMap* trailers, + const StreamInfo::StreamInfo& info) + : headers_(headers), trailers_(trailers), info_(info) {} + absl::optional operator[](CelValue key) const override; + +private: + const HeadersWrapper headers_; + const HeadersWrapper trailers_; + const StreamInfo::StreamInfo& info_; +}; + +class ConnectionWrapper : public BaseWrapper { +public: + ConnectionWrapper(const StreamInfo::StreamInfo& info) : info_(info) {} + absl::optional operator[](CelValue key) const override; + +private: + const StreamInfo::StreamInfo& info_; +}; + +class PeerWrapper : public BaseWrapper { +public: + PeerWrapper(const StreamInfo::StreamInfo& info, bool local) : info_(info), local_(local) {} + absl::optional operator[](CelValue key) const override; + +private: + const StreamInfo::StreamInfo& info_; + const bool local_; +}; + +} // namespace Expr +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/expr/evaluator.cc b/source/extensions/filters/common/expr/evaluator.cc new file mode 100644 index 0000000000000..bd25da52975f8 --- /dev/null +++ b/source/extensions/filters/common/expr/evaluator.cc @@ -0,0 +1,86 @@ +#include "extensions/filters/common/expr/evaluator.h" + +#include "envoy/common/exception.h" + +#include "eval/public/builtin_func_registrar.h" +#include "eval/public/cel_expr_builder_factory.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace Expr { + +BuilderPtr createBuilder(Protobuf::Arena* arena) { + google::api::expr::runtime::InterpreterOptions options; + + // Conformance with spec/go runtimes requires this setting + options.partial_string_match = true; + + if (arena != nullptr) { + options.constant_folding = true; + options.constant_arena = arena; + } + + auto builder = google::api::expr::runtime::CreateCelExpressionBuilder(options); + auto register_status = + google::api::expr::runtime::RegisterBuiltinFunctions(builder->GetRegistry()); + if (!register_status.ok()) { + throw EnvoyException( + absl::StrCat("failed to register built-in functions: ", register_status.message())); + } + return builder; +} + +ExpressionPtr createExpression(Builder& builder, const google::api::expr::v1alpha1::Expr& expr) { + google::api::expr::v1alpha1::SourceInfo source_info; + auto cel_expression_status = builder.CreateExpression(&expr, &source_info); + if (!cel_expression_status.ok()) { + throw EnvoyException( + absl::StrCat("failed to create an expression: ", cel_expression_status.status().message())); + } + return std::move(cel_expression_status.ValueOrDie()); +} + +absl::optional evaluate(const Expression& expr, Protobuf::Arena* arena, + const StreamInfo::StreamInfo& info, + const Http::HeaderMap* request_headers, + const Http::HeaderMap* response_headers, + const Http::HeaderMap* response_trailers) { + google::api::expr::runtime::Activation activation; + const RequestWrapper request(request_headers, info); + const ResponseWrapper response(response_headers, response_trailers, info); + const ConnectionWrapper connection(info); + const PeerWrapper source(info, false); + const PeerWrapper destination(info, true); + activation.InsertValue(Request, CelValue::CreateMap(&request)); + activation.InsertValue(Response, CelValue::CreateMap(&response)); + activation.InsertValue(Metadata, CelValue::CreateMessage(&info.dynamicMetadata(), arena)); + activation.InsertValue(Connection, CelValue::CreateMap(&connection)); + activation.InsertValue(Source, CelValue::CreateMap(&source)); + activation.InsertValue(Destination, CelValue::CreateMap(&destination)); + + auto eval_status = expr.Evaluate(activation, arena); + if (!eval_status.ok()) { + return {}; + } + + return eval_status.ValueOrDie(); +} + +bool matches(const Expression& expr, const StreamInfo::StreamInfo& info, + const Http::HeaderMap& headers) { + Protobuf::Arena arena; + auto eval_status = Expr::evaluate(expr, &arena, info, &headers, nullptr, nullptr); + if (!eval_status.has_value()) { + return false; + } + auto result = eval_status.value(); + return result.IsBool() ? result.BoolOrDie() : false; +} + +} // namespace Expr +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/expr/evaluator.h b/source/extensions/filters/common/expr/evaluator.h new file mode 100644 index 0000000000000..92ccea420d216 --- /dev/null +++ b/source/extensions/filters/common/expr/evaluator.h @@ -0,0 +1,50 @@ +#pragma once + +#include "envoy/stream_info/stream_info.h" + +#include "common/http/headers.h" +#include "common/protobuf/protobuf.h" + +#include "extensions/filters/common/expr/context.h" + +#include "eval/public/cel_expression.h" +#include "eval/public/cel_value.h" + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace Expr { + +using Builder = google::api::expr::runtime::CelExpressionBuilder; +using BuilderPtr = std::unique_ptr; +using Expression = google::api::expr::runtime::CelExpression; +using ExpressionPtr = std::unique_ptr; + +// Creates an expression builder. The optional arena is used to enable constant folding +// for intermediate evaluation results. +// Throws an exception if fails to construct an expression builder. +BuilderPtr createBuilder(Protobuf::Arena* arena); + +// Creates an interpretable expression from a protobuf representation. +// Throws an exception if fails to construct a runtime expression. +ExpressionPtr createExpression(Builder& builder, const google::api::expr::v1alpha1::Expr& expr); + +// Evaluates an expression for a request. The arena is used to hold intermediate computational +// results and potentially the final value. +absl::optional evaluate(const Expression& expr, Protobuf::Arena* arena, + const StreamInfo::StreamInfo& info, + const Http::HeaderMap* request_headers, + const Http::HeaderMap* response_headers, + const Http::HeaderMap* response_trailers); + +// Evaluates an expression and returns true if the expression evaluates to "true". +// Returns false if the expression fails to evaluate. +bool matches(const Expression& expr, const StreamInfo::StreamInfo& info, + const Http::HeaderMap& headers); + +} // namespace Expr +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/common/rbac/BUILD b/source/extensions/filters/common/rbac/BUILD index 6aa4fc4088bde..94398324482f5 100644 --- a/source/extensions/filters/common/rbac/BUILD +++ b/source/extensions/filters/common/rbac/BUILD @@ -33,6 +33,7 @@ envoy_cc_library( "//source/common/common:matchers_lib", "//source/common/http:header_utility_lib", "//source/common/network:cidr_range_lib", + "//source/extensions/filters/common/expr:evaluator_lib", "@envoy_api//envoy/api/v2/core:base_cc", "@envoy_api//envoy/config/rbac/v2:rbac_cc", ], diff --git a/source/extensions/filters/common/rbac/engine.h b/source/extensions/filters/common/rbac/engine.h index 4a07c03d2a312..093eb72fb7786 100644 --- a/source/extensions/filters/common/rbac/engine.h +++ b/source/extensions/filters/common/rbac/engine.h @@ -4,6 +4,7 @@ #include "envoy/http/filter.h" #include "envoy/http/header_map.h" #include "envoy/network/connection.h" +#include "envoy/stream_info/stream_info.h" namespace Envoy { namespace Extensions { @@ -24,24 +25,25 @@ class RoleBasedAccessControlEngine { * @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. - * @param metadata the metadata with additional information about the action/principal. + * @param info the per-request or per-connection stream info with additional information + * about the action/principal. * @param effective_policy_id it will be filled by the matching policy's ID, * which is used to identity the source of the allow/deny. */ virtual bool allowed(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata, + const StreamInfo::StreamInfo& info, 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. - * @param metadata the metadata with additional information about the action/principal. + * @param info the per-request or per-connection stream info with additional information + * about the action/principal. * @param effective_policy_id it will be filled by the matching policy's ID, * which is used to identity the source of the allow/deny. */ - virtual bool allowed(const Network::Connection& connection, - const envoy::api::v2::core::Metadata& metadata, + virtual bool allowed(const Network::Connection& connection, const StreamInfo::StreamInfo& info, std::string* effective_policy_id) const PURE; }; diff --git a/source/extensions/filters/common/rbac/engine_impl.cc b/source/extensions/filters/common/rbac/engine_impl.cc index a35697d74d5f3..4456dd6eafbb9 100644 --- a/source/extensions/filters/common/rbac/engine_impl.cc +++ b/source/extensions/filters/common/rbac/engine_impl.cc @@ -12,19 +12,27 @@ RoleBasedAccessControlEngineImpl::RoleBasedAccessControlEngineImpl( const envoy::config::rbac::v2::RBAC& rules) : allowed_if_matched_(rules.action() == envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW) { + // guard expression builder by presence of a condition in policies for (const auto& policy : rules.policies()) { - policies_.insert(std::make_pair(policy.first, policy.second)); + if (policy.second.has_condition()) { + builder_ = Expr::createBuilder(&constant_arena_); + break; + } + } + + for (const auto& policy : rules.policies()) { + policies_.emplace(policy.first, std::make_unique(policy.second, builder_.get())); } } bool RoleBasedAccessControlEngineImpl::allowed(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata, + const StreamInfo::StreamInfo& info, std::string* effective_policy_id) const { bool matched = false; for (const auto& policy : policies_) { - if (policy.second.matches(connection, headers, metadata)) { + if (policy.second->matches(connection, headers, info)) { matched = true; if (effective_policy_id != nullptr) { *effective_policy_id = policy.first; @@ -40,10 +48,10 @@ bool RoleBasedAccessControlEngineImpl::allowed(const Network::Connection& connec } bool RoleBasedAccessControlEngineImpl::allowed(const Network::Connection& connection, - const envoy::api::v2::core::Metadata& metadata, + const StreamInfo::StreamInfo& info, std::string* effective_policy_id) const { static const Http::HeaderMapImpl* empty_header = new Http::HeaderMapImpl(); - return allowed(connection, *empty_header, metadata, effective_policy_id); + return allowed(connection, *empty_header, info, effective_policy_id); } } // namespace RBAC diff --git a/source/extensions/filters/common/rbac/engine_impl.h b/source/extensions/filters/common/rbac/engine_impl.h index cabbeb31e30fa..43b71fe5d8b03 100644 --- a/source/extensions/filters/common/rbac/engine_impl.h +++ b/source/extensions/filters/common/rbac/engine_impl.h @@ -11,22 +11,23 @@ namespace Filters { namespace Common { namespace RBAC { -class RoleBasedAccessControlEngineImpl : public RoleBasedAccessControlEngine { +class RoleBasedAccessControlEngineImpl : public RoleBasedAccessControlEngine, NonCopyable { public: RoleBasedAccessControlEngineImpl(const envoy::config::rbac::v2::RBAC& rules); bool allowed(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata, - std::string* effective_policy_id) const override; + const StreamInfo::StreamInfo& info, std::string* effective_policy_id) const override; - bool allowed(const Network::Connection& connection, - const envoy::api::v2::core::Metadata& metadata, + bool allowed(const Network::Connection& connection, const StreamInfo::StreamInfo& info, std::string* effective_policy_id) const override; private: const bool allowed_if_matched_; - std::map policies_; + std::map> policies_; + + Protobuf::Arena constant_arena_; + Expr::BuilderPtr builder_; }; } // namespace RBAC diff --git a/source/extensions/filters/common/rbac/matchers.cc b/source/extensions/filters/common/rbac/matchers.cc index 07472b49a0031..d2742e2e9ea15 100644 --- a/source/extensions/filters/common/rbac/matchers.cc +++ b/source/extensions/filters/common/rbac/matchers.cc @@ -70,9 +70,9 @@ AndMatcher::AndMatcher(const envoy::config::rbac::v2::Principal_Set& set) { bool AndMatcher::matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata) const { + const StreamInfo::StreamInfo& info) const { for (const auto& matcher : matchers_) { - if (!matcher->matches(connection, headers, metadata)) { + if (!matcher->matches(connection, headers, info)) { return false; } } @@ -95,9 +95,9 @@ OrMatcher::OrMatcher(const Protobuf::RepeatedPtrField<::envoy::config::rbac::v2: bool OrMatcher::matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata) const { + const StreamInfo::StreamInfo& info) const { for (const auto& matcher : matchers_) { - if (matcher->matches(connection, headers, metadata)) { + if (matcher->matches(connection, headers, info)) { return true; } } @@ -107,17 +107,17 @@ bool OrMatcher::matches(const Network::Connection& connection, bool NotMatcher::matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata) const { - return !matcher_->matches(connection, headers, metadata); + const StreamInfo::StreamInfo& info) const { + return !matcher_->matches(connection, headers, info); } bool HeaderMatcher::matches(const Network::Connection&, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const { + const StreamInfo::StreamInfo&) const { return Envoy::Http::HeaderUtility::matchHeaders(headers, header_); } bool IPMatcher::matches(const Network::Connection& connection, const Envoy::Http::HeaderMap&, - const envoy::api::v2::core::Metadata&) const { + const StreamInfo::StreamInfo&) const { const Envoy::Network::Address::InstanceConstSharedPtr& ip = destination_ ? connection.localAddress() : connection.remoteAddress(); @@ -125,14 +125,14 @@ bool IPMatcher::matches(const Network::Connection& connection, const Envoy::Http } bool PortMatcher::matches(const Network::Connection& connection, const Envoy::Http::HeaderMap&, - const envoy::api::v2::core::Metadata&) const { + const StreamInfo::StreamInfo&) 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 envoy::api::v2::core::Metadata&) const { + const StreamInfo::StreamInfo&) const { const auto* ssl = connection.ssl(); if (!ssl) { // connection was not authenticated return false; @@ -159,20 +159,21 @@ bool AuthenticatedMatcher::matches(const Network::Connection& connection, } bool MetadataMatcher::matches(const Network::Connection&, const Envoy::Http::HeaderMap&, - const envoy::api::v2::core::Metadata& metadata) const { - return matcher_.match(metadata); + const StreamInfo::StreamInfo& info) const { + return matcher_.match(info.dynamicMetadata()); } bool PolicyMatcher::matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata) const { - return permissions_.matches(connection, headers, metadata) && - principals_.matches(connection, headers, metadata); + const StreamInfo::StreamInfo& info) const { + return permissions_.matches(connection, headers, info) && + principals_.matches(connection, headers, info) && + (expr_ == nullptr ? true : Expr::matches(*expr_, info, headers)); } bool RequestedServerNameMatcher::matches(const Network::Connection& connection, const Envoy::Http::HeaderMap&, - const envoy::api::v2::core::Metadata&) const { + const StreamInfo::StreamInfo&) const { return match(connection.requestedServerName()); } diff --git a/source/extensions/filters/common/rbac/matchers.h b/source/extensions/filters/common/rbac/matchers.h index 28b81846e1149..98f369d49e20b 100644 --- a/source/extensions/filters/common/rbac/matchers.h +++ b/source/extensions/filters/common/rbac/matchers.h @@ -11,6 +11,8 @@ #include "common/http/header_utility.h" #include "common/network/cidr_range.h" +#include "extensions/filters/common/expr/evaluator.h" + namespace Envoy { namespace Extensions { namespace Filters { @@ -36,7 +38,7 @@ class Matcher { * @param metadata the additional information about the action/principal. */ virtual bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata) const PURE; + const StreamInfo::StreamInfo& info) const PURE; /** * Creates a shared instance of a matcher based off the rules defined in the Permission config @@ -57,7 +59,7 @@ class Matcher { class AlwaysMatcher : public Matcher { public: bool matches(const Network::Connection&, const Envoy::Http::HeaderMap&, - const envoy::api::v2::core::Metadata&) const override { + const StreamInfo::StreamInfo&) const override { return true; } }; @@ -72,7 +74,7 @@ class AndMatcher : public Matcher { AndMatcher(const envoy::config::rbac::v2::Principal_Set& ids); bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: std::vector matchers_; @@ -90,7 +92,7 @@ class OrMatcher : public Matcher { OrMatcher(const Protobuf::RepeatedPtrField<::envoy::config::rbac::v2::Principal>& ids); bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: std::vector matchers_; @@ -104,7 +106,7 @@ class NotMatcher : public Matcher { : matcher_(Matcher::create(principal)) {} bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: MatcherConstSharedPtr matcher_; @@ -119,7 +121,7 @@ class HeaderMatcher : public Matcher { HeaderMatcher(const envoy::api::v2::route::HeaderMatcher& matcher) : header_(matcher) {} bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: const Envoy::Http::HeaderUtility::HeaderData header_; @@ -135,7 +137,7 @@ class IPMatcher : public Matcher { : range_(Network::Address::CidrRange::create(range)), destination_(destination) {} bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: const Network::Address::CidrRange range_; @@ -150,7 +152,7 @@ class PortMatcher : public Matcher { PortMatcher(const uint32_t port) : port_(port) {} bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: const uint32_t port_; @@ -168,7 +170,7 @@ class AuthenticatedMatcher : public Matcher { : absl::nullopt) {} bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: const absl::optional matcher_; @@ -177,18 +179,27 @@ class AuthenticatedMatcher : public Matcher { /** * 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. + * The condition is a conjunction clause. */ -class PolicyMatcher : public Matcher { +class PolicyMatcher : public Matcher, NonCopyable { public: - PolicyMatcher(const envoy::config::rbac::v2::Policy& policy) - : permissions_(policy.permissions()), principals_(policy.principals()) {} + PolicyMatcher(const envoy::config::rbac::v2::Policy& policy, Expr::Builder* builder) + : permissions_(policy.permissions()), principals_(policy.principals()), + condition_(policy.condition()) { + if (policy.has_condition()) { + expr_ = Expr::createExpression(*builder, condition_); + } + } bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; private: const OrMatcher permissions_; const OrMatcher principals_; + + const google::api::expr::v1alpha1::Expr condition_; + Expr::ExpressionPtr expr_; }; class MetadataMatcher : public Matcher { @@ -196,7 +207,7 @@ class MetadataMatcher : public Matcher { MetadataMatcher(const Envoy::Matchers::MetadataMatcher& matcher) : matcher_(matcher) {} bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata& metadata) const override; + const StreamInfo::StreamInfo& info) const override; private: const Envoy::Matchers::MetadataMatcher matcher_; @@ -212,7 +223,7 @@ class RequestedServerNameMatcher : public Matcher, Envoy::Matchers::StringMatche : Envoy::Matchers::StringMatcher(requested_server_name) {} bool matches(const Network::Connection& connection, const Envoy::Http::HeaderMap& headers, - const envoy::api::v2::core::Metadata&) const override; + const StreamInfo::StreamInfo&) const override; }; } // namespace RBAC diff --git a/source/extensions/filters/common/rbac/utility.h b/source/extensions/filters/common/rbac/utility.h index bcf934f41feb1..684c4204ecb6f 100644 --- a/source/extensions/filters/common/rbac/utility.h +++ b/source/extensions/filters/common/rbac/utility.h @@ -47,16 +47,16 @@ RoleBasedAccessControlFilterStats generateStats(const std::string& prefix, Stats enum class EnforcementMode { Enforced, Shadow }; template -absl::optional createEngine(const ConfigType& config) { - return config.has_rules() ? absl::make_optional(config.rules()) - : absl::nullopt; +std::unique_ptr createEngine(const ConfigType& config) { + return config.has_rules() ? std::make_unique(config.rules()) + : nullptr; } template -absl::optional createShadowEngine(const ConfigType& config) { +std::unique_ptr createShadowEngine(const ConfigType& config) { return config.has_shadow_rules() - ? absl::make_optional(config.shadow_rules()) - : absl::nullopt; + ? std::make_unique(config.shadow_rules()) + : nullptr; } } // namespace RBAC diff --git a/source/extensions/filters/http/rbac/rbac_filter.cc b/source/extensions/filters/http/rbac/rbac_filter.cc index 965888fd3105f..98e6db79b5527 100644 --- a/source/extensions/filters/http/rbac/rbac_filter.cc +++ b/source/extensions/filters/http/rbac/rbac_filter.cc @@ -26,7 +26,7 @@ RoleBasedAccessControlFilterConfig::RoleBasedAccessControlFilterConfig( engine_(Filters::Common::RBAC::createEngine(proto_config)), shadow_engine_(Filters::Common::RBAC::createShadowEngine(proto_config)) {} -const absl::optional& +const Filters::Common::RBAC::RoleBasedAccessControlEngineImpl* RoleBasedAccessControlFilterConfig::engine(const Router::RouteConstSharedPtr route, Filters::Common::RBAC::EnforcementMode mode) const { if (!route || !route->routeEntry()) { @@ -70,14 +70,14 @@ Http::FilterHeadersStatus RoleBasedAccessControlFilter::decodeHeaders(Http::Head headers, callbacks_->streamInfo().dynamicMetadata().DebugString()); std::string effective_policy_id; - const auto& shadow_engine = + const auto shadow_engine = config_->engine(callbacks_->route(), Filters::Common::RBAC::EnforcementMode::Shadow); - if (shadow_engine.has_value()) { + if (shadow_engine != nullptr) { std::string shadow_resp_code = Filters::Common::RBAC::DynamicMetadataKeysSingleton::get().EngineResultAllowed; - if (shadow_engine->allowed(*callbacks_->connection(), headers, - callbacks_->streamInfo().dynamicMetadata(), &effective_policy_id)) { + if (shadow_engine->allowed(*callbacks_->connection(), headers, callbacks_->streamInfo(), + &effective_policy_id)) { ENVOY_LOG(debug, "shadow allowed"); config_->stats().shadow_allowed_.inc(); } else { @@ -102,11 +102,10 @@ Http::FilterHeadersStatus RoleBasedAccessControlFilter::decodeHeaders(Http::Head callbacks_->streamInfo().setDynamicMetadata(HttpFilterNames::get().Rbac, metrics); } - const auto& engine = + const auto engine = config_->engine(callbacks_->route(), Filters::Common::RBAC::EnforcementMode::Enforced); - if (engine.has_value()) { - if (engine->allowed(*callbacks_->connection(), headers, - callbacks_->streamInfo().dynamicMetadata(), nullptr)) { + if (engine != nullptr) { + if (engine->allowed(*callbacks_->connection(), headers, callbacks_->streamInfo(), nullptr)) { ENVOY_LOG(debug, "enforced allowed"); config_->stats().allowed_.inc(); return Http::FilterHeadersStatus::Continue; diff --git a/source/extensions/filters/http/rbac/rbac_filter.h b/source/extensions/filters/http/rbac/rbac_filter.h index 5397a3c31c586..e6b0044580520 100644 --- a/source/extensions/filters/http/rbac/rbac_filter.h +++ b/source/extensions/filters/http/rbac/rbac_filter.h @@ -22,14 +22,15 @@ class RoleBasedAccessControlRouteSpecificFilterConfig : public Router::RouteSpec RoleBasedAccessControlRouteSpecificFilterConfig( const envoy::config::filter::http::rbac::v2::RBACPerRoute& per_route_config); - const absl::optional& + const Filters::Common::RBAC::RoleBasedAccessControlEngineImpl* engine(Filters::Common::RBAC::EnforcementMode mode) const { - return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_ : shadow_engine_; + return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_.get() + : shadow_engine_.get(); } private: - const absl::optional engine_; - const absl::optional shadow_engine_; + std::unique_ptr engine_; + std::unique_ptr shadow_engine_; }; /** @@ -43,20 +44,21 @@ class RoleBasedAccessControlFilterConfig { Filters::Common::RBAC::RoleBasedAccessControlFilterStats& stats() { return stats_; } - const absl::optional& + const Filters::Common::RBAC::RoleBasedAccessControlEngineImpl* engine(const Router::RouteConstSharedPtr route, Filters::Common::RBAC::EnforcementMode mode) const; private: - const absl::optional& + const Filters::Common::RBAC::RoleBasedAccessControlEngineImpl* engine(Filters::Common::RBAC::EnforcementMode mode) const { - return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_ : shadow_engine_; + return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_.get() + : shadow_engine_.get(); } Filters::Common::RBAC::RoleBasedAccessControlFilterStats stats_; - const absl::optional engine_; - const absl::optional shadow_engine_; + std::unique_ptr engine_; + std::unique_ptr shadow_engine_; }; using RoleBasedAccessControlFilterConfigSharedPtr = diff --git a/source/extensions/filters/network/rbac/rbac_filter.cc b/source/extensions/filters/network/rbac/rbac_filter.cc index 0c5008c88b1d8..9bc369f246dcd 100644 --- a/source/extensions/filters/network/rbac/rbac_filter.cc +++ b/source/extensions/filters/network/rbac/rbac_filter.cc @@ -77,11 +77,10 @@ void RoleBasedAccessControlFilter::setDynamicMetadata(std::string shadow_engine_ EngineResult RoleBasedAccessControlFilter::checkEngine(Filters::Common::RBAC::EnforcementMode mode) { - const auto& engine = config_->engine(mode); - if (engine.has_value()) { + const auto engine = config_->engine(mode); + if (engine != nullptr) { std::string effective_policy_id; - if (engine->allowed(callbacks_->connection(), - callbacks_->connection().streamInfo().dynamicMetadata(), + if (engine->allowed(callbacks_->connection(), callbacks_->connection().streamInfo(), &effective_policy_id)) { if (mode == Filters::Common::RBAC::EnforcementMode::Shadow) { ENVOY_LOG(debug, "shadow allowed"); diff --git a/source/extensions/filters/network/rbac/rbac_filter.h b/source/extensions/filters/network/rbac/rbac_filter.h index b548424c63de0..a42214004c5c6 100644 --- a/source/extensions/filters/network/rbac/rbac_filter.h +++ b/source/extensions/filters/network/rbac/rbac_filter.h @@ -26,9 +26,10 @@ class RoleBasedAccessControlFilterConfig { Filters::Common::RBAC::RoleBasedAccessControlFilterStats& stats() { return stats_; } - const absl::optional& + const Filters::Common::RBAC::RoleBasedAccessControlEngineImpl* engine(Filters::Common::RBAC::EnforcementMode mode) const { - return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_ : shadow_engine_; + return mode == Filters::Common::RBAC::EnforcementMode::Enforced ? engine_.get() + : shadow_engine_.get(); } envoy::config::filter::network::rbac::v2::RBAC::EnforcementType enforcementType() const { @@ -38,8 +39,8 @@ class RoleBasedAccessControlFilterConfig { private: Filters::Common::RBAC::RoleBasedAccessControlFilterStats stats_; - const absl::optional engine_; - const absl::optional shadow_engine_; + std::unique_ptr engine_; + std::unique_ptr shadow_engine_; const envoy::config::filter::network::rbac::v2::RBAC::EnforcementType enforcement_type_; }; diff --git a/test/extensions/filters/common/expr/BUILD b/test/extensions/filters/common/expr/BUILD new file mode 100644 index 0000000000000..8ce5555328bf0 --- /dev/null +++ b/test/extensions/filters/common/expr/BUILD @@ -0,0 +1,25 @@ +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 = "context_test", + srcs = ["context_test.cc"], + extension_name = "envoy.filters.http.rbac", + deps = [ + "//source/extensions/filters/common/expr:context_lib", + "//test/mocks/ssl:ssl_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/mocks/upstream:upstream_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/filters/common/expr/context_test.cc b/test/extensions/filters/common/expr/context_test.cc new file mode 100644 index 0000000000000..0e79abe362ecd --- /dev/null +++ b/test/extensions/filters/common/expr/context_test.cc @@ -0,0 +1,363 @@ +#include "common/network/utility.h" + +#include "extensions/filters/common/expr/context.h" + +#include "test/mocks/ssl/mocks.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/mocks/upstream/mocks.h" + +#include "absl/time/time.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::Const; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace Filters { +namespace Common { +namespace Expr { +namespace { + +constexpr absl::string_view Undefined = "undefined"; + +TEST(Context, EmptyHeadersAttributes) { + HeadersWrapper headers(nullptr); + auto header = headers[CelValue::CreateString(Referer)]; + EXPECT_FALSE(header.has_value()); + EXPECT_EQ(0, headers.size()); + EXPECT_TRUE(headers.empty()); +} + +TEST(Context, RequestAttributes) { + NiceMock info; + Http::TestHeaderMapImpl header_map{ + {":method", "POST"}, {":scheme", "http"}, {":path", "/meow?yes=1"}, + {":authority", "kittens.com"}, {"referer", "dogs.com"}, {"user-agent", "envoy-mobile"}, + {"content-length", "10"}, {"x-request-id", "blah"}, + }; + RequestWrapper request(&header_map, info); + + EXPECT_CALL(info, bytesReceived()).WillRepeatedly(Return(10)); + // "2018-04-03T23:06:09.123Z". + const SystemTime start_time(std::chrono::milliseconds(1522796769123)); + EXPECT_CALL(info, startTime()).WillRepeatedly(Return(start_time)); + absl::optional dur = std::chrono::nanoseconds(15000000); + EXPECT_CALL(info, requestComplete()).WillRepeatedly(Return(dur)); + + // stub methods + EXPECT_EQ(0, request.size()); + EXPECT_FALSE(request.empty()); + + { + auto value = request[CelValue::CreateString(Undefined)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = request[CelValue::CreateInt64(13)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = request[CelValue::CreateString(Scheme)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("http", value.value().StringOrDie().value()); + } + { + auto value = request[CelValue::CreateString(Host)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("kittens.com", value.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(Path)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("/meow?yes=1", value.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(UrlPath)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("/meow", value.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(Method)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("POST", value.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(Referer)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("dogs.com", value.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(UserAgent)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("envoy-mobile", value.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(ID)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("blah", value.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(Size)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + EXPECT_EQ(10, value.value().Int64OrDie()); + } + + { + auto value = request[CelValue::CreateString(TotalSize)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + // this includes the headers size + EXPECT_EQ(138, value.value().Int64OrDie()); + } + + { + auto value = request[CelValue::CreateString(Time)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsTimestamp()); + EXPECT_EQ("2018-04-03T23:06:09.123+00:00", absl::FormatTime(value.value().TimestampOrDie())); + } + + { + auto value = request[CelValue::CreateString(Headers)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsMap()); + auto& map = *value.value().MapOrDie(); + EXPECT_FALSE(map.empty()); + EXPECT_EQ(8, map.size()); + + auto header = map[CelValue::CreateString(Referer)]; + EXPECT_TRUE(header.has_value()); + ASSERT_TRUE(header.value().IsString()); + EXPECT_EQ("dogs.com", header.value().StringOrDie().value()); + } + + { + auto value = request[CelValue::CreateString(Duration)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsDuration()); + EXPECT_EQ("15ms", absl::FormatDuration(value.value().DurationOrDie())); + } +} + +TEST(Context, RequestFallbackAttributes) { + NiceMock info; + Http::TestHeaderMapImpl header_map{ + {":method", "POST"}, + {":scheme", "http"}, + {":path", "/meow?yes=1"}, + }; + RequestWrapper request(&header_map, info); + + EXPECT_CALL(info, bytesReceived()).WillRepeatedly(Return(10)); + + { + auto value = request[CelValue::CreateString(Size)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + EXPECT_EQ(10, value.value().Int64OrDie()); + } + + { + auto value = request[CelValue::CreateString(UrlPath)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("/meow", value.value().StringOrDie().value()); + } +} + +TEST(Context, ResponseAttributes) { + NiceMock info; + const std::string header_name = "test-header"; + const std::string trailer_name = "test-trailer"; + Http::TestHeaderMapImpl header_map{{header_name, "a"}}; + Http::TestHeaderMapImpl trailer_map{{trailer_name, "b"}}; + ResponseWrapper response(&header_map, &trailer_map, info); + + EXPECT_CALL(info, responseCode()).WillRepeatedly(Return(404)); + EXPECT_CALL(info, bytesSent()).WillRepeatedly(Return(123)); + + { + auto value = response[CelValue::CreateString(Undefined)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = response[CelValue::CreateInt64(13)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = response[CelValue::CreateString(Size)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + EXPECT_EQ(123, value.value().Int64OrDie()); + } + + { + auto value = response[CelValue::CreateString(Code)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + EXPECT_EQ(404, value.value().Int64OrDie()); + } + + { + auto value = response[CelValue::CreateString(Headers)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsMap()); + auto& map = *value.value().MapOrDie(); + EXPECT_FALSE(map.empty()); + EXPECT_EQ(1, map.size()); + + auto header = map[CelValue::CreateString(header_name)]; + EXPECT_TRUE(header.has_value()); + ASSERT_TRUE(header.value().IsString()); + EXPECT_EQ("a", header.value().StringOrDie().value()); + + auto missing = map[CelValue::CreateString(Undefined)]; + EXPECT_FALSE(missing.has_value()); + } + + { + auto value = response[CelValue::CreateString(Trailers)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsMap()); + auto& map = *value.value().MapOrDie(); + EXPECT_FALSE(map.empty()); + EXPECT_EQ(1, map.size()); + + auto header = map[CelValue::CreateString(trailer_name)]; + EXPECT_TRUE(header.has_value()); + ASSERT_TRUE(header.value().IsString()); + EXPECT_EQ("b", header.value().StringOrDie().value()); + } +} + +TEST(Context, ConnectionAttributes) { + NiceMock info; + std::shared_ptr> host( + new NiceMock()); + NiceMock connection_info; + ConnectionWrapper connection(info); + PeerWrapper source(info, false); + PeerWrapper destination(info, true); + + Network::Address::InstanceConstSharedPtr local = + Network::Utility::parseInternetAddress("1.2.3.4", 123, false); + Network::Address::InstanceConstSharedPtr remote = + Network::Utility::parseInternetAddress("10.20.30.40", 456, false); + Network::Address::InstanceConstSharedPtr upstream = + Network::Utility::parseInternetAddress("10.1.2.3", 679, false); + const std::string sni_name = "kittens.com"; + EXPECT_CALL(info, downstreamLocalAddress()).WillRepeatedly(ReturnRef(local)); + EXPECT_CALL(info, downstreamRemoteAddress()).WillRepeatedly(ReturnRef(remote)); + EXPECT_CALL(info, downstreamSslConnection()).WillRepeatedly(Return(&connection_info)); + EXPECT_CALL(info, upstreamHost()).WillRepeatedly(Return(host)); + EXPECT_CALL(info, requestedServerName()).WillRepeatedly(ReturnRef(sni_name)); + EXPECT_CALL(connection_info, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*host, address()).WillRepeatedly(Return(upstream)); + + { + auto value = connection[CelValue::CreateString(Undefined)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = connection[CelValue::CreateInt64(13)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = source[CelValue::CreateString(Undefined)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = source[CelValue::CreateInt64(13)]; + EXPECT_FALSE(value.has_value()); + } + + { + auto value = destination[CelValue::CreateString(Address)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("1.2.3.4:123", value.value().StringOrDie().value()); + } + + { + auto value = destination[CelValue::CreateString(Port)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + EXPECT_EQ(123, value.value().Int64OrDie()); + } + + { + auto value = source[CelValue::CreateString(Address)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("10.20.30.40:456", value.value().StringOrDie().value()); + } + + { + auto value = source[CelValue::CreateString(Port)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + EXPECT_EQ(456, value.value().Int64OrDie()); + } + + { + auto value = connection[CelValue::CreateString(UpstreamAddress)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ("10.1.2.3:679", value.value().StringOrDie().value()); + } + + { + auto value = connection[CelValue::CreateString(UpstreamPort)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsInt64()); + EXPECT_EQ(679, value.value().Int64OrDie()); + } + + { + auto value = connection[CelValue::CreateString(MTLS)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsBool()); + EXPECT_TRUE(value.value().BoolOrDie()); + } + + { + auto value = connection[CelValue::CreateString(RequestedServerName)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsString()); + EXPECT_EQ(sni_name, value.value().StringOrDie().value()); + } +} + +} // namespace +} // namespace Expr +} // namespace Common +} // namespace Filters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/common/rbac/BUILD b/test/extensions/filters/common/rbac/BUILD index d9b47acebf6ae..1b0671adbcbc5 100644 --- a/test/extensions/filters/common/rbac/BUILD +++ b/test/extensions/filters/common/rbac/BUILD @@ -32,6 +32,7 @@ envoy_extension_cc_test( "//source/extensions/filters/common/rbac:engine_lib", "//test/mocks/network:network_mocks", "//test/mocks/ssl:ssl_mocks", + "//test/mocks/stream_info:stream_info_mocks", "//test/test_common:utility_lib", ], ) diff --git a/test/extensions/filters/common/rbac/engine_impl_test.cc b/test/extensions/filters/common/rbac/engine_impl_test.cc index bdca0c55ae5b0..6d346dca62cf3 100644 --- a/test/extensions/filters/common/rbac/engine_impl_test.cc +++ b/test/extensions/filters/common/rbac/engine_impl_test.cc @@ -4,6 +4,8 @@ #include "test/mocks/network/mocks.h" #include "test/mocks/ssl/mocks.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -25,7 +27,9 @@ void checkEngine(const RBAC::RoleBasedAccessControlEngineImpl& engine, bool expe const Envoy::Http::HeaderMap& headers = Envoy::Http::HeaderMapImpl(), const envoy::api::v2::core::Metadata& metadata = envoy::api::v2::core::Metadata(), std::string* policy_id = nullptr) { - EXPECT_EQ(expected, engine.allowed(connection, headers, metadata, policy_id)); + NiceMock info; + EXPECT_CALL(Const(info), dynamicMetadata()).WillRepeatedly(ReturnRef(metadata)); + EXPECT_EQ(expected, engine.allowed(connection, headers, info, policy_id)); } TEST(RoleBasedAccessControlEngineImpl, Disabled) { @@ -79,6 +83,187 @@ TEST(RoleBasedAccessControlEngineImpl, DeniedBlacklist) { checkEngine(engine, true, conn); } +TEST(RoleBasedAccessControlEngineImpl, BasicCondition) { + envoy::config::rbac::v2::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + policy.mutable_condition()->MergeFrom( + TestUtility::parseYaml(R"EOF( + const_expr: + bool_value: false + )EOF")); + + envoy::config::rbac::v2::RBAC rbac; + rbac.set_action(envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW); + (*rbac.mutable_policies())["foo"] = policy; + RBAC::RoleBasedAccessControlEngineImpl engine(rbac); + checkEngine(engine, false); +} + +TEST(RoleBasedAccessControlEngineImpl, MalformedCondition) { + envoy::config::rbac::v2::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + policy.mutable_condition()->MergeFrom( + TestUtility::parseYaml(R"EOF( + call_expr: + function: undefined_extent + args: + - const_expr: + bool_value: false + )EOF")); + + envoy::config::rbac::v2::RBAC rbac; + rbac.set_action(envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW); + (*rbac.mutable_policies())["foo"] = policy; + + EXPECT_THROW_WITH_REGEX(RBAC::RoleBasedAccessControlEngineImpl engine(rbac), EnvoyException, + "failed to create an expression: .*"); +} + +TEST(RoleBasedAccessControlEngineImpl, MistypedCondition) { + envoy::config::rbac::v2::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + policy.mutable_condition()->MergeFrom( + TestUtility::parseYaml(R"EOF( + const_expr: + int64_value: 13 + )EOF")); + + envoy::config::rbac::v2::RBAC rbac; + rbac.set_action(envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW); + (*rbac.mutable_policies())["foo"] = policy; + RBAC::RoleBasedAccessControlEngineImpl engine(rbac); + checkEngine(engine, false); +} + +TEST(RoleBasedAccessControlEngineImpl, ErrorCondition) { + envoy::config::rbac::v2::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + policy.mutable_condition()->MergeFrom( + TestUtility::parseYaml(R"EOF( + call_expr: + function: _[_] + args: + - select_expr: + operand: + ident_expr: + name: request + field: undefined + - const_expr: + string_value: foo + )EOF")); + + envoy::config::rbac::v2::RBAC rbac; + rbac.set_action(envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW); + (*rbac.mutable_policies())["foo"] = policy; + RBAC::RoleBasedAccessControlEngineImpl engine(rbac); + checkEngine(engine, false, Envoy::Network::MockConnection()); +} + +TEST(RoleBasedAccessControlEngineImpl, HeaderCondition) { + envoy::config::rbac::v2::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + policy.mutable_condition()->MergeFrom( + TestUtility::parseYaml(R"EOF( + call_expr: + function: _==_ + args: + - call_expr: + function: _[_] + args: + - select_expr: + operand: + ident_expr: + name: request + field: headers + - const_expr: + string_value: foo + - const_expr: + string_value: bar + )EOF")); + + envoy::config::rbac::v2::RBAC rbac; + rbac.set_action(envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW); + (*rbac.mutable_policies())["foo"] = policy; + RBAC::RoleBasedAccessControlEngineImpl engine(rbac); + + Envoy::Http::HeaderMapImpl headers; + Envoy::Http::LowerCaseString key("foo"); + std::string value = "bar"; + headers.setReference(key, value); + + checkEngine(engine, true, Envoy::Network::MockConnection(), headers); +} + +TEST(RoleBasedAccessControlEngineImpl, MetadataCondition) { + envoy::config::rbac::v2::Policy policy; + policy.add_permissions()->set_any(true); + policy.add_principals()->set_any(true); + policy.mutable_condition()->MergeFrom( + TestUtility::parseYaml(R"EOF( + call_expr: + function: _==_ + args: + - call_expr: + function: _[_] + args: + - call_expr: + function: _[_] + args: + - select_expr: + operand: + ident_expr: + name: metadata + field: filter_metadata + - const_expr: + string_value: other + - const_expr: + string_value: label + - const_expr: + string_value: prod + )EOF")); + + envoy::config::rbac::v2::RBAC rbac; + rbac.set_action(envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW); + (*rbac.mutable_policies())["foo"] = policy; + RBAC::RoleBasedAccessControlEngineImpl engine(rbac); + + Envoy::Http::HeaderMapImpl headers; + + auto label = MessageUtil::keyValueStruct("label", "prod"); + envoy::api::v2::core::Metadata metadata; + metadata.mutable_filter_metadata()->insert( + Protobuf::MapPair("other", label)); + + checkEngine(engine, true, Envoy::Network::MockConnection(), headers, metadata); +} + +TEST(RoleBasedAccessControlEngineImpl, ConjunctiveCondition) { + envoy::config::rbac::v2::Policy policy; + policy.add_permissions()->set_destination_port(123); + policy.add_principals()->set_any(true); + policy.mutable_condition()->MergeFrom( + TestUtility::parseYaml(R"EOF( + const_expr: + bool_value: false + )EOF")); + + envoy::config::rbac::v2::RBAC rbac; + rbac.set_action(envoy::config::rbac::v2::RBAC_Action::RBAC_Action_ALLOW); + (*rbac.mutable_policies())["foo"] = policy; + RBAC::RoleBasedAccessControlEngineImpl engine(rbac); + + 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); +} + } // namespace } // namespace RBAC } // namespace Common diff --git a/test/extensions/filters/common/rbac/matchers_test.cc b/test/extensions/filters/common/rbac/matchers_test.cc index 1262d82815504..43012da47eda0 100644 --- a/test/extensions/filters/common/rbac/matchers_test.cc +++ b/test/extensions/filters/common/rbac/matchers_test.cc @@ -25,7 +25,9 @@ void checkMatcher( const Envoy::Network::Connection& connection = Envoy::Network::MockConnection(), const Envoy::Http::HeaderMap& headers = Envoy::Http::HeaderMapImpl(), const envoy::api::v2::core::Metadata& metadata = envoy::api::v2::core::Metadata()) { - EXPECT_EQ(expected, matcher.matches(connection, headers, metadata)); + NiceMock info; + EXPECT_CALL(Const(info), dynamicMetadata()).WillRepeatedly(ReturnRef(metadata)); + EXPECT_EQ(expected, matcher.matches(connection, headers, info)); } TEST(AlwaysMatcher, AlwaysMatches) { checkMatcher(RBAC::AlwaysMatcher(), true); } @@ -293,7 +295,7 @@ TEST(PolicyMatcher, PolicyMatcher) { policy.add_principals()->mutable_authenticated()->mutable_principal_name()->set_exact("foo"); policy.add_principals()->mutable_authenticated()->mutable_principal_name()->set_exact("bar"); - RBAC::PolicyMatcher matcher(policy); + RBAC::PolicyMatcher matcher(policy, nullptr); Envoy::Network::MockConnection conn; Envoy::Ssl::MockConnectionInfo ssl; diff --git a/test/extensions/filters/common/rbac/mocks.h b/test/extensions/filters/common/rbac/mocks.h index 6b14a834eec79..50555419dd4cd 100644 --- a/test/extensions/filters/common/rbac/mocks.h +++ b/test/extensions/filters/common/rbac/mocks.h @@ -17,11 +17,10 @@ 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)); + const StreamInfo::StreamInfo&, std::string* effective_policy_id)); - MOCK_CONST_METHOD3(allowed, - bool(const Envoy::Network::Connection&, const envoy::api::v2::core::Metadata&, - std::string* effective_policy_id)); + MOCK_CONST_METHOD3(allowed, bool(const Envoy::Network::Connection&, const StreamInfo::StreamInfo&, + std::string* effective_policy_id)); }; } // namespace RBAC