Skip to content

Commit 51bd55a

Browse files
authored
cel: add string extensions support in CEL expressions (envoyproxy#40510)
## Description This PR adds support for CEL to expose string manipulation functions like `lowerAscii()` etc. in HTTP filters. Fix envoyproxy#39307 --- **Commit Message:** cel: add string extensions support in CEL expressions **Additional Description:** Added support for exposing string manipulation functions in CEL expressions. **Risk Level:** Low **Testing:** Added Unit + Integration Tests **Docs Changes:** Added **Release Notes:** Added --------- Signed-off-by: Rohit Agrawal <rohit.agrawal@databricks.com>
1 parent 2546bcd commit 51bd55a

27 files changed

Lines changed: 2753 additions & 58 deletions

File tree

api/envoy/config/core/v3/cel.proto

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
syntax = "proto3";
2+
3+
package envoy.config.core.v3;
4+
5+
import "udpa/annotations/status.proto";
6+
7+
option java_package = "io.envoyproxy.envoy.config.core.v3";
8+
option java_outer_classname = "CelProto";
9+
option java_multiple_files = true;
10+
option go_package = "github.com/envoyproxy/go-control-plane/envoy/config/core/v3;corev3";
11+
option (udpa.annotations.file_status).package_version_status = ACTIVE;
12+
13+
// [#protodoc-title: CEL Expression Configuration]
14+
15+
// CEL expression evaluation configuration.
16+
// These options control the behavior of the Common Expression Language runtime for
17+
// individual CEL expressions.
18+
message CelExpressionConfig {
19+
// Enable string conversion functions for CEL expressions. When enabled, CEL expressions
20+
// can convert values to strings using the ``string()`` function.
21+
//
22+
// .. attention::
23+
//
24+
// This option is disabled by default to avoid unbounded memory allocation.
25+
// CEL evaluation cost is typically bounded by the expression size, but converting
26+
// arbitrary values (e.g., large messages, lists, or maps) to strings may allocate
27+
// memory proportional to input data size, which can be unbounded and lead to
28+
// memory exhaustion.
29+
bool enable_string_conversion = 1;
30+
31+
// Enable string concatenation for CEL expressions. When enabled, CEL expressions
32+
// can concatenate strings using the ``+`` operator.
33+
//
34+
// .. attention::
35+
//
36+
// This option is disabled by default to avoid unbounded memory allocation.
37+
// While CEL normally bounds evaluation by expression size, enabling string
38+
// concatenation allows building outputs whose size depends on input data,
39+
// potentially causing large intermediate allocations and memory exhaustion.
40+
bool enable_string_concat = 2;
41+
42+
// Enable string manipulation functions for CEL expressions. When enabled, CEL
43+
// expressions can use additional string functions:
44+
//
45+
// * ``replace(old, new)`` - Replaces all occurrences of ``old`` with ``new``.
46+
// * ``split(separator)`` - Splits a string into a list of substrings.
47+
// * ``lowerAscii()`` - Converts ASCII characters to lowercase.
48+
// * ``upperAscii()`` - Converts ASCII characters to uppercase.
49+
//
50+
// .. note::
51+
//
52+
// Standard CEL string functions like ``contains()``, ``startsWith()``, and
53+
// ``endsWith()`` are always available regardless of this setting.
54+
//
55+
// .. attention::
56+
//
57+
// This option is disabled by default to avoid unbounded memory allocation.
58+
// Although CEL generally bounds evaluation by expression size, functions such as
59+
// ``replace``, ``split``, ``lowerAscii()``, and ``upperAscii()`` can allocate memory
60+
// proportional to input data size. Under adversarial inputs this can lead to
61+
// unbounded allocations and memory exhaustion.
62+
bool enable_string_functions = 3;
63+
}

api/envoy/config/rbac/v3/rbac.proto

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ syntax = "proto3";
33
package envoy.config.rbac.v3;
44

55
import "envoy/config/core/v3/address.proto";
6+
import "envoy/config/core/v3/cel.proto";
67
import "envoy/config/core/v3/extension.proto";
78
import "envoy/config/route/v3/route_components.proto";
89
import "envoy/type/matcher/v3/filter_state.proto";
@@ -173,6 +174,7 @@ message RBAC {
173174
// A policy matches if and only if at least one of its permissions match the
174175
// action taking place AND at least one of its principals match the downstream
175176
// AND the condition is true if specified.
177+
// [#next-free-field: 6]
176178
message Policy {
177179
option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v2.Policy";
178180

@@ -199,6 +201,12 @@ message Policy {
199201
// Only be used when condition is not used.
200202
google.api.expr.v1alpha1.CheckedExpr checked_condition = 4
201203
[(udpa.annotations.field_migrate).oneof_promotion = "expression_specifier"];
204+
205+
// CEL expression configuration that modifies the evaluation behavior of the ``condition`` field.
206+
// If specified, string conversion, concatenation, and manipulation functions may be enabled
207+
// for the CEL expression. See :ref:`CelExpressionConfig <envoy_v3_api_msg_config.core.v3.CelExpressionConfig>`
208+
// for more details.
209+
core.v3.CelExpressionConfig cel_config = 5;
202210
}
203211

204212
// SourcedMetadata enables matching against metadata from different sources in the request processing

api/envoy/extensions/access_loggers/filters/cel/v3/BUILD

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package")
55
licenses(["notice"]) # Apache 2
66

77
api_proto_package(
8-
deps = ["@com_github_cncf_xds//udpa/annotations:pkg"],
8+
deps = [
9+
"//envoy/config/core/v3:pkg",
10+
"@com_github_cncf_xds//udpa/annotations:pkg",
11+
],
912
)

api/envoy/extensions/access_loggers/filters/cel/v3/cel.proto

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ syntax = "proto3";
22

33
package envoy.extensions.access_loggers.filters.cel.v3;
44

5+
import "envoy/config/core/v3/cel.proto";
6+
57
import "udpa/annotations/status.proto";
68

79
option java_package = "io.envoyproxy.envoy.extensions.access_loggers.filters.cel.v3";
@@ -25,4 +27,10 @@ message ExpressionFilter {
2527
// * ``response.code >= 400``
2628
// * ``(connection.mtls && request.headers['x-log-mtls'] == 'true') || request.url_path.contains('v1beta3')``
2729
string expression = 1;
30+
31+
// CEL expression configuration that modifies the evaluation behavior of the ``expression`` field.
32+
// If specified, string conversion, concatenation, and manipulation functions may be enabled
33+
// for the filter expression. See :ref:`CelExpressionConfig <envoy_v3_api_msg_config.core.v3.CelExpressionConfig>`
34+
// for more details.
35+
config.core.v3.CelExpressionConfig cel_config = 2;
2836
}

changelogs/current.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ new_features:
105105
change: |
106106
Added support for not-equal operator for access log filter rules, in
107107
:ref:`ComparisonFilter <envoy_v3_api_msg_config.accesslog.v3.ComparisonFilter>`.
108+
- area: cel
109+
change: |
110+
Added per-expression configuration options for CEL evaluator to control string conversion, concatenation,
111+
and string extension functions. CEL expressions in RBAC policies and access logger filters
112+
can now enable string functions such as ``replace()`` and ``split()`` through the new ``cel_config`` field
113+
in their respective configurations.
108114
- area: formatter
109115
change: |
110116
Added support for the following new access log formatters:

docs/root/api-v3/common_messages/common_messages.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Common messages
1010
../service/discovery/v3/discovery.proto
1111
../extensions/filters/common/fault/v3/fault.proto
1212
../config/core/v3/base.proto
13+
../config/core/v3/cel.proto
1314
../extensions/filters/common/matcher/action/v3/skip_action.proto
1415
../extensions/matching/common_inputs/network/v3/network_inputs.proto
1516
../extensions/common/ratelimit/v3/ratelimit.proto

source/extensions/access_loggers/filters/cel/config.cc

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@ Envoy::AccessLog::FilterPtr CELAccessLogExtensionFilterFactory::createFilter(
3232
parse_status.status().ToString());
3333
}
3434

35-
return std::make_unique<CELAccessLogExtensionFilter>(
36-
context.serverFactoryContext().localInfo(),
37-
Extensions::Filters::Common::Expr::getBuilder(context.serverFactoryContext()),
38-
parse_status.value().expr());
35+
// Use the CEL configuration from the filter if available.
36+
auto config_ref = cel_config.has_cel_config()
37+
? Envoy::makeOptRef(cel_config.cel_config())
38+
: Envoy::OptRef<const envoy::config::core::v3::CelExpressionConfig>{};
39+
auto builder =
40+
Extensions::Filters::Common::Expr::getBuilder(context.serverFactoryContext(), config_ref);
41+
42+
return std::make_unique<CELAccessLogExtensionFilter>(context.serverFactoryContext().localInfo(),
43+
builder, parse_status.value().expr());
3944
#else
4045
throw EnvoyException("CEL is not available for use in this environment.");
4146
#endif

source/extensions/filters/common/expr/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ envoy_cc_library(
2626
"@com_google_cel_cpp//eval/public:cel_expression",
2727
"@com_google_cel_cpp//eval/public:cel_value",
2828
"@com_google_cel_cpp//extensions:regex_functions",
29+
"@com_google_cel_cpp//extensions:strings",
30+
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",
2931
],
3032
)
3133

source/extensions/filters/common/expr/evaluator.cc

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
#include "envoy/common/exception.h"
44
#include "envoy/singleton/manager.h"
55

6+
#include "source/common/protobuf/utility.h"
67
#include "source/common/runtime/runtime_features.h"
78

89
#include "extensions/regex_functions.h"
10+
#include "extensions/strings.h"
911

1012
#include "cel/expr/syntax.pb.h"
1113
#include "eval/public/builtin_func_registrar.h"
@@ -101,25 +103,35 @@ ActivationPtr createActivation(const LocalInfo::LocalInfo* local_info,
101103
response_trailers);
102104
}
103105

