Profile gRPC: Define `getVersionedProfile` endpoint
This commit is contained in:
		
							parent
							
								
									5afc058f90
								
							
						
					
					
						commit
						dd18fcaea2
					
				|  | @ -649,7 +649,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration | ||||||
|         .addService(new KeysAnonymousGrpcService(accountsManager, keys)) |         .addService(new KeysAnonymousGrpcService(accountsManager, keys)) | ||||||
|         .addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, |         .addService(ServerInterceptors.intercept(new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, | ||||||
|                 config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor)) |                 config.getBadges(), asyncCdnS3Client, profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters, config.getCdnConfiguration().bucket()), basicCredentialAuthenticationInterceptor)) | ||||||
|         .addService(new ProfileAnonymousGrpcService(accountsManager, profileBadgeConverter)); |         .addService(new ProfileAnonymousGrpcService(accountsManager, profilesManager, profileBadgeConverter)); | ||||||
| 
 | 
 | ||||||
|     RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager); |     RemoteDeprecationFilter remoteDeprecationFilter = new RemoteDeprecationFilter(dynamicConfigurationManager); | ||||||
|     environment.servlets() |     environment.servlets() | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ package org.whispersystems.textsecuregcm.grpc; | ||||||
| import io.grpc.Status; | import io.grpc.Status; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest; | import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileResponse; | import org.signal.chat.profile.GetUnversionedProfileResponse; | ||||||
|  | import org.signal.chat.profile.GetVersionedProfileAnonymousRequest; | ||||||
|  | import org.signal.chat.profile.GetVersionedProfileResponse; | ||||||
| import org.signal.chat.profile.ReactorProfileAnonymousGrpc; | import org.signal.chat.profile.ReactorProfileAnonymousGrpc; | ||||||
| import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; | import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; | ||||||
| import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; | import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; | ||||||
|  | @ -10,16 +12,20 @@ import org.whispersystems.textsecuregcm.identity.IdentityType; | ||||||
| import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; | import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; | ||||||
| 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.ProfilesManager; | ||||||
| import reactor.core.publisher.Mono; | import reactor.core.publisher.Mono; | ||||||
| 
 | 
 | ||||||
| public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.ProfileAnonymousImplBase { | public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.ProfileAnonymousImplBase { | ||||||
|   private final AccountsManager accountsManager; |   private final AccountsManager accountsManager; | ||||||
|  |   private final ProfilesManager profilesManager; | ||||||
|   private final ProfileBadgeConverter profileBadgeConverter; |   private final ProfileBadgeConverter profileBadgeConverter; | ||||||
| 
 | 
 | ||||||
|   public ProfileAnonymousGrpcService( |   public ProfileAnonymousGrpcService( | ||||||
|       final AccountsManager accountsManager, |       final AccountsManager accountsManager, | ||||||
|  |       final ProfilesManager profilesManager, | ||||||
|       final ProfileBadgeConverter profileBadgeConverter) { |       final ProfileBadgeConverter profileBadgeConverter) { | ||||||
|     this.accountsManager = accountsManager; |     this.accountsManager = accountsManager; | ||||||
|  |     this.profilesManager = profilesManager; | ||||||
|     this.profileBadgeConverter = profileBadgeConverter; |     this.profileBadgeConverter = profileBadgeConverter; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -40,6 +46,18 @@ public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.Pro | ||||||
|             profileBadgeConverter)); |             profileBadgeConverter)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @Override | ||||||
|  |   public Mono<GetVersionedProfileResponse> getVersionedProfile(final GetVersionedProfileAnonymousRequest request) { | ||||||
|  |     final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getAccountIdentifier()); | ||||||
|  | 
 | ||||||
|  |     if (targetIdentifier.identityType() != IdentityType.ACI) { | ||||||
|  |       throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()) | ||||||
|  |         .flatMap(targetAccount -> ProfileGrpcHelper.getVersionedProfile(targetAccount, profilesManager, request.getRequest().getVersion())); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|     private Mono<Account> getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) { |     private Mono<Account> getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) { | ||||||
|       return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) |       return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) | ||||||
|         .flatMap(Mono::justOrEmpty) |         .flatMap(Mono::justOrEmpty) | ||||||
|  |  | ||||||
|  | @ -5,18 +5,51 @@ import com.google.protobuf.ByteString; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  | import io.grpc.Status; | ||||||
| import org.signal.chat.profile.Badge; | import org.signal.chat.profile.Badge; | ||||||
| import org.signal.chat.profile.BadgeSvg; | import org.signal.chat.profile.BadgeSvg; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileResponse; | import org.signal.chat.profile.GetUnversionedProfileResponse; | ||||||
|  | import org.signal.chat.profile.GetVersionedProfileResponse; | ||||||
| import org.signal.chat.profile.UserCapabilities; | import org.signal.chat.profile.UserCapabilities; | ||||||
| import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum; | import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum; | ||||||
| import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; | import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; | ||||||
| import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; | import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; | ||||||
| import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; | import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; | ||||||
| import org.whispersystems.textsecuregcm.storage.Account; | import org.whispersystems.textsecuregcm.storage.Account; | ||||||
|  | import org.whispersystems.textsecuregcm.storage.ProfilesManager; | ||||||
|  | import org.whispersystems.textsecuregcm.storage.VersionedProfile; | ||||||
| import org.whispersystems.textsecuregcm.util.ProfileHelper; | import org.whispersystems.textsecuregcm.util.ProfileHelper; | ||||||
|  | import reactor.core.publisher.Mono; | ||||||
| 
 | 
 | ||||||
