mirror of
https://github.com/signalapp/Signal-Server
synced 2026-05-08 20:28:37 +01:00
Update error model in device.proto
This commit is contained in:
committed by
ravi-signal
parent
a9f81e2ba6
commit
8804f28cb8
+2
-2
@@ -5,7 +5,6 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import io.grpc.Status;
|
||||
import org.whispersystems.textsecuregcm.storage.DeviceCapability;
|
||||
|
||||
public class DeviceCapabilityUtil {
|
||||
@@ -19,7 +18,8 @@ public class DeviceCapabilityUtil {
|
||||
case DEVICE_CAPABILITY_TRANSFER -> DeviceCapability.TRANSFER;
|
||||
case DEVICE_CAPABILITY_ATTACHMENT_BACKFILL -> DeviceCapability.ATTACHMENT_BACKFILL;
|
||||
case DEVICE_CAPABILITY_SPARSE_POST_QUANTUM_RATCHET -> DeviceCapability.SPARSE_POST_QUANTUM_RATCHET;
|
||||
case DEVICE_CAPABILITY_UNSPECIFIED, UNRECOGNIZED -> throw Status.INVALID_ARGUMENT.withDescription("Unrecognized device capability").asRuntimeException();
|
||||
case DEVICE_CAPABILITY_UNSPECIFIED, UNRECOGNIZED ->
|
||||
throw GrpcExceptions.invalidArguments("unrecognized device capability");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public class DeviceIdUtil {
|
||||
|
||||
static byte validate(int deviceId) {
|
||||
if (!isValid(deviceId)) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Device ID is out of range").asRuntimeException();
|
||||
throw GrpcExceptions.invalidArguments("device ID is out of range");
|
||||
}
|
||||
|
||||
return (byte) deviceId;
|
||||
|
||||
+26
-49
@@ -6,9 +6,10 @@
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.Status;
|
||||
import com.google.protobuf.Empty;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -25,9 +26,11 @@ import org.signal.chat.device.SetDeviceNameRequest;
|
||||
import org.signal.chat.device.SetDeviceNameResponse;
|
||||
import org.signal.chat.device.SetPushTokenRequest;
|
||||
import org.signal.chat.device.SetPushTokenResponse;
|
||||
import org.signal.chat.errors.NotFound;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.storage.DeviceCapability;
|
||||
@@ -38,8 +41,6 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
|
||||
private final AccountsManager accountsManager;
|
||||
|
||||
private static final int MAX_NAME_LENGTH = 256;
|
||||
|
||||
public DevicesGrpcService(final AccountsManager accountsManager) {
|
||||
this.accountsManager = accountsManager;
|
||||
}
|
||||
@@ -48,8 +49,7 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
public Mono<GetDevicesResponse> getDevices(final GetDevicesRequest request) {
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
|
||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
||||
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
||||
return getAccount(authenticatedDevice.accountIdentifier())
|
||||
.flatMapMany(account -> Flux.fromIterable(account.getDevices()))
|
||||
.reduce(GetDevicesResponse.newBuilder(), (builder, device) -> {
|
||||
final GetDevicesResponse.LinkedDevice.Builder linkedDeviceBuilder = GetDevicesResponse.LinkedDevice.newBuilder();
|
||||
@@ -72,21 +72,18 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
@Override
|
||||
public Mono<RemoveDeviceResponse> removeDevice(final RemoveDeviceRequest request) {
|
||||
if (request.getId() == Device.PRIMARY_ID) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Cannot remove primary device").asRuntimeException();
|
||||
throw GrpcExceptions.invalidArguments("cannot remove primary device");
|
||||
}
|
||||
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
|
||||
if (authenticatedDevice.deviceId() != Device.PRIMARY_ID && request.getId() != authenticatedDevice.deviceId()) {
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("Linked devices cannot remove devices other than themselves")
|
||||
.asRuntimeException();
|
||||
throw GrpcExceptions.badAuthentication("linked devices cannot remove devices other than themselves");
|
||||
}
|
||||
|
||||
final byte deviceId = DeviceIdUtil.validate(request.getId());
|
||||
|
||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
||||
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
||||
return getAccount(authenticatedDevice.accountIdentifier())
|
||||
.flatMap(account -> Mono.fromFuture(accountsManager.removeDevice(account, deviceId)))
|
||||
.thenReturn(RemoveDeviceResponse.newBuilder().build());
|
||||
}
|
||||
@@ -101,30 +98,18 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
authenticatedDevice.deviceId() == deviceId;
|
||||
|
||||
if (!mayChangeName) {
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("Authenticated device is not authorized to change target device name")
|
||||
.asRuntimeException();
|
||||
throw GrpcExceptions.badAuthentication("linked device is not authorized to change target device name");
|
||||
}
|
||||
|
||||
if (request.getName().isEmpty()) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Must specify a device name").asRuntimeException();
|
||||
}
|
||||
|
||||
if (request.getName().size() > MAX_NAME_LENGTH) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Device name must be at most " + MAX_NAME_LENGTH + " bytes")
|
||||
.asRuntimeException();
|
||||
}
|
||||
|
||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
||||
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
||||
.doOnNext(account -> {
|
||||
return getAccount(authenticatedDevice.accountIdentifier())
|
||||
.flatMap(account -> {
|
||||
if (account.getDevice(deviceId).isEmpty()) {
|
||||
throw Status.NOT_FOUND.withDescription("No device found with given ID").asRuntimeException();
|
||||
return Mono.just(SetDeviceNameResponse.newBuilder().setTargetDeviceNotFound(NotFound.getDefaultInstance()).build());
|
||||
}
|
||||
})
|
||||
.flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, deviceId,
|
||||
device -> device.setName(request.getName().toByteArray()))))
|
||||
.thenReturn(SetDeviceNameResponse.newBuilder().build());
|
||||
return Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, deviceId, device ->
|
||||
device.setName(request.getName().toByteArray())))
|
||||
.thenReturn(SetDeviceNameResponse.newBuilder().setSuccess(Empty.getDefaultInstance()).build());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -138,34 +123,23 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
|
||||
case APNS_TOKEN_REQUEST -> {
|
||||
final SetPushTokenRequest.ApnsTokenRequest apnsTokenRequest = request.getApnsTokenRequest();
|
||||
|
||||
if (StringUtils.isBlank(apnsTokenRequest.getApnsToken())) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("APNs token must not be blank").asRuntimeException();
|
||||
}
|
||||
|
||||
apnsToken = StringUtils.stripToNull(apnsTokenRequest.getApnsToken());
|
||||
fcmToken = null;
|
||||
}
|
||||
|
||||
case FCM_TOKEN_REQUEST -> {
|
||||
final SetPushTokenRequest.FcmTokenRequest fcmTokenRequest = request.getFcmTokenRequest();
|
||||
|
||||
if (StringUtils.isBlank(fcmTokenRequest.getFcmToken())) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("FCM token must not be blank").asRuntimeException();
|
||||
}
|
||||
|
||||
apnsToken = null;
|
||||
fcmToken = StringUtils.stripToNull(fcmTokenRequest.getFcmToken());
|
||||
}
|
||||
|
||||
default -> throw Status.INVALID_ARGUMENT.withDescription("No tokens specified").asRuntimeException();
|
||||
default -> throw GrpcExceptions.fieldViolation("token_request", "No tokens specified");
|
||||
}
|
||||
|
||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
||||
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
||||
return getAccount(authenticatedDevice.accountIdentifier())
|
||||
.flatMap(account -> {
|
||||
final Device device = account.getDevice(authenticatedDevice.deviceId())
|
||||
.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException);
|
||||
.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials"));
|
||||
|
||||
final boolean tokenUnchanged =
|
||||
Objects.equals(device.getApnId(), apnsToken) &&
|
||||
@@ -186,8 +160,7 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
public Mono<ClearPushTokenResponse> clearPushToken(final ClearPushTokenRequest request) {
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
|
||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
||||
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
||||
return getAccount(authenticatedDevice.accountIdentifier())
|
||||
.flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(), device -> {
|
||||
if (StringUtils.isNotBlank(device.getApnId())) {
|
||||
device.setUserAgent(device.isPrimary() ? "OWI" : "OWP");
|
||||
@@ -210,11 +183,15 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||
.map(DeviceCapabilityUtil::fromGrpcDeviceCapability)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
|
||||
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
|
||||
return getAccount(authenticatedDevice.accountIdentifier())
|
||||
.flatMap(account ->
|
||||
Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(),
|
||||
d -> d.setCapabilities(capabilities))))
|
||||
.thenReturn(SetCapabilitiesResponse.newBuilder().build());
|
||||
}
|
||||
|
||||
private Mono<Account> getAccount(final UUID accountIdentifier) {
|
||||
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(accountIdentifier))
|
||||
.map(maybeAccount -> maybeAccount.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,27 +9,28 @@ option java_multiple_files = true;
|
||||
|
||||
package org.signal.chat.device;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
import "org/signal/chat/common.proto";
|
||||
import "org/signal/chat/errors.proto";
|
||||
import "org/signal/chat/require.proto";
|
||||
import "org/signal/chat/tag.proto";
|
||||
|
||||
// Provides methods for working with devices attached to a Signal account.
|
||||
service Devices {
|
||||
// Returns a list of devices associated with the caller's account.
|
||||
rpc GetDevices(GetDevicesRequest) returns (GetDevicesResponse) {}
|
||||
|
||||
// Removes a linked device from the caller's account. This call will fail with
|
||||
// a status of `PERMISSION_DENIED` if not called from the primary device
|
||||
// associated with an account. It will also fail with a status of
|
||||
// `INVALID_ARGUMENT` if the targeted device is the primary device associated
|
||||
// with the account.
|
||||
// Removes a linked device from the caller's account.
|
||||
//
|
||||
// Linked devices may only remove themselves. Primary devices may remove
|
||||
// any device other than themselves.
|
||||
rpc RemoveDevice(RemoveDeviceRequest) returns (RemoveDeviceResponse) {}
|
||||
|
||||
// Sets the encrypted human-readable name for a specific devices. Primary
|
||||
// devices may change the name of any device associated with their account,
|
||||
// but linked devices may only change their own name. This call will fail with
|
||||
// a status of `NOT_FOUND` if no device was found with the given identifier.
|
||||
// It will also fail with a status of `PERMISSION_DENIED` if a linked device
|
||||
// tries to change the name of any device other than itself.
|
||||
// but linked devices may only change their own name. The response will
|
||||
// indicate if the target device was not found.
|
||||
rpc SetDeviceName(SetDeviceNameRequest) returns (SetDeviceNameResponse) {}
|
||||
|
||||
// Sets the token(s) the server should use to send new message notifications
|
||||
@@ -78,32 +79,41 @@ message GetDevicesResponse {
|
||||
}
|
||||
|
||||
message RemoveDeviceRequest {
|
||||
// The identifier for the device to remove from the authenticated account.
|
||||
// The identifier for the device to remove from the authenticated account. The
|
||||
// identifier must not be for the primary device.
|
||||
uint32 id = 1;
|
||||
}
|
||||
|
||||
message SetDeviceNameRequest {
|
||||
// A sequence of bytes that encodes an encrypted human-readable name for this
|
||||
// device.
|
||||
bytes name = 1;
|
||||
bytes name = 1 [(require.size) = {min: 1, max: 256}];
|
||||
|
||||
// The identifier for the device for which to set a name.
|
||||
uint32 id = 2;
|
||||
}
|
||||
|
||||
message SetDeviceNameResponse {}
|
||||
message SetDeviceNameResponse {
|
||||
oneof response {
|
||||
// The device name was successfully set
|
||||
google.protobuf.Empty success = 1;
|
||||
|
||||
// No device with the provided identifier was found on the account
|
||||
errors.NotFound target_device_not_found = 2 [(tag.reason) = "not_found"];
|
||||
}
|
||||
}
|
||||
|
||||
message RemoveDeviceResponse {}
|
||||
|
||||
message SetPushTokenRequest {
|
||||
message ApnsTokenRequest {
|
||||
// A "standard" APNs device token.
|
||||
string apns_token = 1;
|
||||
string apns_token = 1 [(require.nonEmpty) = true];
|
||||
}
|
||||
|
||||
message FcmTokenRequest {
|
||||
// An FCM push token.
|
||||
string fcm_token = 1;
|
||||
string fcm_token = 1 [(require.nonEmpty) = true];
|
||||
}
|
||||
|
||||
oneof token_request {
|
||||
|
||||
+9
-9
@@ -6,6 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyByte;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -184,7 +185,7 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||
@Test
|
||||
void removeDeviceNonPrimaryMismatchAuthenticated() {
|
||||
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, (byte) (Device.PRIMARY_ID + 1));
|
||||
assertStatusException(Status.PERMISSION_DENIED, () -> authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder()
|
||||
assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().removeDevice(RemoveDeviceRequest.newBuilder()
|
||||
.setId(17)
|
||||
.build()));
|
||||
|
||||
@@ -201,10 +202,10 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||
|
||||
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
||||
|
||||
final SetDeviceNameResponse ignored = authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||
assertTrue(authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||
.setId(deviceId)
|
||||
.setName(ByteString.copyFrom(deviceName))
|
||||
.build());
|
||||
.build()).hasSuccess());
|
||||
|
||||
verify(device).setName(deviceName);
|
||||
}
|
||||
@@ -239,7 +240,7 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||
|
||||
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
||||
|
||||
assertStatusException(Status.PERMISSION_DENIED,
|
||||
assertStatusException(Status.INVALID_ARGUMENT,
|
||||
() -> authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||
.setId(deviceId)
|
||||
.setName(ByteString.copyFrom(deviceName))
|
||||
@@ -255,11 +256,10 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||
|
||||
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
||||
|
||||
assertStatusException(Status.NOT_FOUND,
|
||||
() -> authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||
.setId(Device.PRIMARY_ID + 1)
|
||||
.setName(ByteString.copyFrom(deviceName))
|
||||
.build()));
|
||||
assertTrue(authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||
.setId(Device.PRIMARY_ID + 1)
|
||||
.setName(ByteString.copyFrom(deviceName))
|
||||
.build()).hasTargetDeviceNotFound());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
||||
Reference in New Issue
Block a user