Allow primary devices to change names of linked devices
This commit is contained in:
parent
712f3affd9
commit
f3b22e04e8
|
@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -19,6 +20,7 @@ import javax.validation.constraints.NotNull;
|
||||||
import javax.ws.rs.BadRequestException;
|
import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
|
import javax.ws.rs.ForbiddenException;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.HEAD;
|
import javax.ws.rs.HEAD;
|
||||||
import javax.ws.rs.HeaderParam;
|
import javax.ws.rs.HeaderParam;
|
||||||
|
@ -27,6 +29,7 @@ import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -192,10 +195,39 @@ public class AccountController {
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("/name/")
|
@Path("/name/")
|
||||||
public void setName(@Mutable @Auth AuthenticatedDevice auth, @NotNull @Valid DeviceName deviceName) {
|
@Operation(summary = "Set a device's encrypted name",
|
||||||
Account account = auth.getAccount();
|
description = """
|
||||||
Device device = auth.getAuthenticatedDevice();
|
Sets the encrypted name for the specified device. Primary devices may change the name of any device associated
|
||||||
accounts.updateDevice(account, device.getId(), d -> d.setName(deviceName.getDeviceName()));
|
with their account, but linked devices may only change their own name. If no device ID is specified, then the
|
||||||
|
authenticated device's ID will be used.
|
||||||
|
""")
|
||||||
|
@ApiResponse(responseCode = "204", description = "Device name changed successfully")
|
||||||
|
@ApiResponse(responseCode = "404", description = "No device found with the given ID")
|
||||||
|
@ApiResponse(responseCode = "403", description = "Not authorized to change the name of the device with the given ID")
|
||||||
|
public void setName(@Mutable @Auth final AuthenticatedDevice auth,
|
||||||
|
@NotNull @Valid final DeviceName deviceName,
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@QueryParam("deviceId")
|
||||||
|
@Schema(description = "The ID of the device for which to set a name; if omitted, the authenticated device will be targeted for a name change",
|
||||||
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
|
final Byte deviceId) {
|
||||||
|
|
||||||
|
final Account account = auth.getAccount();
|
||||||
|
final byte targetDeviceId = deviceId == null ? auth.getAuthenticatedDevice().getId() : deviceId;
|
||||||
|
|
||||||
|
if (account.getDevice(targetDeviceId).isEmpty()) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean mayChangeName = auth.getAuthenticatedDevice().isPrimary() ||
|
||||||
|
auth.getAuthenticatedDevice().getId() == targetDeviceId;
|
||||||
|
|
||||||
|
if (!mayChangeName) {
|
||||||
|
throw new ForbiddenException();
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts.updateDevice(account, targetDeviceId, d -> d.setName(deviceName.deviceName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
|
|
|
@ -5,25 +5,15 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.entities;
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||||
import javax.validation.constraints.NotEmpty;
|
import javax.validation.constraints.NotEmpty;
|
||||||
import javax.validation.constraints.Size;
|
import javax.validation.constraints.Size;
|
||||||
|
|
||||||
public class DeviceName {
|
public record DeviceName(@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||||
|
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||||
@JsonProperty
|
@NotEmpty
|
||||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
@Size(max = 225)
|
||||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
byte[] deviceName) {
|
||||||
@NotEmpty
|
|
||||||
@Size(max = 225)
|
|
||||||
private byte[] deviceName;
|
|
||||||
|
|
||||||
public DeviceName() {}
|
|
||||||
|
|
||||||
public byte[] getDeviceName() {
|
|
||||||
return deviceName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,13 +6,15 @@
|
||||||
package org.whispersystems.textsecuregcm.grpc;
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
|
||||||
public class DeviceIdUtil {
|
public class DeviceIdUtil {
|
||||||
|
|
||||||
static byte validate(int deviceId) {
|
static byte validate(int deviceId) {
|
||||||
if (deviceId > Byte.MAX_VALUE) {
|
if (deviceId < Device.PRIMARY_ID || deviceId > Byte.MAX_VALUE) {
|
||||||
throw Status.INVALID_ARGUMENT.withDescription("Device ID is out of range").asRuntimeException();
|
throw Status.INVALID_ARGUMENT.withDescription("Device ID is out of range").asRuntimeException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (byte) deviceId;
|
return (byte) deviceId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,17 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||||
public Mono<SetDeviceNameResponse> setDeviceName(final SetDeviceNameRequest request) {
|
public Mono<SetDeviceNameResponse> setDeviceName(final SetDeviceNameRequest request) {
|
||||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||||
|
|
||||||
|
final byte deviceId = DeviceIdUtil.validate(request.getId());
|
||||||
|
|
||||||
|
final boolean mayChangeName = authenticatedDevice.deviceId() == Device.PRIMARY_ID ||
|
||||||
|
authenticatedDevice.deviceId() == deviceId;
|
||||||
|
|
||||||
|
if (!mayChangeName) {
|
||||||
|
throw Status.PERMISSION_DENIED
|
||||||
|
.withDescription("Authenticated device is not authorized to change target device name")
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
if (request.getName().isEmpty()) {
|
if (request.getName().isEmpty()) {
|
||||||
throw Status.INVALID_ARGUMENT.withDescription("Must specify a device name").asRuntimeException();
|
throw Status.INVALID_ARGUMENT.withDescription("Must specify a device name").asRuntimeException();
|
||||||
}
|
}
|
||||||
|
@ -100,7 +111,12 @@ public class DevicesGrpcService extends ReactorDevicesGrpc.DevicesImplBase {
|
||||||
|
|
||||||
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))
|
||||||
.flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, authenticatedDevice.deviceId(),
|
.doOnNext(account -> {
|
||||||
|
if (account.getDevice(deviceId).isEmpty()) {
|
||||||
|
throw Status.NOT_FOUND.withDescription("No device found with given ID").asRuntimeException();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flatMap(account -> Mono.fromFuture(() -> accountsManager.updateDeviceAsync(account, deviceId,
|
||||||
device -> device.setName(request.getName().toByteArray()))))
|
device -> device.setName(request.getName().toByteArray()))))
|
||||||
.thenReturn(SetDeviceNameResponse.newBuilder().build());
|
.thenReturn(SetDeviceNameResponse.newBuilder().build());
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,9 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
|
||||||
final ServiceIdentifier serviceIdentifier =
|
final ServiceIdentifier serviceIdentifier =
|
||||||
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getTargetIdentifier());
|
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getTargetIdentifier());
|
||||||
|
|
||||||
final byte deviceId = DeviceIdUtil.validate(request.getRequest().getDeviceId());
|
final byte deviceId = request.getRequest().hasDeviceId()
|
||||||
|
? DeviceIdUtil.validate(request.getRequest().getDeviceId())
|
||||||
|
: KeysGrpcHelper.ALL_DEVICES;
|
||||||
|
|
||||||
return switch (request.getAuthorizationCase()) {
|
return switch (request.getAuthorizationCase()) {
|
||||||
case GROUP_SEND_TOKEN ->
|
case GROUP_SEND_TOKEN ->
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
package org.whispersystems.textsecuregcm.grpc;
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import org.signal.chat.common.EcPreKey;
|
import org.signal.chat.common.EcPreKey;
|
||||||
|
@ -26,7 +25,6 @@ import reactor.util.function.Tuples;
|
||||||
|
|
||||||
class KeysGrpcHelper {
|
class KeysGrpcHelper {
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static final byte ALL_DEVICES = 0;
|
static final byte ALL_DEVICES = 0;
|
||||||
|
|
||||||
static Mono<GetPreKeysResponse> getPreKeys(final Account targetAccount,
|
static Mono<GetPreKeysResponse> getPreKeys(final Account targetAccount,
|
||||||
|
|
|
@ -122,7 +122,9 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase {
|
||||||
final ServiceIdentifier targetIdentifier =
|
final ServiceIdentifier targetIdentifier =
|
||||||
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier());
|
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier());
|
||||||
|
|
||||||
final byte deviceId = DeviceIdUtil.validate(request.getDeviceId());
|
final byte deviceId = request.hasDeviceId()
|
||||||
|
? DeviceIdUtil.validate(request.getDeviceId())
|
||||||
|
: KeysGrpcHelper.ALL_DEVICES;
|
||||||
|
|
||||||
final String rateLimitKey = authenticatedDevice.accountIdentifier() + "." +
|
final String rateLimitKey = authenticatedDevice.accountIdentifier() + "." +
|
||||||
authenticatedDevice.deviceId() + "__" +
|
authenticatedDevice.deviceId() + "__" +
|
||||||
|
|
|
@ -27,6 +27,14 @@ service Devices {
|
||||||
*/
|
*/
|
||||||
rpc RemoveDevice(RemoveDeviceRequest) returns (RemoveDeviceResponse) {}
|
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.
|
||||||
|
*/
|
||||||
rpc SetDeviceName(SetDeviceNameRequest) returns (SetDeviceNameResponse) {}
|
rpc SetDeviceName(SetDeviceNameRequest) returns (SetDeviceNameResponse) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -95,6 +103,11 @@ message SetDeviceNameRequest {
|
||||||
* device.
|
* device.
|
||||||
*/
|
*/
|
||||||
bytes name = 1;
|
bytes name = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier for the device for which to set a name.
|
||||||
|
*/
|
||||||
|
uint32 id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetDeviceNameResponse {}
|
message SetDeviceNameResponse {}
|
||||||
|
|
|
@ -154,7 +154,7 @@ message GetPreKeysRequest {
|
||||||
* retrieve pre-keys. If not set, pre-keys are returned for all devices
|
* retrieve pre-keys. If not set, pre-keys are returned for all devices
|
||||||
* associated with the targeted account.
|
* associated with the targeted account.
|
||||||
*/
|
*/
|
||||||
uint32 device_id = 2;
|
optional uint32 device_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetPreKeysAnonymousRequest {
|
message GetPreKeysAnonymousRequest {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.eq;
|
import static org.mockito.Mockito.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.reset;
|
import static org.mockito.Mockito.reset;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -63,6 +64,7 @@ import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||||
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.DeviceName;
|
||||||
import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
|
import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
|
||||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||||
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
|
||||||
|
@ -81,6 +83,7 @@ import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptio
|
||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||||
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.RegistrationRecoveryPasswordsManager;
|
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
|
||||||
import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
|
import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
|
||||||
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
|
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
|
||||||
|
@ -999,4 +1002,72 @@ class AccountControllerTest {
|
||||||
verify(AuthHelper.VALID_ACCOUNT).setUsernameLinkDetails(eq(newHandle.usernameLinkHandle()), eq(encryptedUsername));
|
verify(AuthHelper.VALID_ACCOUNT).setUsernameLinkDetails(eq(newHandle.usernameLinkHandle()), eq(encryptedUsername));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetDeviceName() {
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/accounts/name/")
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, Device.PRIMARY_ID, AuthHelper.VALID_PASSWORD_3_PRIMARY))
|
||||||
|
.put(Entity.json(new DeviceName(TestRandomUtil.nextBytes(64))))) {
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
verify(accountsManager).updateDevice(eq(AuthHelper.VALID_ACCOUNT_3), eq(Device.PRIMARY_ID), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetLinkedDeviceNameFromPrimary() {
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/accounts/name/")
|
||||||
|
.queryParam("deviceId", AuthHelper.VALID_DEVICE_3_LINKED_ID)
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, Device.PRIMARY_ID, AuthHelper.VALID_PASSWORD_3_PRIMARY))
|
||||||
|
.put(Entity.json(new DeviceName(TestRandomUtil.nextBytes(64))))) {
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
verify(accountsManager).updateDevice(eq(AuthHelper.VALID_ACCOUNT_3), eq(AuthHelper.VALID_DEVICE_3_LINKED_ID), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetLinkedDeviceNameFromLinked() {
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/accounts/name/")
|
||||||
|
.queryParam("deviceId", AuthHelper.VALID_DEVICE_3_LINKED_ID)
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, AuthHelper.VALID_DEVICE_3_LINKED_ID, AuthHelper.VALID_PASSWORD_3_LINKED))
|
||||||
|
.put(Entity.json(new DeviceName(TestRandomUtil.nextBytes(64))))) {
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(204);
|
||||||
|
verify(accountsManager).updateDevice(eq(AuthHelper.VALID_ACCOUNT_3), eq(AuthHelper.VALID_DEVICE_3_LINKED_ID), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetDeviceNameDeviceNotFound() {
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/accounts/name/")
|
||||||
|
.queryParam("deviceId", Device.MAXIMUM_DEVICE_ID)
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, AuthHelper.VALID_PASSWORD_3_PRIMARY))
|
||||||
|
.put(Entity.json(new DeviceName(TestRandomUtil.nextBytes(64))))) {
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(404);
|
||||||
|
verify(accountsManager, never()).updateDevice(any(), anyByte(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSetPrimaryDeviceNameFromLinked() {
|
||||||
|
try (final Response response = resources.getJerseyTest()
|
||||||
|
.target("/v1/accounts/name/")
|
||||||
|
.queryParam("deviceId", Device.PRIMARY_ID)
|
||||||
|
.request()
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_3, AuthHelper.VALID_DEVICE_3_LINKED_ID, AuthHelper.VALID_PASSWORD_3_LINKED))
|
||||||
|
.put(Entity.json(new DeviceName(TestRandomUtil.nextBytes(64))))) {
|
||||||
|
|
||||||
|
assertThat(response.getStatus()).isEqualTo(403);
|
||||||
|
verify(accountsManager, never()).updateDevice(any(), anyByte(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
@ -187,12 +186,66 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||||
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
||||||
|
|
||||||
final SetDeviceNameResponse ignored = authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
final SetDeviceNameResponse ignored = authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||||
|
.setId(deviceId)
|
||||||
.setName(ByteString.copyFrom(deviceName))
|
.setName(ByteString.copyFrom(deviceName))
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
verify(device).setName(deviceName);
|
verify(device).setName(deviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setLinkedDeviceNameFromPrimary() {
|
||||||
|
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, Device.PRIMARY_ID);
|
||||||
|
|
||||||
|
final byte deviceId = Device.PRIMARY_ID + 1;
|
||||||
|
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
when(authenticatedAccount.getDevice(deviceId)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
||||||
|
|
||||||
|
final SetDeviceNameResponse ignored = authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||||
|
.setId(deviceId)
|
||||||
|
.setName(ByteString.copyFrom(deviceName))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
verify(device).setName(deviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setPrimaryDeviceNameFromLinkedDevice() {
|
||||||
|
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, (byte) (Device.PRIMARY_ID + 1));
|
||||||
|
|
||||||
|
final byte deviceId = Device.PRIMARY_ID;
|
||||||
|
|
||||||
|
final Device device = mock(Device.class);
|
||||||
|
when(authenticatedAccount.getDevice(deviceId)).thenReturn(Optional.of(device));
|
||||||
|
|
||||||
|
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
||||||
|
|
||||||
|
assertStatusException(Status.PERMISSION_DENIED,
|
||||||
|
() -> authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||||
|
.setId(deviceId)
|
||||||
|
.setName(ByteString.copyFrom(deviceName))
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
verify(device, never()).setName(deviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void setDeviceNameNotFound() {
|
||||||
|
mockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, Device.PRIMARY_ID);
|
||||||
|
when(authenticatedAccount.getDevice(anyByte())).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
final byte[] deviceName = TestRandomUtil.nextBytes(128);
|
||||||
|
|
||||||
|
assertStatusException(Status.NOT_FOUND,
|
||||||
|
() -> authenticatedServiceStub().setDeviceName(SetDeviceNameRequest.newBuilder()
|
||||||
|
.setId(Device.PRIMARY_ID + 1)
|
||||||
|
.setName(ByteString.copyFrom(deviceName))
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void setDeviceNameIllegalArgument(final SetDeviceNameRequest request) {
|
void setDeviceNameIllegalArgument(final SetDeviceNameRequest request) {
|
||||||
|
@ -203,11 +256,25 @@ class DevicesGrpcServiceTest extends SimpleBaseGrpcTest<DevicesGrpcService, Devi
|
||||||
private static Stream<Arguments> setDeviceNameIllegalArgument() {
|
private static Stream<Arguments> setDeviceNameIllegalArgument() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
// No device name
|
// No device name
|
||||||
Arguments.of(SetDeviceNameRequest.newBuilder().build()),
|
Arguments.of(SetDeviceNameRequest.newBuilder()
|
||||||
|
.setId(Device.PRIMARY_ID)
|
||||||
|
.build()),
|
||||||
|
|
||||||
// Excessively-long device name
|
// Excessively-long device name
|
||||||
Arguments.of(SetDeviceNameRequest.newBuilder()
|
Arguments.of(SetDeviceNameRequest.newBuilder()
|
||||||
.setName(ByteString.copyFrom(RandomStringUtils.randomAlphanumeric(1024).getBytes(StandardCharsets.UTF_8)))
|
.setId(Device.PRIMARY_ID)
|
||||||
|
.setName(ByteString.copyFrom(TestRandomUtil.nextBytes(1024)))
|
||||||
|
.build()),
|
||||||
|
|
||||||
|
// No device ID
|
||||||
|
Arguments.of(SetDeviceNameRequest.newBuilder()
|
||||||
|
.setName(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
|
||||||
|
.build()),
|
||||||
|
|
||||||
|
// Out-of-bounds device ID
|
||||||
|
Arguments.of(SetDeviceNameRequest.newBuilder()
|
||||||
|
.setId(Device.MAXIMUM_DEVICE_ID + 1)
|
||||||
|
.setName(ByteString.copyFrom(TestRandomUtil.nextBytes(32)))
|
||||||
.build())
|
.build())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,6 @@ import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.signal.chat.common.EcPreKey;
|
import org.signal.chat.common.EcPreKey;
|
||||||
import org.signal.chat.common.EcSignedPreKey;
|
import org.signal.chat.common.EcSignedPreKey;
|
||||||
|
@ -306,9 +304,8 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
||||||
verifyNoInteractions(keysManager);
|
verifyNoInteractions(keysManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@Test
|
||||||
@ValueSource(bytes = {KeysGrpcHelper.ALL_DEVICES, 1})
|
void getPreKeysDeviceNotFound() {
|
||||||
void getPreKeysDeviceNotFound(final byte deviceId) {
|
|
||||||
final UUID accountIdentifier = UUID.randomUUID();
|
final UUID accountIdentifier = UUID.randomUUID();
|
||||||
|
|
||||||
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
|
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
|
||||||
|
@ -329,7 +326,7 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
|
||||||
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
.setTargetIdentifier(ServiceIdentifier.newBuilder()
|
||||||
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
||||||
.setUuid(UUIDUtil.toByteString(accountIdentifier)))
|
.setUuid(UUIDUtil.toByteString(accountIdentifier)))
|
||||||
.setDeviceId(deviceId))
|
.setDeviceId(Device.PRIMARY_ID))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.EnumSource;
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.signal.chat.common.EcPreKey;
|
import org.signal.chat.common.EcPreKey;
|
||||||
import org.signal.chat.common.EcSignedPreKey;
|
import org.signal.chat.common.EcSignedPreKey;
|
||||||
|
@ -592,9 +591,8 @@ class KeysGrpcServiceTest extends SimpleBaseGrpcTest<KeysGrpcService, KeysGrpc.K
|
||||||
.build()));
|
.build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@Test
|
||||||
@ValueSource(bytes = {KeysGrpcHelper.ALL_DEVICES, 1})
|
void getPreKeysDeviceNotFound() {
|
||||||
void getPreKeysDeviceNotFound(final byte deviceId) {
|
|
||||||
final UUID accountIdentifier = UUID.randomUUID();
|
final UUID accountIdentifier = UUID.randomUUID();
|
||||||
|
|
||||||
final Account targetAccount = mock(Account.class);
|
final Account targetAccount = mock(Account.class);
|
||||||
|
@ -611,7 +609,7 @@ class KeysGrpcServiceTest extends SimpleBaseGrpcTest<KeysGrpcService, KeysGrpc.K
|
||||||
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
.setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI)
|
||||||
.setUuid(UUIDUtil.toByteString(accountIdentifier))
|
.setUuid(UUIDUtil.toByteString(accountIdentifier))
|
||||||
.build())
|
.build())
|
||||||
.setDeviceId(deviceId)
|
.setDeviceId(Device.PRIMARY_ID)
|
||||||
.build()));
|
.build()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,8 @@ public class AuthHelper {
|
||||||
public static Device VALID_DEVICE_3_PRIMARY = mock(Device.class);
|
public static Device VALID_DEVICE_3_PRIMARY = mock(Device.class);
|
||||||
public static Device VALID_DEVICE_3_LINKED = mock(Device.class);
|
public static Device VALID_DEVICE_3_LINKED = mock(Device.class);
|
||||||
|
|
||||||
|
public static final byte VALID_DEVICE_3_LINKED_ID = Device.PRIMARY_ID + 1;
|
||||||
|
|
||||||
private static SaltedTokenHash VALID_CREDENTIALS = mock(SaltedTokenHash.class);
|
private static SaltedTokenHash VALID_CREDENTIALS = mock(SaltedTokenHash.class);
|
||||||
private static SaltedTokenHash VALID_CREDENTIALS_TWO = mock(SaltedTokenHash.class);
|
private static SaltedTokenHash VALID_CREDENTIALS_TWO = mock(SaltedTokenHash.class);
|
||||||
private static SaltedTokenHash VALID_CREDENTIALS_3_PRIMARY = mock(SaltedTokenHash.class);
|
private static SaltedTokenHash VALID_CREDENTIALS_3_PRIMARY = mock(SaltedTokenHash.class);
|
||||||
|
@ -136,7 +138,7 @@ public class AuthHelper {
|
||||||
when(VALID_DEVICE_TWO.getId()).thenReturn(Device.PRIMARY_ID);
|
when(VALID_DEVICE_TWO.getId()).thenReturn(Device.PRIMARY_ID);
|
||||||
when(UNDISCOVERABLE_DEVICE.getId()).thenReturn(Device.PRIMARY_ID);
|
when(UNDISCOVERABLE_DEVICE.getId()).thenReturn(Device.PRIMARY_ID);
|
||||||
when(VALID_DEVICE_3_PRIMARY.getId()).thenReturn(Device.PRIMARY_ID);
|
when(VALID_DEVICE_3_PRIMARY.getId()).thenReturn(Device.PRIMARY_ID);
|
||||||
when(VALID_DEVICE_3_LINKED.getId()).thenReturn((byte) 2);
|
when(VALID_DEVICE_3_LINKED.getId()).thenReturn(VALID_DEVICE_3_LINKED_ID);
|
||||||
|
|
||||||
when(UNDISCOVERABLE_DEVICE.isPrimary()).thenReturn(true);
|
when(UNDISCOVERABLE_DEVICE.isPrimary()).thenReturn(true);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue