Update error model in device.proto

This commit is contained in:
Ravi Khadiwala
2026-01-30 12:45:37 -06:00
committed by ravi-signal
parent a9f81e2ba6
commit 8804f28cb8
5 changed files with 62 additions and 75 deletions

View File

@@ -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");
};
}

View File

@@ -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;

View File

@@ -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")));
}
}