mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 15:18:01 +01:00
Convert Profiles gRPC to SimpleGrpc
This commit is contained in:
@@ -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<GetUnversionedProfileResponse> 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> 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<GetVersionedProfileResponse> 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<GetExpiringProfileKeyCredentialResponse> 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<Account> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetVersionedProfileResponse> 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> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SetProfileResponse> 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<String> 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<Void> 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<Mono<?>> updates = new ArrayList<>(2);
|
||||
final Account account = accountsManager.getByAccountIdentifier(
|
||||
authenticatedDevice.accountIdentifier()).orElseThrow(Status.UNAUTHENTICATED::asException);
|
||||
final Optional<VersionedProfile> 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<AccountBadge> updatedBadges = Optional.of(request.getBadgeIdsList())
|
||||
.map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, a.getBadges()))
|
||||
.orElseGet(a::getBadges);
|
||||
final Optional<String> 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<AccountBadge> 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<GetUnversionedProfileResponse> 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<GetVersionedProfileResponse> 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<GetExpiringProfileKeyCredentialResponse> 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<Account> 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<Integer> allowedLengths) {
|
||||
private static void checkByteStringLength(final ByteString byteString, final String errorMessage,
|
||||
final List<Integer> 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) {
|
||||
|
||||
Reference in New Issue
Block a user