Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
250 changes: 250 additions & 0 deletions xds/src/main/java/io/grpc/xds/internal/extauthz/ExtAuthzConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
/*
* Copyright 2025 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.extauthz;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules;
import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz;
import io.grpc.Status;
import io.grpc.internal.GrpcUtil;
import io.grpc.xds.internal.MatcherParser;
import io.grpc.xds.internal.Matchers;
import io.grpc.xds.internal.grpcservice.GrpcServiceConfig;
import io.grpc.xds.internal.grpcservice.GrpcServiceParseException;
import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
* Represents the configuration for the external authorization (ext_authz) filter. This class
* encapsulates the settings defined in the
* {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto, providing a
* structured, immutable representation for use within gRPC. It includes configurations for the gRPC
* service used for authorization, header mutation rules, and other filter behaviors.
*/
@AutoValue
public abstract class ExtAuthzConfig {

/** Creates a new builder for creating {@link ExtAuthzConfig} instances. */
public static Builder builder() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically name it newBuilder()

return new AutoValue_ExtAuthzConfig.Builder().allowedHeaders(ImmutableList.of())
.disallowedHeaders(ImmutableList.of()).statusOnError(Status.PERMISSION_DENIED)
.filterEnabled(Matchers.FractionMatcher.create(100, 100));
}

/**
* Parses the {@link io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz} proto to
* create an {@link ExtAuthzConfig} instance.
*
* @param extAuthzProto The ext_authz proto to parse.
* @return An {@link ExtAuthzConfig} instance.
* @throws ExtAuthzParseException if the proto is invalid or contains unsupported features.
*/
public static ExtAuthzConfig fromProto(ExtAuthz extAuthzProto) throws ExtAuthzParseException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of having our own config object is to insulate things from the xDS protos. That was originally because we needed to support xDS v2 and v3, but the code structure still stands. That is to say, converting to the config object would ordinarily be in a separate class; probably the ExtAuthzFilter class in this case.

