Skip to content

Commit d67c384

Browse files
committed
feat(xds): Add ExtAuthzServerInterceptor and tests
This commit introduces the `ExtAuthzServerInterceptor`, a server interceptor that performs external authorization for incoming RPCs. The interceptor checks if the external authorization filter is enabled. If it is, it calls the external authorization service and handles the response. It supports both unary and streaming RPCs. The interceptor handles the following scenarios: - Allow responses: The RPC is allowed to proceed. - Deny responses: The RPC is denied with a `PERMISSION_DENIED` status. - Authorization service errors: The RPC is either denied or allowed to proceed based on the `failure_mode_allow` configuration. This commit also includes comprehensive integration tests for the `ExtAuthzServerInterceptor`, covering various scenarios and configurations.
1 parent 6d534b5 commit d67c384

File tree

2 files changed

+819
-0
lines changed

2 files changed

+819
-0
lines changed
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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.protobuf.util.Timestamps;
20+
import io.envoyproxy.envoy.service.auth.v3.AuthorizationGrpc;
21+
import io.envoyproxy.envoy.service.auth.v3.CheckRequest;
22+
import io.envoyproxy.envoy.service.auth.v3.CheckResponse;
23+
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
24+
import io.grpc.ForwardingServerCallListener;
25+
import io.grpc.Metadata;
26+
import io.grpc.ServerCall;
27+
import io.grpc.ServerCall.Listener;
28+
import io.grpc.ServerCallHandler;
29+
import io.grpc.ServerInterceptor;
30+
import io.grpc.stub.StreamObserver;
31+
import io.grpc.xds.internal.Matchers.FractionMatcher;
32+
import io.grpc.xds.internal.ThreadSafeRandom;
33+
import io.grpc.xds.internal.headermutations.HeaderMutations.ResponseHeaderMutations;
34+
import io.grpc.xds.internal.headermutations.HeaderMutator;
35+
import java.util.concurrent.atomic.AtomicReference;
36+
37+
/**
38+
* A server interceptor that performs external authorization for incoming RPCs.
39+
*/
40+
public final class ExtAuthzServerInterceptor implements ServerInterceptor {
41+
42+
/**
43+
* A factory for creating {@link ExtAuthzServerInterceptor} instances.
44+
*/
45+
@FunctionalInterface
46+
public interface Factory {
47+
/**
48+
* Creates a new {@link ExtAuthzServerInterceptor}.
49+
*
50+
* @param config the external authorization configuration.
51+
* @param authzStub the gRPC stub for the authorization service.
52+
* @param random the random number generator for filter matching.
53+
* @param checkRequestBuilder the builder for creating authorization check requests.
54+
* @param responseHandler the handler for processing authorization responses.
55+
* @param headerMutator the mutator for applying header mutations.
56+
* @return a new {@link ServerInterceptor}.
57+
*/
58+
ServerInterceptor create(ExtAuthzConfig config, AuthorizationGrpc.AuthorizationStub authzStub,
59+
ThreadSafeRandom random, CheckRequestBuilder checkRequestBuilder,
60+
CheckResponseHandler responseHandler, HeaderMutator headerMutator);
61+
}
62+
63+
/**
64+
* A factory for creating {@link ExtAuthzServerInterceptor} instances. This is the only supported
65+
* way to create a new ExtAuthzServerInterceptor.
66+
*/
67+
public static final Factory INSTANCE = ExtAuthzServerInterceptor::new;
68+
69+
private final ExtAuthzConfig config;
70+
private final AuthorizationGrpc.AuthorizationStub authzStub;
71+
private final ThreadSafeRandom random;
72+
private final CheckRequestBuilder checkRequestBuilder;
73+
private final CheckResponseHandler responseHandler;
74+
private final HeaderMutator headerMutator;
75+
76+
private ExtAuthzServerInterceptor(ExtAuthzConfig config,
77+
AuthorizationGrpc.AuthorizationStub authzStub, ThreadSafeRandom random,
78+
CheckRequestBuilder checkRequestBuilder,
79+
CheckResponseHandler responseHandler, HeaderMutator headerMutator) {
80+
this.config = config;
81+
this.random = random;
82+
this.authzStub = authzStub;
83+
this.checkRequestBuilder = checkRequestBuilder;
84+
this.responseHandler = responseHandler;
85+
this.headerMutator = headerMutator;
86+
}
87+
88+
/**
89+
* Intercepts an incoming call to perform external authorization.
90+
*
91+
* @param call the server call to intercept.
92+
* @param headers the headers of the incoming call.
93+
* @param next the next handler in the chain.
94+
* @return a listener for the server call.
95+
*/
96+
@Override
97+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
98+
final Metadata headers, ServerCallHandler<ReqT, RespT> next) {
99+
FractionMatcher filterEnabled = config.filterEnabled();
100+
if (random.nextInt(filterEnabled.denominator()) < filterEnabled.numerator()) {
101+
if (config.denyAtDisable()) {
102+
call.close(config.statusOnError(), new Metadata());
103+
return new ServerCall.Listener<ReqT>() {};
104+
}
105+
return next.startCall(call, headers);
106+
}
107+
ExtAuthzForwardingListener<ReqT, RespT> listener = new ExtAuthzForwardingListener<>(config,
108+
authzStub, headers, call, next, checkRequestBuilder, responseHandler, headerMutator);
109+
listener.startAuthzCall();
110+
return listener;
111+
}
112+
113+
/**
114+
* A forwarding server call listener that handles the external authorization process.
115+
*/
116+
private static final class ExtAuthzForwardingListener<ReqT, RespT>
117+
extends ForwardingServerCallListener<ReqT> {
118+
private static final String X_ENVOY_AUTH_FAILURE_MODE_ALLOWED =
119+
"x-envoy-auth-failure-mode-allowed";
120+
121+
private final ExtAuthzConfig config;
122+
private final AuthorizationGrpc.AuthorizationStub authzStub;
123+
private final Metadata headers;
124+
private final ServerCall<ReqT, RespT> realServerCall;
125+
private final ServerCallHandler<ReqT, RespT> serverCallHandler;
126+
private final CheckRequestBuilder checkRequestBuilder;
127+
private final CheckResponseHandler responseHandler;
128+
private final HeaderMutator headerMutator;
129+
private final AtomicReference<ServerCall.Listener<ReqT>> delegateListener;
130+
131+
/**
132+
* Constructs a new {@link ExtAuthzForwardingListener}.
133+
*/
134+
ExtAuthzForwardingListener(ExtAuthzConfig config,
135+
AuthorizationGrpc.AuthorizationStub authzStub, Metadata headers,
136+
ServerCall<ReqT, RespT> serverCall, ServerCallHandler<ReqT, RespT> serverCallHandler,
137+
CheckRequestBuilder checkRequestBuilder, CheckResponseHandler responseHandler,
138+
HeaderMutator headerMutator) {
139+
this.config = config;
140+
this.authzStub = authzStub;
141+
this.headers = headers;
142+
this.realServerCall = serverCall;
143+
this.serverCallHandler = serverCallHandler;
144+
this.checkRequestBuilder = checkRequestBuilder;
145+
this.responseHandler = responseHandler;
146+
this.headerMutator = headerMutator;
147+
this.delegateListener =
148+
new AtomicReference<ServerCall.Listener<ReqT>>(new ServerCall.Listener<ReqT>() {});
149+
}
150+
151+
/**
152+
* Starts the external authorization call.
153+
*/
154+
void startAuthzCall() {
155+
CheckRequest checkRequest = checkRequestBuilder.buildRequest(realServerCall, headers,
156+
Timestamps.fromMillis(System.currentTimeMillis()));
157+
StreamObserver<CheckResponse> observer = new StreamObserver<CheckResponse>() {
158+
@Override
159+
public void onNext(CheckResponse value) {
160+
// The handleResponse method may add or modify headers based on the authorization
161+
// response.
162+
AuthzResponse authzResponse = responseHandler.handleResponse(value, headers);
163+
if (authzResponse.decision() == AuthzResponse.Decision.ALLOW) {
164+
AuthzServerCall<ReqT, RespT> authzServerCall = new AuthzServerCall<>(realServerCall,
165+
authzResponse.responseHeaderMutations(), headerMutator);
166+
delegateListener
167+
.set(serverCallHandler.startCall(authzServerCall, authzResponse.headers().get()));
168+
} else {
169+
// A deny response is guaranteed to have a status set, so the `get` without
170+
// check is safe.
171+
realServerCall.close(authzResponse.status().get(), new Metadata());
172+
}
173+
}
174+
175+
@Override
176+
public void onError(Throwable t) {
177+
if (config.failureModeAllow()) {
178+
if (config.failureModeAllowHeaderAdd()) {
179+
Metadata.Key<String> key = Metadata.Key.of(X_ENVOY_AUTH_FAILURE_MODE_ALLOWED,
180+
Metadata.ASCII_STRING_MARSHALLER);
181+
headers.put(key, "true");
182+
}
183+
delegateListener.set(serverCallHandler.startCall(realServerCall, headers));
184+
} else {
185+
realServerCall.close(config.statusOnError().withCause(t), new Metadata());
186+
}
187+
}
188+
189+
@Override
190+
public void onCompleted() {
191+
// No-op. The authorization service uses a unary RPC, so we only expect one response.
192+
}
193+
};
194+
authzStub.check(checkRequest, observer);
195+
}
196+
197+
@Override
198+
protected Listener<ReqT> delegate() {
199+
return delegateListener.get();
200+
}
201+
}
202+
203+
/**
204+
* A server call that applies response header mutations from the authorization service.
205+
*/
206+
private static class AuthzServerCall<ReqT, RespT>
207+
extends SimpleForwardingServerCall<ReqT, RespT> {
208+
private final ResponseHeaderMutations responseHeaderMutations;
209+
private final HeaderMutator headerMutator;
210+
211+
/**
212+
* Constructs a new {@link AuthzServerCall}.
213+
*
214+
* @param delegate the original server call.
215+
* @param responseHeaderMutations the response header mutations to apply.
216+
* @param headerMutator the mutator for applying header mutations.
217+
*/
218+
private AuthzServerCall(ServerCall<ReqT, RespT> delegate,
219+
ResponseHeaderMutations responseHeaderMutations, HeaderMutator headerMutator) {
220+
super(delegate);
221+
this.responseHeaderMutations = responseHeaderMutations;
222+
this.headerMutator = headerMutator;
223+
}
224+
225+
/**
226+
* Sends the headers after applying any mutations from the authorization service.
227+
*
228+
* @param headers the headers to send.
229+
*/
230+
@Override
231+
public void sendHeaders(Metadata headers) {
232+
headerMutator.applyResponseMutations(responseHeaderMutations, headers);
233+
super.sendHeaders(headers);
234+
}
235+
}
236+
}

0 commit comments

Comments
 (0)