| public class ProfileGrpcHelper { | public class ProfileGrpcHelper { | ||||||
|  |   static Mono<GetVersionedProfileResponse> getVersionedProfile(final Account account, | ||||||
|  |       final ProfilesManager profilesManager, | ||||||
|  |       final String requestVersion) { | ||||||
|  |     return Mono.fromFuture(() -> profilesManager.getAsync(account.getUuid(), requestVersion)) | ||||||
|  |         .map(maybeProfile -> { | ||||||
|  |           if (maybeProfile.isEmpty()) { | ||||||
|  |             throw Status.NOT_FOUND.withDescription("Profile version not found").asRuntimeException(); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           final GetVersionedProfileResponse.Builder responseBuilder = GetVersionedProfileResponse.newBuilder(); | ||||||
|  | 
 | ||||||
|  |           maybeProfile.map(VersionedProfile::name).map(ByteString::copyFrom).ifPresent(responseBuilder::setName); | ||||||
|  |           maybeProfile.map(VersionedProfile::about).map(ByteString::copyFrom).ifPresent(responseBuilder::setAbout); | ||||||
|  |           maybeProfile.map(VersionedProfile::aboutEmoji).map(ByteString::copyFrom).ifPresent(responseBuilder::setAboutEmoji); | ||||||
|  |           maybeProfile.map(VersionedProfile::avatar).ifPresent(responseBuilder::setAvatar); | ||||||
|  | 
 | ||||||
|  |           // Allow requests where either the version matches the latest version on Account or the latest version on Account | ||||||
|  |           // is empty to read the payment address. | ||||||
|  |           maybeProfile | ||||||
|  |               .filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(requestVersion)).orElse(true)) | ||||||
|  |               .map(VersionedProfile::paymentAddress) | ||||||
|  |               .map(ByteString::copyFrom) | ||||||
|  |               .ifPresent(responseBuilder::setPaymentAddress); | ||||||
|  | 
 | ||||||
|  |           return responseBuilder.build(); | ||||||
|  |         }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @VisibleForTesting |   @VisibleForTesting | ||||||
|   static List<Badge> buildBadges(final List<org.whispersystems.textsecuregcm.entities.Badge> badges) { |   static List<Badge> buildBadges(final List<org.whispersystems.textsecuregcm.entities.Badge> badges) { | ||||||
|     final ArrayList<Badge> grpcBadges = new ArrayList<>(); |     final ArrayList<Badge> grpcBadges = new ArrayList<>(); | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ import com.google.protobuf.ByteString; | ||||||
| import io.grpc.Status; | import io.grpc.Status; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileRequest; | import org.signal.chat.profile.GetUnversionedProfileRequest; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileResponse; | 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.SetProfileRequest.AvatarChange; | ||||||
| import org.signal.chat.profile.ProfileAvatarUploadAttributes; | import org.signal.chat.profile.ProfileAvatarUploadAttributes; | ||||||
| import org.signal.chat.profile.ReactorProfileGrpc; | import org.signal.chat.profile.ReactorProfileGrpc; | ||||||
|  | @ -15,6 +17,7 @@ import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; | ||||||
| import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; | import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; | ||||||
| import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; | import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; | ||||||
| import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; | import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; | ||||||
|  | import org.whispersystems.textsecuregcm.identity.IdentityType; | ||||||
| import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; | import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; | ||||||
| import org.whispersystems.textsecuregcm.limits.RateLimiters; | import org.whispersystems.textsecuregcm.limits.RateLimiters; | ||||||
| import org.whispersystems.textsecuregcm.s3.PolicySigner; | import org.whispersystems.textsecuregcm.s3.PolicySigner; | ||||||
|  | @ -169,6 +172,20 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase { | ||||||
|             profileBadgeConverter)); |             profileBadgeConverter)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @Override | ||||||
|  |   public Mono<GetVersionedProfileResponse> getVersionedProfile(final GetVersionedProfileRequest request) { | ||||||
|  |     final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); | ||||||
|  |     final ServiceIdentifier targetIdentifier = | ||||||
|  |         ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getAccountIdentifier()); | ||||||
|  | 
 | ||||||
|  |     if (targetIdentifier.identityType() != IdentityType.ACI) { | ||||||
|  |       throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier) | ||||||
|  |         .flatMap(account -> ProfileGrpcHelper.getVersionedProfile(account, profilesManager, request.getVersion())); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private Mono<Account> validateRateLimitAndGetAccount(final UUID requesterUuid, |   private Mono<Account> validateRateLimitAndGetAccount(final UUID requesterUuid, | ||||||
|       final ServiceIdentifier targetIdentifier) { |       final ServiceIdentifier targetIdentifier) { | ||||||
|     return rateLimiters.getProfileLimiter().validateReactive(requesterUuid) |     return rateLimiters.getProfileLimiter().validateReactive(requesterUuid) | ||||||
|  |  | ||||||
|  | @ -162,7 +162,7 @@ message GetVersionedProfileRequest { | ||||||
|   /** |   /** | ||||||
|    * The profile version to retrieve. |    * The profile version to retrieve. | ||||||
|    */ |    */ | ||||||
|   bytes version = 2; |   string version = 2; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| message GetVersionedProfileAnonymousRequest { | message GetVersionedProfileAnonymousRequest { | ||||||
|  |  | ||||||
|  | @ -31,6 +31,9 @@ import org.signal.chat.common.ServiceIdentifier; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest; | import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileRequest; | import org.signal.chat.profile.GetUnversionedProfileRequest; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileResponse; | 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.chat.profile.ProfileAnonymousGrpc; | ||||||
| import org.signal.libsignal.protocol.IdentityKey; | import org.signal.libsignal.protocol.IdentityKey; | ||||||
| import org.signal.libsignal.protocol.ecc.Curve; | 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.identity.AciServiceIdentifier; | ||||||
| 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.ProfilesManager; | ||||||
|  | import org.whispersystems.textsecuregcm.storage.VersionedProfile; | ||||||
|  | import org.whispersystems.textsecuregcm.tests.util.ProfileHelper; | ||||||
| import org.whispersystems.textsecuregcm.util.UUIDUtil; | import org.whispersystems.textsecuregcm.util.UUIDUtil; | ||||||
|  | import javax.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
| public class ProfileAnonymousGrpcServiceTest { | public class ProfileAnonymousGrpcServiceTest { | ||||||
|   private Account account; |   private Account account; | ||||||
|   private AccountsManager accountsManager; |   private AccountsManager accountsManager; | ||||||
|  |   private ProfilesManager profilesManager; | ||||||
|   private ProfileBadgeConverter profileBadgeConverter; |   private ProfileBadgeConverter profileBadgeConverter; | ||||||
|   private ProfileAnonymousGrpc.ProfileAnonymousBlockingStub profileAnonymousBlockingStub; |   private ProfileAnonymousGrpc.ProfileAnonymousBlockingStub profileAnonymousBlockingStub; | ||||||
| 
 | 
 | ||||||
|  | @ -58,16 +66,19 @@ public class ProfileAnonymousGrpcServiceTest { | ||||||
|   void setup() { |   void setup() { | ||||||
|     account = mock(Account.class); |     account = mock(Account.class); | ||||||
|     accountsManager = mock(AccountsManager.class); |     accountsManager = mock(AccountsManager.class); | ||||||
|  |     profilesManager = mock(ProfilesManager.class); | ||||||
|     profileBadgeConverter = mock(ProfileBadgeConverter.class); |     profileBadgeConverter = mock(ProfileBadgeConverter.class); | ||||||
| 
 | 
 | ||||||
|     final Metadata metadata = new Metadata(); |     final Metadata metadata = new Metadata(); | ||||||
|     metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us"); |     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()) |     profileAnonymousBlockingStub = ProfileAnonymousGrpc.newBlockingStub(GRPC_SERVER_EXTENSION.getChannel()) | ||||||
|         .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); |         .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); | ||||||
| 
 | 
 | ||||||
|     final ProfileAnonymousGrpcService profileAnonymousGrpcService = new ProfileAnonymousGrpcService( |     final ProfileAnonymousGrpcService profileAnonymousGrpcService = new ProfileAnonymousGrpcService( | ||||||
|         accountsManager, |         accountsManager, | ||||||
|  |         profilesManager, | ||||||
|         profileBadgeConverter |         profileBadgeConverter | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  | @ -166,4 +177,153 @@ public class ProfileAnonymousGrpcServiceTest { | ||||||
|         Arguments.of(IdentityType.IDENTITY_TYPE_ACI, false, true) |         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()); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -21,6 +21,8 @@ import org.signal.chat.common.IdentityType; | ||||||
| import org.signal.chat.common.ServiceIdentifier; | import org.signal.chat.common.ServiceIdentifier; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileRequest; | import org.signal.chat.profile.GetUnversionedProfileRequest; | ||||||
| import org.signal.chat.profile.GetUnversionedProfileResponse; | 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.SetProfileRequest.AvatarChange; | ||||||
| import org.signal.chat.profile.ProfileGrpc; | import org.signal.chat.profile.ProfileGrpc; | ||||||
| import org.signal.chat.profile.SetProfileRequest; | 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.ProfilesManager; | ||||||
| import org.whispersystems.textsecuregcm.storage.VersionedProfile; | import org.whispersystems.textsecuregcm.storage.VersionedProfile; | ||||||
| import org.whispersystems.textsecuregcm.tests.util.AuthHelper; | import org.whispersystems.textsecuregcm.tests.util.AuthHelper; | ||||||
|  | import org.whispersystems.textsecuregcm.tests.util.ProfileHelper; | ||||||
| import org.whispersystems.textsecuregcm.util.UUIDUtil; | import org.whispersystems.textsecuregcm.util.UUIDUtil; | ||||||
| import reactor.core.publisher.Mono; | import reactor.core.publisher.Mono; | ||||||
| import software.amazon.awssdk.services.s3.S3AsyncClient; | import software.amazon.awssdk.services.s3.S3AsyncClient; | ||||||
| import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; | import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; | ||||||
|  | import javax.annotation.Nullable; | ||||||
| import java.security.SecureRandom; | import java.security.SecureRandom; | ||||||
| import java.time.Clock; | import java.time.Clock; | ||||||
| import java.time.Duration; | import java.time.Duration; | ||||||
|  | @ -140,6 +144,7 @@ public class ProfileGrpcServiceTest { | ||||||
|         PhoneNumberUtil.PhoneNumberFormat.E164); |         PhoneNumberUtil.PhoneNumberFormat.E164); | ||||||
|     final Metadata metadata = new Metadata(); |     final Metadata metadata = new Metadata(); | ||||||
|     metadata.put(AcceptLanguageInterceptor.ACCEPTABLE_LANGUAGES_GRPC_HEADER, "en-us"); |     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()) |     profileBlockingStub = ProfileGrpc.newBlockingStub(GRPC_SERVER_EXTENSION.getChannel()) | ||||||
|         .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); |         .withInterceptors(MetadataUtils.newAttachHeadersInterceptor(metadata)); | ||||||
|  | @ -472,4 +477,121 @@ public class ProfileGrpcServiceTest { | ||||||
| 
 | 
 | ||||||
|     verifyNoInteractions(accountsManager); |     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()); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 Katherine Yen
						Katherine Yen