Skip to content

Commit 55be36c

Browse files
committed
feat(xds): Implement request builder for external authorization
This commit introduces the `CheckRequestBuilder` library, which is responsible for constructing the `CheckRequest` message sent to the external authorization service. The `CheckRequestBuilder` gathers information from various sources, including: - `ServerCall` attributes (local and remote addresses, SSL session). - `MethodDescriptor` (full method name). - Request headers. It uses this information to populate the `AttributeContext` of the `CheckRequest` message, which provides the authorization service with the necessary context to make an authorization decision. This commit also introduces the `ExtAuthzCertificateProvider`, a helper class for extracting certificate information, such as the principal and PEM-encoded certificate. Unit tests for the new components are also included.
1 parent 8ffe2d8 commit 55be36c

File tree

4 files changed

+938
-0
lines changed

4 files changed

+938
-0
lines changed
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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.io.BaseEncoding;
21+
import com.google.protobuf.Timestamp;
22+
import io.envoyproxy.envoy.config.core.v3.Address;
23+
import io.envoyproxy.envoy.config.core.v3.SocketAddress;
24+
import io.envoyproxy.envoy.service.auth.v3.AttributeContext;
25+
import io.envoyproxy.envoy.service.auth.v3.CheckRequest;
26+
import io.grpc.Grpc;
27+
import io.grpc.Metadata;
28+
import io.grpc.MethodDescriptor;
29+
import io.grpc.ServerCall;
30+
import io.grpc.xds.internal.Matchers;
31+
import java.io.UnsupportedEncodingException;
32+
import java.net.InetSocketAddress;
33+
import java.security.cert.Certificate;
34+
import java.security.cert.CertificateEncodingException;
35+
import java.security.cert.X509Certificate;
36+
import java.util.ArrayList;
37+
import java.util.List;
38+
import java.util.Locale;
39+
import java.util.Optional;
40+
import java.util.logging.Level;
41+
import java.util.logging.Logger;
42+
import javax.net.ssl.SSLPeerUnverifiedException;
43+
import javax.net.ssl.SSLSession;
44+
45+
/**
46+
* Interface for building external authorization check requests.
47+
*/
48+
public interface CheckRequestBuilder {
49+
50+
/**
51+
* A factory for creating {@link CheckRequestBuilder} instances.
52+
*/
53+
@FunctionalInterface
54+
interface Factory {
55+
/**
56+
* Creates a new instance of the CheckRequestBuilder.
57+
*
58+
* @param config The external authorization configuration.
59+
* @param certificateProvider The provider for certificate information.
60+
* @return A new CheckRequestBuilder instance.
61+
*/
62+
CheckRequestBuilder create(ExtAuthzConfig config,
63+
ExtAuthzCertificateProvider certificateProvider);
64+
}
65+
66+
/** The default factory for creating {@link CheckRequestBuilder} instances. */
67+
Factory INSTANCE = CheckRequestBuilderImpl::new;
68+
69+
/**
70+
* Builds a CheckRequest for a server-side call.
71+
*
72+
* @param serverCall The server call.
73+
* @param headers The request headers.
74+
* @param requestTime The time of the request.
75+
* @return A new CheckRequest.
76+
*/
77+
CheckRequest buildRequest(ServerCall<?, ?> serverCall, Metadata headers, Timestamp requestTime);
78+
79+
/**
80+
* Builds a CheckRequest for a client-side call.
81+
*
82+
* @param methodDescriptor The method descriptor of the call.
83+
* @param headers The request headers.
84+
* @param requestTime The time of the request.
85+
* @return A new CheckRequest.
86+
*/
87+
CheckRequest buildRequest(MethodDescriptor<?, ?> methodDescriptor, Metadata headers,
88+
Timestamp requestTime);
89+
90+
/**
91+
* Implementation of the CheckRequestBuilder interface.
92+
*/
93+
final class CheckRequestBuilderImpl implements CheckRequestBuilder {
94+
private static final Logger logger = Logger.getLogger(CheckRequestBuilderImpl.class.getName());
95+
96+
private static final String METHOD = "POST";
97+
private static final String PROTOCOL = "HTTP/2";
98+
private static final long SIZE = -1;
99+
100+
private final ExtAuthzConfig config;
101+
private final ExtAuthzCertificateProvider certificateProvider;
102+
103+
CheckRequestBuilderImpl(ExtAuthzConfig config,
104+
ExtAuthzCertificateProvider certificateProvider) {
105+
this.config = config;
106+
this.certificateProvider = certificateProvider;
107+
}
108+
109+
@Override
110+
public CheckRequest buildRequest(MethodDescriptor<?, ?> methodDescriptor, Metadata headers,
111+
Timestamp requestTime) {
112+
return build(CheckRequestParams.builder().setMethodDescriptor(methodDescriptor)
113+
.setHeaders(headers).setRequestTime(requestTime).build());
114+
}
115+
116+
@Override
117+
public CheckRequest buildRequest(ServerCall<?, ?> serverCall, Metadata headers,
118+
Timestamp requestTime) {
119+
CheckRequestParams.Builder paramsBuilder =
120+
CheckRequestParams.builder().setMethodDescriptor(serverCall.getMethodDescriptor())
121+
.setHeaders(headers).setRequestTime(requestTime);
122+
java.net.SocketAddress localAddress =
123+
serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR);
124+
if (localAddress != null) {
125+
paramsBuilder.setLocalAddress(localAddress);
126+
}
127+
java.net.SocketAddress remoteAddress =
128+
serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
129+
if (remoteAddress != null) {
130+
paramsBuilder.setRemoteAddress(remoteAddress);
131+
}
132+
SSLSession sslSession = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION);
133+
if (sslSession != null) {
134+
paramsBuilder.setSslSession(sslSession);
135+
}
136+
return build(paramsBuilder.build());
137+
}
138+
139+
private CheckRequest build(CheckRequestParams params) {
140+
AttributeContext.Builder attrBuilder = AttributeContext.newBuilder();
141+
if (params.remoteAddress().isPresent()) {
142+
attrBuilder.setSource(buildSource(params.remoteAddress().get(), params.sslSession()));
143+
}
144+
if (params.localAddress().isPresent()) {
145+
attrBuilder
146+
.setDestination(buildDestination(params.localAddress().get(), params.sslSession()));
147+
}
148+
attrBuilder.setRequest(buildAttributeRequest(params.headers(),
149+
params.methodDescriptor().getFullMethodName(), params.requestTime()));
150+
return CheckRequest.newBuilder().setAttributes(attrBuilder).build();
151+
}
152+
153+
private AttributeContext.Peer buildSource(java.net.SocketAddress socketAddress,
154+
Optional<SSLSession> sslSession) {
155+
AttributeContext.Peer.Builder peerBuilder = buildPeer(socketAddress).toBuilder();
156+
if (sslSession.isPresent()) {
157+
try {
158+
Certificate[] certs = sslSession.get().getPeerCertificates();
159+
if (certs != null && certs.length > 0 && certs[0] instanceof X509Certificate) {
160+
X509Certificate cert = (X509Certificate) certs[0];
161+
peerBuilder.setPrincipal(certificateProvider.getPrincipal(cert));
162+
if (config.includePeerCertificate()) {
163+
try {
164+
peerBuilder.setCertificate(certificateProvider.getUrlPemEncodedCertificate(cert));
165+
} catch (UnsupportedEncodingException | CertificateEncodingException e) {
166+
logger.log(Level.WARNING,
167+
"Error encoding peer certificate. "
168+
+ "This is not expected, but if it happens, the certificate should not "
169+
+ "be set according to the spec.",
170+
e);
171+
}
172+
}
173+
}
174+
} catch (SSLPeerUnverifiedException e) {
175+
logger.log(Level.FINE,
176+
"Peer is not authenticated. "
177+
+ "This is expected, principal and certificate should not be set "
178+
+ "according to the spec.",
179+
e);
180+
}
181+
}
182+
return peerBuilder.build();
183+
}
184+
185+
private AttributeContext.Peer buildDestination(java.net.SocketAddress socketAddress,
186+
Optional<SSLSession> sslSession) {
187+
AttributeContext.Peer.Builder peerBuilder = buildPeer(socketAddress).toBuilder();
188+
if (sslSession.isPresent()) {
189+
Certificate[] certs = sslSession.get().getLocalCertificates();
190+
if (certs != null && certs.length > 0 && certs[0] instanceof X509Certificate) {
191+
peerBuilder.setPrincipal(certificateProvider.getPrincipal((X509Certificate) certs[0]));
192+
}
193+
}
194+
return peerBuilder.build();
195+
}
196+
197+
private AttributeContext.Peer buildPeer(java.net.SocketAddress socketAddress) {
198+
AttributeContext.Peer.Builder peerBuilder = AttributeContext.Peer.newBuilder();
199+
if (socketAddress instanceof InetSocketAddress) {
200+
InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress;
201+
peerBuilder.setAddress(Address.newBuilder()
202+
.setSocketAddress(SocketAddress.newBuilder()
203+
.setAddress(inetSocketAddress.getAddress().getHostAddress())
204+
.setPortValue(inetSocketAddress.getPort()))
205+
.build());
206+
}
207+
return peerBuilder.build();
208+
}
209+
210+
private AttributeContext.Request buildAttributeRequest(Metadata headers, String fullMethodName,
211+
Timestamp requestTime) {
212+
AttributeContext.Request.Builder reqBuilder = AttributeContext.Request.newBuilder();
213+
reqBuilder.setTime(requestTime);
214+
AttributeContext.HttpRequest.Builder httpReqBuilder =
215+
AttributeContext.HttpRequest.newBuilder();
216+
httpReqBuilder.setPath(fullMethodName);
217+
httpReqBuilder.setMethod(METHOD);
218+
httpReqBuilder.setProtocol(PROTOCOL);
219+
httpReqBuilder.setSize(SIZE);
220+
for (String key : headers.keys()) {
221+
if (!isAllowed(key)) {
222+
continue;
223+
}
224+
Optional<String> value;
225+
if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
226+
value = getBinaryHeaderValue(headers, key);
227+
} else {
228+
value = getAsciiHeaderValue(headers, key);
229+
}
230+
value.ifPresent(
231+
headerValue -> httpReqBuilder.putHeaders(key.toLowerCase(Locale.ROOT), headerValue));
232+
}
233+
reqBuilder.setHttp(httpReqBuilder);
234+
return reqBuilder.build();
235+
}
236+
237+
private Optional<String> getBinaryHeaderValue(Metadata headers, String key) {
238+
Iterable<byte[]> binaryValues =
239+
headers.getAll(Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER));
240+
if (binaryValues == null) {
241+
// Unreachable code, since we iterate over the keys. Exists for defensive programming.
242+
return Optional.empty();
243+
}
244+
List<String> base64Values = new ArrayList<>();
245+
for (byte[] value : binaryValues) {
246+
base64Values.add(BaseEncoding.base64().encode(value));
247+
}
248+
return Optional.of(String.join(",", base64Values));
249+
}
250+
251+
private Optional<String> getAsciiHeaderValue(Metadata headers, String key) {
252+
Iterable<String> stringValues =
253+
headers.getAll(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER));
254+
if (stringValues == null) {
255+
// Unreachable code, since we iterate over the keys. Exists for defensive programming.
256+
return Optional.empty();
257+
}
258+
return Optional.of(String.join(",", stringValues));
259+
}
260+
261+
private boolean isAllowed(String header) {
262+
for (Matchers.StringMatcher matcher : config.disallowedHeaders()) {
263+
if (matcher.matches(header)) {
264+
return false;
265+
}
266+
}
267+
if (config.allowedHeaders().isEmpty()) {
268+
return true;
269+
}
270+
for (Matchers.StringMatcher matcher : config.allowedHeaders()) {
271+
if (matcher.matches(header)) {
272+
return true;
273+
}
274+
}
275+
return false;
276+
}
277+
278+
@AutoValue
279+
abstract static class CheckRequestParams {
280+
abstract Metadata headers();
281+
282+
abstract MethodDescriptor<?, ?> methodDescriptor();
283+
284+
abstract Timestamp requestTime();
285+
286+
abstract Optional<java.net.SocketAddress> localAddress();
287+
288+
abstract Optional<java.net.SocketAddress> remoteAddress();
289+
290+
abstract Optional<SSLSession> sslSession();
291+
292+
static Builder builder() {
293+
Builder builder =
294+
new AutoValue_CheckRequestBuilder_CheckRequestBuilderImpl_CheckRequestParams.Builder();
295+
return builder;
296+
}
297+
298+
@AutoValue.Builder
299+
abstract static class Builder {
300+
abstract Builder setHeaders(Metadata headers);
301+
302+
abstract Builder setMethodDescriptor(MethodDescriptor<?, ?> method);
303+
304+
abstract Builder setRequestTime(Timestamp time);
305+
306+
abstract Builder setLocalAddress(java.net.SocketAddress localAddress);
307+
308+
abstract Builder setRemoteAddress(java.net.SocketAddress remoteAddress);
309+
310+
abstract Builder setSslSession(SSLSession sslSession);
311+
312+
abstract CheckRequestParams build();
313+
}
314+
}
315+
}
316+
}

0 commit comments

Comments
 (0)