mirror of https://github.com/grpc/grpc-java.git
binder: Expose client identity via a new abstract 'PeerUid' type (#9952)
The actual remote uid was kept private to prevent misuse.
This commit is contained in:
parent
b09473b0d3
commit
b8444d563d
|
@ -66,6 +66,7 @@ dependencies {
|
|||
exclude group: 'junit', module: 'junit'
|
||||
}
|
||||
testImplementation libraries.truth
|
||||
testImplementation project(':grpc-testing')
|
||||
|
||||
androidTestAnnotationProcessor libraries.auto.value
|
||||
androidTestImplementation project(':grpc-testing')
|
||||
|
|
|
@ -21,11 +21,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
@ -37,7 +33,6 @@ import io.grpc.ManagedChannel;
|
|||
import io.grpc.Metadata;
|
||||
import io.grpc.MethodDescriptor;
|
||||
import io.grpc.NameResolverRegistry;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
|
@ -91,12 +86,14 @@ public final class BinderChannelSmokeTest {
|
|||
FakeNameResolverProvider fakeNameResolverProvider;
|
||||
ManagedChannel channel;
|
||||
AtomicReference<Metadata> headersCapture = new AtomicReference<>();
|
||||
AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
ServerCallHandler<String, String> callHandler =
|
||||
ServerCalls.asyncUnaryCall(
|
||||
(req, respObserver) -> {
|
||||
clientUidCapture.set(PeerUids.REMOTE_PEER.get());
|
||||
respObserver.onNext(req);
|
||||
respObserver.onCompleted();
|
||||
});
|
||||
|
@ -104,6 +101,7 @@ public final class BinderChannelSmokeTest {
|
|||
ServerCallHandler<String, String> singleLargeResultCallHandler =
|
||||
ServerCalls.asyncUnaryCall(
|
||||
(req, respObserver) -> {
|
||||
clientUidCapture.set(PeerUids.REMOTE_PEER.get());
|
||||
respObserver.onNext(createLargeString(SLIGHTLY_MORE_THAN_ONE_BLOCK));
|
||||
respObserver.onCompleted();
|
||||
});
|
||||
|
@ -118,7 +116,8 @@ public final class BinderChannelSmokeTest {
|
|||
.addMethod(singleLargeResultMethod, singleLargeResultCallHandler)
|
||||
.addMethod(bidiMethod, bidiCallHandler)
|
||||
.build(),
|
||||
TestUtils.recordRequestHeadersInterceptor(headersCapture));
|
||||
TestUtils.recordRequestHeadersInterceptor(headersCapture),
|
||||
PeerUids.newPeerIdentifyingServerInterceptor());
|
||||
|
||||
AndroidComponentAddress serverAddress = HostServices.allocateService(appContext);
|
||||
fakeNameResolverProvider = new FakeNameResolverProvider(SERVER_TARGET_URI, serverAddress);
|
||||
|
@ -162,6 +161,12 @@ public final class BinderChannelSmokeTest {
|
|||
assertThat(doCall("Hello").get()).isEqualTo("Hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPeerUidIsRecorded() throws Exception {
|
||||
assertThat(doCall("Hello").get()).isEqualTo("Hello");
|
||||
assertThat(clientUidCapture.get()).isEqualTo(PeerUid.forCurrentProcess());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEmptyMessage() throws Exception {
|
||||
assertThat(doCall("").get()).isEmpty();
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright 2023 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 io.grpc.ExperimentalApi;
|
||||
|
||||
/**
|
||||
* Identifies a gRPC/binder client or server by Android/Linux UID
|
||||
* (https://source.android.com/security/app-sandbox).
|
||||
*
|
||||
* <p>Use {@link PeerUids#REMOTE_PEER} to obtain the client's {@link PeerUid} from the server's
|
||||
* {@link io.grpc.Context}
|
||||
*
|
||||
* <p>The actual integer uid is intentionally not exposed to prevent misuse. If you want the uid for
|
||||
* access control, consider one of the existing {@link SecurityPolicies} instead (or propose a new
|
||||
* one). If you want the uid to pass to some other Android API, consider one of the static wrapper
|
||||
* methods of {@link PeerUids} instead (or propose a new one).
|
||||
*/
|
||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
|
||||
public final class PeerUid {
|
||||
|
||||
private final int uid;
|
||||
|
||||
/** Constructs a new instance. Intentionally non-public to prevent misuse. */
|
||||
PeerUid(int uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
/** Returns an identifier for the current process. */
|
||||
public static PeerUid forCurrentProcess() {
|
||||
return new PeerUid(android.os.Process.myUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this peer's Android/Linux uid.
|
||||
*
|
||||
* <p>Intentionally non-public to prevent misuse.
|
||||
*/
|
||||
int getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object otherObj) {
|
||||
if (this == otherObj) {
|
||||
return true;
|
||||
}
|
||||
if (otherObj == null || getClass() != otherObj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
PeerUid otherPeerUid = (PeerUid) otherObj;
|
||||
return uid == otherPeerUid.uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Integer.valueOf(uid).hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PeerUid{" + uid + '}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2023 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.base.Preconditions.checkNotNull;
|
||||
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.UserHandle;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import io.grpc.Context;
|
||||
import io.grpc.Contexts;
|
||||
import io.grpc.ExperimentalApi;
|
||||
import io.grpc.Metadata;
|
||||
import io.grpc.ServerCall;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.binder.internal.BinderTransport;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Static methods that operate on {@link PeerUid}. */
|
||||
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8022")
|
||||
public final class PeerUids {
|
||||
/**
|
||||
* The client's authentic identity will be stored under this key in the server {@link Context}.
|
||||
*
|
||||
* <p>In order for this key to be populated, the interceptor returned by {@link
|
||||
* #newPeerIdentifyingServerInterceptor} must be attached to the service. Note that the Context
|
||||
* must be propagated correctly across threads for this key to be populated when read from other
|
||||
* threads.
|
||||
*/
|
||||
public static final Context.Key<PeerUid> REMOTE_PEER = Context.key("remote-peer");
|
||||
|
||||
/**
|
||||
* Returns package names associated with the given peer's uid according to {@link
|
||||
* PackageManager#getPackagesForUid(int)}.
|
||||
*
|
||||
* <p><em>WARNING</em>: Apps installed from untrusted sources can set any package name they want.
|
||||
* Don't depend on package names for security -- use {@link SecurityPolicies} instead.
|
||||
*/
|
||||
public static String[] getInsecurePackagesForUid(PackageManager packageManager, PeerUid who) {
|
||||
return packageManager.getPackagesForUid(who.getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the "official name" associated with this uid, as specified by {@link
|
||||
* PackageManager#getNameForUid(int)}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getNameForUid(PackageManager packageManager, PeerUid who) {
|
||||
return packageManager.getNameForUid(who.getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link UserHandle} associated with this uid according to {@link
|
||||
* UserHandle#getUserHandleForUid}.
|
||||
*/
|
||||
@RequiresApi(api = VERSION_CODES.N)
|
||||
public static UserHandle getUserHandleForUid(PeerUid who) {
|
||||
return UserHandle.getUserHandleForUid(who.getUid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an interceptor that exposes the client's identity in the {@link Context} under {@link
|
||||
* #REMOTE_PEER}.
|
||||
*
|
||||
* <p>The returned interceptor only works with the Android Binder transport. If installed
|
||||
* elsewhere, all intercepted requests will fail without ever reaching application-layer
|
||||
* processing.
|
||||
*/
|
||||
public static ServerInterceptor newPeerIdentifyingServerInterceptor() {
|
||||
return new ServerInterceptor() {
|
||||
@Override
|
||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
||||
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
|
||||
Context context = Context.current();
|
||||
PeerUid client =
|
||||
new PeerUid(
|
||||
checkNotNull(
|
||||
call.getAttributes().get(BinderTransport.REMOTE_UID),
|
||||
"Expected BinderTransport attribute REMOTE_UID was missing. Is this "
|
||||
+ "interceptor installed on an unsupported type of Server?"));
|
||||
return Contexts.interceptCall(context.withValue(REMOTE_PEER, client), call, headers, next);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private PeerUids() {}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2023 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.testing.EqualsTester;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class PeerUidTest {
|
||||
|
||||
@Test
|
||||
public void shouldImplementEqualsAndHashCode() {
|
||||
new EqualsTester()
|
||||
.addEqualityGroup(new PeerUid(123), new PeerUid(123))
|
||||
.addEqualityGroup(new PeerUid(456))
|
||||
.testEquals();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright 2023 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 java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import io.grpc.Attributes;
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.MethodDescriptor;
|
||||
import io.grpc.ServerCallHandler;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.ServerInterceptors;
|
||||
import io.grpc.ServerServiceDefinition;
|
||||
import io.grpc.ServerTransportFilter;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.grpc.binder.internal.BinderTransport;
|
||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
import io.grpc.inprocess.InProcessServerBuilder;
|
||||
import io.grpc.stub.ClientCalls;
|
||||
import io.grpc.stub.ServerCalls;
|
||||
import io.grpc.testing.GrpcCleanupRule;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class PeerUidsTest {
|
||||
@Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule();
|
||||
|
||||
private static final int FAKE_UID = 12345;
|
||||
|
||||
private final MethodDescriptor<String, String> method =
|
||||
MethodDescriptor.newBuilder(StringMarshaller.INSTANCE, StringMarshaller.INSTANCE)
|
||||
.setFullMethodName("test/method")
|
||||
.setType(MethodDescriptor.MethodType.UNARY)
|
||||
.build();
|
||||
|
||||
private final AtomicReference<PeerUid> clientUidCapture = new AtomicReference<>();
|
||||
|
||||
@Test
|
||||
public void keyPopulatedWithInterceptor() throws Exception {
|
||||
makeServiceCall(/* populateUid= */ true, /* includeInterceptor= */ true);
|
||||
assertThat(clientUidCapture.get()).isEqualTo(new PeerUid(FAKE_UID));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keyNotPopulatedWithoutInterceptor() throws Exception {
|
||||
makeServiceCall(/* populateUid= */ true, /* includeInterceptor= */ false);
|
||||
assertThat(clientUidCapture.get()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exceptionThrownWithoutUid() throws Exception {
|
||||
assertThrows(
|
||||
StatusRuntimeException.class,
|
||||
() -> makeServiceCall(/* populateUid= */ false, /* includeInterceptor= */ true));
|
||||
}
|
||||
|
||||
private void makeServiceCall(boolean populateUid, boolean includeInterceptor) throws Exception {
|
||||
ServerCallHandler<String, String> callHandler =
|
||||
ServerCalls.asyncUnaryCall(
|
||||
(req, respObserver) -> {
|
||||
clientUidCapture.set(PeerUids.REMOTE_PEER.get());
|
||||
respObserver.onNext(req);
|
||||
respObserver.onCompleted();
|
||||
});
|
||||
ImmutableList<ServerInterceptor> interceptors;
|
||||
if (includeInterceptor) {
|
||||
interceptors = ImmutableList.of(PeerUids.newPeerIdentifyingServerInterceptor());
|
||||
} else {
|
||||
interceptors = ImmutableList.of();
|
||||
}
|
||||
ServerServiceDefinition serviceDef =
|
||||
ServerInterceptors.intercept(
|
||||
ServerServiceDefinition.builder("test").addMethod(method, callHandler).build(),
|
||||
interceptors);
|
||||
|
||||
InProcessServerBuilder server =
|
||||
InProcessServerBuilder.forName("test").directExecutor().addService(serviceDef);
|
||||
if (populateUid) {
|
||||
server.addTransportFilter(
|
||||
new ServerTransportFilter() {
|
||||
@Override
|
||||
public Attributes transportReady(Attributes attributes) {
|
||||
return attributes.toBuilder().set(BinderTransport.REMOTE_UID, FAKE_UID).build();
|
||||
}
|
||||
});
|
||||
}
|
||||
grpcCleanup.register(server.build().start());
|
||||
ManagedChannel channel = InProcessChannelBuilder.forName("test").directExecutor().build();
|
||||
|
||||
ClientCalls.blockingUnaryCall(channel, method, CallOptions.DEFAULT, "hello");
|
||||
}
|
||||
|
||||
private static class StringMarshaller implements MethodDescriptor.Marshaller<String> {
|
||||
public static final StringMarshaller INSTANCE = new StringMarshaller();
|
||||
|
||||
@Override
|
||||
public InputStream stream(String value) {
|
||||
return new ByteArrayInputStream(value.getBytes(UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(InputStream stream) {
|
||||
try {
|
||||
return new String(ByteStreams.toByteArray(stream), UTF_8);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue