Skip to content

Commit 987f1a3

Browse files
committed
feat(xds): Add ExternalAuthorizationFilter
This commit introduces the `ExternalAuthorizationFilter`, an implementation of the `Filter` interface that provides external authorization capabilities. The `ExternalAuthorizationFilter` is responsible for: - Parsing `ExtAuthz` and `ExtAuthzPerRoute` configurations. - Creating `ExtAuthzClientInterceptor` and `ExtAuthzServerInterceptor` to handle client and server-side authorization. - Managing the lifecycle of the authorization stub using a `StubManager`. The `StubManager` is a new class that manages the lifecycle of the `AuthorizationStub`, including creating and caching the gRPC channel and stub based on the provided configuration. This ensures that a single channel and stub are reused for the same configuration, improving performance and resource utilization.
1 parent c146267 commit 987f1a3

File tree

4 files changed

+781
-0
lines changed

4 files changed

+781
-0
lines changed
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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;
18+
19+
import com.google.protobuf.Any;
20+
import com.google.protobuf.InvalidProtocolBufferException;
21+
import com.google.protobuf.Message;
22+
import io.envoyproxy.envoy.extensions.filters.http.ext_authz.v3.ExtAuthz;
23+
import io.envoyproxy.envoy.service.auth.v3.AuthorizationGrpc;
24+
import io.grpc.ClientInterceptor;
25+
import io.grpc.ServerInterceptor;
26+
import io.grpc.xds.internal.ThreadSafeRandom;
27+
import io.grpc.xds.internal.ThreadSafeRandom.ThreadSafeRandomImpl;
28+
import io.grpc.xds.internal.extauthz.BufferingAuthzClientCall;
29+
import io.grpc.xds.internal.extauthz.CheckRequestBuilder;
30+
import io.grpc.xds.internal.extauthz.CheckResponseHandler;
31+
import io.grpc.xds.internal.extauthz.ExtAuthzCertificateProvider;
32+
import io.grpc.xds.internal.extauthz.ExtAuthzClientInterceptor;
33+
import io.grpc.xds.internal.extauthz.ExtAuthzConfig;
34+
import io.grpc.xds.internal.extauthz.ExtAuthzParseException;
35+
import io.grpc.xds.internal.extauthz.ExtAuthzServerInterceptor;
36+
import io.grpc.xds.internal.extauthz.StubManager;
37+
import io.grpc.xds.internal.grpcservice.InsecureGrpcChannelFactory;
38+
import io.grpc.xds.internal.headermutations.HeaderMutationFilter;
39+
import io.grpc.xds.internal.headermutations.HeaderMutator;
40+
import java.util.concurrent.ScheduledExecutorService;
41+
import javax.annotation.Nullable;
42+
43+
final class ExtAuthzFilter implements Filter {
44+
45+
private static final String TYPE_URL =
46+
"type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz";
47+
48+
private static final String TYPE_URL_OVERRIDE_CONFIG =
49+
"type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute";
50+
51+
52+
static final class ExtAuthzFilterConfig implements Filter.FilterConfig {
53+
54+
private final ExtAuthzConfig extAuthzConfig;
55+
56+
ExtAuthzFilterConfig(ExtAuthzConfig extAuthzConfig) {
57+
this.extAuthzConfig = extAuthzConfig;
58+
}
59+
60+
public ExtAuthzConfig extAuthzConfig() {
61+
return extAuthzConfig;
62+
}
63+
64+
@Override
65+
public String typeUrl() {
66+
return ExtAuthzFilter.TYPE_URL;
67+
}
68+
69+
public static ExtAuthzFilterConfig fromProto(ExtAuthz extAuthzProto)
70+
throws ExtAuthzParseException {
71+
return new ExtAuthzFilterConfig(ExtAuthzConfig.fromProto(extAuthzProto));
72+
}
73+
}
74+
75+
// Placeholder for the external authorization filter's override config.
76+
static final class ExtAuthzFilterConfigOverride implements Filter.FilterConfig {
77+
@Override
78+
public final String typeUrl() {
79+
return ExtAuthzFilter.TYPE_URL_OVERRIDE_CONFIG;
80+
}
81+
}
82+
83+
static final class Provider implements Filter.Provider {
84+
85+
@Override
86+
public String[] typeUrls() {
87+
return new String[] {TYPE_URL, TYPE_URL_OVERRIDE_CONFIG};
88+
}
89+
90+
@Override
91+
public boolean isClientFilter() {
92+
return true;
93+
}
94+
95+
@Override
96+
public boolean isServerFilter() {
97+
return true;
98+
}
99+
100+
@Override
101+
public ExtAuthzFilter newInstance(String name) {
102+
// Create a dedicated scheduler for this filter instance's StubManager
103+
StubManager stubManager = StubManager.create(InsecureGrpcChannelFactory.getInstance());
104+
return new ExtAuthzFilter(stubManager, ThreadSafeRandomImpl.INSTANCE,
105+
BufferingAuthzClientCall.FACTORY_INSTANCE, ExtAuthzCertificateProvider.create(),
106+
CheckRequestBuilder.INSTANCE, CheckResponseHandler.INSTANCE,
107+
ExtAuthzClientInterceptor.INSTANCE, ExtAuthzServerInterceptor.INSTANCE,
108+
HeaderMutationFilter.INSTANCE, HeaderMutator.create());
109+
}
110+
111+
@Override
112+
public ConfigOrError<ExtAuthzFilterConfig> parseFilterConfig(Message rawProtoMessage) {
113+
ExtAuthz extAuthzProto;
114+
if (!(rawProtoMessage instanceof Any)) {
115+
return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
116+
}
117+
Any anyMessage = (Any) rawProtoMessage;
118+
try {
119+
extAuthzProto = anyMessage.unpack(ExtAuthz.class);
120+
return ConfigOrError.fromConfig(ExtAuthzFilterConfig.fromProto(extAuthzProto));
121+
} catch (InvalidProtocolBufferException | ExtAuthzParseException e) {
122+
return ConfigOrError.fromError("Invalid proto: " + e);
123+
}
124+
}
125+
126+
@Override
127+
public ConfigOrError<ExtAuthzFilterConfigOverride> parseFilterConfigOverride(
128+
Message rawProtoMessage) {
129+
if (!(rawProtoMessage instanceof Any)) {
130+
return ConfigOrError.fromError("Invalid config type: " + rawProtoMessage.getClass());
131+
}
132+
return ConfigOrError.fromConfig(new ExtAuthzFilterConfigOverride());
133+
}
134+
}
135+
136+
private final StubManager stubManager;
137+
private final ThreadSafeRandom random;
138+
private final BufferingAuthzClientCall.Factory bufferingAuthzClientCallFactory;
139+
private final ExtAuthzCertificateProvider certificateProvider;
140+
private final CheckRequestBuilder.Factory checkRequestBuilderFactory;
141+
private final CheckResponseHandler.Factory checkResponseHandlerFactory;
142+
private final ExtAuthzClientInterceptor.Factory extAuthzClientInterceptorFactory;
143+
private final ExtAuthzServerInterceptor.Factory extAuthzServerInterceptorFactory;
144+
private final HeaderMutationFilter.Factory headerMutationFilterFactory;
145+
private final HeaderMutator headerMutator;
146+
147+
148+
ExtAuthzFilter(StubManager stubManager, ThreadSafeRandom random,
149+
BufferingAuthzClientCall.Factory bufferingAuthzClientCallFactory,
150+
ExtAuthzCertificateProvider certificateProvider,
151+
CheckRequestBuilder.Factory checkRequestBuilderFactory,
152+
CheckResponseHandler.Factory checkResponseHandlerFactory,
153+
ExtAuthzClientInterceptor.Factory extAuthzClientInterceptorFactory,
154+
ExtAuthzServerInterceptor.Factory extAuthzServerInterceptorFactory,
155+
HeaderMutationFilter.Factory headerMutationFilterFactory, HeaderMutator headerMutator) {
156+
this.stubManager = stubManager;
157+
this.random = random;
158+
this.bufferingAuthzClientCallFactory = bufferingAuthzClientCallFactory;
159+
this.certificateProvider = certificateProvider;
160+
this.checkRequestBuilderFactory = checkRequestBuilderFactory;
161+
this.checkResponseHandlerFactory = checkResponseHandlerFactory;
162+
this.extAuthzClientInterceptorFactory = extAuthzClientInterceptorFactory;
163+
this.extAuthzServerInterceptorFactory = extAuthzServerInterceptorFactory;
164+
this.headerMutationFilterFactory = headerMutationFilterFactory;
165+
this.headerMutator = headerMutator;
166+
}
167+
168+
@Nullable
169+
@Override
170+
public ClientInterceptor buildClientInterceptor(FilterConfig config,
171+
@Nullable FilterConfig overrideConfig, ScheduledExecutorService scheduler) {
172+
if (overrideConfig != null) {
173+
return null;
174+
}
175+
if (!(config instanceof ExtAuthzFilterConfig)) {
176+
return null;
177+
}
178+
ExtAuthzFilterConfig extAuthzFilterConfig = (ExtAuthzFilterConfig) config;
179+
AuthorizationGrpc.AuthorizationStub stub =
180+
stubManager.getStub(extAuthzFilterConfig.extAuthzConfig());
181+
ExtAuthzConfig extAuthzConfig = extAuthzFilterConfig.extAuthzConfig();
182+
return extAuthzClientInterceptorFactory.create(extAuthzConfig, stub,
183+
random, bufferingAuthzClientCallFactory,
184+
checkRequestBuilderFactory.create(extAuthzConfig, certificateProvider),
185+
checkResponseHandlerFactory.create(headerMutator,
186+
headerMutationFilterFactory.create(extAuthzConfig.decoderHeaderMutationRules()),
187+
extAuthzConfig),
188+
headerMutator);
189+
}
190+
191+
@Nullable
192+
@Override
193+
public ServerInterceptor buildServerInterceptor(FilterConfig config,
194+
@Nullable FilterConfig overrideConfig) {
195+
if (overrideConfig != null) {
196+
return null;
197+
}
198+
if (!(config instanceof ExtAuthzFilterConfig)) {
199+
return null;
200+
}
201+
ExtAuthzFilterConfig extAuthzFilterConfig = (ExtAuthzFilterConfig) config;
202+
AuthorizationGrpc.AuthorizationStub stub =
203+
stubManager.getStub(extAuthzFilterConfig.extAuthzConfig());
204+
ExtAuthzConfig extAuthzConfig = extAuthzFilterConfig.extAuthzConfig();
205+
return extAuthzServerInterceptorFactory.create(extAuthzConfig, stub, random,
206+
checkRequestBuilderFactory.create(extAuthzConfig, certificateProvider),
207+
checkResponseHandlerFactory.create(headerMutator,
208+
headerMutationFilterFactory.create(extAuthzConfig.decoderHeaderMutationRules()),
209+
extAuthzConfig),
210+
headerMutator);
211+
}
212+
213+
@Override
214+
public void close() {
215+
stubManager.close();
216+
}
217+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 io.envoyproxy.envoy.service.auth.v3.AuthorizationGrpc;
21+
import io.grpc.ManagedChannel;
22+
import io.grpc.xds.internal.grpcservice.GrpcServiceConfig.GoogleGrpcConfig;
23+
import io.grpc.xds.internal.grpcservice.GrpcServiceConfigChannelFactory;
24+
import java.util.Optional;
25+
import javax.annotation.concurrent.GuardedBy;
26+
27+
/**
28+
* Manages the lifecycle of the authorization stub.
29+
*/
30+
public interface StubManager {
31+
/** Creates a new instance of {@code StubManager}. */
32+
static StubManager create(GrpcServiceConfigChannelFactory channelFactory) {
33+
return new StubManagerImpl(channelFactory);
34+
}
35+
36+
/**
37+
* Returns a stub for the given configuration.
38+
*/
39+
AuthorizationGrpc.AuthorizationStub getStub(ExtAuthzConfig config);
40+
41+
/**
42+
* Frees underlying resources on shutdown.
43+
*/
44+
public void close();
45+
46+
/**
47+
* Default implementation of {@link StubManager}.
48+
*/
49+
final class StubManagerImpl implements StubManager {
50+
51+
private final GrpcServiceConfigChannelFactory channelFactory;
52+
private final Object lock = new Object();
53+
54+
@GuardedBy("lock")
55+
private Optional<StubHolder> stubHolder = Optional.empty();
56+
57+
private StubManagerImpl(GrpcServiceConfigChannelFactory channelFactory) { // NOPMD
58+
this.channelFactory = channelFactory;
59+
}
60+
61+
@Override
62+
public AuthorizationGrpc.AuthorizationStub getStub(ExtAuthzConfig config) {
63+
GoogleGrpcConfig googleGrpc = config.grpcService().googleGrpc();
64+
ChannelKey newChannelKey =
65+
ChannelKey.of(googleGrpc.target(), googleGrpc.hashedChannelCredentials().hash());
66+
67+
synchronized (lock) {
68+
if (stubHolder.isPresent() && stubHolder.get().channelKey().equals(newChannelKey)) {
69+
return stubHolder.get().stub();
70+
}
71+
Optional<ManagedChannel> oldChannel = stubHolder.map(StubHolder::channel);
72+
ManagedChannel newChannel = channelFactory.createChannel(config.grpcService());
73+
stubHolder = Optional.of(
74+
StubHolder.create(newChannelKey, newChannel, AuthorizationGrpc.newStub(newChannel)));
75+
oldChannel.ifPresent(ManagedChannel::shutdown);
76+
return stubHolder.get().stub();
77+
}
78+
}
79+
80+
@AutoValue
81+
abstract static class ChannelKey {
82+
static ChannelKey of(String target, int hash) {
83+
return new AutoValue_StubManager_StubManagerImpl_ChannelKey(target, hash);
84+
}
85+
86+
abstract String target();
87+
88+
abstract int hash();
89+
}
90+
91+
@AutoValue
92+
abstract static class StubHolder {
93+
static StubHolder create(ChannelKey channelKey, ManagedChannel channel,
94+
AuthorizationGrpc.AuthorizationStub stub) {
95+
return new AutoValue_StubManager_StubManagerImpl_StubHolder(channelKey, channel, stub);
96+
}
97+
98+
abstract ChannelKey channelKey();
99+
100+
abstract ManagedChannel channel();
101+
102+
abstract AuthorizationGrpc.AuthorizationStub stub();
103+
}
104+
105+
@Override
106+
public void close() {
107+
synchronized (lock) {
108+
stubHolder.ifPresent(holder -> {
109+
holder.channel().shutdown();
110+
});
111+
}
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)