Skip to content

Commit eb4c0ea

Browse files
committed
binder: Add unit tests for TransportAuthorizationState.java
RobolectricBinderSecurityTest has some tests already but some corners of this class are hard to reach without flakiness at the Channel/Server layer.
1 parent abf8d8c commit eb4c0ea

2 files changed

Lines changed: 157 additions & 1 deletion

File tree

‎binder/src/main/java/io/grpc/binder/internal/BinderTransportSecurity.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package io.grpc.binder.internal;
1818

19+
import com.google.common.annotations.VisibleForTesting;
1920
import com.google.common.util.concurrent.FutureCallback;
2021
import com.google.common.util.concurrent.Futures;
2122
import com.google.common.util.concurrent.ListenableFuture;
@@ -162,7 +163,8 @@ private static Status statusFromFailedAuthorizationFuture(Throwable cause) {
162163
* Maintains the authorization state for a single transport instance. This class lives for the
163164
* lifetime of a single transport.
164165
*/
165-
private static final class TransportAuthorizationState {
166+
@VisibleForTesting
167+
static final class TransportAuthorizationState {
166168
private final int uid;
167169
private final ServerPolicyChecker serverPolicyChecker;
168170
private final ConcurrentHashMap<String, ListenableFuture<Status>> serviceAuthorization;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2024 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.binder.internal;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static java.util.concurrent.TimeUnit.*;
21+
import static org.junit.Assert.assertThrows;
22+
23+
import com.google.common.util.concurrent.ListenableFuture;
24+
import com.google.common.util.concurrent.SettableFuture;
25+
import com.google.protobuf.Empty;
26+
import io.grpc.MethodDescriptor;
27+
import io.grpc.Status;
28+
import io.grpc.binder.internal.BinderTransportSecurity.ServerPolicyChecker;
29+
import io.grpc.binder.internal.BinderTransportSecurity.TransportAuthorizationState;
30+
import io.grpc.protobuf.lite.ProtoLiteUtils;
31+
import java.util.NoSuchElementException;
32+
import java.util.concurrent.ExecutionException;
33+
import java.util.concurrent.ExecutorService;
34+
import java.util.concurrent.Executors;
35+
import java.util.concurrent.LinkedBlockingQueue;
36+
import java.util.concurrent.TimeUnit;
37+
import org.junit.After;
38+
import org.junit.Before;
39+
import org.junit.Test;
40+
import org.junit.runner.RunWith;
41+
import org.robolectric.RobolectricTestRunner;
42+
43+
@RunWith(RobolectricTestRunner.class)
44+
public final class TransportAuthorizationStateTest {
45+
46+
private static final int UID = 12345;
47+
private static final String NONCODEGEN_SERVICE_NAME = "test.noncodegen.service";
48+
private static final MethodDescriptor<Empty, Empty> NONCODEGEN_METHOD =
49+
MethodDescriptor.<Empty, Empty>newBuilder()
50+
.setType(MethodDescriptor.MethodType.UNARY)
51+
.setFullMethodName(NONCODEGEN_SERVICE_NAME + "/NonCodegenMethod")
52+
.setRequestMarshaller(ProtoLiteUtils.marshaller(Empty.getDefaultInstance()))
53+
.setResponseMarshaller(ProtoLiteUtils.marshaller(Empty.getDefaultInstance()))
54+
.setSampledToLocalTracing(false)
55+
.build();
56+
57+
private static final String CODEGEN_SERVICE_NAME = "test.codegen.service";
58+
private static final MethodDescriptor<Empty, Empty> CODEGEN_METHOD =
59+
MethodDescriptor.<Empty, Empty>newBuilder()
60+
.setType(MethodDescriptor.MethodType.UNARY)
61+
.setFullMethodName(CODEGEN_SERVICE_NAME + "/CodegenMethod")
62+
.setRequestMarshaller(ProtoLiteUtils.marshaller(Empty.getDefaultInstance()))
63+
.setResponseMarshaller(ProtoLiteUtils.marshaller(Empty.getDefaultInstance()))
64+
.setSampledToLocalTracing(true)
65+
.build();
66+
67+
private ExecutorService executor;
68+
private FakeServerPolicyChecker fakePolicyChecker;
69+
private TransportAuthorizationState authState;
70+
71+
@Before
72+
public void setUp() {
73+
executor = Executors.newSingleThreadExecutor();
74+
fakePolicyChecker = new FakeServerPolicyChecker();
75+
authState = new TransportAuthorizationState(UID, fakePolicyChecker, executor);
76+
}
77+
78+
@After
79+
public void tearDown() throws Exception {
80+
assertThat(executor.shutdownNow()).isEmpty();
81+
assertThat(executor.awaitTermination(5, SECONDS)).isTrue();
82+
}
83+
84+
@Test
85+
public void checkAuthorization_doesNotCacheNonCodegenMethods() throws Exception {
86+
ListenableFuture<Status> authResult1 = authState.checkAuthorization(NONCODEGEN_METHOD);
87+
assertThat(authResult1.isDone()).isFalse();
88+
89+
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.OK);
90+
assertThat(authResult1.get()).isEqualTo(Status.OK);
91+
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
92+
93+
// Because it's a non-codegen method, the auth result should not be cached.
94+
ListenableFuture<Status> authResult2 = authState.checkAuthorization(NONCODEGEN_METHOD);
95+
assertThat(authResult2.isDone()).isFalse();
96+
97+
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.PERMISSION_DENIED);
98+
assertThat(authResult2.get()).isEqualTo(Status.PERMISSION_DENIED);
99+
}
100+
101+
@Test
102+
public void checkAuthorization_cachesCodegenMethods() throws Exception {
103+
ListenableFuture<Status> authResult1 = authState.checkAuthorization(CODEGEN_METHOD);
104+
assertThat(authResult1.isDone()).isFalse();
105+
106+
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.OK);
107+
assertThat(authResult1.get()).isEqualTo(Status.OK);
108+
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
109+
110+
// Because it's a codegen method, the auth result should be cached for the life of the object.
111+
ListenableFuture<Status> authResult2 = authState.checkAuthorization(CODEGEN_METHOD);
112+
assertThat(authResult2.isDone()).isTrue();
113+
assertThat(authResult2.get()).isEqualTo(Status.OK);
114+
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
115+
}
116+
117+
@Test
118+
public void checkAuthorization_failedFuture_notCached() throws Exception {
119+
ListenableFuture<Status> authResult1 = authState.checkAuthorization(CODEGEN_METHOD);
120+
assertThat(authResult1.isDone()).isFalse();
121+
122+
fakePolicyChecker.takeNextAuthRequestOrDie().setException(new IllegalStateException("oops"));
123+
124+
ExecutionException exception = assertThrows(ExecutionException.class, authResult1::get);
125+
assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class);
126+
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
127+
128+
// Failed futures must not be cached, even for codegen methods.
129+
ListenableFuture<Status> authResult2 = authState.checkAuthorization(CODEGEN_METHOD);
130+
assertThat(authResult2.isDone()).isFalse();
131+
132+
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.OK);
133+
assertThat(authResult2.get()).isEqualTo(Status.OK);
134+
}
135+
136+
private static final class FakeServerPolicyChecker implements ServerPolicyChecker {
137+
final LinkedBlockingQueue<SettableFuture<Status>> statusesToSet = new LinkedBlockingQueue<>();
138+
139+
@Override
140+
public ListenableFuture<Status> checkAuthorizationForServiceAsync(int uid, String serviceName) {
141+
SettableFuture<Status> pendingResult = SettableFuture.create();
142+
statusesToSet.add(pendingResult);
143+
return pendingResult;
144+
}
145+
146+
SettableFuture<Status> takeNextAuthRequestOrDie() throws InterruptedException {
147+
SettableFuture<Status> item = statusesToSet.poll(10, SECONDS);
148+
if (item == null) {
149+
throw new NoSuchElementException("Queue timed out waiting for item");
150+
}
151+
return item;
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)