diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java index 46696ab58..92beb562d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcService.java @@ -15,7 +15,7 @@ import org.signal.chat.profile.GetUnversionedProfileAnonymousRequest; 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.SimpleProfileAnonymousGrpc; import org.signal.libsignal.zkgroup.ServerSecretParams; import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; @@ -25,9 +25,8 @@ import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager; -import reactor.core.publisher.Mono; -public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.ProfileAnonymousImplBase { +public class ProfileAnonymousGrpcService extends SimpleProfileAnonymousGrpc.ProfileAnonymousImplBase { private final AccountsManager accountsManager; private final ProfilesManager profilesManager; private final ProfileBadgeConverter profileBadgeConverter; @@ -47,72 +46,68 @@ public class ProfileAnonymousGrpcService extends ReactorProfileAnonymousGrpc.Pro } @Override - public Mono getUnversionedProfile(final GetUnversionedProfileAnonymousRequest request) { + public GetUnversionedProfileResponse getUnversionedProfile(final GetUnversionedProfileAnonymousRequest request) throws StatusException { final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getServiceIdentifier()); // Callers must be authenticated to request unversioned profiles by PNI if (targetIdentifier.identityType() == IdentityType.PNI) { - throw Status.UNAUTHENTICATED.asRuntimeException(); + throw Status.UNAUTHENTICATED.asException(); } - final Mono account = switch (request.getAuthenticationCase()) { + final Account account = switch (request.getAuthenticationCase()) { case GROUP_SEND_TOKEN -> { - try { - groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), targetIdentifier); + groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), targetIdentifier); - yield Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) - .flatMap(Mono::justOrEmpty) - .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())); - } catch (final StatusException e) { - yield Mono.error(e); - } + yield accountsManager.getByServiceIdentifier(targetIdentifier) + .orElseThrow(Status.NOT_FOUND::asException); } case UNIDENTIFIED_ACCESS_KEY -> getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()); - default -> Mono.error(Status.INVALID_ARGUMENT.asException()); + default -> throw Status.INVALID_ARGUMENT.asException(); }; - return account.map(targetAccount -> ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier, + return ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier, null, - targetAccount, - profileBadgeConverter)); + account, + profileBadgeConverter); } @Override - public Mono getVersionedProfile(final GetVersionedProfileAnonymousRequest request) { + public GetVersionedProfileResponse getVersionedProfile(final GetVersionedProfileAnonymousRequest request) throws StatusException { final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getAccountIdentifier()); if (targetIdentifier.identityType() != IdentityType.ACI) { - throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asException(); } - return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()) - .flatMap(targetAccount -> ProfileGrpcHelper.getVersionedProfile(targetAccount, profilesManager, request.getRequest().getVersion())); + final Account targetAccount = getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()); + return ProfileGrpcHelper.getVersionedProfile(targetAccount, profilesManager, request.getRequest().getVersion()); } @Override - public Mono getExpiringProfileKeyCredential( - final GetExpiringProfileKeyCredentialAnonymousRequest request) { + public GetExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredential( + final GetExpiringProfileKeyCredentialAnonymousRequest request) throws StatusException { final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getAccountIdentifier()); if (targetIdentifier.identityType() != IdentityType.ACI) { - throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asRuntimeException(); + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asException(); } if (request.getRequest().getCredentialType() != CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) { - throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asRuntimeException(); + throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asException(); } - return getTargetAccountAndValidateUnidentifiedAccess(targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()) - .flatMap(account -> ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(account.getUuid(), - request.getRequest().getVersion(), request.getRequest().getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations)); + final Account account = getTargetAccountAndValidateUnidentifiedAccess( + targetIdentifier, request.getUnidentifiedAccessKey().toByteArray()); + return ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(account.getUuid(), + request.getRequest().getVersion(), request.getRequest().getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations); } - private Mono getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) { - return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) - .flatMap(Mono::justOrEmpty) - .filter(targetAccount -> UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, unidentifiedAccessKey)) - .switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException())); + private Account getTargetAccountAndValidateUnidentifiedAccess(final ServiceIdentifier targetIdentifier, final byte[] unidentifiedAccessKey) throws StatusException { + + return accountsManager.getByServiceIdentifier(targetIdentifier) + .filter(targetAccount -> UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, unidentifiedAccessKey)) + .orElseThrow(Status.UNAUTHENTICATED::asException); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java index d39849dbe..5cc4d957d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcHelper.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.UUID; +import io.grpc.StatusException; import org.signal.chat.profile.Badge; import org.signal.chat.profile.BadgeSvg; import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse; @@ -30,36 +31,31 @@ import org.whispersystems.textsecuregcm.storage.DeviceCapability; import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.VersionedProfile; import org.whispersystems.textsecuregcm.util.ProfileHelper; -import reactor.core.publisher.Mono; public class ProfileGrpcHelper { - static Mono getVersionedProfile(final Account account, + static 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 String requestVersion) throws StatusException { - final GetVersionedProfileResponse.Builder responseBuilder = GetVersionedProfileResponse.newBuilder(); + final VersionedProfile profile = profilesManager.get(account.getUuid(), requestVersion) + .orElseThrow(Status.NOT_FOUND.withDescription("Profile version not found")::asException); - 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); - maybeProfile.map(VersionedProfile::phoneNumberSharing).map(ByteString::copyFrom).ifPresent(responseBuilder::setPhoneNumberSharing); + final GetVersionedProfileResponse.Builder responseBuilder = GetVersionedProfileResponse.newBuilder(); - // 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); + responseBuilder + .setName(ByteString.copyFrom(profile.name())) + .setAbout(ByteString.copyFrom(profile.about())) + .setAboutEmoji(ByteString.copyFrom(profile.aboutEmoji())) + .setAvatar(profile.avatar()) + .setPhoneNumberSharing(ByteString.copyFrom(profile.phoneNumberSharing())); - return responseBuilder.build(); - }); + // 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. + if (account.getCurrentProfileVersion().map(v -> v.equals(requestVersion)).orElse(true)) { + responseBuilder.setPaymentAddress(ByteString.copyFrom(profile.paymentAddress())); + } + + return responseBuilder.build(); } @VisibleForTesting @@ -127,27 +123,26 @@ public class ProfileGrpcHelper { return responseBuilder.build(); } - static Mono getExpiringProfileKeyCredentialResponse( + static GetExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse( final UUID targetUuid, final String version, final byte[] encodedCredentialRequest, final ProfilesManager profilesManager, - final ServerZkProfileOperations zkProfileOperations) { - return Mono.fromFuture(profilesManager.getAsync(targetUuid, version)) - .flatMap(Mono::justOrEmpty) - .map(profile -> { - final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse; - try { - profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential(encodedCredentialRequest, - profile, new ServiceId.Aci(targetUuid), zkProfileOperations); - } catch (VerificationFailedException | InvalidInputException e) { - throw Status.INVALID_ARGUMENT.withCause(e).asRuntimeException(); - } + final ServerZkProfileOperations zkProfileOperations) throws StatusException { - return GetExpiringProfileKeyCredentialResponse.newBuilder() - .setProfileKeyCredential(ByteString.copyFrom(profileKeyCredentialResponse.serialize())) - .build(); - }) - .switchIfEmpty(Mono.error(Status.NOT_FOUND.withDescription("Profile version not found").asException())); + final VersionedProfile profile = profilesManager.get(targetUuid, version) + .orElseThrow(Status.NOT_FOUND.withDescription("Profile version not found")::asException); + + final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse; + try { + profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential(encodedCredentialRequest, + profile, new ServiceId.Aci(targetUuid), zkProfileOperations); + } catch (VerificationFailedException | InvalidInputException e) { + throw Status.INVALID_ARGUMENT.withCause(e).asException(); + } + + return GetExpiringProfileKeyCredentialResponse.newBuilder() + .setProfileKeyCredential(ByteString.copyFrom(profileKeyCredentialResponse.serialize())) + .build(); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java index 0419282b5..6ebc9f12d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ProfileGrpcService.java @@ -9,13 +9,13 @@ import com.google.protobuf.ByteString; import io.grpc.Status; import java.time.Clock; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; +import io.grpc.StatusException; import org.signal.chat.profile.CredentialType; import org.signal.chat.profile.GetExpiringProfileKeyCredentialRequest; import org.signal.chat.profile.GetExpiringProfileKeyCredentialResponse; @@ -24,10 +24,10 @@ import org.signal.chat.profile.GetUnversionedProfileResponse; import org.signal.chat.profile.GetVersionedProfileRequest; import org.signal.chat.profile.GetVersionedProfileResponse; import org.signal.chat.profile.ProfileAvatarUploadAttributes; -import org.signal.chat.profile.ReactorProfileGrpc; import org.signal.chat.profile.SetProfileRequest; import org.signal.chat.profile.SetProfileRequest.AvatarChange; import org.signal.chat.profile.SetProfileResponse; +import org.signal.chat.profile.SimpleProfileGrpc; import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; @@ -35,6 +35,7 @@ import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter; import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration; import org.whispersystems.textsecuregcm.configuration.BadgesConfiguration; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.limits.RateLimiters; @@ -48,10 +49,8 @@ import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.VersionedProfile; import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.util.ProfileHelper; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase { +public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase { private final Clock clock; private final AccountsManager accountsManager; @@ -93,133 +92,132 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase { } @Override - public Mono setProfile(final SetProfileRequest request) { + public SetProfileResponse setProfile(final SetProfileRequest request) throws StatusException { validateRequest(request); - return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) - .flatMap(authenticatedDevice -> Mono.zip( - Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) - .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)), - Mono.fromFuture(() -> profilesManager.getAsync(authenticatedDevice.accountIdentifier(), request.getVersion())) - )) - .doOnNext(accountAndMaybeProfile -> { - if (!request.getPaymentAddress().isEmpty()) { - final boolean hasDisallowedPrefix = - dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream() - .anyMatch(prefix -> accountAndMaybeProfile.getT1().getNumber().startsWith(prefix)); - if (hasDisallowedPrefix && accountAndMaybeProfile.getT2().map(VersionedProfile::paymentAddress).isEmpty()) { - throw Status.PERMISSION_DENIED.asRuntimeException(); - } - } - }) - .flatMap(accountAndMaybeProfile -> { - final Account account = accountAndMaybeProfile.getT1(); - final Optional currentAvatar = accountAndMaybeProfile.getT2().map(VersionedProfile::avatar) - .filter(avatar -> avatar.startsWith("profiles/")); - final AvatarData avatarData = switch (AvatarChangeUtil.fromGrpcAvatarChange(request.getAvatarChange())) { - case AVATAR_CHANGE_UNCHANGED -> new AvatarData(currentAvatar, currentAvatar, Optional.empty()); - case AVATAR_CHANGE_CLEAR -> new AvatarData(currentAvatar, Optional.empty(), Optional.empty()); - case AVATAR_CHANGE_UPDATE -> { - final String updateAvatarObjectName = ProfileHelper.generateAvatarObjectName(); - yield new AvatarData(currentAvatar, Optional.of(updateAvatarObjectName), - Optional.of(generateAvatarUploadForm(updateAvatarObjectName))); - } - }; - final Mono profileSetMono = Mono.fromFuture(() -> profilesManager.setAsync(account.getUuid(), - new VersionedProfile( - request.getVersion(), - request.getName().toByteArray(), - avatarData.finalAvatar().orElse(null), - request.getAboutEmoji().toByteArray(), - request.getAbout().toByteArray(), - request.getPaymentAddress().toByteArray(), - request.getPhoneNumberSharing().toByteArray(), - request.getCommitment().toByteArray()))); + final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); - final List> updates = new ArrayList<>(2); + final Account account = accountsManager.getByAccountIdentifier( + authenticatedDevice.accountIdentifier()).orElseThrow(Status.UNAUTHENTICATED::asException); + final Optional maybeProfile = profilesManager.get( + authenticatedDevice.accountIdentifier(), request.getVersion()); - updates.add(Mono.fromFuture(() -> accountsManager.updateAsync(account, a -> { + if (!request.getPaymentAddress().isEmpty()) { + final boolean hasDisallowedPrefix = + dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream() + .anyMatch(prefix -> account.getNumber().startsWith(prefix)); + if (hasDisallowedPrefix && maybeProfile.map(VersionedProfile::paymentAddress).isEmpty()) { + throw Status.PERMISSION_DENIED.asException(); + } + } - final List updatedBadges = Optional.of(request.getBadgeIdsList()) - .map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, a.getBadges())) - .orElseGet(a::getBadges); + final Optional currentAvatar = maybeProfile.map(VersionedProfile::avatar) + .filter(avatar -> avatar.startsWith("profiles/")); - a.setBadges(clock, updatedBadges); - a.setCurrentProfileVersion(request.getVersion()); - }))); + final AvatarData avatarData = switch (AvatarChangeUtil.fromGrpcAvatarChange(request.getAvatarChange())) { + case AVATAR_CHANGE_UNCHANGED -> new AvatarData(currentAvatar, currentAvatar, Optional.empty()); + case AVATAR_CHANGE_CLEAR -> new AvatarData(currentAvatar, Optional.empty(), Optional.empty()); + case AVATAR_CHANGE_UPDATE -> { + final String updateAvatarObjectName = ProfileHelper.generateAvatarObjectName(); + yield new AvatarData(currentAvatar, Optional.of(updateAvatarObjectName), + Optional.of(generateAvatarUploadForm(updateAvatarObjectName))); + } + }; - if (request.getAvatarChange() != AvatarChange.AVATAR_CHANGE_UNCHANGED && avatarData.currentAvatar().isPresent()) { - updates.add(Mono.fromFuture(() -> profilesManager.deleteAvatar(avatarData.currentAvatar.get()))); - } - return profileSetMono.thenMany(Flux.merge(updates)).then(Mono.just(avatarData)); - }) - .map(avatarData -> avatarData.uploadAttributes() - .map(avatarUploadAttributes -> SetProfileResponse.newBuilder().setAttributes(avatarUploadAttributes).build()) - .orElse(SetProfileResponse.newBuilder().build()) - ); + profilesManager.set(account.getUuid(), + new VersionedProfile( + request.getVersion(), + request.getName().toByteArray(), + avatarData.finalAvatar().orElse(null), + request.getAboutEmoji().toByteArray(), + request.getAbout().toByteArray(), + request.getPaymentAddress().toByteArray(), + request.getPhoneNumberSharing().toByteArray(), + request.getCommitment().toByteArray())); + + accountsManager.update(account, a -> { + + final List updatedBadges = Optional.of(request.getBadgeIdsList()) + .map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, + a.getBadges())) + .orElseGet(a::getBadges); + + a.setBadges(clock, updatedBadges); + a.setCurrentProfileVersion(request.getVersion()); + }); + + if (request.getAvatarChange() != AvatarChange.AVATAR_CHANGE_UNCHANGED && avatarData.currentAvatar().isPresent()) { + profilesManager.deleteAvatar(avatarData.currentAvatar().get()); + } + + return avatarData.uploadAttributes() + .map(avatarUploadAttributes -> SetProfileResponse.newBuilder().setAttributes(avatarUploadAttributes).build()) + .orElse(SetProfileResponse.newBuilder().build()); } @Override - public Mono getUnversionedProfile(final GetUnversionedProfileRequest request) { + public GetUnversionedProfileResponse getUnversionedProfile(final GetUnversionedProfileRequest request) throws StatusException, RateLimitExceededException { final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); final ServiceIdentifier targetIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getServiceIdentifier()); - return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier) - .map(targetAccount -> ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier, + final Account targetAccount = validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier); + + return ProfileGrpcHelper.buildUnversionedProfileResponse(targetIdentifier, authenticatedDevice.accountIdentifier(), targetAccount, - profileBadgeConverter)); + profileBadgeConverter); } @Override - public Mono getVersionedProfile(final GetVersionedProfileRequest request) { + public GetVersionedProfileResponse getVersionedProfile(final GetVersionedProfileRequest request) throws StatusException, RateLimitExceededException { 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(); + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asException(); } - return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier) - .flatMap(account -> ProfileGrpcHelper.getVersionedProfile(account, profilesManager, request.getVersion())); + final Account account = validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier); + + return ProfileGrpcHelper.getVersionedProfile(account, profilesManager, request.getVersion()); } @Override - public Mono getExpiringProfileKeyCredential( - final GetExpiringProfileKeyCredentialRequest request) { + public GetExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredential( + final GetExpiringProfileKeyCredentialRequest request) throws StatusException, RateLimitExceededException { 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(); + throw Status.INVALID_ARGUMENT.withDescription("Expected ACI service identifier").asException(); } if (request.getCredentialType() != CredentialType.CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY) { - throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asRuntimeException(); + throw Status.INVALID_ARGUMENT.withDescription("Expected expiring profile key credential type").asException(); } - return validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier) - .flatMap(targetAccount -> ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(targetAccount.getUuid(), - request.getVersion(), request.getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations)); + final Account targetAccount = validateRateLimitAndGetAccount(authenticatedDevice.accountIdentifier(), targetIdentifier); + + return ProfileGrpcHelper.getExpiringProfileKeyCredentialResponse(targetAccount.getUuid(), + request.getVersion(), request.getCredentialRequest().toByteArray(), profilesManager, zkProfileOperations); } - private Mono validateRateLimitAndGetAccount(final UUID requesterUuid, - final ServiceIdentifier targetIdentifier) { - return rateLimiters.getProfileLimiter().validateReactive(requesterUuid) - .then(Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) - .flatMap(Mono::justOrEmpty)) - .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())); + private Account validateRateLimitAndGetAccount(final UUID requesterUuid, + final ServiceIdentifier targetIdentifier) throws RateLimitExceededException, StatusException { + rateLimiters.getProfileLimiter().validate(requesterUuid); + + return accountsManager.getByServiceIdentifier(targetIdentifier).orElseThrow(Status.NOT_FOUND::asException); } - private void validateRequest(final SetProfileRequest request) { + private void validateRequest(final SetProfileRequest request) throws StatusException { if (request.getVersion().isEmpty()) { - throw Status.INVALID_ARGUMENT.withDescription("Missing version").asRuntimeException(); + throw Status.INVALID_ARGUMENT.withDescription("Missing version").asException(); } if (request.getCommitment().isEmpty()) { - throw Status.INVALID_ARGUMENT.withDescription("Missing profile commitment").asRuntimeException(); + throw Status.INVALID_ARGUMENT.withDescription("Missing profile commitment").asException(); } checkByteStringLength(request.getName(), "Invalid name length", List.of(81, 285)); @@ -228,7 +226,9 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase { checkByteStringLength(request.getPaymentAddress(), "Invalid mobile coin address length", List.of(0, 582)); } - private static void checkByteStringLength(final ByteString byteString, final String errorMessage, final List allowedLengths) { + private static void checkByteStringLength(final ByteString byteString, final String errorMessage, + final List allowedLengths) throws StatusException { + final int byteStringLength = byteString.toByteArray().length; for (int allowedLength : allowedLengths) { @@ -237,7 +237,7 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase { } } - throw Status.INVALID_ARGUMENT.withDescription(errorMessage).asRuntimeException(); + throw Status.INVALID_ARGUMENT.withDescription(errorMessage).asException(); } private ProfileAvatarUploadAttributes generateAvatarUploadForm(final String objectName) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java index 1860bb78e..4e9669475 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/ProfileAnonymousGrpcServiceTest.java @@ -27,7 +27,6 @@ import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import javax.annotation.Nullable; import org.junit.jupiter.api.Test; @@ -135,7 +134,7 @@ public class ProfileAnonymousGrpcServiceTest extends SimpleBaseGrpcTest { @@ -174,7 +174,6 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); - verify(profilesManager).setAsync(eq(account.getUuid()), profileArgumentCaptor.capture()); + verify(profilesManager).set(eq(account.getUuid()), profileArgumentCaptor.capture()); final VersionedProfile profile = profileArgumentCaptor.getValue(); @@ -267,9 +265,7 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().setProfile(request), @@ -422,7 +418,7 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().getVersionedProfile(request)); } @@ -638,8 +632,8 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest