Profile gRPC: Define getVersionedProfile endpoint

This commit is contained in:
Katherine Yen
2023-08-30 14:47:11 -07:00
committed by GitHub
parent 5afc058f90
commit dd18fcaea2
7 changed files with 354 additions and 4 deletions

View File

@@ -31,6 +31,9 @@ import org.signal.chat.common.ServiceIdentifier;
import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest;
import org.signal.chat.profile.GetUnversionedProfileRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileAnonymousRequest;
import org.signal.chat.profile.GetVersionedProfileRequest;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.ProfileAnonymousGrpc;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ecc.Curve;
@@ -43,11 +46,16 @@ import org.whispersystems.textsecuregcm.entities.UserCapabilities;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.ProfileHelper;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import javax.annotation.Nullable;
public class ProfileAnonymousGrpcServiceTest {
private Account account;
private AccountsManager accountsManager;
private ProfilesManager profilesManager;
private ProfileBadgeConverter profileBadgeConverter;
private ProfileAnonymousGrpc.ProfileAnonymousBlockingStub profileAnonymousBlockingStub;
@@ -58,16 +66,19 @@ public class ProfileAnonymousGrpcServiceTest {
void setup() {
account = mock(Account.class);
accountsManager = mock(AccountsManager.class);
profilesManager = mock(ProfilesManager.class);
profileBadgeConverter = mock(ProfileBadgeConverter.class);
final Metadata metadata = new Metadata();
metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us");
metadata.put(UserAgentInterceptor.USER_AGENT_GRPC_HEADER, "Signal-Android/1.2.3");
profileAnonymousBlockingStub = ProfileAnonymousGrpc.newBlockingStub(GRPC_SERVER_EXTENSION.getChannel())
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
final ProfileAnonymousGrpcService profileAnonymousGrpcService = new ProfileAnonymousGrpcService(
accountsManager,
profilesManager,
profileBadgeConverter
);
@@ -166,4 +177,153 @@ public class ProfileAnonymousGrpcServiceTest {
Arguments.of(IdentityType.IDENTITY_TYPE_ACI, false, true)
);
}
@ParameterizedTest
@MethodSource
void getVersionedProfile(final String requestVersion,
@Nullable final String accountVersion,
final boolean expectResponseHasPaymentAddress) {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
final VersionedProfile profile = mock(VersionedProfile.class);
final byte[] name = ProfileHelper.generateRandomByteArray(81);
final byte[] emoji = ProfileHelper.generateRandomByteArray(60);
final byte[] about = ProfileHelper.generateRandomByteArray(156);
final byte[] paymentAddress = ProfileHelper.generateRandomByteArray(582);
final String avatar = "profiles/" + ProfileHelper.generateRandomBase64FromByteArray(16);
when(profile.name()).thenReturn(name);
when(profile.aboutEmoji()).thenReturn(emoji);
when(profile.about()).thenReturn(about);
when(profile.paymentAddress()).thenReturn(paymentAddress);
when(profile.avatar()).thenReturn(avatar);
when(account.getCurrentProfileVersion()).thenReturn(Optional.ofNullable(accountVersion));
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile)));
final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion(requestVersion)
.build())
.build();
final GetVersionedProfileResponse response = profileAnonymousBlockingStub.getVersionedProfile(request);
final GetVersionedProfileResponse.Builder expectedResponseBuilder = GetVersionedProfileResponse.newBuilder()
.setName(ByteString.copyFrom(name))
.setAbout(ByteString.copyFrom(about))
.setAboutEmoji(ByteString.copyFrom(emoji))
.setAvatar(avatar);
if (expectResponseHasPaymentAddress) {
expectedResponseBuilder.setPaymentAddress(ByteString.copyFrom(paymentAddress));
}
assertEquals(expectedResponseBuilder.build(), response);
}
private static Stream<Arguments> getVersionedProfile() {
return Stream.of(
Arguments.of("version1", "version1", true),
Arguments.of("version1", null, true),
Arguments.of("version1", "version2", false)
);
}
@Test
void getVersionedProfileVersionNotFound() {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.empty()));
final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build())
.build();
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileAnonymousBlockingStub.getVersionedProfile(request));
assertEquals(Status.NOT_FOUND.getCode(), statusRuntimeException.getStatus().getCode());
}
@ParameterizedTest
@MethodSource
void getVersionedProfileUnauthenticated(final boolean missingUnidentifiedAccessKey,
final boolean accountNotFound) {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(
CompletableFuture.completedFuture(accountNotFound ? Optional.empty() : Optional.of(account)));
final GetVersionedProfileAnonymousRequest.Builder requestBuilder = GetVersionedProfileAnonymousRequest.newBuilder()
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build());
if (!missingUnidentifiedAccessKey) {
requestBuilder.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey));
}
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileAnonymousBlockingStub.getVersionedProfile(requestBuilder.build()));
assertEquals(Status.UNAUTHENTICATED.getCode(), statusRuntimeException.getStatus().getCode());
}
private static Stream<Arguments> getVersionedProfileUnauthenticated() {
return Stream.of(
Arguments.of(true, false),
Arguments.of(false, true)
);
}
@Test
void getVersionedProfilePniInvalidArgument() {
final byte[] unidentifiedAccessKey = new byte[16];
new SecureRandom().nextBytes(unidentifiedAccessKey);
final GetVersionedProfileAnonymousRequest request = GetVersionedProfileAnonymousRequest.newBuilder()
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.setRequest(GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_PNI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build())
.build();
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileAnonymousBlockingStub.getVersionedProfile(request));
assertEquals(Status.INVALID_ARGUMENT.getCode(), statusRuntimeException.getStatus().getCode());
}
}

