Allow linked devices to unlink themselves via the gRPC API
This commit is contained in:
parent
fc3e547dce
commit
5892dc71fa
|
@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
@ -28,26 +27,17 @@ import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||||
|
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
private final KeysManager keysManager;
|
|
||||||
private final MessagesManager messagesManager;
|
|
||||||
|
|
||||||
private static final int MAX_NAME_LENGTH = 256;
|
private static final int MAX_NAME_LENGTH = 256;
|
||||||
|
|
||||||
public DevicesGrpcService(final AccountsManager accountsManager,
|
public DevicesGrpcService(final AccountsManager accountsManager) {
|
||||||
final KeysManager keysManager,
|
|
||||||
final MessagesManager messagesManager) {
|
|
||||||
|
|
||||||
this.accountsManager = accountsManager;
|
this.accountsManager = accountsManager;
|
||||||
this.keysManager = keysManager;
|
|
||||||
this.messagesManager = messagesManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -79,9 +69,15 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||||
throw Status.INVALID_ARGUMENT.withDescription("Cannot remove primary device").asRuntimeException();
|
throw Status.INVALID_ARGUMENT.withDescription("Cannot remove primary device").asRuntimeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
final byte deviceId = DeviceIdUtil.validate(request.getId());
|
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||||
|
|
||||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedPrimaryDevice();
|
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID && request.getId() != authenticatedDevice.deviceId()) {
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("Linked devices cannot remove devices other than themselves")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte deviceId = DeviceIdUtil.validate(request.getId());
|
||||||
|
|
||||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
||||||
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
||||||
|
|
|
@ -50,8 +50,6 @@ import org.signal.chat.device.SetPushTokenResponse;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.Device;
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
|
||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
|
|
||||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||||
|
|
||||||
class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, DevicesGrpc.DevicesBlockingStub> {
|
class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, DevicesGrpc.DevicesBlockingStub> {
|
||||||
|
@ -59,16 +57,9 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||||
@Mock
|
@Mock
|
||||||
private AccountsManager accountsManager;
|
private AccountsManager accountsManager;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private KeysManager keysManager;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private MessagesManager messagesManager;
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private Account authenticatedAccount;
|
private Account authenticatedAccount;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected DevicesGrpcService createServiceBeforeEachTest() {
|
protected DevicesGrpcService createServiceBeforeEachTest() {
|
||||||
when(authenticatedAccount.getUuid()).thenReturn(AUTHENTICATED_ACI);
|
when(authenticatedAccount.getUuid()).thenReturn(AUTHENTICATED_ACI);
|
||||||
|
@ -76,6 +67,9 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||||
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
|
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
|
||||||
.thenReturn(CompletableFuture.completedFuture(Optional.of(authenticatedAccount)));
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(authenticatedAccount)));
|
||||||
|
|
||||||
|
when(accountsManager.removeDevice(any(), anyByte()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(authenticatedAccount));
|
||||||
|
|
||||||
when(accountsManager.updateAsync(any(), any()))
|
when(accountsManager.updateAsync(any(), any()))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
final Account account = invocation.getArgument(0);
|
final Account account = invocation.getArgument(0);
|
||||||
|
@ -97,10 +91,7 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||||
return CompletableFuture.completedFuture(account);
|
return CompletableFuture.completedFuture(account);
|
||||||
});
|
});
|
||||||
|
|
||||||
when(keysManager.deleteSingleUsePreKeys(any(), anyByte())).thenReturn(CompletableFuture.completedFuture(null));
|
return new DevicesGrpcService(accountsManager);
|
||||||
when(messagesManager.clear(any(), anyByte())).thenReturn(CompletableFuture.completedFuture(null));
|
|
||||||
|
|
||||||
return new DevicesGrpcService(accountsManager, keysManager, messagesManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -146,9 +137,6 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||||
void removeDevice() {
|
void removeDevice() {
|
||||||
final byte deviceId = 17;
|
final byte deviceId = 17;
|
||||||
|
|
||||||
when(accountsManager.removeDevice(any(), anyByte()))
|
|
||||||
.thenReturn(CompletableFuture.completedFuture(authenticatedAccount));
|
|
||||||
|
|
||||||
final RemoveDeviceResponse ignored = authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder()
|
final RemoveDeviceResponse ignored = authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder()
|
||||||
.setId(deviceId)
|
.setId(deviceId)
|
||||||
.build());
|
.build());
|
||||||
|
@ -166,7 +154,20 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void removeDeviceNonPrimaryAuthenticated() {
|
void removeDeviceNonPrimaryMatchAuthenticated() {
|
||||||
|
final byte deviceId = Device.PRIMARY_ID + 1;
|
||||||
|
|
||||||
|
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, deviceId);
|
||||||
|
|
||||||
|
final RemoveDeviceResponse ignored = authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder()
|
||||||
|
.setId(deviceId)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
verify(accountsManager).removeDevice(authenticatedAccount, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removeDeviceNonPrimaryMismatchAuthenticated() {
|
||||||
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, (byte) (Device.PRIMARY_ID + 1));
|
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, (byte) (Device.PRIMARY_ID + 1));
|
||||||
assertStatusException(Status.PERMISSION_DENIED, () -> authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder()
|
assertStatusException(Status.PERMISSION_DENIED, () -> authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder()
|
||||||
.setId(17)
|
.setId(17)
|
||||||
|
|
Loading…
Reference in New Issue