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
97 changes: 97 additions & 0 deletions xds/src/main/java/io/grpc/xds/internal/extauthz/AuthzResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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.grpc.Status;
import io.grpc.xds.internal.headermutations.HeaderMutations;
import java.util.Optional;

/**
* Represents the outcome of an authorization check, detailing whether the request is allowed or
* denied and including any associated headers or status information.
*/
@AutoValue
abstract class AuthzResponse {

/** Defines the authorization decision. */
public enum Decision {
/** The request is permitted. */
ALLOW,
/** The request is rejected. */
DENY,
}

private static final HeaderMutations EMPTY_MUTATIONS =
HeaderMutations.create(ImmutableList.of(), ImmutableList.of());

/**
* Creates a builder for an ALLOW response, initializing with the specified request header
* mutations.
*/
static Builder allow(HeaderMutations requestHeaderMutations) {
return new AutoValue_AuthzResponse.Builder().setDecision(Decision.ALLOW)
.setResponseHeaderMutations(EMPTY_MUTATIONS)
.setRequestHeaderMutations(requestHeaderMutations);
}

/** Creates a builder for a DENY response, initializing with the specified status. */
static Builder deny(Status status) {
return new AutoValue_AuthzResponse.Builder().setDecision(Decision.DENY)
.setResponseHeaderMutations(EMPTY_MUTATIONS)
.setRequestHeaderMutations(EMPTY_MUTATIONS)
.setStatus(status);
}

/** Returns the authorization decision. */
public abstract Decision decision();

/**
* For DENY decisions, this provides the status to be returned to the calling client. It is empty
* for ALLOW decisions.
*/
public abstract Optional<Status> status();

/**
* Returns mutations to be applied to the request headers. This is used for ALLOW decisions.
*/
public abstract HeaderMutations requestHeaderMutations();

/**
* Returns mutations to be applied to the response headers. This is used for both ALLOW and DENY
* decisions.
*/
public abstract HeaderMutations responseHeaderMutations();

/** Builder for creating {@link AuthzResponse} instances. */
@AutoValue.Builder
abstract static class Builder {

abstract Builder setDecision(Decision decision);

abstract Builder setStatus(Status status);

public abstract Builder setRequestHeaderMutations(
HeaderMutations requestHeaderMutations);

public abstract Builder setResponseHeaderMutations(
HeaderMutations responseHeaderMutations);

public abstract AuthzResponse build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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.common.collect.ImmutableList;
import io.envoyproxy.envoy.service.auth.v3.CheckResponse;
import io.envoyproxy.envoy.service.auth.v3.DeniedHttpResponse;
import io.envoyproxy.envoy.service.auth.v3.OkHttpResponse;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.internal.GrpcUtil;
import io.grpc.xds.internal.grpcservice.HeaderValue;
import io.grpc.xds.internal.grpcservice.HeaderValueValidationUtils;
import io.grpc.xds.internal.headermutations.HeaderMutationDisallowedException;
import io.grpc.xds.internal.headermutations.HeaderMutationFilter;
import io.grpc.xds.internal.headermutations.HeaderMutations;
import io.grpc.xds.internal.headermutations.HeaderValueOption;
import javax.annotation.concurrent.ThreadSafe;

/**
* Handles the response from the external authorization service, processing it to determine the
* authorization decision and applying any necessary header mutations.
*/
@ThreadSafe
public class CheckResponseHandler {
private final HeaderMutationFilter headerMutationFilter;

public CheckResponseHandler(HeaderMutationFilter headerMutationFilter) {
this.headerMutationFilter = headerMutationFilter;
}

AuthzResponse handleResponse(final CheckResponse response) {
try {
if (response.getStatus().getCode() == Status.Code.OK.value()) {
return handleOkResponse(response);
} else {
return handleNotOkResponse(response);
}
} catch (HeaderMutationDisallowedException e) {
return AuthzResponse.deny(e.getStatus()).build();
}
}

private AuthzResponse handleOkResponse(final CheckResponse response)
throws HeaderMutationDisallowedException {
if (!response.hasOkResponse()) {
return AuthzResponse.allow(
HeaderMutations.create(ImmutableList.of(), ImmutableList.of())).build();
}
OkHttpResponse okResponse = response.getOkResponse();
CheckResponseMutations allowedMutations = buildHeaderMutationsFromOkResponse(okResponse);

return AuthzResponse.allow(allowedMutations.requestMutations())
.setResponseHeaderMutations(allowedMutations.responseMutations()).build();
}

private CheckResponseMutations buildHeaderMutationsFromOkResponse(OkHttpResponse okResponse)
throws HeaderMutationDisallowedException {
HeaderMutations requestMutations = HeaderMutations.create(
convertHeaders(okResponse.getHeadersList()),
ImmutableList.copyOf(okResponse.getHeadersToRemoveList()));
HeaderMutations responseMutations = HeaderMutations.create(
convertHeaders(okResponse.getResponseHeadersToAddList()),
ImmutableList.of());
return CheckResponseMutations.create(
headerMutationFilter.filter(requestMutations),
headerMutationFilter.filter(responseMutations));
}

private AuthzResponse handleNotOkResponse(CheckResponse response)
throws HeaderMutationDisallowedException {
String baseMsg = "RPC denied by external authorization server";
String outerMsg = response.getStatus().getMessage();
String description = outerMsg.isEmpty() ? baseMsg : baseMsg + ": " + outerMsg;

if (!response.hasDeniedResponse()) {
return AuthzResponse.deny(Status.PERMISSION_DENIED.withDescription(description)).build();
}
DeniedHttpResponse deniedResponse = response.getDeniedResponse();
CheckResponseMutations allowedMutations =
buildHeaderMutationsFromDeniedResponse(deniedResponse);

Status status = Status.PERMISSION_DENIED;
if (deniedResponse.hasStatus()) {
status = GrpcUtil.httpStatusToGrpcStatus(deniedResponse.getStatus().getCodeValue());
}
// Per gRFC A92: deniedResponse.body is ignored for gRPC (doesn't apply to gRPC).
return AuthzResponse.deny(status.withDescription(description))
.setResponseHeaderMutations(allowedMutations.responseMutations()).build();
}

private CheckResponseMutations buildHeaderMutationsFromDeniedResponse(
DeniedHttpResponse deniedResponse) throws HeaderMutationDisallowedException {
HeaderMutations requestMutations =
HeaderMutations.create(ImmutableList.of(), ImmutableList.of());
HeaderMutations responseMutations = HeaderMutations.create(
convertHeaders(deniedResponse.getHeadersList()),
ImmutableList.of());
return CheckResponseMutations.create(
headerMutationFilter.filter(requestMutations),
headerMutationFilter.filter(responseMutations));
}

private ImmutableList<HeaderValueOption> convertHeaders(
java.util.List<io.envoyproxy.envoy.config.core.v3.HeaderValueOption> headersList)
throws HeaderMutationDisallowedException {
ImmutableList.Builder<HeaderValueOption> builder = ImmutableList.builder();
for (io.envoyproxy.envoy.config.core.v3.HeaderValueOption optionProto : headersList) {
io.envoyproxy.envoy.config.core.v3.HeaderValue header = optionProto.getHeader();
String key = header.getKey();
HeaderValue internalHeader;
if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
internalHeader = HeaderValue.create(key, header.getRawValue());
} else {
internalHeader = HeaderValue.create(key, header.getValue());
}
if (HeaderValueValidationUtils.isDisallowed(internalHeader)) {
continue;
}
HeaderValueOption.HeaderAppendAction action;
switch (optionProto.getAppendAction()) {
case APPEND_IF_EXISTS_OR_ADD:
action = HeaderValueOption.HeaderAppendAction.APPEND_IF_EXISTS_OR_ADD;
break;
case ADD_IF_ABSENT:
action = HeaderValueOption.HeaderAppendAction.ADD_IF_ABSENT;
break;
case OVERWRITE_IF_EXISTS_OR_ADD:
action = HeaderValueOption.HeaderAppendAction.OVERWRITE_IF_EXISTS_OR_ADD;
break;
case OVERWRITE_IF_EXISTS:
action = HeaderValueOption.HeaderAppendAction.OVERWRITE_IF_EXISTS;
break;
case UNRECOGNIZED:
default:
// Envoy Parity / Spec Parity: Unconditionally reject invalid/unrecognized append actions
throw new HeaderMutationDisallowedException(
"Unrecognized HeaderAppendAction: " + optionProto.getAppendAction());
}
builder
.add(HeaderValueOption.create(internalHeader, action));
}
return builder.build();
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 io.grpc.xds.internal.headermutations.HeaderMutations;

/**
* A collection of header mutations for an external authorization response.
* It contains separate mutations for request headers and response headers.
*/
@AutoValue
abstract class CheckResponseMutations {

static CheckResponseMutations create(HeaderMutations requestMutations,
HeaderMutations responseMutations) {
return new AutoValue_CheckResponseMutations(requestMutations, responseMutations);
}

public abstract HeaderMutations requestMutations();

public abstract HeaderMutations responseMutations();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import io.grpc.Status;
import io.grpc.xds.internal.extauthz.AuthzResponse.Decision;
import io.grpc.xds.internal.headermutations.HeaderMutations;
import io.grpc.xds.internal.headermutations.HeaderValueOption;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class AuthzResponseTest {
@Test
public void testAllow() {
HeaderMutations requestMutations =
HeaderMutations.create(ImmutableList.of(), ImmutableList.of());
AuthzResponse response = AuthzResponse.allow(requestMutations).build();
assertThat(response.decision()).isEqualTo(Decision.ALLOW);
assertThat(response.requestHeaderMutations()).isEqualTo(requestMutations);
assertThat(response.status()).isEmpty();
assertThat(response.responseHeaderMutations().headers()).isEmpty();
}

@Test
public void testAllowWithHeaderMutations() {
HeaderMutations requestMutations =
HeaderMutations.create(ImmutableList.of(), ImmutableList.of());
HeaderMutations responseMutations =
HeaderMutations.create(
ImmutableList.of(
HeaderValueOption.create(
io.grpc.xds.internal.grpcservice.HeaderValue.create("key", "value"),
HeaderValueOption.HeaderAppendAction.APPEND_IF_EXISTS_OR_ADD)),
ImmutableList.of());
AuthzResponse response =
AuthzResponse.allow(requestMutations)
.setResponseHeaderMutations(responseMutations)
.build();
assertThat(response.decision()).isEqualTo(Decision.ALLOW);
assertThat(response.requestHeaderMutations()).isEqualTo(requestMutations);
assertThat(response.responseHeaderMutations()).isEqualTo(responseMutations);
}

@Test
public void testDeny() {
Status status = Status.PERMISSION_DENIED.withDescription("reason");
AuthzResponse response = AuthzResponse.deny(status).build();
assertThat(response.decision()).isEqualTo(Decision.DENY);
assertThat(response.status()).hasValue(status);
assertThat(response.requestHeaderMutations().headers()).isEmpty();
assertThat(response.responseHeaderMutations().headers()).isEmpty();
}

@Test
public void testDenyWithResponseMutations() {
Status status = Status.PERMISSION_DENIED.withDescription("reason");
HeaderMutations responseMutations =
HeaderMutations.create(
ImmutableList.of(
HeaderValueOption.create(
io.grpc.xds.internal.grpcservice.HeaderValue.create("x-deny-info", "blocked"),
HeaderValueOption.HeaderAppendAction.APPEND_IF_EXISTS_OR_ADD)),
ImmutableList.of());
AuthzResponse response = AuthzResponse.deny(status)
.setResponseHeaderMutations(responseMutations)
.build();
assertThat(response.decision()).isEqualTo(Decision.DENY);
assertThat(response.status()).hasValue(status);
assertThat(response.responseHeaderMutations()).isEqualTo(responseMutations);
assertThat(response.requestHeaderMutations().headers()).isEmpty();
}
}
Loading
Loading