Skip to content

Commit 6d534b5

Browse files
committed
feat(xds): Add ExtAuthzClientInterceptor and related components
This commit introduces the client-side implementation of the external authorization filter. The main component is the `ExtAuthzClientInterceptor`, which intercepts outgoing RPCs and performs external authorization checks. It uses a `BufferingAuthzClientCall` to buffer the outgoing RPC until the authorization decision is received from the authorization service. The following new classes are introduced: - `ExtAuthzClientInterceptor`: The main client interceptor for external authorization. - `BufferingAuthzClientCall`: A `ClientCall` implementation that buffers requests until an authorization decision is made. - `CallBuffer`: A helper class for `BufferingAuthzClientCall` to manage the buffered calls. - `FailingClientCall`: A utility `ClientCall` that immediately fails, used when the filter is disabled and configured to deny calls. This commit also includes comprehensive unit and integration tests for the new components.
1 parent 5b64820 commit 6d534b5

File tree

10 files changed

+1563
-11
lines changed

10 files changed

+1563
-11
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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.internal;
18+
19+
import io.grpc.ClientCall;
20+
import io.grpc.Metadata;
21+
import io.grpc.Status;
22+
import javax.annotation.Nullable;
23+
24+
/**
25+
* A {@link ClientCall} that fails immediately upon starting.
26+
*/
27+
public final class FailingClientCall<ReqT, RespT> extends ClientCall<ReqT, RespT> {
28+
29+
private final Status error;
30+
31+
/**
32+
* Creates a new call that will fail with the given error.
33+
*/
34+
public FailingClientCall(Status error) {
35+
this.error = error;
36+
}
37+
38+
/**
39+
* Immediately fails the call by calling {@link Listener#onClose}.
40+
*/
41+
@Override
42+
public void start(Listener<RespT> responseListener, Metadata headers) {
43+
responseListener.onClose(error, new Metadata());
44+
}
45+
46+
@Override
47+
public void request(int numMessages) {}
48+
49+
@Override
50+
public void cancel(@Nullable String message, @Nullable Throwable cause) {}
51+
52+
@Override
53+
public void halfClose() {}
54+
55+
@Override
56+
public void sendMessage(ReqT message) {}
57+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2016 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.internal;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.mockito.ArgumentMatchers.eq;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.verifyNoMoreInteractions;
23+
24+
import io.grpc.ClientCall;
25+
import io.grpc.Metadata;
26+
import io.grpc.Status;
27+
import org.junit.Rule;
28+
import org.junit.Test;
29+
import org.junit.runner.RunWith;
30+
import org.junit.runners.JUnit4;
31+
import org.mockito.ArgumentCaptor;
32+
import org.mockito.Mock;
33+
import org.mockito.junit.MockitoJUnit;
34+
import org.mockito.junit.MockitoRule;
35+
36+
/** Unit tests for {@link FailingClientCall}. */
37+
@RunWith(JUnit4.class)
38+
public class FailingClientCallTest {
39+
40+
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
41+
42+
@Mock
43+
private ClientCall.Listener<Object> mockListener;
44+
45+
@Test
46+
public void startCallsOnClose() {
47+
Status error = Status.UNAVAILABLE.withDescription("test error");
48+
FailingClientCall<Object, Object> call = new FailingClientCall<>(error);
49+
Metadata metadata = new Metadata();
50+
call.start(mockListener, metadata);
51+
52+
ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
53+
verify(mockListener).onClose(eq(error), metadataCaptor.capture());
54+
assertEquals(0, metadataCaptor.getValue().keys().size());
55+
verifyNoMoreInteractions(mockListener);
56+
}
57+
58+
@Test
59+
public void otherMethodsAreNoOps() {
60+
Status error = Status.UNAVAILABLE.withDescription("test error");
61+
FailingClientCall<Object, Object> call = new FailingClientCall<>(error);
62+
Metadata metadata = new Metadata();
63+
64+
call.start(mockListener, metadata); // Must call start first
65+
66+
call.request(1);
67+
call.cancel("message", new RuntimeException("cause"));
68+
call.halfClose();
69+
call.sendMessage(new Object());
70+
71+
ArgumentCaptor<Metadata> metadataCaptor = ArgumentCaptor.forClass(Metadata.class);
72+
verify(mockListener).onClose(eq(error), metadataCaptor.capture());
73+
assertEquals(0, metadataCaptor.getValue().keys().size());
74+
verifyNoMoreInteractions(mockListener);
75+
}
76+
}

xds/src/main/java/io/grpc/xds/ThreadSafeRandom.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,34 @@
1616

1717
package io.grpc.xds;
1818

19-
import java.util.concurrent.ThreadLocalRandom;
2019
import javax.annotation.concurrent.ThreadSafe;
2120

22-
@ThreadSafe // Except for impls/mocks in tests
23-
interface ThreadSafeRandom {
24-
int nextInt(int bound);
25-
26-
long nextLong();
27-
28-
long nextLong(long bound);
21+
// TODO(sauravzg): Remove this class once all usages within xds are migrated to
22+
// the internal version.
23+
@ThreadSafe
24+
interface ThreadSafeRandom extends io.grpc.xds.internal.ThreadSafeRandom {
2925

3026
final class ThreadSafeRandomImpl implements ThreadSafeRandom {
3127

3228
static final ThreadSafeRandom instance = new ThreadSafeRandomImpl();
29+
private final io.grpc.xds.internal.ThreadSafeRandom delegate =
30+
io.grpc.xds.internal.ThreadSafeRandom.ThreadSafeRandomImpl.INSTANCE;
3331

3432
private ThreadSafeRandomImpl() {}
3533

3634
@Override
3735
public int nextInt(int bound) {
38-
return ThreadLocalRandom.current().nextInt(bound);
36+
return delegate.nextInt(bound);
3937
}
4038

4139
@Override
4240
public long nextLong() {
43-
return ThreadLocalRandom.current().nextLong();
41+
return delegate.nextLong();
4442
}
4543

4644
@Override
4745
public long nextLong(long bound) {
48-
return ThreadLocalRandom.current().nextLong(bound);
46+
return delegate.nextLong(bound);
4947
}
5048
}
5149
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2023 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;
18+
19+
import java.util.concurrent.ThreadLocalRandom;
20+
import javax.annotation.concurrent.ThreadSafe;
21+
22+
/**
23+
* A thread-safe random number generator. This is intended for internal use only.
24+
*/
25+
@ThreadSafe // Except for impls/mocks in tests
26+
public interface ThreadSafeRandom {
27+
int nextInt(int bound);
28+
29+
long nextLong();
30+
31+
long nextLong(long bound);
32+
33+
final class ThreadSafeRandomImpl implements ThreadSafeRandom {
34+
35+
public static final ThreadSafeRandom INSTANCE = new ThreadSafeRandomImpl();
36+
37+
private ThreadSafeRandomImpl() {}
38+
39+
@Override
40+
public int nextInt(int bound) {
41+
return ThreadLocalRandom.current().nextInt(bound);
42+
}
43+
44+
@Override
45+
public long nextLong() {
46+
return ThreadLocalRandom.current().nextLong();
47+
}
48+
49+
@Override
50+
public long nextLong(long bound) {
51+
return ThreadLocalRandom.current().nextLong(bound);
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)