Skip to content

Commit 5b082ca

Browse files
authored
Do not cache failed futures for async security policies indefinitely. (#10743)
Currently, if caching is enabled (as is often the case) and AsyncSecurityPolicy returns a failed future, then this future is cached forever, without giving the SecurityPolicy implementation a chance to be retried. Going forward, new invocations will trigger new security checks if the last one could not be completed successfuly.
1 parent 062f7a2 commit 5b082ca

File tree

2 files changed

+92
-3
lines changed

2 files changed

+92
-3
lines changed

binder/src/androidTest/java/io/grpc/binder/BinderSecurityTest.java

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.common.util.concurrent.Futures;
2727
import com.google.common.util.concurrent.ListenableFuture;
2828
import com.google.common.util.concurrent.MoreExecutors;
29+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2930
import com.google.protobuf.Empty;
3031
import io.grpc.CallOptions;
3132
import io.grpc.ManagedChannel;
@@ -45,6 +46,8 @@
4546
import java.util.HashMap;
4647
import java.util.List;
4748
import java.util.Map;
49+
import java.util.concurrent.atomic.AtomicReference;
50+
4851
import javax.annotation.Nullable;
4952
import org.junit.After;
5053
import org.junit.Before;
@@ -76,6 +79,7 @@ public void setupServiceDefinitionsAndMethods() {
7679
MethodDescriptor.newBuilder(marshaller, marshaller)
7780
.setFullMethodName(name)
7881
.setType(MethodDescriptor.MethodType.UNARY)
82+
.setSampledToLocalTracing(true)
7983
.build();
8084
ServerCallHandler<Empty, Empty> callHandler =
8185
ServerCalls.asyncUnaryCall(
@@ -139,12 +143,16 @@ private void assertCallSuccess(MethodDescriptor<Empty, Empty> method) {
139143
.isNotNull();
140144
}
141145

142-
private void assertCallFailure(MethodDescriptor<Empty, Empty> method, Status status) {
146+
@CanIgnoreReturnValue
147+
private StatusRuntimeException assertCallFailure(
148+
MethodDescriptor<Empty, Empty> method, Status status) {
143149
try {
144150
ClientCalls.blockingUnaryCall(channel, method, CallOptions.DEFAULT, null);
145-
fail();
151+
fail("Expected call to " + method.getFullMethodName() + " to fail but it succeeded.");
152+
throw new AssertionError(); // impossible
146153
} catch (StatusRuntimeException sre) {
147154
assertThat(sre.getStatus().getCode()).isEqualTo(status.getCode());
155+
return sre;
148156
}
149157
}
150158

@@ -172,6 +180,70 @@ public void testServerDisallowsCalls() throws Exception {
172180
}
173181
}
174182

183+
@Test
184+
public void testFailedFuturesPropagateOriginalException() throws Exception {
185+
String errorMessage = "something went wrong";
186+
IllegalStateException originalException = new IllegalStateException(errorMessage);
187+
createChannel(
188+
ServerSecurityPolicy.newBuilder()
189+
.servicePolicy("foo", new AsyncSecurityPolicy() {
190+
@Override
191+
ListenableFuture<Status> checkAuthorizationAsync(int uid) {
192+
return Futures.immediateFailedFuture(originalException);
193+
}
194+
})
195+
.build(),
196+
SecurityPolicies.internalOnly());
197+
MethodDescriptor<Empty, Empty> method = methods.get("foo/method0");
198+
199+
StatusRuntimeException sre = assertCallFailure(method, Status.INTERNAL);
200+
assertThat(sre.getStatus().getDescription()).contains(errorMessage);
201+
}
202+
203+
@Test
204+
public void testFailedFuturesAreNotCachedPermanently() throws Exception {
205+
AtomicReference<Boolean> firstAttempt = new AtomicReference<>(true);
206+
createChannel(
207+
ServerSecurityPolicy.newBuilder()
208+
.servicePolicy("foo", new AsyncSecurityPolicy() {
209+
@Override
210+
ListenableFuture<Status> checkAuthorizationAsync(int uid) {
211+
if (firstAttempt.getAndSet(false)) {
212+
return Futures.immediateFailedFuture(new IllegalStateException());
213+
}
214+
return Futures.immediateFuture(Status.OK);
215+
}
216+
})
217+
.build(),
218+
SecurityPolicies.internalOnly());
219+
MethodDescriptor<Empty, Empty> method = methods.get("foo/method0");
220+
221+
assertCallFailure(method, Status.INTERNAL);
222+
assertCallSuccess(method);
223+
}
224+
225+
@Test
226+
public void testCancelledFuturesAreNotCachedPermanently() throws Exception {
227+
AtomicReference<Boolean> firstAttempt = new AtomicReference<>(true);
228+
createChannel(
229+
ServerSecurityPolicy.newBuilder()
230+
.servicePolicy("foo", new AsyncSecurityPolicy() {
231+
@Override
232+
ListenableFuture<Status> checkAuthorizationAsync(int uid) {
233+
if (firstAttempt.getAndSet(false)) {
234+
return Futures.immediateCancelledFuture();
235+
}
236+
return Futures.immediateFuture(Status.OK);
237+
}
238+
})
239+
.build(),
240+
SecurityPolicies.internalOnly());
241+
MethodDescriptor<Empty, Empty> method = methods.get("foo/method0");
242+
243+
assertCallFailure(method, Status.INTERNAL);
244+
assertCallSuccess(method);
245+
}
246+
175247
@Test
176248
public void testClientDoesntTrustServer() throws Exception {
177249
createChannel(SecurityPolicies.serverInternalOnly(), policy((uid) -> false));

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.common.util.concurrent.FutureCallback;
2020
import com.google.common.util.concurrent.Futures;
2121
import com.google.common.util.concurrent.ListenableFuture;
22+
import com.google.common.util.concurrent.MoreExecutors;
2223

2324
import io.grpc.Attributes;
2425
import io.grpc.Internal;
@@ -32,6 +33,7 @@
3233
import io.grpc.Status;
3334
import io.grpc.internal.GrpcAttributes;
3435

36+
import java.util.concurrent.CancellationException;
3537
import java.util.concurrent.ConcurrentHashMap;
3638
import java.util.concurrent.ExecutionException;
3739
import java.util.concurrent.Executor;
@@ -110,9 +112,13 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
110112
Status authStatus;
111113
try {
112114
authStatus = Futures.getDone(authStatusFuture);
113-
} catch (ExecutionException e) {
115+
} catch (ExecutionException | CancellationException e) {
114116
// Failed futures are treated as an internal error rather than a security rejection.
115117
authStatus = Status.INTERNAL.withCause(e);
118+
@Nullable String message = e.getMessage();
119+
if (message != null) {
120+
authStatus = authStatus.withDescription(message);
121+
}
116122
}
117123

118124
if (authStatus.isOk()) {
@@ -179,6 +185,8 @@ ListenableFuture<Status> checkAuthorization(MethodDescriptor<?, ?> method) {
179185
if (useCache) {
180186
@Nullable ListenableFuture<Status> authorization = serviceAuthorization.get(serviceName);
181187
if (authorization != null) {
188+
// Authorization check exists and is a pending or successful future (even if for a
189+
// failed authorization).
182190
return authorization;
183191
}
184192
}
@@ -193,6 +201,15 @@ ListenableFuture<Status> checkAuthorization(MethodDescriptor<?, ?> method) {
193201
serverPolicyChecker.checkAuthorizationForServiceAsync(uid, serviceName);
194202
if (useCache) {
195203
serviceAuthorization.putIfAbsent(serviceName, authorization);
204+
Futures.addCallback(authorization, new FutureCallback<Status>() {
205+
@Override
206+
public void onSuccess(Status result) {}
207+
208+
@Override
209+
public void onFailure(Throwable t) {
210+
serviceAuthorization.remove(serviceName, authorization);
211+
}
212+
}, MoreExecutors.directExecutor());
196213
}
197214
return authorization;
198215
}

0 commit comments

Comments
 (0)