binder: Allow async/slow implementations of authorization checks (#10596)

Allow a security policy to returns a `ListenableFuture<Status>` that
callers can implement to perform slower auth checks (like network
calls, disk I/O etc.) without necessarily blocking the gRPC calling
thread.

Partially addresses: https://github.com/grpc/grpc-java/issues/10566
This commit is contained in:
Mateus Azis 2023-10-20 13:53:12 -07:00 committed by GitHub
parent ccf9101828
commit 8966680015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 17 deletions

View File

@ -25,6 +25,7 @@ import io.grpc.ServerStreamTracer;
import io.grpc.binder.AndroidComponentAddress;
import io.grpc.binder.BindServiceFlags;
import io.grpc.binder.BinderChannelCredentials;
import io.grpc.binder.BinderInternal;
import io.grpc.binder.HostServices;
import io.grpc.binder.InboundParcelablePolicy;
import io.grpc.binder.SecurityPolicies;
@ -69,7 +70,7 @@ public final class BinderTransportTest extends AbstractTransportTest {
BinderServer binderServer = new BinderServer(addr,
executorServicePool,
streamTracerFactories,
SecurityPolicies.serverInternalOnly(),
BinderInternal.createPolicyChecker(SecurityPolicies.serverInternalOnly()),
InboundParcelablePolicy.DEFAULT);
HostServices.configureService(addr,

View File

@ -18,6 +18,7 @@ package io.grpc.binder;
import android.os.IBinder;
import io.grpc.Internal;
import io.grpc.binder.internal.BinderTransportSecurity;
/**
* Helper class to expose IBinderReceiver methods for legacy internal builders.
@ -31,4 +32,14 @@ public class BinderInternal {
public static void setIBinder(IBinderReceiver receiver, IBinder binder) {
receiver.set(binder);
}
/**
* Creates a {@link BinderTransportSecurity.ServerPolicyChecker} from a
* {@link ServerSecurityPolicy}. This exposes to callers an interface to check security policies
* without causing hard dependencies on a specific class.
*/
public static BinderTransportSecurity.ServerPolicyChecker createPolicyChecker(
ServerSecurityPolicy securityPolicy) {
return securityPolicy::checkAuthorizationForServiceAsync;
}
}

View File

@ -23,11 +23,11 @@ import android.app.Service;
import android.os.IBinder;
import com.google.errorprone.annotations.DoNotCall;
import io.grpc.ExperimentalApi;
import io.grpc.ForwardingServerBuilder;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.binder.internal.BinderServer;
import io.grpc.binder.internal.BinderTransportSecurity;
import io.grpc.ForwardingServerBuilder;
import io.grpc.internal.FixedObjectPool;
import io.grpc.internal.GrpcUtil;
import io.grpc.internal.ServerImplBuilder;
@ -84,7 +84,7 @@ public final class BinderServerBuilder
listenAddress,
schedulerPool,
streamTracerFactories,
securityPolicy,
BinderInternal.createPolicyChecker(securityPolicy),
inboundParcelablePolicy);
BinderInternal.setIBinder(binderReceiver, server.getHostBinder());
return server;

View File

@ -17,6 +17,8 @@
package io.grpc.binder;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.Status;
import java.util.HashMap;
import java.util.Map;
@ -42,18 +44,42 @@ public final class ServerSecurityPolicy {
}
/**
* Return whether the given Android UID is authorized to access a particular service.
* Returns whether the given Android UID is authorized to access a particular service.
*
* <b>IMPORTANT</b>: This method may block for extended periods of time.
* <p><b>IMPORTANT</b>: This method may block for extended periods of time.
*
* @param uid The Android UID to authenticate.
* @param serviceName The name of the gRPC service being called.
* @deprecated Application code should not need to call this method.
*/
@CheckReturnValue
@Deprecated
public Status checkAuthorizationForService(int uid, String serviceName) {
return perServicePolicies.getOrDefault(serviceName, defaultPolicy).checkAuthorization(uid);
}
/**
* Returns whether the given Android UID is authorized to access a particular service.
*
* <p>This method never throws an exception. If the execution of the security policy check
* fails, a failed future with such exception is returned.
*
* @param uid The Android UID to authenticate.
* @param serviceName The name of the gRPC service being called.
* @return a future with the result of the authorization check. A failed future represents a
* failure to perform the authorization check, not that the access is denied.
*/
@CheckReturnValue
ListenableFuture<Status> checkAuthorizationForServiceAsync(int uid, String serviceName) {
SecurityPolicy securityPolicy = perServicePolicies.getOrDefault(serviceName, defaultPolicy);
try {
Status status = securityPolicy.checkAuthorization(uid);
return Futures.immediateFuture(status);
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
public static Builder newBuilder() {
return new Builder();
}

View File

@ -58,7 +58,7 @@ public final class BinderServer implements InternalServer, LeakSafeOneWayBinder.
private final ImmutableList<ServerStreamTracer.Factory> streamTracerFactories;
private final AndroidComponentAddress listenAddress;
private final LeakSafeOneWayBinder hostServiceBinder;
private final ServerSecurityPolicy serverSecurityPolicy;
private final BinderTransportSecurity.ServerPolicyChecker serverPolicyChecker;
private final InboundParcelablePolicy inboundParcelablePolicy;
@GuardedBy("this")
@ -74,13 +74,13 @@ public final class BinderServer implements InternalServer, LeakSafeOneWayBinder.
AndroidComponentAddress listenAddress,
ObjectPool<ScheduledExecutorService> executorServicePool,
List<? extends ServerStreamTracer.Factory> streamTracerFactories,
ServerSecurityPolicy serverSecurityPolicy,
BinderTransportSecurity.ServerPolicyChecker serverPolicyChecker,
InboundParcelablePolicy inboundParcelablePolicy) {
this.listenAddress = listenAddress;
this.executorServicePool = executorServicePool;
this.streamTracerFactories =
ImmutableList.copyOf(checkNotNull(streamTracerFactories, "streamTracerFactories"));
this.serverSecurityPolicy = checkNotNull(serverSecurityPolicy, "serverSecurityPolicy");
this.serverPolicyChecker = checkNotNull(serverPolicyChecker, "serverPolicyChecker");
this.inboundParcelablePolicy = inboundParcelablePolicy;
hostServiceBinder = new LeakSafeOneWayBinder(this);
}
@ -150,7 +150,7 @@ public final class BinderServer implements InternalServer, LeakSafeOneWayBinder.
.set(BinderTransport.REMOTE_UID, callingUid)
.set(BinderTransport.SERVER_AUTHORITY, listenAddress.getAuthority())
.set(BinderTransport.INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy);
BinderTransportSecurity.attachAuthAttrs(attrsBuilder, callingUid, serverSecurityPolicy);
BinderTransportSecurity.attachAuthAttrs(attrsBuilder, callingUid, serverPolicyChecker);
// Create a new transport and let our listener know about it.
BinderTransport.BinderServerTransport transport =
new BinderTransport.BinderServerTransport(

View File

@ -16,6 +16,7 @@
package io.grpc.binder.internal;
import com.google.common.util.concurrent.ListenableFuture;
import io.grpc.Attributes;
import io.grpc.Internal;
import io.grpc.Metadata;
@ -26,9 +27,9 @@ import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import io.grpc.binder.ServerSecurityPolicy;
import io.grpc.internal.GrpcAttributes;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import javax.annotation.CheckReturnValue;
/**
@ -60,15 +61,15 @@ public final class BinderTransportSecurity {
*
* @param builder The {@link Attributes.Builder} for the transport being created.
* @param remoteUid The remote UID of the transport.
* @param securityPolicy The policy to enforce on this transport.
* @param serverPolicyChecker The policy checker for this transport.
*/
@Internal
public static void attachAuthAttrs(
Attributes.Builder builder, int remoteUid, ServerSecurityPolicy securityPolicy) {
Attributes.Builder builder, int remoteUid, ServerPolicyChecker serverPolicyChecker) {
builder
.set(
TRANSPORT_AUTHORIZATION_STATE,
new TransportAuthorizationState(remoteUid, securityPolicy))
new TransportAuthorizationState(remoteUid, serverPolicyChecker))
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY);
}
@ -99,12 +100,12 @@ public final class BinderTransportSecurity {
*/
private static final class TransportAuthorizationState {
private final int uid;
private final ServerSecurityPolicy policy;
private final ServerPolicyChecker serverPolicyChecker;
private final ConcurrentHashMap<String, Status> serviceAuthorization;
TransportAuthorizationState(int uid, ServerSecurityPolicy policy) {
TransportAuthorizationState(int uid, ServerPolicyChecker serverPolicyChecker) {
this.uid = uid;
this.policy = policy;
this.serverPolicyChecker = serverPolicyChecker;
serviceAuthorization = new ConcurrentHashMap<>(8);
}
@ -123,11 +124,47 @@ public final class BinderTransportSecurity {
return authorization;
}
}
authorization = policy.checkAuthorizationForService(uid, serviceName);
try {
// TODO(10566): provide a synchronous version of "checkAuthorization" to avoid blocking the
// calling thread on the completion of the future.
authorization =
serverPolicyChecker.checkAuthorizationForServiceAsync(uid, serviceName).get();
} catch (ExecutionException e) {
// Do not cache this failure since it may be transient.
return Status.fromThrowable(e);
} catch (InterruptedException e) {
// Do not cache this failure since it may be transient.
Thread.currentThread().interrupt();
return Status.CANCELLED.withCause(e);
}
if (useCache) {
serviceAuthorization.putIfAbsent(serviceName, authorization);
}
return authorization;
}
}
/**
* Decides whether a given Android UID is authorized to access some resource.
*
* <p>This class provides the asynchronous version of {@link io.grpc.binder.SecurityPolicy},
* allowing implementations of authorization logic that involves slow or asynchronous calls
* without necessarily blocking the calling thread.
*
* @see io.grpc.binder.SecurityPolicy
*/
public interface ServerPolicyChecker {
/**
* Returns whether the given Android UID is authorized to access a particular service.
*
* <p>This method never throws an exception. If the execution of the security policy check
* fails, a failed future with such exception is returned.
*
* @param uid The Android UID to authenticate.
* @param serviceName The name of the gRPC service being called.
* @return a future with the result of the authorization check. A failed future represents a
* failure to perform the authorization check, not that the access is denied.
*/
ListenableFuture<Status> checkAuthorizationForServiceAsync(int uid, String serviceName);
}
}