Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.re2j:re2j:1.8",
"com.google.s2a.proto.v2:s2a-proto:0.1.3",
"com.google.truth:truth:1.4.5",
"dev.cel:runtime:0.12.0",
"dev.cel:protobuf:0.12.0",
"dev.cel:common:0.12.0",
"com.squareup.okhttp:okhttp:2.7.5",
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
"io.netty:netty-buffer:4.1.132.Final",
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ subprojects {
ignoreGradleMetadataRedirection()
}
}
maven { url 'https://central.sonatype.com/repository/maven-snapshots/' }
}

tasks.withType(JavaCompile).configureEach {
Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ checkstyle = "com.puppycrawl.tools:checkstyle:10.26.1"
# checkstyle 10.0+ requires Java 11+
# See https://checkstyle.sourceforge.io/releasenotes_old_8-35_10-26.html#Release_10.0
# checkForUpdates: checkstylejava8:9.+
cel-runtime = "dev.cel:runtime:0.12.0"
cel-protobuf = "dev.cel:protobuf:0.12.0"
cel-compiler = "dev.cel:compiler:0.12.0"
checkstylejava8 = "com.puppycrawl.tools:checkstyle:9.3"
commons-math3 = "org.apache.commons:commons-math3:3.6.1"
conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2"
Expand Down
3 changes: 3 additions & 0 deletions repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.re2j:re2j:1.8",
"com.google.s2a.proto.v2:s2a-proto:0.1.3",
"com.google.truth:truth:1.4.5",
"dev.cel:runtime:0.12.0",
"dev.cel:protobuf:0.12.0",
"dev.cel:common:0.12.0",
"com.squareup.okhttp:okhttp:2.7.5",
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
"io.netty:netty-buffer:4.1.132.Final",
Expand Down
5 changes: 5 additions & 0 deletions xds/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ java_library(
artifact("com.google.errorprone:error_prone_annotations"),
artifact("com.google.guava:guava"),
artifact("com.google.re2j:re2j"),
artifact("dev.cel:runtime"),
artifact("dev.cel:protobuf"),
artifact("dev.cel:common"),
artifact("io.netty:netty-buffer"),
artifact("io.netty:netty-codec"),
artifact("io.netty:netty-common"),
Expand Down Expand Up @@ -97,6 +100,8 @@ JAR_JAR_RULES = [
"rule com.google.api.expr.** io.grpc.xds.shaded.com.google.api.expr.@1",
"rule com.google.security.** io.grpc.xds.shaded.com.google.security.@1",
"rule dev.cel.expr.** io.grpc.xds.shaded.dev.cel.expr.@1",
"rule dev.cel.** io.grpc.xds.shaded.dev.cel.@1",
"rule cel.** io.grpc.xds.shaded.cel.@1",
"rule envoy.annotations.** io.grpc.xds.shaded.envoy.annotations.@1",
"rule io.envoyproxy.** io.grpc.xds.shaded.io.envoyproxy.@1",
"rule udpa.annotations.** io.grpc.xds.shaded.udpa.annotations.@1",
Expand Down
11 changes: 11 additions & 0 deletions xds/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,18 @@ dependencies {
libraries.re2j,
libraries.auto.value.annotations,
libraries.protobuf.java.util
implementation(libraries.cel.runtime) {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
implementation(libraries.cel.protobuf) {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
def nettyDependency = implementation project(':grpc-netty')

testImplementation project(':grpc-api')
testImplementation project(':grpc-rls')
testImplementation project(':grpc-inprocess')
testImplementation libraries.cel.compiler
testImplementation testFixtures(project(':grpc-core')),
testFixtures(project(':grpc-api')),
testFixtures(project(':grpc-util'))
Expand Down Expand Up @@ -175,13 +182,15 @@ tasks.named("javadoc").configure {
exclude 'io/grpc/xds/XdsNameResolverProvider.java'
exclude 'io/grpc/xds/internal/**'
exclude 'io/grpc/xds/Internal*'
exclude 'dev/cel/**'
}

def prefixName = 'io.grpc.xds'
tasks.named("shadowJar").configure {
archiveClassifier = null
dependencies {
include(project(':grpc-xds'))
include(dependency('dev.cel:.*'))
}
// Relocated packages commonly need exclusions in jacocoTestReport and javadoc
// Keep in sync with BUILD.bazel's JAR_JAR_RULES
Expand All @@ -198,6 +207,8 @@ tasks.named("shadowJar").configure {
// TODO: missing java_package option in .proto
relocate 'udpa.annotations', "${prefixName}.shaded.udpa.annotations"
relocate 'xds.annotations', "${prefixName}.shaded.xds.annotations"
relocate 'dev.cel', "${prefixName}.shaded.dev.cel"
relocate 'cel', "${prefixName}.shaded.cel"
exclude "**/*.proto"
}

Expand Down
134 changes: 134 additions & 0 deletions xds/src/main/java/io/grpc/xds/internal/matcher/CelCommon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds.internal.matcher;

import com.google.common.collect.ImmutableSet;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelOptions;
import dev.cel.common.ast.CelReference;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelRuntimeFactory;
import dev.cel.runtime.CelStandardFunctions;
import dev.cel.runtime.CelStandardFunctions.StandardFunction;
import dev.cel.runtime.standard.AddOperator.AddOverload;
import java.util.Map;
import java.util.regex.Pattern;

/**
* Shared utilities for CEL-based matchers and extractors.
*/
final class CelCommon {
private static final CelOptions CEL_OPTIONS = CelOptions.newBuilder()
.enableComprehension(false)
.maxRegexProgramSize(100)
.build();
private static final String REQUEST_VARIABLE = "request";
private static final CelStandardFunctions FUNCTIONS =
CelStandardFunctions.newBuilder()
.filterFunctions((func, over) -> {
if (func == StandardFunction.STRING) {
return false;
}
if (func == StandardFunction.ADD) {
return !over.equals(AddOverload.ADD_STRING)
&& !over.equals(AddOverload.ADD_LIST);
}
Comment thread
shivaspeaks marked this conversation as resolved.
return true;
})
.build();



private static final ImmutableSet<String> ALLOWED_EXACT_OVERLOAD_IDS = ImmutableSet.of(
"equals", "not_equals", "logical_and", "logical_or", "logical_not");

/**
* Regular expression pattern to validate internal CEL overload IDs.
*
* <p>Standard CEL operators and conversion functions often have empty names in the
* AST and are identified solely by their overload IDs (e.g., {@code equals} for
* {@code ==}, {@code divide_int64} for {@code /}).
*
* <p>This pattern matches allowed overload IDs by their prefixes (e.g.,
* {@code divide}, {@code size}), optionally followed by numeric types
* (e.g., {@code int64}) and type-specific suffixes (e.g., {@code _string},
* {@code _int64}).
*/
private static final Pattern ALLOWED_OVERLOAD_ID_PREFIX_PATTERN = Pattern.compile(
"^(size|matches|contains|startsWith|endsWith|starts_with|ends_with|"
+ "timestamp|duration|in|index|has|int|uint|double|string|bytes|bool|"
+ "less|less_equals|greater|greater_equals|"
+ "add|subtract|multiply|divide|modulo|negate)"
+ "[0-9]*(_.*)?$");
Comment thread
shivaspeaks marked this conversation as resolved.

static final CelRuntime RUNTIME = CelRuntimeFactory.standardCelRuntimeBuilder()
.setStandardEnvironmentEnabled(false)
.setStandardFunctions(FUNCTIONS)
.setOptions(CEL_OPTIONS)
.build();

private CelCommon() {}

/**
* Validates that the AST only references the allowed variable ("request")
* and supported functions as defined in gRFC A106.
*/
static void checkAllowedReferences(CelAbstractSyntaxTree ast) {
for (Map.Entry<Long, CelReference> entry : ast.getReferenceMap().entrySet()) {
CelReference ref = entry.getValue();

// Check for variables (where overloadIds is empty)
if (!ref.value().isPresent() && ref.overloadIds().isEmpty()) {
if (!REQUEST_VARIABLE.equals(ref.name())) {
throw new IllegalArgumentException(
"CEL expression references unknown variable: " + ref.name());
}
} else if (!ref.overloadIds().isEmpty()) {
String name = ref.name();
if (name.isEmpty()) {
boolean allowed = false;
for (String id : ref.overloadIds()) {
if (id.equals("add_string") || id.equals("add_list") || id.endsWith("_to_string")) {
allowed = false;
break;
}
if (ALLOWED_EXACT_OVERLOAD_IDS.contains(id)
|| ALLOWED_OVERLOAD_ID_PREFIX_PATTERN.matcher(id).matches()) {
allowed = true;
break;
Comment thread
shivaspeaks marked this conversation as resolved.
}
}
if (!allowed) {
throw new IllegalArgumentException(
"CEL expression references unknown function with overload IDs: "
+ ref.overloadIds());
}
} else {
// Standard conversion functions (like string(x)) are named in the AST.
// We must explicitly reject 'string' here since it's disabled in the environment.
if (name.equals("string")) {
throw new IllegalArgumentException(
"CEL expression references unknown function with overload IDs: "
+ ref.overloadIds());
}
throw new IllegalArgumentException(
Comment thread
shivaspeaks marked this conversation as resolved.
"CEL expression references unsupported named function: " + name);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.grpc.xds.internal.matcher;

import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.types.SimpleType;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime;
import dev.cel.runtime.CelVariableResolver;
import javax.annotation.Nullable;

/**
* Executes compiled CEL expressions that extract a string.
*/
public final class CelStringExtractor {
private final CelRuntime.Program program;
@Nullable
private final String defaultValue;

private CelStringExtractor(CelRuntime.Program program, @Nullable String defaultValue) {
this.program = program;
this.defaultValue = defaultValue;
}

/**
* Compiles the AST into a CelStringExtractor with an optional default value.
* Throws an Exception if evaluation fails during compilation setup.
*/
public static CelStringExtractor compile(CelAbstractSyntaxTree ast, @Nullable String defaultValue)
throws CelEvaluationException {
if (ast.getResultType() != SimpleType.STRING && ast.getResultType() != SimpleType.DYN) {
throw new IllegalArgumentException(
"CEL expression must evaluate to string, got: " + ast.getResultType());
}
CelCommon.checkAllowedReferences(ast);
CelRuntime.Program program = CelCommon.RUNTIME.createProgram(ast);
return new CelStringExtractor(program, defaultValue);
}

/**
* Compiles the AST into a CelStringExtractor with no default value.
* Throws an Exception if evaluation fails during compilation setup.
*/
public static CelStringExtractor compile(CelAbstractSyntaxTree ast)
throws CelEvaluationException {
return compile(ast, null);
}

/**
* Evaluates the CEL expression and returns the string result.
* Returns the default value if the result is not a string or if evaluation
* fails.
*/
public String extract(Object input) throws CelEvaluationException {
if (input instanceof CelVariableResolver) {
try {
Object result = program.eval((CelVariableResolver) input);

if (result instanceof String) {
return (String) result;
}
} catch (CelEvaluationException e) {
if (defaultValue == null) {
throw e;
}
}
} else if (defaultValue == null) {
throw new CelEvaluationException(
"Unsupported input type for CEL evaluation: "
+ (input == null ? "null" : input.getClass().getName()));
}
Comment thread
shivaspeaks marked this conversation as resolved.
return defaultValue;
}
}
Loading
Loading