mirror of https://github.com/grpc/grpc-java.git
binder: Introduce server pre-authorization (#12127)
grpc-binder clients authorize servers by checking the UID of the sender of the SETUP_TRANSPORT Binder transaction against some SecurityPolicy. But merely binding to an unauthorized server to learn its UID can enable "keep-alive" and "background activity launch" abuse, even if security policy ultimately decides the connection is unauthorized. Pre-authorization mitigates this kind of abuse by looking up and authorizing a candidate server Application's UID before binding to it. Pre-auth is especially important when the server's address is not fixed in advance but discovered by PackageManager lookup.
This commit is contained in:
parent
6dfa03c51c
commit
94532a6b56
|
@ -72,6 +72,7 @@ dependencies {
|
|||
androidTestImplementation testFixtures(project(':grpc-core'))
|
||||
|
||||
testFixturesImplementation libraries.guava.testlib
|
||||
testFixturesImplementation testFixtures(project(':grpc-core'))
|
||||
}
|
||||
|
||||
import net.ltgt.gradle.errorprone.CheckSeverity
|
||||
|
|
|
@ -172,6 +172,12 @@ public final class BinderClientTransportTest {
|
|||
return this;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeServer) {
|
||||
factoryBuilder.setPreAuthorizeServers(preAuthorizeServer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinderTransport.BinderClientTransport build() {
|
||||
return factoryBuilder
|
||||
.buildClientTransportFactory()
|
||||
|
@ -372,11 +378,12 @@ public final class BinderClientTransportTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
|
||||
public void testBlackHoleSecurityPolicyAuthTimeout() throws Exception {
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
transport =
|
||||
new BinderClientTransportBuilder()
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.setPreAuthorizeServer(false)
|
||||
.setReadyTimeoutMillis(1_234)
|
||||
.build();
|
||||
transport.start(transportListener).run();
|
||||
|
@ -387,15 +394,39 @@ public final class BinderClientTransportTest {
|
|||
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
|
||||
assertThat(transportStatus.getDescription()).contains("1234");
|
||||
transportListener.awaitTermination();
|
||||
|
||||
// If the transport gave up waiting on auth, it should cancel its request.
|
||||
assertThat(authRequest.isCancelled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncSecurityPolicyFailure() throws Exception {
|
||||
public void testBlackHoleSecurityPolicyPreAuthTimeout() throws Exception {
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
|
||||
transport =
|
||||
new BinderClientTransportBuilder()
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.setPreAuthorizeServer(true)
|
||||
.setReadyTimeoutMillis(1_234)
|
||||
.build();
|
||||
transport.start(transportListener).run();
|
||||
// Take the next authRequest but don't respond to it, in order to trigger the ready timeout.
|
||||
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
|
||||
|
||||
Status transportStatus = transportListener.awaitShutdown();
|
||||
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
|
||||
assertThat(transportStatus.getDescription()).contains("1234");
|
||||
transportListener.awaitTermination();
|
||||
// If the transport gave up waiting on auth, it should cancel its request.
|
||||
assertThat(preAuthRequest.isCancelled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncSecurityPolicyAuthFailure() throws Exception {
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
transport =
|
||||
new BinderClientTransportBuilder()
|
||||
.setPreAuthorizeServer(false)
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.build();
|
||||
RuntimeException exception = new NullPointerException();
|
||||
transport.start(transportListener).run();
|
||||
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
|
||||
|
@ -406,15 +437,55 @@ public final class BinderClientTransportTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncSecurityPolicySuccess() throws Exception {
|
||||
public void testAsyncSecurityPolicyPreAuthFailure() throws Exception {
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
|
||||
transport =
|
||||
new BinderClientTransportBuilder()
|
||||
.setPreAuthorizeServer(true)
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.build();
|
||||
RuntimeException exception = new NullPointerException();
|
||||
transport.start(transportListener).run();
|
||||
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
|
||||
Status transportStatus = transportListener.awaitShutdown();
|
||||
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
|
||||
assertThat(transportStatus.getCause()).isEqualTo(exception);
|
||||
transportListener.awaitTermination();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncSecurityPolicyAuthSuccess() throws Exception {
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
transport =
|
||||
new BinderClientTransportBuilder()
|
||||
.setPreAuthorizeServer(false)
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.build();
|
||||
transport.start(transportListener).run();
|
||||
securityPolicy
|
||||
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
|
||||
.setResult(Status.PERMISSION_DENIED);
|
||||
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
|
||||
Status transportStatus = transportListener.awaitShutdown();
|
||||
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
|
||||
assertThat(transportStatus.getDescription()).contains("xyzzy");
|
||||
transportListener.awaitTermination();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncSecurityPolicyPreAuthSuccess() throws Exception {
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
transport =
|
||||
new BinderClientTransportBuilder()
|
||||
.setPreAuthorizeServer(true)
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.build();
|
||||
transport.start(transportListener).run();
|
||||
securityPolicy
|
||||
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
|
||||
.setResult(Status.PERMISSION_DENIED.withDescription("xyzzy"));
|
||||
Status transportStatus = transportListener.awaitShutdown();
|
||||
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
|
||||
assertThat(transportStatus.getDescription()).contains("xyzzy");
|
||||
transportListener.awaitTermination();
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ package io.grpc.binder;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.os.UserHandle;
|
||||
import io.grpc.Attributes;
|
||||
import io.grpc.EquivalentAddressGroup;
|
||||
import io.grpc.ExperimentalApi;
|
||||
import io.grpc.NameResolver;
|
||||
|
||||
|
@ -46,4 +48,16 @@ public final class ApiConstants {
|
|||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/10173")
|
||||
public static final NameResolver.Args.Key<UserHandle> TARGET_ANDROID_USER =
|
||||
NameResolver.Args.Key.create("target-android-user");
|
||||
|
||||
/**
|
||||
* Lets you override a Channel's pre-auth configuration (see {@link
|
||||
* BinderChannelBuilder#preAuthorizeServers(boolean)}) for a given {@link EquivalentAddressGroup}.
|
||||
*
|
||||
* <p>A {@link NameResolver} that discovers servers from an untrusted source like PackageManager
|
||||
* can use this to force server pre-auth and prevent abuse.
|
||||
*/
|
||||
@EquivalentAddressGroup.Attr
|
||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
|
||||
public static final Attributes.Key<Boolean> PRE_AUTH_SERVER_OVERRIDE =
|
||||
Attributes.Key.create("pre-auth-server-override");
|
||||
}
|
||||
|
|
|
@ -279,6 +279,35 @@ public final class BinderChannelBuilder extends ForwardingChannelBuilder<BinderC
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks servers against this Channel's {@link SecurityPolicy} *before* binding.
|
||||
*
|
||||
* <p>Android users can be tricked into installing a malicious app with the same package name as a
|
||||
* legitimate server. That's why we don't send calls to a server until it has been authorized by
|
||||
* an appropriate {@link SecurityPolicy}. But merely binding to a malicious server can enable
|
||||
* "keep-alive" and "background activity launch" abuse, even if it's ultimately unauthorized.
|
||||
* Pre-authorization mitigates these threats by performing a preliminary {@link SecurityPolicy}
|
||||
* check against a server app's PackageManager-registered identity without actually creating an
|
||||
* instance of it. This is especially important for security when the server's direct address
|
||||
* isn't known in advance but rather resolved via target URI or discovered by other means.
|
||||
*
|
||||
* <p>Note that, unlike ordinary authorization, pre-authorization is performed against the server
|
||||
* app's UID, not the UID of the process hosting the bound Service. These can be different, most
|
||||
* commonly due to services that set `android:isolatedProcess=true`.
|
||||
*
|
||||
* <p>Pre-authorization is strongly recommended but it remains optional for now because of this
|
||||
* behavior change and the small performance cost.
|
||||
*
|
||||
* <p>The default value of this property is false but it will become true in a future release.
|
||||
* Clients that require a particular behavior should configure it explicitly using this method
|
||||
* rather than relying on the default.
|
||||
*/
|
||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12191")
|
||||
public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) {
|
||||
transportFactoryBuilder.setPreAuthorizeServers(preAuthorize);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) {
|
||||
checkState(
|
||||
|
|
|
@ -16,10 +16,12 @@
|
|||
|
||||
package io.grpc.binder.internal;
|
||||
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.MainThread;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusException;
|
||||
|
||||
/** An interface for managing a {@code Binder} connection. */
|
||||
interface Bindable {
|
||||
|
@ -45,6 +47,19 @@ interface Bindable {
|
|||
void onUnbound(Status reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches details about the remote Service from PackageManager without binding to it.
|
||||
*
|
||||
* <p>Resolving an untrusted address before binding to it lets you screen out problematic servers
|
||||
* before giving them a chance to run. However, note that the identity/existence of the resolved
|
||||
* Service can change between the time this method returns and the time you actually bind/connect
|
||||
* to it. For example, suppose the target package gets uninstalled or upgraded right after this
|
||||
* method returns. In {@link Observer#onBound}, you should verify that the server you resolved is
|
||||
* the same one you connected to.
|
||||
*/
|
||||
@AnyThread
|
||||
ServiceInfo resolve() throws StatusException;
|
||||
|
||||
/**
|
||||
* Attempt to bind with the remote service.
|
||||
*
|
||||
|
|
|
@ -55,6 +55,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
|||
final InboundParcelablePolicy inboundParcelablePolicy;
|
||||
final OneWayBinderProxy.Decorator binderDecorator;
|
||||
final long readyTimeoutMillis;
|
||||
final boolean preAuthorizeServers; // TODO(jdcormie): Default to true.
|
||||
|
||||
ScheduledExecutorService executorService;
|
||||
Executor offloadExecutor;
|
||||
|
@ -75,6 +76,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
|||
inboundParcelablePolicy = checkNotNull(builder.inboundParcelablePolicy);
|
||||
binderDecorator = checkNotNull(builder.binderDecorator);
|
||||
readyTimeoutMillis = builder.readyTimeoutMillis;
|
||||
preAuthorizeServers = builder.preAuthorizeServers;
|
||||
|
||||
executorService = scheduledExecutorPool.getObject();
|
||||
offloadExecutor = offloadExecutorPool.getObject();
|
||||
|
@ -128,6 +130,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
|||
InboundParcelablePolicy inboundParcelablePolicy = InboundParcelablePolicy.DEFAULT;
|
||||
OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR;
|
||||
long readyTimeoutMillis = 60_000;
|
||||
boolean preAuthorizeServers;
|
||||
|
||||
@Override
|
||||
public BinderClientTransportFactory buildClientTransportFactory() {
|
||||
|
@ -216,5 +219,11 @@ public final class BinderClientTransportFactory implements ClientTransportFactor
|
|||
this.readyTimeoutMillis = readyTimeoutMillis;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Whether to check server addresses against the SecurityPolicy *before* binding to them. */
|
||||
public Builder setPreAuthorizeServers(boolean preAuthorizeServers) {
|
||||
this.preAuthorizeServers = preAuthorizeServers;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,11 @@ package io.grpc.binder.internal;
|
|||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.util.concurrent.Futures.immediateFuture;
|
||||
import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.DeadObjectException;
|
||||
import android.os.IBinder;
|
||||
|
@ -78,7 +80,6 @@ import java.util.concurrent.Executor;
|
|||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -574,6 +575,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
|
||||
private final long readyTimeoutMillis;
|
||||
private final PingTracker pingTracker;
|
||||
private final boolean preAuthorizeServer;
|
||||
|
||||
@Nullable private ManagedClientTransport.Listener clientTransportListener;
|
||||
|
||||
|
@ -585,6 +587,10 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
@GuardedBy("this")
|
||||
@Nullable private ListenableFuture<Status> authResultFuture; // null before we check auth.
|
||||
|
||||
@GuardedBy("this")
|
||||
@Nullable
|
||||
private ListenableFuture<Status> preAuthResultFuture; // null before we pre-auth.
|
||||
|
||||
/**
|
||||
* Constructs a new transport instance.
|
||||
*
|
||||
|
@ -609,6 +615,9 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
this.securityPolicy = factory.securityPolicy;
|
||||
this.offloadExecutor = offloadExecutorPool.getObject();
|
||||
this.readyTimeoutMillis = factory.readyTimeoutMillis;
|
||||
Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE);
|
||||
this.preAuthorizeServer =
|
||||
preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers;
|
||||
numInUseStreams = new AtomicInteger();
|
||||
pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id));
|
||||
|
||||
|
@ -650,7 +659,16 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
synchronized (BinderClientTransport.this) {
|
||||
if (inState(TransportState.NOT_STARTED)) {
|
||||
setState(TransportState.SETUP);
|
||||
serviceBinding.bind();
|
||||
try {
|
||||
if (preAuthorizeServer) {
|
||||
preAuthorize(serviceBinding.resolve());
|
||||
} else {
|
||||
serviceBinding.bind();
|
||||
}
|
||||
} catch (StatusException e) {
|
||||
shutdownInternal(e.getStatus(), true);
|
||||
return;
|
||||
}
|
||||
if (readyTimeoutMillis >= 0) {
|
||||
readyTimeoutFuture =
|
||||
getScheduledExecutorService()
|
||||
|
@ -664,6 +682,43 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
};
|
||||
}
|
||||
|
||||
@GuardedBy("this")
|
||||
private void preAuthorize(ServiceInfo serviceInfo) {
|
||||
// It's unlikely, but the identity/existence of this Service could change by the time we
|
||||
// actually connect. It doesn't matter though, because:
|
||||
// - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer
|
||||
// will eventually retry using a new transport instance that will see the Service's new state.
|
||||
// - If pre-auth succeeds (but would fail against the server's new state), we might give an
|
||||
// unauthorized server a chance to run, but the connection will still fail by SecurityPolicy
|
||||
// check later in handshake. Pre-auth remains effective at mitigating abuse because malware
|
||||
// can't typically control the exact timing of its installation.
|
||||
preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid);
|
||||
Futures.addCallback(
|
||||
preAuthResultFuture,
|
||||
new FutureCallback<Status>() {
|
||||
@Override
|
||||
public void onSuccess(Status result) {
|
||||
handlePreAuthResult(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
handleAuthResult(t);
|
||||
}
|
||||
},
|
||||
offloadExecutor);
|
||||
}
|
||||
|
||||
private synchronized void handlePreAuthResult(Status authorization) {
|
||||
if (inState(TransportState.SETUP)) {
|
||||
if (!authorization.isOk()) {
|
||||
shutdownInternal(authorization, true);
|
||||
} else {
|
||||
serviceBinding.bind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void onReadyTimeout() {
|
||||
if (inState(TransportState.SETUP)) {
|
||||
readyTimeoutFuture = null;
|
||||
|
@ -758,6 +813,9 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
readyTimeoutFuture.cancel(false);
|
||||
readyTimeoutFuture = null;
|
||||
}
|
||||
if (preAuthResultFuture != null) {
|
||||
preAuthResultFuture.cancel(false); // No effect if already complete.
|
||||
}
|
||||
if (authResultFuture != null) {
|
||||
authResultFuture.cancel(false); // No effect if already complete.
|
||||
}
|
||||
|
@ -780,11 +838,7 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
shutdownInternal(
|
||||
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
|
||||
} else {
|
||||
authResultFuture =
|
||||
(securityPolicy instanceof AsyncSecurityPolicy)
|
||||
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
|
||||
: Futures.submit(
|
||||
() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
|
||||
authResultFuture = checkServerAuthorizationAsync(remoteUid);
|
||||
Futures.addCallback(
|
||||
authResultFuture,
|
||||
new FutureCallback<Status>() {
|
||||
|
@ -803,6 +857,12 @@ public abstract class BinderTransport implements IBinder.DeathRecipient {
|
|||
}
|
||||
}
|
||||
|
||||
private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
|
||||
return (securityPolicy instanceof AsyncSecurityPolicy)
|
||||
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
|
||||
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
|
||||
}
|
||||
|
||||
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
|
||||
if (inState(TransportState.SETUP)) {
|
||||
if (!authorization.isOk()) {
|
||||
|
|
|
@ -23,14 +23,22 @@ import android.content.ComponentName;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.UserHandle;
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.MainThread;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusException;
|
||||
import io.grpc.binder.BinderChannelCredentials;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
@ -85,6 +93,8 @@ final class ServiceBinding implements Bindable, ServiceConnection {
|
|||
private final Observer observer;
|
||||
private final Executor mainThreadExecutor;
|
||||
|
||||
private static volatile Method queryIntentServicesAsUserMethod;
|
||||
|
||||
@GuardedBy("this")
|
||||
private State state;
|
||||
|
||||
|
@ -247,6 +257,71 @@ final class ServiceBinding implements Bindable, ServiceConnection {
|
|||
}
|
||||
}
|
||||
|
||||
// Sadly the PackageManager#resolveServiceAsUser() API we need isn't part of the SDK or even a
|
||||
// @SystemApi as of this writing. Modern Android prevents even system apps from calling it, by any
|
||||
// means (https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces).
|
||||
// So instead we call queryIntentServicesAsUser(), which does more than we need but *is* a
|
||||
// @SystemApi in all the SDK versions where we support cross-user Channels.
|
||||
@Nullable
|
||||
private static ResolveInfo resolveServiceAsUser(
|
||||
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
|
||||
List<ResolveInfo> results =
|
||||
queryIntentServicesAsUser(packageManager, intent, flags, targetUserHandle);
|
||||
// The first query result is "what would be returned by resolveService", per the javadoc.
|
||||
return (results != null && !results.isEmpty()) ? results.get(0) : null;
|
||||
}
|
||||
|
||||
// The cross-user Channel feature requires the client to be a system app so we assume @SystemApi
|
||||
// queryIntentServicesAsUser() is visible to us at runtime. It would be visible at build time too,
|
||||
// if our host system app were written to call it directly. We only have to use reflection here
|
||||
// because grpc-java is a library built outside the Android source tree where the compiler can't
|
||||
// see the "non-SDK" @SystemApis that we need.
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked") // Safe by PackageManager#queryIntentServicesAsUser spec in AOSP.
|
||||
private static List<ResolveInfo> queryIntentServicesAsUser(
|
||||
PackageManager packageManager, Intent intent, int flags, UserHandle targetUserHandle) {
|
||||
try {
|
||||
if (queryIntentServicesAsUserMethod == null) {
|
||||
synchronized (ServiceBinding.class) {
|
||||
if (queryIntentServicesAsUserMethod == null) {
|
||||
queryIntentServicesAsUserMethod =
|
||||
PackageManager.class.getMethod(
|
||||
"queryIntentServicesAsUser", Intent.class, int.class, UserHandle.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (List<ResolveInfo>)
|
||||
queryIntentServicesAsUserMethod.invoke(packageManager, intent, flags, targetUserHandle);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new VerifyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Override
|
||||
public ServiceInfo resolve() throws StatusException {
|
||||
checkState(sourceContext != null);
|
||||
PackageManager packageManager = sourceContext.getPackageManager();
|
||||
int flags = 0;
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
// Filter out non-'directBootAware' <service>s when 'targetUserHandle' is locked. Here's why:
|
||||
// Callers want 'bindIntent' to #resolve() to the same thing a follow-up call to #bind() will.
|
||||
// But bindService() *always* ignores services that can't presently be created for lack of
|
||||
// 'directBootAware'-ness. This flag explicitly tells resolveService() to act the same way.
|
||||
flags |= PackageManager.MATCH_DIRECT_BOOT_AUTO;
|
||||
}
|
||||
ResolveInfo resolveInfo =
|
||||
targetUserHandle != null
|
||||
? resolveServiceAsUser(packageManager, bindIntent, flags, targetUserHandle)
|
||||
: packageManager.resolveService(bindIntent, flags);
|
||||
if (resolveInfo == null) {
|
||||
throw Status.UNIMPLEMENTED // Same status code as when bindService() returns false.
|
||||
.withDescription("resolveService(" + bindIntent + " / " + targetUserHandle + ") was null")
|
||||
.asException();
|
||||
}
|
||||
return resolveInfo.serviceInfo;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private void clearReferences() {
|
||||
sourceContext = null;
|
||||
|
|
|
@ -22,7 +22,13 @@ import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
|
|||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.core.content.pm.ApplicationInfoBuilder;
|
||||
import androidx.test.core.content.pm.PackageInfoBuilder;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
@ -46,11 +52,13 @@ import org.junit.After;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
|
||||
import org.robolectric.annotation.LooperMode;
|
||||
import org.robolectric.annotation.LooperMode.Mode;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||
@LooperMode(Mode.INSTRUMENTATION_TEST)
|
||||
public final class RobolectricBinderSecurityTest {
|
||||
|
||||
|
@ -62,10 +70,33 @@ public final class RobolectricBinderSecurityTest {
|
|||
private ManagedChannel channel;
|
||||
private Server server;
|
||||
|
||||
@Parameter public boolean preAuthServersParam;
|
||||
|
||||
@Parameters(name = "preAuthServersParam={0}")
|
||||
public static ImmutableList<Boolean> data() {
|
||||
return ImmutableList.of(true, false);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
ApplicationInfo serverAppInfo =
|
||||
ApplicationInfoBuilder.newBuilder().setPackageName(context.getPackageName()).build();
|
||||
serverAppInfo.uid = android.os.Process.myUid();
|
||||
PackageInfo serverPkgInfo =
|
||||
PackageInfoBuilder.newBuilder()
|
||||
.setPackageName(serverAppInfo.packageName)
|
||||
.setApplicationInfo(serverAppInfo)
|
||||
.build();
|
||||
shadowOf(context.getPackageManager()).installPackage(serverPkgInfo);
|
||||
|
||||
ServiceInfo serviceInfo = new ServiceInfo();
|
||||
serviceInfo.name = "SomeService";
|
||||
serviceInfo.packageName = serverAppInfo.packageName;
|
||||
serviceInfo.applicationInfo = serverAppInfo;
|
||||
shadowOf(context.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||
|
||||
AndroidComponentAddress listenAddress =
|
||||
AndroidComponentAddress.forRemoteComponent(context.getPackageName(), "HostService");
|
||||
AndroidComponentAddress.forRemoteComponent(serviceInfo.packageName, serviceInfo.name);
|
||||
|
||||
MethodDescriptor<Empty, Empty> methodDesc = getMethodDescriptor();
|
||||
ServerCallHandler<Empty, Empty> callHandler =
|
||||
|
@ -110,6 +141,7 @@ public final class RobolectricBinderSecurityTest {
|
|||
checkNotNull(binderReceiver.get()));
|
||||
channel =
|
||||
BinderChannelBuilder.forAddress(listenAddress, context)
|
||||
.preAuthorizeServers(preAuthServersParam)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,13 +16,29 @@
|
|||
|
||||
package io.grpc.binder.internal;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.core.content.pm.ApplicationInfoBuilder;
|
||||
import androidx.test.core.content.pm.PackageInfoBuilder;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import io.grpc.Attributes;
|
||||
import io.grpc.ServerStreamTracer;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.binder.AndroidComponentAddress;
|
||||
import io.grpc.binder.ApiConstants;
|
||||
import io.grpc.binder.AsyncSecurityPolicy;
|
||||
import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest;
|
||||
import io.grpc.internal.AbstractTransportTest;
|
||||
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
|
||||
import io.grpc.internal.GrpcUtil;
|
||||
|
@ -33,12 +49,20 @@ import io.grpc.internal.SharedResourcePool;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
|
||||
import org.robolectric.annotation.LooperMode;
|
||||
import org.robolectric.annotation.LooperMode.Mode;
|
||||
import org.robolectric.shadows.ShadowBinder;
|
||||
|
||||
/**
|
||||
* All of the AbstractTransportTest cases applied to {@link BinderTransport} running in a
|
||||
|
@ -52,7 +76,7 @@ import org.robolectric.annotation.LooperMode.Mode;
|
|||
* meaning test cases don't run on the main thread. This supports the AbstractTransportTest approach
|
||||
* where the test thread frequently blocks waiting for transport state changes to take effect.
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||
@LooperMode(Mode.INSTRUMENTATION_TEST)
|
||||
public final class RobolectricBinderTransportTest extends AbstractTransportTest {
|
||||
|
||||
|
@ -64,14 +88,57 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
|||
private final ObjectPool<Executor> serverExecutorPool =
|
||||
SharedResourcePool.forResource(GrpcUtil.SHARED_CHANNEL_EXECUTOR);
|
||||
|
||||
@Rule public MockitoRule mocks = MockitoJUnit.rule();
|
||||
|
||||
@Mock AsyncSecurityPolicy mockClientSecurityPolicy;
|
||||
|
||||
ApplicationInfo serverAppInfo;
|
||||
PackageInfo serverPkgInfo;
|
||||
ServiceInfo serviceInfo;
|
||||
|
||||
private int nextServerAddress;
|
||||
|
||||
@Parameter public boolean preAuthServersParam;
|
||||
|
||||
@Parameters(name = "preAuthServersParam={0}")
|
||||
public static ImmutableList<Boolean> data() {
|
||||
return ImmutableList.of(true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
serverAppInfo =
|
||||
ApplicationInfoBuilder.newBuilder().setPackageName("the.server.package").build();
|
||||
serverAppInfo.uid = android.os.Process.myUid();
|
||||
serverPkgInfo =
|
||||
PackageInfoBuilder.newBuilder()
|
||||
.setPackageName(serverAppInfo.packageName)
|
||||
.setApplicationInfo(serverAppInfo)
|
||||
.build();
|
||||
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
|
||||
|
||||
serviceInfo = new ServiceInfo();
|
||||
serviceInfo.name = "SomeService";
|
||||
serviceInfo.packageName = serverAppInfo.packageName;
|
||||
serviceInfo.applicationInfo = serverAppInfo;
|
||||
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void requestRealisticBindServiceBehavior() {
|
||||
shadowOf(application).setBindServiceCallsOnServiceConnectedDirectly(false);
|
||||
shadowOf(application).setUnbindServiceCallsOnServiceDisconnected(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InternalServer newServer(List<ServerStreamTracer.Factory> streamTracerFactories) {
|
||||
AndroidComponentAddress listenAddr = AndroidComponentAddress.forBindIntent(
|
||||
new Intent()
|
||||
.setClassName(application.getPackageName(), "HostService")
|
||||
.setAction("io.grpc.action.BIND." + nextServerAddress++));
|
||||
AndroidComponentAddress listenAddr =
|
||||
AndroidComponentAddress.forBindIntent(
|
||||
new Intent()
|
||||
.setClassName(serviceInfo.packageName, serviceInfo.name)
|
||||
.setAction("io.grpc.action.BIND." + nextServerAddress++));
|
||||
|
||||
BinderServer binderServer =
|
||||
new BinderServer.Builder()
|
||||
|
@ -81,6 +148,7 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
|||
.setStreamTracerFactories(streamTracerFactories)
|
||||
.build();
|
||||
|
||||
shadowOf(application.getPackageManager()).addServiceIfNotPresent(listenAddr.getComponent());
|
||||
shadowOf(application)
|
||||
.setComponentNameAndServiceForBindServiceForIntent(
|
||||
listenAddr.asBindIntent(), listenAddr.getComponent(), binderServer.getHostBinder());
|
||||
|
@ -97,22 +165,30 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
|||
return newServer(streamTracerFactories);
|
||||
}
|
||||
|
||||
BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() {
|
||||
return new BinderClientTransportFactory.Builder()
|
||||
.setPreAuthorizeServers(preAuthServersParam)
|
||||
.setSourceContext(application)
|
||||
.setScheduledExecutorPool(executorServicePool)
|
||||
.setOffloadExecutorPool(offloadExecutorPool);
|
||||
}
|
||||
|
||||
BinderClientTransportBuilder newClientTransportBuilder() {
|
||||
return new BinderClientTransportBuilder()
|
||||
.setFactory(newClientTransportFactoryBuilder().buildClientTransportFactory())
|
||||
.setServerAddress(server.getListenSocketAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ManagedClientTransport newClientTransport(InternalServer server) {
|
||||
BinderClientTransportFactory.Builder builder =
|
||||
new BinderClientTransportFactory.Builder()
|
||||
.setSourceContext(application)
|
||||
.setScheduledExecutorPool(executorServicePool)
|
||||
.setOffloadExecutorPool(offloadExecutorPool);
|
||||
|
||||
ClientTransportOptions options = new ClientTransportOptions();
|
||||
options.setEagAttributes(eagAttrs());
|
||||
options.setChannelLogger(transportLogger());
|
||||
|
||||
return new BinderTransport.BinderClientTransport(
|
||||
builder.buildClientTransportFactory(),
|
||||
(AndroidComponentAddress) server.getListenSocketAddress(),
|
||||
options);
|
||||
return newClientTransportBuilder()
|
||||
.setServerAddress(server.getListenSocketAddress())
|
||||
.setOptions(options)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -120,6 +196,74 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
|
|||
return ((AndroidComponentAddress) server.getListenSocketAddress()).getAuthority();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientAuthorizesServerUidsInOrder() throws Exception {
|
||||
// TODO(jdcormie): In real Android, Binder#getCallingUid is thread-local but Robolectric only
|
||||
// lets us fake value this *globally*. So the ShadowBinder#setCallingUid() here unrealistically
|
||||
// affects the server's view of the client's uid too. For now this doesn't matter because this
|
||||
// test never exercises server SecurityPolicy.
|
||||
ShadowBinder.setCallingUid(11111); // UID of the server *process*.
|
||||
|
||||
serverPkgInfo.applicationInfo.uid = 22222; // UID of the server *app*, which can be different.
|
||||
shadowOf(application.getPackageManager()).installPackage(serverPkgInfo);
|
||||
shadowOf(application.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||
server = newServer(ImmutableList.of());
|
||||
server.start(serverListener);
|
||||
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
client =
|
||||
newClientTransportBuilder()
|
||||
.setFactory(
|
||||
newClientTransportFactoryBuilder()
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.buildClientTransportFactory())
|
||||
.build();
|
||||
runIfNotNull(client.start(mockClientTransportListener));
|
||||
|
||||
if (preAuthServersParam) {
|
||||
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||
assertThat(preAuthRequest.uid).isEqualTo(22222);
|
||||
verify(mockClientTransportListener, never()).transportReady();
|
||||
preAuthRequest.setResult(Status.OK);
|
||||
}
|
||||
|
||||
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||
assertThat(authRequest.uid).isEqualTo(11111);
|
||||
verify(mockClientTransportListener, never()).transportReady();
|
||||
authRequest.setResult(Status.OK);
|
||||
|
||||
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void eagAttributeCanOverrideChannelPreAuthServerSetting() throws Exception {
|
||||
server.start(serverListener);
|
||||
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
|
||||
ClientTransportOptions options = new ClientTransportOptions();
|
||||
options.setEagAttributes(
|
||||
Attributes.newBuilder().set(ApiConstants.PRE_AUTH_SERVER_OVERRIDE, true).build());
|
||||
client =
|
||||
newClientTransportBuilder()
|
||||
.setOptions(options)
|
||||
.setFactory(
|
||||
newClientTransportFactoryBuilder()
|
||||
.setPreAuthorizeServers(preAuthServersParam) // To be overridden.
|
||||
.setSecurityPolicy(securityPolicy)
|
||||
.buildClientTransportFactory())
|
||||
.build();
|
||||
runIfNotNull(client.start(mockClientTransportListener));
|
||||
|
||||
AuthRequest preAuthRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||
verify(mockClientTransportListener, never()).transportReady();
|
||||
preAuthRequest.setResult(Status.OK);
|
||||
|
||||
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS);
|
||||
verify(mockClientTransportListener, never()).transportReady();
|
||||
authRequest.setResult(Status.OK);
|
||||
|
||||
verify(mockClientTransportListener, timeout(TIMEOUT_MS)).transportReady();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("See BinderTransportTest#socketStats.")
|
||||
@Override
|
||||
|
|
|
@ -19,6 +19,7 @@ package io.grpc.binder.internal;
|
|||
import static android.content.Context.BIND_AUTO_CREATE;
|
||||
import static android.os.Looper.getMainLooper;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
|
@ -27,6 +28,7 @@ import android.app.admin.DevicePolicyManager;
|
|||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.UserHandle;
|
||||
|
@ -34,6 +36,7 @@ import androidx.core.content.ContextCompat;
|
|||
import androidx.test.core.app.ApplicationProvider;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.Status.Code;
|
||||
import io.grpc.StatusException;
|
||||
import io.grpc.binder.BinderChannelCredentials;
|
||||
import io.grpc.binder.internal.Bindable.Observer;
|
||||
import java.util.Arrays;
|
||||
|
@ -59,6 +62,7 @@ public final class ServiceBindingTest {
|
|||
|
||||
private Application appContext;
|
||||
private ComponentName serviceComponent;
|
||||
private ServiceInfo serviceInfo = new ServiceInfo();
|
||||
private ShadowApplication shadowApplication;
|
||||
private TestObserver observer;
|
||||
private ServiceBinding binding;
|
||||
|
@ -67,13 +71,17 @@ public final class ServiceBindingTest {
|
|||
public void setUp() {
|
||||
appContext = ApplicationProvider.getApplicationContext();
|
||||
serviceComponent = new ComponentName("DUMMY", "SERVICE");
|
||||
serviceInfo.packageName = serviceComponent.getPackageName();
|
||||
serviceInfo.name = serviceComponent.getClassName();
|
||||
observer = new TestObserver();
|
||||
|
||||
shadowApplication = shadowOf(appContext);
|
||||
shadowApplication.setComponentNameAndServiceForBindService(serviceComponent, mockBinder);
|
||||
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||
|
||||
// Don't call onServiceDisconnected() upon unbindService(), just like the real Android doesn't.
|
||||
shadowApplication.setUnbindServiceCallsOnServiceDisconnected(false);
|
||||
shadowApplication.setBindServiceCallsOnServiceConnectedDirectly(false);
|
||||
|
||||
binding = newBuilder().build();
|
||||
shadowOf(getMainLooper()).idle();
|
||||
|
@ -276,6 +284,49 @@ public final class ServiceBindingTest {
|
|||
assertThat(binding.isSourceContextCleared()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolve() throws Exception {
|
||||
serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field.
|
||||
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||
ServiceInfo resolvedServiceInfo = binding.resolve();
|
||||
assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = 33)
|
||||
public void testResolveWithTargetUserHandle() throws Exception {
|
||||
serviceInfo.processName = "x"; // ServiceInfo has no equals() so look for one distinctive field.
|
||||
// Robolectric just ignores the user arg to resolveServiceAsUser() so this is all we can do.
|
||||
shadowOf(appContext.getPackageManager()).addOrUpdateService(serviceInfo);
|
||||
binding = newBuilder().setTargetUserHandle(generateUserHandle(/* userId= */ 0)).build();
|
||||
ServiceInfo resolvedServiceInfo = binding.resolve();
|
||||
assertThat(resolvedServiceInfo.processName).isEqualTo(serviceInfo.processName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveNonExistentServiceThrows() throws Exception {
|
||||
ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService");
|
||||
binding = newBuilder().setTargetComponent(doesNotExistService).build();
|
||||
StatusException statusException = assertThrows(StatusException.class, binding::resolve);
|
||||
assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED);
|
||||
assertThat(statusException.getStatus().getDescription()).contains("does.not.exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = 33)
|
||||
public void testResolveNonExistentServiceWithTargetUserThrows() throws Exception {
|
||||
ComponentName doesNotExistService = new ComponentName("does.not.exist", "NoService");
|
||||
binding =
|
||||
newBuilder()
|
||||
.setTargetUserHandle(generateUserHandle(/* userId= */ 12345))
|
||||
.setTargetComponent(doesNotExistService)
|
||||
.build();
|
||||
StatusException statusException = assertThrows(StatusException.class, binding::resolve);
|
||||
assertThat(statusException.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED);
|
||||
assertThat(statusException.getStatus().getDescription()).contains("does.not.exist");
|
||||
assertThat(statusException.getStatus().getDescription()).contains("12345");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = 30)
|
||||
public void testBindWithDeviceAdmin() throws Exception {
|
||||
|
@ -284,7 +335,7 @@ public final class ServiceBindingTest {
|
|||
allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0);
|
||||
binding =
|
||||
newBuilder()
|
||||
.setTargetUserHandle(UserHandle.getUserHandleForUid(/* userId= */ 0))
|
||||
.setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0))
|
||||
.setTargetUserHandle(generateUserHandle(/* userId= */ 0))
|
||||
.setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent))
|
||||
.build();
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2025 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.internal;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import io.grpc.ChannelLogger;
|
||||
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
|
||||
import io.grpc.internal.TestUtils.NoopChannelLogger;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
/**
|
||||
* Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to
|
||||
* mention irrelevant details (go/tott/719).
|
||||
*/
|
||||
public class BinderClientTransportBuilder {
|
||||
private BinderClientTransportFactory factory;
|
||||
private SocketAddress serverAddress;
|
||||
private ChannelLogger channelLogger = new NoopChannelLogger();
|
||||
private io.grpc.internal.ClientTransportFactory.ClientTransportOptions options =
|
||||
new ClientTransportOptions();
|
||||
|
||||
public BinderClientTransportBuilder setServerAddress(SocketAddress serverAddress) {
|
||||
this.serverAddress = checkNotNull(serverAddress);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinderClientTransportBuilder setChannelLogger(ChannelLogger channelLogger) {
|
||||
this.channelLogger = checkNotNull(channelLogger);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinderClientTransportBuilder setOptions(ClientTransportOptions options) {
|
||||
this.options = checkNotNull(options);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinderClientTransportBuilder setFactory(BinderClientTransportFactory factory) {
|
||||
this.factory = checkNotNull(factory);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BinderTransport.BinderClientTransport build() {
|
||||
return factory.newClientTransport(
|
||||
checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger));
|
||||
}
|
||||
}
|
|
@ -92,7 +92,7 @@ public abstract class AbstractTransportTest {
|
|||
*/
|
||||
public static final int TEST_FLOW_CONTROL_WINDOW = 65 * 1024;
|
||||
|
||||
private static final int TIMEOUT_MS = 5000;
|
||||
protected static final int TIMEOUT_MS = 5000;
|
||||
|
||||
protected static final String GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES =
|
||||
"GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES";
|
||||
|
@ -2163,7 +2163,7 @@ public abstract class AbstractTransportTest {
|
|||
return true;
|
||||
}
|
||||
|
||||
private static void runIfNotNull(Runnable runnable) {
|
||||
protected static void runIfNotNull(Runnable runnable) {
|
||||
if (runnable != null) {
|
||||
runnable.run();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue