Skip to content

Commit 5b64820

Browse files
committed
feat(xds): Implement response handling for external authorization
This commit introduces the `CheckResponseHandler` and `AuthzResponse` classes, which are responsible for processing responses from the external authorization service. The `CheckResponseHandler` parses the `CheckResponse` protobuf, determines whether the request should be allowed or denied, and applies any header mutations specified in the response. It handles both `OkHttpResponse` and `DeniedHttpResponse` messages. The `AuthzResponse` class is a value object that represents the outcome of the authorization check, encapsulating the decision (allow or deny), the status to be returned to the client (for deny decisions), and any header mutations. This commit also includes unit tests for the new components.
1 parent 328631a commit 5b64820

File tree

4 files changed

+496
-0
lines changed

4 files changed

+496
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.grpc.Metadata;
22+
import io.grpc.Status;
23+
import io.grpc.xds.internal.headermutations.HeaderMutations.ResponseHeaderMutations;
24+
import java.util.Optional;
25+
26+
/**
27+
* Represents the outcome of an authorization check, detailing whether the request is allowed or
28+
* denied and including any associated headers or status information.
29+
*/
30+
@AutoValue
31+
public abstract class AuthzResponse {
32+
33+
/** Defines the authorization decision. */
34+
public enum Decision {
35+
/** The request is permitted. */
36+
ALLOW,
37+
/** The request is rejected. */
38+
DENY,
39+
}
40+
41+
/** Creates a builder for an ALLOW response, initializing with the specified headers. */
42+
public static Builder allow(Metadata headers) {
43+
return new AutoValue_AuthzResponse.Builder().setDecision(Decision.ALLOW)
44+
.setResponseHeaderMutations(ResponseHeaderMutations.create(ImmutableList.of()))
45+
.setHeaders(headers);
46+
}
47+
48+
/** Creates a builder for a DENY response, initializing with the specified status. */
49+
public static Builder deny(Status status) {
50+
return new AutoValue_AuthzResponse.Builder().setDecision(Decision.DENY)
51+
.setResponseHeaderMutations(ResponseHeaderMutations.create(ImmutableList.of()))
52+
.setStatus(status);
53+
}
54+
55+
/** Returns the authorization decision. */
56+
public abstract Decision decision();
57+
58+
/**
59+
* For DENY decisions, this provides the status to be returned to the calling client. It is empty
60+
* for ALLOW decisions.
61+
*/
62+
public abstract Optional<Status> status();
63+
64+
/**
65+
* For ALLOW decisions, this provides the headers to be appended to the request headers for
66+
* upstream. It is empty for DENY decisions.
67+
*/
68+
public abstract Optional<Metadata> headers();
69+
70+
/**
71+
* Returns mutations to be applied to the response headers. This is used for both ALLOW and DENY
72+
* decisions.
73+
*/
74+
public abstract ResponseHeaderMutations responseHeaderMutations();
75+
76+
/** Builder for creating {@link AuthzResponse} instances. */
77+
@AutoValue.Builder
78+
public abstract static class Builder {
79+
80+
abstract Builder setDecision(Decision decision);
81+
82+
abstract Builder setStatus(Status status);
83+
84+
abstract Builder setHeaders(Metadata headers);
85+
86+
public abstract Builder setResponseHeaderMutations(
87+
ResponseHeaderMutations responseHeaderMutations);
88+
89+
public abstract AuthzResponse build();
90+
}
91+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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.common.collect.ImmutableList;
20+
import io.envoyproxy.envoy.service.auth.v3.CheckResponse;
21+
import io.envoyproxy.envoy.service.auth.v3.DeniedHttpResponse;
22+
import io.envoyproxy.envoy.service.auth.v3.OkHttpResponse;
23+
import io.grpc.Metadata;
24+
import io.grpc.Status;
25+
import io.grpc.internal.GrpcUtil;
26+
import io.grpc.xds.internal.headermutations.HeaderMutationDisallowedException;
27+
import io.grpc.xds.internal.headermutations.HeaderMutationFilter;
28+
import io.grpc.xds.internal.headermutations.HeaderMutations;
29+
import io.grpc.xds.internal.headermutations.HeaderMutator;
30+
31+
/**
32+
* Handles the response from the external authorization service, processing it to determine the
33+
* authorization decision and applying any necessary header mutations.
34+
*/
35+
public interface CheckResponseHandler {
36+
37+
/**
38+
* A factory for creating {@link CheckResponseHandler} instances.
39+
*/
40+
@FunctionalInterface
41+
interface Factory {
42+
/**
43+
* Creates a new ResponseHandler.
44+
*
45+
* @param headerMutator Utility to apply header mutations.
46+
* @param headerMutationFilter Filter to apply to header mutations.
47+
* @param config The external authorization configuration.
48+
*/
49+
CheckResponseHandler create(HeaderMutator headerMutator,
50+
HeaderMutationFilter headerMutationFilter, ExtAuthzConfig config);
51+
}
52+
53+
/**
54+
* The default factory for creating {@link CheckResponseHandler} instances.
55+
*/
56+
Factory INSTANCE = ResponseHandlerImpl::new;
57+
58+
/**
59+
* Processes the CheckResponse from the external authorization service.
60+
*
61+
* @param response The response from the authorization service.
62+
* @param headers The request headers, which may be mutated as part of handling the response.
63+
* @return An {@link AuthzResponse} indicating the outcome of the authorization check.
64+
*/
65+
AuthzResponse handleResponse(final CheckResponse response, Metadata headers);
66+
67+
/** Default implementation of {@link CheckResponseHandler}. */
68+
static final class ResponseHandlerImpl implements CheckResponseHandler {
69+
private final HeaderMutator headerMutator;
70+
private final HeaderMutationFilter headerMutationFilter;
71+
private final ExtAuthzConfig config;
72+
73+
ResponseHandlerImpl(HeaderMutator headerMutator, // NOPMD
74+
HeaderMutationFilter headerMutationFilter, ExtAuthzConfig config) {
75+
this.headerMutator = headerMutator;
76+
this.headerMutationFilter = headerMutationFilter;
77+
this.config = config;
78+
}
79+
80+
@Override
81+
public AuthzResponse handleResponse(final CheckResponse response, Metadata headers) {
82+
try {
83+
if (response.getStatus().getCode() == Status.Code.OK.value()) {
84+
return handleOkResponse(response, headers);
85+
} else {
86+
return handleNotOkResponse(response);
87+
}
88+
} catch (HeaderMutationDisallowedException e) {
89+
return AuthzResponse.deny(e.getStatus()).build();
90+
}
91+
}
92+
93+
private AuthzResponse handleOkResponse(final CheckResponse response, Metadata headers)
94+
throws HeaderMutationDisallowedException {
95+
if (!response.hasOkResponse()) {
96+
return AuthzResponse.allow(headers).build();
97+
}
98+
OkHttpResponse okResponse = response.getOkResponse();
99+
HeaderMutations requestedMutations = buildHeaderMutationsFromOkResponse(okResponse);
100+
HeaderMutations allowedMutations = headerMutationFilter.filter(requestedMutations);
101+
102+
applyMutations(allowedMutations, headers);
103+
return AuthzResponse.allow(headers)
104+
.setResponseHeaderMutations(allowedMutations.responseMutations()).build();
105+
}
106+
107+
private HeaderMutations buildHeaderMutationsFromOkResponse(OkHttpResponse okResponse) {
108+
return HeaderMutations.create(
109+
HeaderMutations.RequestHeaderMutations.create(
110+
ImmutableList.copyOf(okResponse.getHeadersList()),
111+
ImmutableList.copyOf(okResponse.getHeadersToRemoveList())),
112+
HeaderMutations.ResponseHeaderMutations
113+
.create(ImmutableList.copyOf(okResponse.getResponseHeadersToAddList())));
114+
}
115+
116+
private AuthzResponse handleNotOkResponse(CheckResponse response)
117+
throws HeaderMutationDisallowedException {
118+
Status statusToReturn = config.statusOnError();
119+
if (!response.hasDeniedResponse()) {
120+
return AuthzResponse.deny(statusToReturn).build();
121+
}
122+
DeniedHttpResponse deniedResponse = response.getDeniedResponse();
123+
HeaderMutations requestedMutations = buildHeaderMutationsFromDeniedResponse(deniedResponse);
124+
HeaderMutations allowedMutations = headerMutationFilter.filter(requestedMutations);
125+
126+
Status status = statusToReturn;
127+
if (deniedResponse.hasStatus()) {
128+
status = GrpcUtil.httpStatusToGrpcStatus(deniedResponse.getStatus().getCodeValue())
129+
.withDescription(deniedResponse.getBody());
130+
}
131+
return AuthzResponse.deny(status)
132+
.setResponseHeaderMutations(allowedMutations.responseMutations()).build();
133+
}
134+
135+
private HeaderMutations buildHeaderMutationsFromDeniedResponse(
136+
DeniedHttpResponse deniedResponse) {
137+
return HeaderMutations.create(
138+
HeaderMutations.RequestHeaderMutations.create(ImmutableList.of(), ImmutableList.of()),
139+
HeaderMutations.ResponseHeaderMutations
140+
.create(ImmutableList.copyOf(deniedResponse.getHeadersList())));
141+
}
142+
143+
144+
private void applyMutations(final HeaderMutations mutations, Metadata headers) {
145+
headerMutator.applyRequestMutations(mutations.requestMutations(), headers);
146+
}
147+
}
148+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 static com.google.common.truth.Truth.assertThat;
20+
21+
import com.google.common.collect.ImmutableList;
22+
import io.envoyproxy.envoy.config.core.v3.HeaderValue;
23+
import io.envoyproxy.envoy.config.core.v3.HeaderValueOption;
24+
import io.grpc.Metadata;
25+
import io.grpc.Status;
26+
import io.grpc.xds.internal.extauthz.AuthzResponse.Decision;
27+
import io.grpc.xds.internal.headermutations.HeaderMutations.ResponseHeaderMutations;
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
import org.junit.runners.JUnit4;
31+
32+
@RunWith(JUnit4.class)
33+
public class AuthzResponseTest {
34+
@Test
35+
public void testAllow() {
36+
Metadata headers = new Metadata();
37+
headers.put(Metadata.Key.of("foo", Metadata.ASCII_STRING_MARSHALLER), "bar");
38+
AuthzResponse response = AuthzResponse.allow(headers).build();
39+
assertThat(response.decision()).isEqualTo(Decision.ALLOW);
40+
assertThat(response.headers()).hasValue(headers);
41+
assertThat(response.status()).isEmpty();
42+
assertThat(response.responseHeaderMutations().headers()).isEmpty();
43+
}
44+
45+
@Test
46+
public void testAllowWithHeaderMutations() {
47+
Metadata headers = new Metadata();
48+
ResponseHeaderMutations mutations =
49+
ResponseHeaderMutations.create(ImmutableList.of(HeaderValueOption.newBuilder()
50+
.setHeader(HeaderValue.newBuilder().setKey("key").setValue("value")).build()));
51+
AuthzResponse response =
52+
AuthzResponse.allow(headers).setResponseHeaderMutations(mutations).build();
53+
assertThat(response.decision()).isEqualTo(Decision.ALLOW);
54+
assertThat(response.responseHeaderMutations()).isEqualTo(mutations);
55+
}
56+
57+
@Test
58+
public void testDeny() {
59+
Status status = Status.PERMISSION_DENIED.withDescription("reason");
60+
AuthzResponse response = AuthzResponse.deny(status).build();
61+
assertThat(response.decision()).isEqualTo(Decision.DENY);
62+
assertThat(response.status()).hasValue(status);
63+
assertThat(response.headers()).isEmpty();
64+
assertThat(response.responseHeaderMutations().headers()).isEmpty();
65+
}
66+
}

0 commit comments

Comments
 (0)