Skip to content

Support AysncSecurityPolicy in SecurityPolicies.allOf. #12158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
47 changes: 45 additions & 2 deletions binder/src/main/java/io/grpc/binder/SecurityPolicies.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package io.grpc.binder;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.util.concurrent.Futures.immediateFuture;

import android.annotation.SuppressLint;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
Expand All @@ -32,6 +35,9 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.Hashing;
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;
Expand Down Expand Up @@ -333,6 +339,9 @@ private static boolean checkPackageSignature(
* Creates a {@link SecurityPolicy} that allows access if and only if *all* of the specified
* {@code securityPolicies} allow access.
*
* <p>If any of the policies is an {@link AsyncSecurityPolicy}, then all policies may be evaluated
* concurrently to speed up the success scenario.
*
* @param securityPolicies the security policies that all must allow access.
* @throws NullPointerException if any of the inputs are {@code null}.
* @throws IllegalArgumentException if {@code securityPolicies} is empty.
Expand All @@ -341,10 +350,17 @@ public static SecurityPolicy allOf(SecurityPolicy... securityPolicies) {
Preconditions.checkNotNull(securityPolicies, "securityPolicies");
Preconditions.checkArgument(securityPolicies.length > 0, "securityPolicies must not be empty");

return allOfSecurityPolicy(securityPolicies);
boolean anyAsync =
Arrays
.stream(securityPolicies)
.anyMatch(policy -> policy instanceof AsyncSecurityPolicy);

return anyAsync
? allOfSecurityPolicyAsync(securityPolicies)
: allOfSecurityPolicySync(securityPolicies);
}

private static SecurityPolicy allOfSecurityPolicy(SecurityPolicy... securityPolicies) {
private static SecurityPolicy allOfSecurityPolicySync(SecurityPolicy... securityPolicies) {
return new SecurityPolicy() {
@Override
public Status checkAuthorization(int uid) {
Expand All @@ -360,6 +376,33 @@ public Status checkAuthorization(int uid) {
};
}

private static SecurityPolicy allOfSecurityPolicyAsync(SecurityPolicy... securityPolicies) {
return new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
ImmutableList<ListenableFuture<Status>> allStatuses =
Arrays.stream(securityPolicies).map(policy -> {
AsyncSecurityPolicy asyncPolicy =
policy instanceof AsyncSecurityPolicy ? (AsyncSecurityPolicy) policy :
new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return immediateFuture(policy.checkAuthorization(uid));
Copy link
Member

@jdcormie jdcormie Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to be a valid implementation of the checkAuthorizationAsync() API contract because it blocks the calling thread.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ALL of your input securityPolicies are AsyncSecurityPolicy, I think you can implement an async allOf(). But if even one of your inputs is not an AsyncSecurityPolicy, someone needs to provide an Executor to make this possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My goal was to not force the clients to pick another API if they have any AsyncSecurityPolicy, specially because the current method is still compatible (though inefficient) with AsyncSecurityPolicy, so I felt like having two methods could be confusing.

This doesn't seem to be a valid implementation of the checkAuthorizationAsync() API contract because it blocks the calling thread.

My intention was to treat all policies as AsyncSecurityPolicy by obtaining a List<ListenableFuture<Status>> and transforming them without blocking. If one of the policies is synchronous, we just produce an immediate future.

This could be harmful, yes, if that policy were slow, but I see that as API misuse: the integrator should have provided an AsyncSecurityPolicy instead and we wouldn't monopolize a thread waiting on it.

Please let me know what you think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDK what more to say except that implementations of checkAuthorizationAsync() aren't allowed to block. If they were, there wouldn't be much reason for AsyncSecurityPolicy to even exist: Plain old SecurityPolicy.checkAuthorization() has always had the property that it may or may not block. And we've always been able to turn checkAuthorization() into a ListenableFuture using Futures.submit(securityPolicy::checkAuthorization, blockingExecutor). What makes checkAuthorizationAsync() interesting is that it promises not to block so I can call it from any thread.

Hiding a blocking call to checkAuthorization() behind checkAuthorizationAsync() actively defeats one of your two most important callers here:

ListenableFuture<Status> authFuture =
(securityPolicy instanceof AsyncSecurityPolicy)
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
: Futures.submit(
() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);

This could be harmful, yes, if that policy were slow, but I see that as API misuse: the integrator should have provided an AsyncSecurityPolicy instead and we wouldn't monopolize a thread waiting on it.

OK but your javadoc doesn't say any of that. Even if it did, we want APIs that are difficult to misuse and, ideally, impossible to misuse, by way of checks at compile time.

}
};
return asyncPolicy.checkAuthorizationAsync(uid);
}).collect(toImmutableList());
ListenableFuture<List<Status>> futureStatuses = Futures.allAsList(allStatuses);

return Futures
.transform(
futureStatuses,statuses ->
statuses.stream().filter(status -> !status.isOk()).findFirst().orElse(Status.OK),
MoreExecutors.directExecutor());
}
};
}

/**
* Creates a {@link SecurityPolicy} that allows access if *any* of the specified {@code
* securityPolicies} allow access.
Expand Down
43 changes: 39 additions & 4 deletions binder/src/test/java/io/grpc/binder/SecurityPoliciesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.robolectric.Shadows.shadowOf;

import android.app.admin.DevicePolicyManager;
Expand All @@ -35,9 +36,11 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.Status;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -523,15 +526,38 @@ public void testAllOf_succeedsIfAllSecurityPoliciesAllowed() throws Exception {

@Test
public void testAllOf_failsIfOneSecurityPoliciesNotAllowed() throws Exception {
policy =
SecurityPolicies.allOf(
SecurityPolicies.internalOnly(),
SecurityPolicies.permissionDenied("Not allowed SecurityPolicy"));

assertThat(policy.checkAuthorization(MY_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorization(MY_UID).getDescription())
.contains("Not allowed SecurityPolicy");
}

@Test
public void testAllOfAsync_succeedsIfAllSecurityPoliciesAllowed() {
policy =
SecurityPolicies.allOf(
SecurityPolicies.internalOnly(),
makeAsyncPolicy(uid -> immediateFuture(Status.OK)));

assertThat(policy.checkAuthorization(MY_UID).getCode()).isEqualTo(Status.OK.getCode());
}

@Test
public void testAllOfAsync_failsIfOneSecurityPoliciesNotAllowed() {
policy =
SecurityPolicies.allOf(
SecurityPolicies.internalOnly(),
SecurityPolicies.permissionDenied("Not allowed SecurityPolicy"));
makeAsyncPolicy(uid -> immediateFuture(Status.OK)),
makeAsyncPolicy(uid -> immediateFuture(Status.ABORTED)),
makeAsyncPolicy(uid -> immediateFuture(Status.INVALID_ARGUMENT)));

assertThat(policy.checkAuthorization(MY_UID).getCode())
.isEqualTo(Status.PERMISSION_DENIED.getCode());
assertThat(policy.checkAuthorization(MY_UID).getDescription())
.contains("Not allowed SecurityPolicy");
.isEqualTo(Status.Code.ABORTED);
}

@Test
Expand Down Expand Up @@ -703,4 +729,13 @@ public void testOneOfSignatureSha256Hash_failsIfPackageNameMatchAndOneOfSignatur
private static byte[] getSha256Hash(Signature signature) {
return Hashing.sha256().hashBytes(signature.toByteArray()).asBytes();
}

private static AsyncSecurityPolicy makeAsyncPolicy(Function<Integer, ListenableFuture<Status>> statusProvider) {
return new AsyncSecurityPolicy() {
@Override
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
return statusProvider.apply(uid);
}
};
}
}
Loading