Skip to content
Closed
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
110 changes: 110 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,110 @@
/*
* 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 allow(requestHeaderMutations, EMPTY_MUTATIONS);
}

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

/** Creates a builder for a DENY response, initializing with the specified status. */
static Builder deny(Status status) {
return deny(status, EMPTY_MUTATIONS);
}

/** Creates a builder for a DENY response with status and response trailer mutations. */
static Builder deny(Status status, HeaderMutations responseHeaderMutations) {
return new AutoValue_AuthzResponse.Builder().setDecision(Decision.DENY)
.setResponseHeaderMutations(responseHeaderMutations)
.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,153 @@
/*
* 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();
}
CheckResponseMutations allowedMutations =
buildHeaderMutationsFromOkResponse(response.getOkResponse());

return AuthzResponse.allow(
allowedMutations.requestMutations(), 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();
HeaderMutations responseMutations = buildResponseTrailerMutations(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), responseMutations).build();
}

private HeaderMutations buildResponseTrailerMutations(
DeniedHttpResponse deniedResponse) throws HeaderMutationDisallowedException {
return headerMutationFilter.filter(HeaderMutations.create(
convertHeaders(deniedResponse.getHeadersList()),
ImmutableList.of()));
}

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();
}
Loading
Loading