104-
BuilderInstanceSharedPtr createBuilder(Protobuf::Arena* arena) {
106+
BuilderConstPtr createBuilder(OptRef<const envoy::config::core::v3::CelExpressionConfig> config,
107+
Protobuf::Arena* arena) {
105108
ASSERT_IS_MAIN_OR_TEST_THREAD();
106109
google::api::expr::runtime::InterpreterOptions options;
107110

108-
// Security-oriented defaults
111+
// Security-oriented defaults.
109112
options.enable_comprehension = false;
110113
options.enable_regex = true;
111114
options.regex_max_program_size = 100;
112115
options.enable_qualified_identifier_rewrites = true;
113-
options.enable_string_conversion = false;
114-
options.enable_string_concat = false;
116+
117+
// Resolve options from configuration or fall back to security-oriented defaults.
118+
bool enable_string_functions = false;
119+
if (config.has_value()) {
120+
options.enable_string_conversion = config->enable_string_conversion();
121+
options.enable_string_concat = config->enable_string_concat();
122+
enable_string_functions = config->enable_string_functions();
123+
} else {
124+
options.enable_string_conversion = false;
125+
options.enable_string_concat = false;
126+
}
115127
options.enable_list_concat = false;
116128

117-
// Performance-oriented defaults
129+
// Performance-oriented defaults.
118130
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_cel_regex_precompilation")) {
119131
options.enable_regex_precompilation = true;
120132
}
121133

122-
// Enable constant folding (performance optimization)
134+
// Enable constant folding with arena if provided for RBAC backward compatibility optimization.
123135
if (arena != nullptr) {
124136
options.constant_folding = true;
125137
options.constant_arena = arena;
@@ -138,14 +150,61 @@ BuilderInstanceSharedPtr createBuilder(Protobuf::Arena* arena) {
138150
throw CelException(absl::StrCat("failed to register extension regex functions: ",
139151
ext_register_status.message()));
140152
}
141-
return std::make_shared<BuilderInstance>(std::move(builder));
153+
// Register string extension functions only if enabled in configuration.
154+
if (enable_string_functions) {
155+
auto string_register_status =
156+
cel::extensions::RegisterStringsFunctions(builder->GetRegistry(), options);
157+
if (!string_register_status.ok()) {
158+
throw CelException(absl::StrCat("failed to register extension string functions: ",
159+
string_register_status.message()));
160+
}
161+
}
162+
return builder;
163+
}
164+
165+
BuilderInstanceSharedConstPtr BuilderCache::getOrCreateBuilder(
166+
OptRef<const envoy::config::core::v3::CelExpressionConfig> config) {
167+
ASSERT_IS_MAIN_OR_TEST_THREAD();
168+
169+
ConfigHash hash = 0;
170+
if (config.has_value()) {
171+
// Use MessageUtil::hash for proto hashing.
172+
hash = MessageUtil::hash(config.ref());
173+
}
174+
175+
auto it = builders_.find(hash);
176+
if (it != builders_.end()) {
177+
auto locked_builder = it->second.lock();
178+
if (locked_builder) {
179+
return locked_builder;
180+
}
181+
}
182+
183+
// Create new builder with the configuration.
184+
auto builder = createBuilder(config);
185+
auto instance = std::make_shared<BuilderInstance>(std::move(builder), shared_from_this());
186+
// Store as weak_ptr to allow release after xDS unload.
187+
builders_[hash] = instance;
188+
return instance;
142189
}
143190

144-
SINGLETON_MANAGER_REGISTRATION(expression_builder);
191+
SINGLETON_MANAGER_REGISTRATION(builder_cache);
192+
193+
BuilderInstanceSharedConstPtr
194+
getBuilder(Server::Configuration::CommonFactoryContext& context,
195+
OptRef<const envoy::config::core::v3::CelExpressionConfig> config) {
196+
auto cache = context.singletonManager().getTyped<BuilderCache>(
197+
SINGLETON_MANAGER_REGISTERED_NAME(builder_cache),
198+
[] { return std::make_shared<BuilderCache>(); });
199+
return cache->getOrCreateBuilder(config);
200+
}
145201

146-
BuilderInstanceSharedConstPtr getBuilder(Server::Configuration::CommonFactoryContext& context) {
147-
return context.singletonManager().getTyped<BuilderInstance>(
148-
SINGLETON_MANAGER_REGISTERED_NAME(expression_builder), [] { return createBuilder(nullptr); });
202+
absl::StatusOr<CompiledExpression>
203+
CompiledExpression::Create(Server::Configuration::CommonFactoryContext& context,
204+
const cel::expr::Expr& expr,
205+
OptRef<const envoy::config::core::v3::CelExpressionConfig> config) {
206+
auto builder = getBuilder(context, config);
207+
return Create(builder, expr);
149208
}
150209

151210
absl::StatusOr<CompiledExpression>
@@ -165,13 +224,13 @@ CompiledExpression::Create(const BuilderInstanceSharedConstPtr& builder,
165224
absl::StatusOr<CompiledExpression>
166225
CompiledExpression::Create(const BuilderInstanceSharedConstPtr& builder,
167226
const xds::type::v3::CelExpression& xds_expr) {
168-
// First try to get expression from the new CEL canonical format
227+
// First try to get expression from the new CEL canonical format.
169228
if (xds_expr.has_cel_expr_checked()) {
170229
return Create(builder, xds_expr.cel_expr_checked().expr());
171230
} else if (xds_expr.has_cel_expr_parsed()) {
172231
return Create(builder, xds_expr.cel_expr_parsed().expr());
173232
}
174-
// Fallback to handling legacy formats for backward compatibility
233+
// Fallback to handling legacy formats for backward compatibility.
175234
switch (xds_expr.expr_specifier_case()) {
176235
case xds::type::v3::CelExpression::ExprSpecifierCase::kParsedExpr:
177236
return Create(builder, xds_expr.parsed_expr().expr());

0 commit comments

Comments
 (0)