Right now the PR series is awkward, because nothing (even considering #12496) actually uses this parsing code.

if (!extAuthzProto.hasGrpcService()) {
throw new ExtAuthzParseException(
"unsupported ExtAuthz service type: only grpc_service is " + "supported");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a single string literal.

}
GrpcServiceConfig grpcServiceConfig;
try {
grpcServiceConfig = GrpcServiceConfig.fromProto(extAuthzProto.getGrpcService());
} catch (GrpcServiceParseException e) {
throw new ExtAuthzParseException("Failed to parse GrpcService config: " + e.getMessage(), e);
}
Builder builder = builder().grpcService(grpcServiceConfig)
.failureModeAllow(extAuthzProto.getFailureModeAllow())
.failureModeAllowHeaderAdd(extAuthzProto.getFailureModeAllowHeaderAdd())
.includePeerCertificate(extAuthzProto.getIncludePeerCertificate())
.denyAtDisable(extAuthzProto.getDenyAtDisable().getDefaultValue().getValue());

if (extAuthzProto.hasFilterEnabled()) {
builder.filterEnabled(parsePercent(extAuthzProto.getFilterEnabled().getDefaultValue()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move XdsRouteConfigureResource.parseFractionMatcher to MatcherParser and use it here.

}

if (extAuthzProto.hasStatusOnError()) {
builder.statusOnError(
GrpcUtil.httpStatusToGrpcStatus(extAuthzProto.getStatusOnError().getCodeValue()));
}

if (extAuthzProto.hasAllowedHeaders()) {
builder.allowedHeaders(extAuthzProto.getAllowedHeaders().getPatternsList().stream()
.map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList()));
}

if (extAuthzProto.hasDisallowedHeaders()) {
builder.disallowedHeaders(extAuthzProto.getDisallowedHeaders().getPatternsList().stream()
.map(MatcherParser::parseStringMatcher).collect(ImmutableList.toImmutableList()));
}

if (extAuthzProto.hasDecoderHeaderMutationRules()) {
builder.decoderHeaderMutationRules(
parseHeaderMutationRules(extAuthzProto.getDecoderHeaderMutationRules()));
}

return builder.build();
}

/**
* The gRPC service configuration for the external authorization service. This is a required
* field.
*
* @see ExtAuthz#getGrpcService()
*/
public abstract GrpcServiceConfig grpcService();

/**
* Changes the filter's behavior on errors from the authorization service. If {@code true}, the
* filter will accept the request even if the authorization service fails or returns an error.
*
* @see ExtAuthz#getFailureModeAllow()
*/
public abstract boolean failureModeAllow();

/**
* Determines if the {@code x-envoy-auth-failure-mode-allowed} header is added to the request when
* {@link #failureModeAllow()} is true.
*
* @see ExtAuthz#getFailureModeAllowHeaderAdd()
*/
public abstract boolean failureModeAllowHeaderAdd();

/**
* Specifies if the peer certificate is sent to the external authorization service.
*
* @see ExtAuthz#getIncludePeerCertificate()
*/
public abstract boolean includePeerCertificate();

/**
* The gRPC status returned to the client when the authorization server returns an error or is
* unreachable. Defaults to {@code PERMISSION_DENIED}.
*
* @see io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz#getStatusOnError()
*/
public abstract Status statusOnError();

/**
* Specifies whether to deny requests when the filter is disabled. Defaults to {@code false}.
*
* @see ExtAuthz#getDenyAtDisable()
*/
public abstract boolean denyAtDisable();

/**
* The fraction of requests that will be checked by the authorization service. Defaults to all
* requests.
*
* @see ExtAuthz#getFilterEnabled()
*/
public abstract Matchers.FractionMatcher filterEnabled();

/**
* Specifies which request headers are sent to the authorization service. If not set, all headers
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does "if not set" mean, since this is not nullable/optional?

* are sent.
*
* @see ExtAuthz#getAllowedHeaders()
*/
public abstract ImmutableList<Matchers.StringMatcher> allowedHeaders();

/**
* Specifies which request headers are not sent to the authorization service. This overrides
* {@link #allowedHeaders()}.
*
* @see ExtAuthz#getDisallowedHeaders()
*/
public abstract ImmutableList<Matchers.StringMatcher> disallowedHeaders();

/**
* Rules for what modifications an ext_authz server may make to request headers.
*
* @see ExtAuthz#getDecoderHeaderMutationRules()
*/
public abstract Optional<HeaderMutationRulesConfig> decoderHeaderMutationRules();

@AutoValue.Builder
public abstract static class Builder {
public abstract Builder grpcService(GrpcServiceConfig grpcService);

public abstract Builder failureModeAllow(boolean failureModeAllow);

public abstract Builder failureModeAllowHeaderAdd(boolean failureModeAllowHeaderAdd);

public abstract Builder includePeerCertificate(boolean includePeerCertificate);

public abstract Builder statusOnError(Status statusOnError);

public abstract Builder denyAtDisable(boolean denyAtDisable);

public abstract Builder filterEnabled(Matchers.FractionMatcher filterEnabled);

public abstract Builder allowedHeaders(Iterable<Matchers.StringMatcher> allowedHeaders);

public abstract Builder disallowedHeaders(Iterable<Matchers.StringMatcher> disallowedHeaders);

public abstract Builder decoderHeaderMutationRules(HeaderMutationRulesConfig rules);

public abstract ExtAuthzConfig build();
}


private static Matchers.FractionMatcher parsePercent(
io.envoyproxy.envoy.type.v3.FractionalPercent proto) throws ExtAuthzParseException {
int denominator;
switch (proto.getDenominator()) {
case HUNDRED:
denominator = 100;
break;
case TEN_THOUSAND:
denominator = 10_000;
break;
case MILLION:
denominator = 1_000_000;
break;
case UNRECOGNIZED:
default:
throw new ExtAuthzParseException("Unknown denominator type: " + proto.getDenominator());
}
return Matchers.FractionMatcher.create(proto.getNumerator(), denominator);
}

private static HeaderMutationRulesConfig parseHeaderMutationRules(HeaderMutationRules proto)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not specific to ext_authz (it is part of ext_proc as well), so I'd expect it to be shared.

throws ExtAuthzParseException {
HeaderMutationRulesConfig.Builder builder = HeaderMutationRulesConfig.builder();
builder.disallowAll(proto.getDisallowAll().getValue());
builder.disallowIsError(proto.getDisallowIsError().getValue());
if (proto.hasAllowExpression()) {
builder.allowExpression(
parseRegex(proto.getAllowExpression().getRegex(), "allow_expression"));
}
if (proto.hasDisallowExpression()) {
builder.disallowExpression(
parseRegex(proto.getDisallowExpression().getRegex(), "disallow_expression"));
}
return builder.build();
}

private static Pattern parseRegex(String regex, String fieldName) throws ExtAuthzParseException {
try {
return Pattern.compile(regex);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use re2j, like in io.grpc.xds.internal.Matchers

} catch (PatternSyntaxException e) {
throw new ExtAuthzParseException(
"Invalid regex pattern for " + fieldName + ": " + e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2025 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.extauthz;

/**
* A custom exception for signaling errors during the parsing of external authorization
* (ext_authz) configurations.
*/
public class ExtAuthzParseException extends Exception {

private static final long serialVersionUID = 0L;

public ExtAuthzParseException(String message) {
super(message);
}

public ExtAuthzParseException(String message, Throwable cause) {
super(message, cause);
}
}
Loading
Loading