Skip to content

Commit 65f51c0

Browse files
committed
feat(xds): Add configuration objects for ExtAuthz and GrpcService
This commit introduces configuration objects for the external authorization (ExtAuthz) filter and the gRPC service it uses. These classes provide a structured, immutable representation of the configuration defined in the xDS protobuf messages. The main new classes are: - `ExtAuthzConfig`: Represents the configuration for the `ExtAuthz` filter, including settings for the gRPC service, header mutation rules, and other filter behaviors. - `GrpcServiceConfig`: Represents the configuration for a gRPC service, including the target URI, credentials, and other settings. - `HeaderMutationRulesConfig`: Represents the configuration for header mutation rules. This commit also includes parsers to create these configuration objects from the corresponding protobuf messages, as well as unit tests for the new classes.
1 parent 8107c1b commit 65f51c0

File tree

11 files changed

+1414
-0
lines changed

11 files changed

+1414
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds.internal.extauthz;
18+
19+
import com.google.auto.value.AutoValue;
20+
import com.google.common.collect.ImmutableList;
21+
import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules;
22+
import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz;
23+
import io.grpc.Status;
24+
import io.grpc.internal.GrpcUtil;
25+
import io.grpc.xds.internal.MatcherParser;
26+
import io.grpc.xds.internal.Matchers;
27+
import io.grpc.xds.internal.grpcservice.GrpcServiceConfig;
28+
import io.grpc.xds.internal.grpcservice.GrpcServiceParseException;
29+
import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig;
30+
import java.util.Optional;
31+
import java.util.regex.Pattern;
32+
import java.util.regex.PatternSyntaxException;
33+
34+
/**
35+
* Represents the configuration for the external authorization (ext_authz) filter. This class
36+
* encapsulates the settings defined in the
37+
* {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto, providing a
38+
* structured, immutable representation for use within gRPC. It includes configurations for the gRPC
39+
* service used for authorization, header mutation rules, and other filter behaviors.
40+
*/
41+
@AutoValue
42+
public abstract class ExtAuthzConfig {
43+
44+
/** Creates a new builder for creating {@link ExtAuthzConfig} instances. */
45+
public static Builder builder() {
46+
return new AutoValue_ExtAuthzConfig.Builder().allowedHeaders(ImmutableList.of())
47+
.disallowedHeaders(ImmutableList.of()).statusOnError(Status.PERMISSION_DENIED)
48+
.filterEnabled(Matchers.FractionMatcher.create(100, 100));
49+
}
50+
51+
/**
52+
* Parses the {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto to
53+
* create an {@link ExtAuthzConfig} instance.
54+
*
55+
* @param extAuthzProto The ext_authz proto to parse.
56+
* @return An {@link ExtAuthzConfig} instance.
57+
* @throws ExtAuthzParseException if the proto is invalid or contains unsupported features.
58+
*/
59+
public static ExtAuthzConfig fromProto(ExtAuthz extAuthzProto) throws ExtAuthzParseException {
60+
if (!extAuthzProto.hasGrpcService()) {
61+
throw new ExtAuthzParseException(
62+
"unsupported ExtAuthz service type: only grpc_service is " + "supported");
63+
}
64+
GrpcServiceConfig grpcServiceConfig;
65+
try {
66+
grpcServiceConfig = GrpcServiceConfig.fromProto(extAuthzProto.getGrpcService());
67+
} catch (GrpcServiceParseException e) {
68+
throw new ExtAuthzParseException("Failed to parse GrpcService config: " + e.getMessage(), e);
69+
}
70+
Builder builder = builder().grpcService(grpcServiceConfig)
71+
.failureModeAllow(extAuthzProto.getFailureModeAllow())
72+
.failureModeAllowHeaderAdd(extAuthzProto.getFailureModeAllowHeaderAdd())
73+
.includePeerCertificate(extAuthzProto.getIncludePeerCertificate())
74+
.denyAtDisable(extAuthzProto.getDenyAtDisable().getDefaultValue().getValue());
75+
76+
if (extAuthzProto.hasFilterEnabled()) {
77+
builder.filterEnabled(parsePercent(extAuthzProto.getFilterEnabled().getDefaultValue()));
78+
}
79+
80+
if (extAuthzProto.hasStatusOnError()) {
81+
builder.statusOnError(
82+
GrpcUtil.httpStatusToGrpcStatus(extAuthzProto.getStatusOnError().getCodeValue()));
83+
}
84+
85+
if (extAuthzProto.hasAllowedHeaders()) {
86+
builder.allowedHeaders(extAuthzProto.getAllowedHeaders().getPatternsList().stream()
87+
.map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList()));
88+
}
89+
90+
if (extAuthzProto.hasDisallowedHeaders()) {
91+
builder.disallowedHeaders(extAuthzProto.getDisallowedHeaders().getPatternsList().stream()
92+
.map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList()));
93+
}
94+
95+
if (extAuthzProto.hasDecoderHeaderMutationRules()) {
96+
builder.decoderHeaderMutationRules(
97+
parseHeaderMutationRules(extAuthzProto.getDecoderHeaderMutationRules()));
98+
}
99+
100+
return builder.build();
101+
}
102+
103+
/**
104+
* The gRPC service configuration for the external authorization service. This is a required
105+
* field.
106+
*
107+
* @see ExtAuthz#getGrpcService()
108+
*/
109+
public abstract GrpcServiceConfig grpcService();
110+
111+
/**
112+
* Changes the filter's behavior on errors from the authorization service. If {@code true}, the
113+
* filter will accept the request even if the authorization service fails or returns an error.
114+
*
115+
* @see ExtAuthz#getFailureModeAllow()
116+
*/
117+
public abstract boolean failureModeAllow();
118+
119+
/**
120+
* Determines if the {@code x-envoy-auth-failure-mode-allowed} header is added to the request when
121+
* {@link #failureModeAllow()} is true.
122+
*
123+
* @see ExtAuthz#getFailureModeAllowHeaderAdd()
124+
*/
125+
public abstract boolean failureModeAllowHeaderAdd();
126+
127+
/**
128+
* Specifies if the peer certificate is sent to the external authorization service.
129+
*
130+
* @see ExtAuthz#getIncludePeerCertificate()
131+
*/
132+
public abstract boolean includePeerCertificate();
133+
134+
/**
135+
* The gRPC status returned to the client when the authorization server returns an error or is
136+
* unreachable. Defaults to {@code PERMISSION_DENIED}.
137+
*
138+
* @see io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz#getStatusOnError()
139+
*/
140+
public abstract Status statusOnError();
141+
142+
/**
143+
* Specifies whether to deny requests when the filter is disabled. Defaults to {@code false}.
144+
*
145+
* @see ExtAuthz#getDenyAtDisable()
146+
*/
147+
public abstract boolean denyAtDisable();
148+
149+
/**
150+
* The fraction of requests that will be checked by the authorization service. Defaults to all
151+
* requests.
152+
*
153+
* @see ExtAuthz#getFilterEnabled()
154+
*/
155+
public abstract Matchers.FractionMatcher filterEnabled();
156+
157+
/**
158+
* Specifies which request headers are sent to the authorization service. If not set, all headers
159+
* are sent.
160+
*
161+
* @see ExtAuthz#getAllowedHeaders()
162+
*/
163+
public abstract ImmutableList<Matchers.StringMatcher> allowedHeaders();
164+
165+
/**
166+
* Specifies which request headers are not sent to the authorization service. This overrides
167+
* {@link #allowedHeaders()}.
168+
*
169+
* @see ExtAuthz#getDisallowedHeaders()
170+
*/
171+
public abstract ImmutableList<Matchers.StringMatcher> disallowedHeaders();
172+
173+
/**
174+
* Rules for what modifications an ext_authz server may make to request headers.
175+
*
176+
* @see ExtAuthz#getDecoderHeaderMutationRules()
177+
*/
178+
public abstract Optional<HeaderMutationRulesConfig> decoderHeaderMutationRules();
179+
180+
@AutoValue.Builder
181+
public abstract static class Builder {
182+
public abstract Builder grpcService(GrpcServiceConfig grpcService);
183+
184+
public abstract Builder failureModeAllow(boolean failureModeAllow);
185+
186+
public abstract Builder failureModeAllowHeaderAdd(boolean failureModeAllowHeaderAdd);
187+
188+
public abstract Builder includePeerCertificate(boolean includePeerCertificate);
189+
190+
public abstract Builder statusOnError(Status statusOnError);
191+
192+
public abstract Builder denyAtDisable(boolean denyAtDisable);
193+
194+
public abstract Builder filterEnabled(Matchers.FractionMatcher filterEnabled);
195+
196+
public abstract Builder allowedHeaders(Iterable<Matchers.StringMatcher> allowedHeaders);
197+
198+
public abstract Builder disallowedHeaders(Iterable<Matchers.StringMatcher> disallowedHeaders);
199+
200+
public abstract Builder decoderHeaderMutationRules(HeaderMutationRulesConfig rules);
201+
202+
public abstract ExtAuthzConfig build();
203+
}
204+
205+
206+
private static Matchers.FractionMatcher parsePercent(
207+
io.envoyproxy.envoy.type.v3.FractionalPercent proto) throws ExtAuthzParseException {
208+
int denominator;
209+
switch (proto.getDenominator()) {
210+
case HUNDRED:
211+
denominator = 100;
212+
break;
213+
case TEN_THOUSAND:
214+
denominator = 10_000;
215+
break;
216+
case MILLION:
217+
denominator = 1_000_000;
218+
break;
219+
case UNRECOGNIZED:
220+
default:
221+
throw new ExtAuthzParseException("Unknown denominator type: " + proto.getDenominator());
222+
}
223+
return Matchers.FractionMatcher.create(proto.getNumerator(), denominator);
224+
}
225+
226+
private static HeaderMutationRulesConfig parseHeaderMutationRules(HeaderMutationRules proto)
227+
throws ExtAuthzParseException {
228+
HeaderMutationRulesConfig.Builder builder = HeaderMutationRulesConfig.builder();
229+
builder.disallowAll(proto.getDisallowAll().getValue());
230+
builder.disallowIsError(proto.getDisallowIsError().getValue());
231+
if (proto.hasAllowExpression()) {
232+
builder.allowExpression(
233+
parseRegex(proto.getAllowExpression().getRegex(), "allow_expression"));
234+
}
235+
if (proto.hasDisallowExpression()) {
236+
builder.disallowExpression(
237+
parseRegex(proto.getDisallowExpression().getRegex(), "disallow_expression"));
238+
}
239+
return builder.build();
240+
}
241+
242+
private static Pattern parseRegex(String regex, String fieldName) throws ExtAuthzParseException {
243+
try {
244+
return Pattern.compile(regex);
245+
} catch (PatternSyntaxException e) {
246+
throw new ExtAuthzParseException(
247+
"Invalid regex pattern for " + fieldName + ": " + e.getMessage(), e);
248+
}
249+
}
250+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.xds.internal.extauthz;
18+
19+
/**
20+
* A custom exception for signaling errors during the parsing of external authorization
21+
* (ext_authz) configurations.
22+
*/
23+
public class ExtAuthzParseException extends Exception {
24+
25+
private static final long serialVersionUID = 0L;
26+
27+
public ExtAuthzParseException(String message) {
28+
super(message);
29+
}
30+
31+
public ExtAuthzParseException(String message, Throwable cause) {
32+
super(message, cause);
33+
}
34+
}

0 commit comments

Comments
 (0)