Skip to content

Commit

Permalink
filter: add conditions to access control filter (#7716)
Browse files Browse the repository at this point in the history
Introduces a generic expression-based admission filter using https://github.com/google/cel-cpp.
This is a follow-up to discussion in #6751.
The advantage of this approach is:
1. Un-opinionated about the policy structure since the only config is an expression. This is friendly towards control planes which can bear the complexity of translation, analysis, and evolution of policies.
2. Multi-language, CEL supports go, java, and c++ runtimes.
3. Inter-operability with other filters using request `metadata`. Companion filters can populate metadata about requests and resources that affect policy decisions.
4. Generic utility, it can be used for custom metric labels, access log entries, etc.

The dis-advantage of this approach is that its performance is lower than domain-optimized interpreters. On a fair example, the interpreter evaluates in around 1ms (see https://github.com/google/cel-cpp/blob/master/eval/tests/benchmark_test.cc#L591) vs ~150ns for hand-written C++ native code. There is space for improvement (especially if WASM can be used as a compilation target), but ultimately the generic expression form carries a cost.

Conditions are added to support RBAC filter for complementing the existing principal/permission model. They add support for the extended checks (e.g. time of query, resource-bound), but add no cost unless used.

Description: add expression-based admission filter
Risk Level: low
Testing:
Docs Changes:
Release Notes:

Signed-off-by: Kuat Yessenov <[email protected]>
  • Loading branch information
kyessenov authored and lizan committed Aug 19, 2019
1 parent 869981f commit f90e1b0
Show file tree
Hide file tree
Showing 29 changed files with 1,188 additions and 90 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 4 additions & 3 deletions api/bazel/api_build_system.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions api/envoy/config/rbac/v2/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
],
)
8 changes: 7 additions & 1 deletion api/envoy/config/rbac/v2/rbac.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions bazel/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions bazel/repository_locations.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,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"],
),
)
1 change: 1 addition & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Version history
* rbac: added support for DNS SAN as :ref:`principal_name <envoy_api_field_config.rbac.v2.Principal.Authenticated.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 <envoy_api_field_config.rbac.v2.Policy.condition>`.
* router: added :ref:`rq_retry_skipped_request_not_complete <config_http_filters_router_stats>` counter stat to router stats.
* router check tool: add coverage reporting & enforcement.
* router check tool: add comprehensive coverage reporting.
Expand Down
34 changes: 34 additions & 0 deletions source/extensions/filters/common/expr/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
164 changes: 164 additions & 0 deletions source/extensions/filters/common/expr/context.cc
Original file line number Diff line number Diff line change
@@ -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<CelValue> convertHeaderEntry(const Http::HeaderEntry* header) {
if (header == nullptr) {
return {};
}
return CelValue::CreateString(header->value().getStringView());
}

} // namespace

absl::optional<CelValue> 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<CelValue> 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<CelValue> 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<CelValue> 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<CelValue> 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
Loading

0 comments on commit f90e1b0

Please sign in to comment.