View File

@@ -21,6 +21,8 @@ import org.signal.chat.common.IdentityType;
import org.signal.chat.common.ServiceIdentifier;
import org.signal.chat.profile.GetUnversionedProfileRequest;
import org.signal.chat.profile.GetUnversionedProfileResponse;
import org.signal.chat.profile.GetVersionedProfileRequest;
import org.signal.chat.profile.GetVersionedProfileResponse;
import org.signal.chat.profile.SetProfileRequest.AvatarChange;
import org.signal.chat.profile.ProfileGrpc;
import org.signal.chat.profile.SetProfileRequest;
@@ -55,10 +57,12 @@ import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.tests.util.ProfileHelper;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import reactor.core.publisher.Mono;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import javax.annotation.Nullable;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
@@ -140,6 +144,7 @@ public class ProfileGrpcServiceTest {
PhoneNumberUtil.PhoneNumberFormat.E164);
final Metadata metadata = new Metadata();
metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us");
metadata.put(UserAgentInterceptor.USER_AGENT_GRPC_HEADER, "Signal-Android/1.2.3");
profileBlockingStub = ProfileGrpc.newBlockingStub(GRPC_SERVER_EXTENSION.getChannel())
.withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata));
@@ -472,4 +477,121 @@ public class ProfileGrpcServiceTest {
verifyNoInteractions(accountsManager);
}
@ParameterizedTest
@MethodSource
void getVersionedProfile(final String requestVersion, @Nullable final String accountVersion, final boolean expectResponseHasPaymentAddress) {
final VersionedProfile profile = mock(VersionedProfile.class);
final byte[] name = ProfileHelper.generateRandomByteArray(81);
final byte[] emoji = ProfileHelper.generateRandomByteArray(60);
final byte[] about = ProfileHelper.generateRandomByteArray(156);
final byte[] paymentAddress = ProfileHelper.generateRandomByteArray(582);
final String avatar = "profiles/" + ProfileHelper.generateRandomBase64FromByteArray(16);
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion(requestVersion)
.build();
when(profile.name()).thenReturn(name);
when(profile.about()).thenReturn(about);
when(profile.aboutEmoji()).thenReturn(emoji);
when(profile.avatar()).thenReturn(avatar);
when(profile.paymentAddress()).thenReturn(paymentAddress);
when(account.getCurrentProfileVersion()).thenReturn(Optional.ofNullable(accountVersion));
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(Optional.of(profile)));
final GetVersionedProfileResponse response = profileBlockingStub.getVersionedProfile(request);
final GetVersionedProfileResponse.Builder expectedResponseBuilder = GetVersionedProfileResponse.newBuilder()
.setName(ByteString.copyFrom(name))
.setAbout(ByteString.copyFrom(about))
.setAboutEmoji(ByteString.copyFrom(emoji))
.setAvatar(avatar);
if (expectResponseHasPaymentAddress) {
expectedResponseBuilder.setPaymentAddress(ByteString.copyFrom(paymentAddress));
}
assertEquals(expectedResponseBuilder.build(), response);
}
private static Stream<Arguments> getVersionedProfile() {
return Stream.of(
Arguments.of("version1", "version1", true),
Arguments.of("version1", null, true),
Arguments.of("version1", "version2", false)
);
}
@ParameterizedTest
@MethodSource
void getVersionedProfileAccountOrProfileNotFound(final boolean missingAccount, final boolean missingProfile) {
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("versionWithNoProfile")
.build();
when(accountsManager.getByServiceIdentifierAsync(any())).thenReturn(CompletableFuture.completedFuture(missingAccount ? Optional.empty() : Optional.of(account)));
when(profilesManager.getAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(missingProfile ? Optional.empty() : Optional.of(profile)));
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class,
() -> profileBlockingStub.getVersionedProfile(request));
assertEquals(Status.NOT_FOUND.getCode(), statusRuntimeException.getStatus().getCode());
}
private static Stream<Arguments> getVersionedProfileAccountOrProfileNotFound() {
return Stream.of(
Arguments.of(true, false),
Arguments.of(false, true)
);
}
@Test
void getVersionedProfileRatelimited() {
final Duration retryAfterDuration = Duration.ofMinutes(7);
when(rateLimiter.validateReactive(any(UUID.class)))
.thenReturn(Mono.error(new RateLimitExceededException(retryAfterDuration, false)));
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build();
final StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
() -> profileBlockingStub.getVersionedProfile(request));
assertEquals(Status.Code.RESOURCE_EXHAUSTED, exception.getStatus().getCode());
assertNotNull(exception.getTrailers());
assertEquals(retryAfterDuration, exception.getTrailers().get(RateLimitUtil.RETRY_AFTER_DURATION_KEY));
verifyNoInteractions(accountsManager);
verifyNoInteractions(profilesManager);
}
@Test
void getVersionedProfilePniInvalidArgument() {
final GetVersionedProfileRequest request = GetVersionedProfileRequest.newBuilder()
.setAccountIdentifier(ServiceIdentifier.newBuilder()
.setIdentityType(IdentityType.IDENTITY_TYPE_PNI)
.setUuid(ByteString.copyFrom(UUIDUtil.toBytes(UUID.randomUUID())))
.build())
.setVersion("someVersion")
.build();
final StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
() -> profileBlockingStub.getVersionedProfile(request));
assertEquals(Status.INVALID_ARGUMENT.getCode(), exception.getStatus().getCode());
}
}