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
94 changes: 94 additions & 0 deletions binder/src/main/java/io/grpc/binder/AsyncSecurityPolicies.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2026 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.binder;

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.errorprone.annotations.CheckReturnValue;
import io.grpc.ExperimentalApi;
import io.grpc.Status;
import java.util.concurrent.Executor;

/** Static factory methods for creating asynchronous security policies. */
@CheckReturnValue
public final class AsyncSecurityPolicies {

private AsyncSecurityPolicies() {}

/**
* Adapts a future {@link AsyncSecurityPolicy} into an instance of {@link AsyncSecurityPolicy}
* that can be used right away.
*
* <p>Use this when your actual security policy is loaded asynchronously (e.g., from disk or
* network) and is not immediately available at gRPC channel or server initialization. For
* example, Android IPC servers can't defer "listening" while some slow or async initialization
* process completes. Instead, Android *tells* a server to initialize itself just-in-time for the
* first client connection. And this instruction arrives as a callback to {@code
* Service#onCreate()} then {@code Service#onBind()} on the app's main thread, where blocking to
* load a security policy would risk an "Application Not Responding" (ANR) error.
*
* <p>The returned policy will defer all authorization decisions until {@code futurePolicy}
* completes. So any calls or connections needing authorization will wait. After the future
* completes, all new and pending authorization checks are delegated to the loaded policy. If
* {@code futurePolicy} completes with an exception, all authorization checks will fail.
*
* @param futurePolicy The future that will resolve to the delegate security policy.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public static AsyncSecurityPolicy fromFuture(
final ListenableFuture<? extends AsyncSecurityPolicy> futurePolicy) {
Preconditions.checkNotNull(futurePolicy, "futurePolicy");
return new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return Futures.transformAsync(
Futures.nonCancellationPropagating(futurePolicy),
policy -> policy.checkAuthorizationAsync(uid),
MoreExecutors.directExecutor());
}
};
}

/**
* Adapts a future synchronous {@link SecurityPolicy} into an {@link AsyncSecurityPolicy}.
*
* <p>See {@link #fromFuture(ListenableFuture)} for details on the use case.
*
* <p>Because the delegate policy is synchronous and may perform blocking operations (like disk
* I/O), the actual authorization checks are offloaded to the provided {@code offloadExecutor}.
*
* @param futurePolicy The future that will resolve to the delegate security policy.
* @param offloadExecutor The executor on which to run the delegate's blocking {@link
* SecurityPolicy#checkAuthorization} call.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
public static AsyncSecurityPolicy fromFuture(
final ListenableFuture<? extends SecurityPolicy> futurePolicy, Executor offloadExecutor) {
Preconditions.checkNotNull(futurePolicy, "futurePolicy");
return new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return Futures.transform(
Futures.nonCancellationPropagating(futurePolicy),
policy -> policy.checkAuthorization(uid),
offloadExecutor);
}
};
}
}
268 changes: 268 additions & 0 deletions binder/src/test/java/io/grpc/binder/AsyncSecurityPoliciesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/*
* Copyright 2026 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.binder;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

import android.os.Process;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import io.grpc.Status;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public final class AsyncSecurityPoliciesTest {

private static final int MY_UID = Process.myUid();

@Test
public void testFromFuture_asyncPolicy_succeeds() throws Exception {
SettableFuture<AsyncSecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
assertThat(authFuture.isDone()).isFalse();

AsyncSecurityPolicy delegatePolicy =
new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return Futures.immediateFuture(Status.OK.withDescription("yay"));
}
};

futurePolicy.set(delegatePolicy);

assertThat(authFuture.isDone()).isTrue();
assertThat(authFuture.get().getDescription()).isEqualTo("yay");
}

@Test
public void testFromFuture_asyncPolicy_fails() throws Exception {
SettableFuture<AsyncSecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
assertThat(authFuture.isDone()).isFalse();

AsyncSecurityPolicy delegatePolicy =
new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return Futures.immediateFuture(Status.PERMISSION_DENIED.withDescription("ouch"));
}
};

futurePolicy.set(delegatePolicy);

assertThat(authFuture.isDone()).isTrue();
assertThat(authFuture.get().getDescription()).isEqualTo("ouch");
}

@Test
public void testFromFuture_asyncPolicy_futureFails() throws Exception {
SettableFuture<AsyncSecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
assertThat(authFuture.isDone()).isFalse();

Exception exception = new RuntimeException("failed to load policy");
futurePolicy.setException(exception);

assertThat(authFuture.isDone()).isTrue();
try {
authFuture.get();
fail("Expected ExecutionException");
} catch (java.util.concurrent.ExecutionException e) {
assertThat(e.getCause()).isEqualTo(exception);
}
}

@Test
public void testFromFuture_syncPolicy_succeeds() throws Exception {
SettableFuture<SecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy =
AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
assertThat(authFuture.isDone()).isFalse();

SecurityPolicy delegatePolicy =
new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return Status.OK.withDescription("yay");
}
};

futurePolicy.set(delegatePolicy);

assertThat(authFuture.isDone()).isTrue();
assertThat(authFuture.get().getDescription()).isEqualTo("yay");
}

@Test
public void testFromFuture_syncPolicy_fails() throws Exception {
SettableFuture<SecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy =
AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
assertThat(authFuture.isDone()).isFalse();

SecurityPolicy delegatePolicy =
new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return Status.PERMISSION_DENIED.withDescription("ouch");
}
};

futurePolicy.set(delegatePolicy);

assertThat(authFuture.isDone()).isTrue();
assertThat(authFuture.get().getDescription()).isEqualTo("ouch");
}

@Test
public void testFromFuture_syncPolicy_futureFails() throws Exception {
SettableFuture<SecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy =
AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
assertThat(authFuture.isDone()).isFalse();

Exception exception = new RuntimeException("failed to load policy");
futurePolicy.setException(exception);

assertThat(authFuture.isDone()).isTrue();
try {
authFuture.get();
fail("Expected ExecutionException");
} catch (java.util.concurrent.ExecutionException e) {
assertThat(e.getCause()).isEqualTo(exception);
}
}

@Test
public void testFromFuture_syncPolicy_usesExecutor() throws Exception {
SettableFuture<SecurityPolicy> futurePolicy = SettableFuture.create();
AtomicInteger executeCount = new AtomicInteger(0);
Executor executor =
r -> {
executeCount.incrementAndGet();
r.run();
};
AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy, executor);

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
SecurityPolicy delegatePolicy =
new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return Status.OK.withDescription("yay");
}
};
futurePolicy.set(delegatePolicy);

assertThat(authFuture.get().getDescription()).isEqualTo("yay");
assertThat(executeCount.get()).isEqualTo(1);
}

@Test
public void testFromFuture_asyncPolicy_cancellationDoesNotPropagateBackwards() throws Exception {
SettableFuture<AsyncSecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
authFuture.cancel(true);

assertThat(futurePolicy.isCancelled()).isFalse();
}

@Test
public void testFromFuture_syncPolicy_cancellationDoesNotPropagateBackwards() throws Exception {
SettableFuture<SecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy =
AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());

ListenableFuture<Status> authFuture = asyncPolicy.checkAuthorizationAsync(MY_UID);
authFuture.cancel(true);

assertThat(futurePolicy.isCancelled()).isFalse();
}

@Test
public void testFromFuture_asyncPolicy_cancellationDoesNotPoisonSubsequentCalls()
throws Exception {
SettableFuture<AsyncSecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy = AsyncSecurityPolicies.fromFuture(futurePolicy);

ListenableFuture<Status> authFuture1 = asyncPolicy.checkAuthorizationAsync(MY_UID);
authFuture1.cancel(true);

// Now try again
ListenableFuture<Status> authFuture2 = asyncPolicy.checkAuthorizationAsync(MY_UID);

AsyncSecurityPolicy delegatePolicy =
new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return Futures.immediateFuture(Status.OK.withDescription("yay"));
}
};
futurePolicy.set(delegatePolicy);

assertThat(authFuture2.isDone()).isTrue();
assertThat(authFuture2.get().getDescription()).isEqualTo("yay");
}

@Test
public void testFromFuture_syncPolicy_cancellationDoesNotPoisonSubsequentCalls()
throws Exception {
SettableFuture<SecurityPolicy> futurePolicy = SettableFuture.create();
AsyncSecurityPolicy asyncPolicy =
AsyncSecurityPolicies.fromFuture(futurePolicy, MoreExecutors.directExecutor());

ListenableFuture<Status> authFuture1 = asyncPolicy.checkAuthorizationAsync(MY_UID);
authFuture1.cancel(true);

// Now try again
ListenableFuture<Status> authFuture2 = asyncPolicy.checkAuthorizationAsync(MY_UID);

SecurityPolicy delegatePolicy =
new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
return Status.OK.withDescription("yay");
}
};
futurePolicy.set(delegatePolicy);

assertThat(authFuture2.isDone()).isTrue();
assertThat(authFuture2.get().getDescription()).isEqualTo("yay");
}
}