mirror of
https://github.com/signalapp/Signal-Server
synced 2026-10-06 06:37:53 +01:00
Don't assume presence of phone numbers when checking for prohibited payment regions
This commit is contained in:
1 parent
78e7e992da
commit
2f1843d5d2
11 files changed
+242
-60
No files matched your search
@@ -1093,7 +1093,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new CallingGrpcService(cloudflareTurnCredentialsManager, rateLimiters),
|
||||
new CredentialsGrpcService(accountsManager, certificateGenerator, zkAuthOperations, callingGenericZkSecretParams, rateLimiters, Clock.systemUTC(), ExternalServiceDefinitions.createExternalServiceList(config, Clock.systemUTC())),
|
||||
new KeysGrpcService(accountsManager, keysManager, rateLimiters),
|
||||
new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, config.getBadges(), profileCdnPolicyGenerator, chatGenericZkSecretParams, profileBadgeConverter, rateLimiters),
|
||||
new ProfileGrpcService(clock, accountsManager, profilesManager, asnInfoProviderSupplier, dynamicConfigurationManager, config.getBadges(), profileCdnPolicyGenerator, chatGenericZkSecretParams, profileBadgeConverter, rateLimiters),
|
||||
new MessagesGrpcService(accountsManager, rateLimiters, messageSender, messageByteLimitCardinalityEstimator, spamChecker, messageDispatcher, Clock.systemUTC()),
|
||||
new BackupsGrpcService(accountsManager, backupAuthManager, backupMetrics),
|
||||
new DevicesGrpcService(accountsManager),
|
||||
@@ -1263,8 +1263,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new MessageController(rateLimiters, messageByteLimitCardinalityEstimator, messageSender, accountsManager,
|
||||
phoneNumberIdentifiers, reportMessageManager, groupZkSecretParams, spamChecker, Clock.systemUTC()),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
|
||||
profileBadgeConverter, config.getBadges(), profileCdnPolicyGenerator,
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, asnInfoProviderSupplier,
|
||||
dynamicConfigurationManager, profileBadgeConverter, config.getBadges(), profileCdnPolicyGenerator,
|
||||
groupZkSecretParams, zkProfileOperations, batchIdentityCheckExecutor),
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class DynamicConfiguration {
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
private DynamicPaymentsConfiguration payments = new DynamicPaymentsConfiguration();
|
||||
private DynamicPaymentsConfiguration payments = DynamicPaymentsConfiguration.DEFAULT;
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
|
||||
+13
-5
@@ -6,15 +6,23 @@
|
||||
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class DynamicPaymentsConfiguration {
|
||||
public record DynamicPaymentsConfiguration(@JsonProperty @NotNull List<String> disallowedPrefixes,
|
||||
@JsonProperty @NotNull List<String> disallowedAsnRegions) {
|
||||
|
||||
@JsonProperty
|
||||
private List<String> disallowedPrefixes = Collections.emptyList();
|
||||
public static DynamicPaymentsConfiguration DEFAULT =
|
||||
new DynamicPaymentsConfiguration(Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
public List<String> getDisallowedPrefixes() {
|
||||
return disallowedPrefixes;
|
||||
public DynamicPaymentsConfiguration {
|
||||
if (disallowedPrefixes == null) {
|
||||
disallowedPrefixes = DEFAULT.disallowedPrefixes();
|
||||
}
|
||||
|
||||
if (disallowedAsnRegions == null) {
|
||||
disallowedAsnRegions = DEFAULT.disallowedAsnRegions();
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-17
@@ -50,6 +50,7 @@ import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.Nullable;
|
||||
import org.glassfish.jersey.server.ManagedAsync;
|
||||
@@ -64,6 +65,7 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialRespons
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfoProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.Anonymous;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.GroupSendTokenHeader;
|
||||
@@ -80,6 +82,7 @@ import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||
@@ -108,6 +111,7 @@ public class ProfileController {
|
||||
private final RateLimiters rateLimiters;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final AccountsManager accountsManager;
|
||||
private final Supplier<AsnInfoProvider> asnInfoProviderSupplier;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final ProfileBadgeConverter profileBadgeConverter;
|
||||
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
|
||||
@@ -124,25 +128,27 @@ public class ProfileController {
|
||||
private static final String DUPLICATE_AUTHENTICATION_COUNTER_NAME = name(ProfileController.class, "duplicateAuthentication");
|
||||
|
||||
public ProfileController(
|
||||
Clock clock,
|
||||
RateLimiters rateLimiters,
|
||||
AccountsManager accountsManager,
|
||||
ProfilesManager profilesManager,
|
||||
DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
ProfileBadgeConverter profileBadgeConverter,
|
||||
BadgesConfiguration badgesConfiguration,
|
||||
PostPolicyGenerator policyGenerator,
|
||||
ServerSecretParams serverSecretParams,
|
||||
ServerZkProfileOperations zkProfileOperations,
|
||||
Executor batchIdentityCheckExecutor) {
|
||||
final Clock clock,
|
||||
final RateLimiters rateLimiters,
|
||||
final AccountsManager accountsManager,
|
||||
final ProfilesManager profilesManager,
|
||||
final Supplier<AsnInfoProvider> asnInfoProviderSupplier,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final ProfileBadgeConverter profileBadgeConverter,
|
||||
final BadgesConfiguration badgesConfiguration,
|
||||
final PostPolicyGenerator policyGenerator,
|
||||
final ServerSecretParams serverSecretParams,
|
||||
final ServerZkProfileOperations zkProfileOperations,
|
||||
final Executor batchIdentityCheckExecutor) {
|
||||
this.clock = clock;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.accountsManager = accountsManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.asnInfoProviderSupplier = asnInfoProviderSupplier;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.profileBadgeConverter = profileBadgeConverter;
|
||||
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap(
|
||||
BadgeConfiguration::getId, Function.identity()));
|
||||
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream()
|
||||
.collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity()));
|
||||
this.serverSecretParams = serverSecretParams;
|
||||
this.zkProfileOperations = zkProfileOperations;
|
||||
this.policyGenerator = policyGenerator;
|
||||
@@ -161,10 +167,12 @@ public class ProfileController {
|
||||
description = "If the request changed the avatar, the response body contains an upload form.")))
|
||||
@ApiResponse(responseCode = "400", description = "Invalid create profile request.")
|
||||
@ApiResponse(responseCode = "401", description = "Account authentication check failed.")
|
||||
@ApiResponse(responseCode = "403", description = "The request contained a payment address, but payments are not supported in the region of the account’s phone number.")
|
||||
@ApiResponse(responseCode = "403", description = "The request contained a payment address, but payments are not supported in the region of the account’s phone number or the caller's ASN if the account does not have a phone number.")
|
||||
@ApiResponse(responseCode = "412", description = "The requesting account has the profiles_v2 capability")
|
||||
@ApiResponse(responseCode = "422", description = "Invalid request format")
|
||||
public Response setProfile(@Auth AuthenticatedDevice auth, @NotNull @Valid CreateProfileRequest request) {
|
||||
public Response setProfile(@Auth AuthenticatedDevice auth,
|
||||
@NotNull @Valid CreateProfileRequest request,
|
||||
@Context final ContainerRequestContext requestContext) {
|
||||
|
||||
final Account account = accountsManager.getByAccountIdentifier(auth.accountIdentifier())
|
||||
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
|
||||
@@ -176,7 +184,12 @@ public class ProfileController {
|
||||
final Optional<VersionedProfileV1> currentProfile =
|
||||
profilesManager.getV1(auth.accountIdentifier(), request.version());
|
||||
|
||||
if (request.paymentAddress() != null && request.paymentAddress().length != 0 && ProfileHelper.isPaymentAddressUpdateForbidden(account, Optional.empty(), currentProfile, dynamicConfigurationManager)) {
|
||||
final String remoteAddress = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||
|
||||
if (request.paymentAddress() != null &&
|
||||
request.paymentAddress().length != 0 &&
|
||||
ProfileHelper.isPaymentAddressUpdateForbidden(account, Optional.empty(), currentProfile, remoteAddress, asnInfoProviderSupplier.get(), dynamicConfigurationManager)) {
|
||||
|
||||
return Response.status(Response.Status.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
@@ -198,7 +211,7 @@ public class ProfileController {
|
||||
currentAvatar.ifPresent(profilesManager::deleteAvatar);
|
||||
}
|
||||
|
||||
accountsManager.update(account.getIdentifier(IdentityType.ACI), a -> {
|
||||
accountsManager.update(account.getAccountIdentifier(), a -> {
|
||||
|
||||
final List<AccountBadge> updatedBadges = request.badges()
|
||||
.map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges, a.getBadges()))
|
||||
|
||||
+13
-11
@@ -16,6 +16,7 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import org.signal.chat.common.S3UploadForm;
|
||||
import org.signal.chat.errors.FailedPrecondition;
|
||||
@@ -37,8 +38,7 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfoProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
|
||||
@@ -46,7 +46,6 @@ 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;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
@@ -63,11 +62,10 @@ import org.whispersystems.textsecuregcm.util.ProfileHelper;
|
||||
|
||||
public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ProfileGrpcService.class);
|
||||
|
||||
private final Clock clock;
|
||||
private final AccountsManager accountsManager;
|
||||
private final ProfilesManager profilesManager;
|
||||
private final Supplier<AsnInfoProvider> asnInfoProviderSupplier;
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
@@ -83,6 +81,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
final Clock clock,
|
||||
final AccountsManager accountsManager,
|
||||
final ProfilesManager profilesManager,
|
||||
final Supplier<AsnInfoProvider> asnInfoProviderSupplier,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final BadgesConfiguration badgesConfiguration,
|
||||
final PostPolicyGenerator policyGenerator,
|
||||
@@ -92,9 +91,10 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
this.clock = clock;
|
||||
this.accountsManager = accountsManager;
|
||||
this.profilesManager = profilesManager;
|
||||
this.asnInfoProviderSupplier = asnInfoProviderSupplier;
|
||||
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap(
|
||||
BadgeConfiguration::getId, Function.identity()));
|
||||
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream()
|
||||
.collect(Collectors.toMap(BadgeConfiguration::getId, Function.identity()));
|
||||
this.policyGenerator = policyGenerator;
|
||||
this.genericServerSecretParams = genericServerSecretParams;
|
||||
this.profileBadgeConverter = profileBadgeConverter;
|
||||
@@ -132,7 +132,9 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
final Optional<VersionedProfileV1> maybeV1Profile = profilesManager.getV1(
|
||||
authenticatedDevice.accountIdentifier(), HexFormat.of().formatHex(version));
|
||||
|
||||
if (!request.getPaymentAddress().isEmpty() && ProfileHelper.isPaymentAddressUpdateForbidden(account, maybeProfile, maybeV1Profile, dynamicConfigurationManager)) {
|
||||
if (!request.getPaymentAddress().isEmpty() &&
|
||||
ProfileHelper.isPaymentAddressUpdateForbidden(account, maybeProfile, maybeV1Profile, RequestAttributesUtil.getRemoteAddress().getHostAddress(), asnInfoProviderSupplier.get(), dynamicConfigurationManager)) {
|
||||
|
||||
return SetProfileResponse.newBuilder()
|
||||
.setPaymentsForbiddenInRegion(PaymentsForbiddenInRegion.getDefaultInstance())
|
||||
.build();
|
||||
@@ -172,7 +174,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
);
|
||||
|
||||
try {
|
||||
profilesManager.set(account.getIdentifier(IdentityType.ACI), v1Profile, profile,
|
||||
profilesManager.set(account.getAccountIdentifier(), v1Profile, profile,
|
||||
request.getExpectedCurrentDataHash().isEmpty() ? null : request.getExpectedCurrentDataHash().toByteArray());
|
||||
|
||||
} catch (WriteConflictException _) {
|
||||
@@ -184,7 +186,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
}
|
||||
|
||||
try {
|
||||
accountsManager.updateCurrentProfileVersion(account.getIdentifier(IdentityType.ACI), version, expectedCurrentVersion, a -> {
|
||||
accountsManager.updateCurrentProfileVersion(account.getAccountIdentifier(), version, expectedCurrentVersion, a -> {
|
||||
|
||||
final List<AccountBadge> updatedBadges = Optional.of(request.getBadgeIdsList())
|
||||
.map(badges -> ProfileHelper.mergeBadgeIdsWithExistingAccountBadges(clock, badgeConfigurationMap, badges,
|
||||
@@ -235,7 +237,7 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
request.getAvatarCredentialsRequest().toByteArray());
|
||||
|
||||
final AvatarUploadCredentialResponse credentialResponse = credentialRequest.issueCredential(
|
||||
new ServiceId.Aci(account.getIdentifier(IdentityType.ACI)),
|
||||
new ServiceId.Aci(account.getAccountIdentifier()),
|
||||
account.getZkCredentialKey().get(),
|
||||
Objects.requireNonNull(account.getZkCredentialKeyRotationId()),
|
||||
clock.instant().truncatedTo(ChronoUnit.DAYS),
|
||||
|
||||
@@ -21,8 +21,10 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialRespons
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfoProvider;
|
||||
import org.whispersystems.textsecuregcm.configuration.BadgeConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPaymentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -41,12 +43,12 @@ public class ProfileHelper {
|
||||
final Map<String, BadgeConfiguration> badgeConfigurationMap,
|
||||
final List<String> badgeIds,
|
||||
final List<AccountBadge> accountBadges) {
|
||||
LinkedHashMap<String, AccountBadge> existingBadges = new LinkedHashMap<>(accountBadges.size());
|
||||
final LinkedHashMap<String, AccountBadge> existingBadges = new LinkedHashMap<>(accountBadges.size());
|
||||
for (final AccountBadge accountBadge : accountBadges) {
|
||||
existingBadges.putIfAbsent(accountBadge.id(), accountBadge);
|
||||
}
|
||||
|
||||
LinkedHashMap<String, AccountBadge> result = new LinkedHashMap<>(accountBadges.size());
|
||||
final LinkedHashMap<String, AccountBadge> result = new LinkedHashMap<>(accountBadges.size());
|
||||
for (final String badgeId : badgeIds) {
|
||||
|
||||
// duplicate in the list, ignore it
|
||||
@@ -104,17 +106,27 @@ public class ProfileHelper {
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public static boolean isPaymentAddressUpdateForbidden(
|
||||
final Account account, final Optional<VersionedProfile> maybeProfile,
|
||||
final Account account,
|
||||
final Optional<VersionedProfile> maybeProfile,
|
||||
final Optional<VersionedProfileV1> maybeV1Profile,
|
||||
final String ipAddress,
|
||||
final AsnInfoProvider asnInfoProvider,
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||
|
||||
final Optional<byte[]> currentPaymentAddress = maybeProfile.map(VersionedProfile::paymentAddress)
|
||||
.or(() -> maybeV1Profile.map(VersionedProfileV1::paymentAddress));
|
||||
final boolean hasDisallowedPrefix = dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration()
|
||||
.getDisallowedPrefixes().stream()
|
||||
.anyMatch(prefix -> account.getNumber().startsWith(prefix));
|
||||
|
||||
return hasDisallowedPrefix && currentPaymentAddress.filter(a -> a.length != 0).isEmpty();
|
||||
final DynamicPaymentsConfiguration paymentsConfiguration =
|
||||
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration();
|
||||
|
||||
final boolean hasDisallowedRegion = account.getNumberOptional()
|
||||
.map(phoneNumber -> paymentsConfiguration.disallowedPrefixes().stream().anyMatch(phoneNumber::startsWith))
|
||||
.orElseGet(() -> asnInfoProvider.lookup(ipAddress)
|
||||
.map(asnInfo -> paymentsConfiguration.disallowedAsnRegions().stream()
|
||||
.anyMatch(region -> region.equalsIgnoreCase(asnInfo.regionCode())))
|
||||
.orElse(false));
|
||||
|
||||
return hasDisallowedRegion && currentPaymentAddress.filter(a -> a.length != 0).isEmpty();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -157,8 +157,9 @@ message SetProfileResponse {
|
||||
// The current data hash did not match the request's expectation, indicating
|
||||
// another device on the account may have written an update the caller does not know about.
|
||||
errors.FailedPrecondition expected_data_write_conflict = 2 [(tag.reason) = "expected_data_write_conflict"];
|
||||
// Payments are not permitted in the account's region, based on its phone number.
|
||||
// The request should be retried without `payment_address.
|
||||
// Payments are not permitted in the account's region, based on its phone
|
||||
// number or the caller's IP address if the account does not have a phone
|
||||
// number. The request should be retried without `payment_address.
|
||||
PaymentsForbiddenInRegion payments_forbidden_in_region = 3 [(tag.reason) = "payments_forbidden_in_region"];
|
||||
// The current version did not match the request's expectation, indicating
|
||||
// another device on the account may have created a new version the caller does not know about.
|
||||
|
||||
+2
-2
@@ -252,7 +252,7 @@ class DynamicConfigurationTest {
|
||||
final DynamicConfiguration emptyConfig =
|
||||
DynamicConfigurationManager.parseConfiguration(emptyConfigYaml, DynamicConfiguration.class).orElseThrow();
|
||||
|
||||
assertTrue(emptyConfig.getPaymentsConfiguration().getDisallowedPrefixes().isEmpty());
|
||||
assertTrue(emptyConfig.getPaymentsConfiguration().disallowedPrefixes().isEmpty());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -266,7 +266,7 @@ class DynamicConfigurationTest {
|
||||
DynamicConfigurationManager.parseConfiguration(paymentsConfigYaml, DynamicConfiguration.class).orElseThrow()
|
||||
.getPaymentsConfiguration();
|
||||
|
||||
assertEquals(List.of("+44"), config.getDisallowedPrefixes());
|
||||
assertEquals(List.of("+44"), config.disallowedPrefixes());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -79,6 +79,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
||||
import org.signal.libsignal.zkgroup.profiles.ServerZkProfileOperations;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfoProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
||||
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
|
||||
@@ -161,6 +162,7 @@ class ProfileControllerTest {
|
||||
rateLimiters,
|
||||
accountsManager,
|
||||
profilesManager,
|
||||
() -> AsnInfoProvider.EMPTY,
|
||||
dynamicConfigurationManager,
|
||||
new ProfileBadgeConverter() {
|
||||
@Override
|
||||
@@ -202,7 +204,7 @@ class ProfileControllerTest {
|
||||
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||
when(dynamicConfiguration.getPaymentsConfiguration()).thenReturn(dynamicPaymentsConfiguration);
|
||||
when(dynamicPaymentsConfiguration.getDisallowedPrefixes()).thenReturn(Collections.emptyList());
|
||||
when(dynamicPaymentsConfiguration.disallowedPrefixes()).thenReturn(Collections.emptyList());
|
||||
|
||||
when(rateLimiters.getProfileLimiter()).thenReturn(rateLimiter);
|
||||
when(rateLimiters.getUsernameLookupLimiter()).thenReturn(usernameRateLimiter);
|
||||
@@ -212,7 +214,6 @@ class ProfileControllerTest {
|
||||
when(profileAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_TWO_IDENTITY_KEY);
|
||||
when(profileAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY);
|
||||
when(profileAccount.getAccountIdentifier()).thenReturn(AuthHelper.VALID_UUID_TWO);
|
||||
when(profileAccount.getIdentifier(IdentityType.ACI)).thenReturn(AuthHelper.VALID_UUID_TWO);
|
||||
when(profileAccount.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI_TWO);
|
||||
when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty());
|
||||
when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH));
|
||||
@@ -223,7 +224,6 @@ class ProfileControllerTest {
|
||||
capabilitiesAccount = mock(Account.class);
|
||||
|
||||
when(capabilitiesAccount.getAccountIdentifier()).thenReturn(AuthHelper.VALID_UUID);
|
||||
when(capabilitiesAccount.getIdentifier(IdentityType.ACI)).thenReturn(AuthHelper.VALID_UUID);
|
||||
when(capabilitiesAccount.getIdentityKey(IdentityType.ACI)).thenReturn(ACCOUNT_IDENTITY_KEY);
|
||||
when(capabilitiesAccount.getIdentityKey(IdentityType.PNI)).thenReturn(ACCOUNT_PHONE_NUMBER_IDENTITY_KEY);
|
||||
|
||||
@@ -806,7 +806,7 @@ class ProfileControllerTest {
|
||||
|
||||
@Test
|
||||
void testSetProfilePaymentAddressCountryNotAllowed() throws InvalidInputException {
|
||||
when(dynamicPaymentsConfiguration.getDisallowedPrefixes())
|
||||
when(dynamicPaymentsConfiguration.disallowedPrefixes())
|
||||
.thenReturn(List.of(AuthHelper.VALID_NUMBER_TWO.substring(0, 3)));
|
||||
|
||||
final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID));
|
||||
@@ -836,7 +836,7 @@ class ProfileControllerTest {
|
||||
@MethodSource
|
||||
void testSetProfilePaymentAddressCountryNotAllowedExistingPaymentAddress(
|
||||
@Nullable final byte[] existingPaymentAddressOnProfile, final byte[] requestPaymentAddress, final boolean expectAllowed) throws InvalidInputException {
|
||||
when(dynamicPaymentsConfiguration.getDisallowedPrefixes())
|
||||
when(dynamicPaymentsConfiguration.disallowedPrefixes())
|
||||
.thenReturn(List.of(AuthHelper.VALID_NUMBER_TWO.substring(0, 3)));
|
||||
|
||||
final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID));
|
||||
|
||||
+8
-7
@@ -81,6 +81,7 @@ import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialRequestContext;
|
||||
import org.signal.libsignal.zkgroup.avatars.AvatarUploadCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfoProvider;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
|
||||
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
|
||||
import org.whispersystems.textsecuregcm.badges.ProfileBadgeConverter;
|
||||
@@ -183,8 +184,7 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
when(dynamicConfiguration.getPaymentsConfiguration()).thenReturn(dynamicPaymentsConfiguration);
|
||||
|
||||
when(account.getAccountIdentifier()).thenReturn(AUTHENTICATED_ACI);
|
||||
when(account.getIdentifier(org.whispersystems.textsecuregcm.identity.IdentityType.ACI)).thenReturn(AUTHENTICATED_ACI);
|
||||
when(account.getNumber()).thenReturn(phoneNumber);
|
||||
when(account.getNumberOptional()).thenReturn(Optional.of(phoneNumber));
|
||||
when(account.getBadges()).thenReturn(Collections.emptyList());
|
||||
when(account.hasCapability(DeviceCapability.PROFILES_V2)).thenReturn(true);
|
||||
|
||||
@@ -198,7 +198,8 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||
when(dynamicConfiguration.getPaymentsConfiguration()).thenReturn(dynamicPaymentsConfiguration);
|
||||
when(dynamicPaymentsConfiguration.getDisallowedPrefixes()).thenReturn(Collections.emptyList());
|
||||
when(dynamicPaymentsConfiguration.disallowedPrefixes()).thenReturn(Collections.emptyList());
|
||||
when(dynamicPaymentsConfiguration.disallowedAsnRegions()).thenReturn(Collections.emptyList());
|
||||
|
||||
when(profilesManager.deleteAvatar(anyString())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
@@ -206,6 +207,7 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
clock,
|
||||
accountsManager,
|
||||
profilesManager,
|
||||
() -> AsnInfoProvider.EMPTY,
|
||||
dynamicConfigurationManager,
|
||||
badgesConfiguration,
|
||||
policyGenerator,
|
||||
@@ -510,10 +512,9 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
.setV1Request(V1_REQUEST)
|
||||
.build();
|
||||
final String disallowedCountryCode = String.format("+%d", disallowedPhoneNumber.getCountryCode());
|
||||
when(dynamicPaymentsConfiguration.getDisallowedPrefixes()).thenReturn(List.of(disallowedCountryCode));
|
||||
when(account.getNumber()).thenReturn(PhoneNumberUtil.getInstance().format(
|
||||
disallowedPhoneNumber,
|
||||
PhoneNumberUtil.PhoneNumberFormat.E164));
|
||||
when(dynamicPaymentsConfiguration.disallowedPrefixes()).thenReturn(List.of(disallowedCountryCode));
|
||||
when(account.getNumberOptional()).thenReturn(Optional.of(PhoneNumberUtil.getInstance().format(
|
||||
disallowedPhoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164)));
|
||||
when(profilesManager.getV1(any(), anyString())).thenReturn(Optional.of(profile));
|
||||
|
||||
final SetProfileResponse response = authenticatedServiceStub().setProfile(request);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfo;
|
||||
import org.whispersystems.textsecuregcm.asn.AsnInfoProvider;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPaymentsConfiguration;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfileV1;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ProfileHelperTest {
|
||||
|
||||
private enum PhoneNumberType {
|
||||
ALLOWED,
|
||||
FORBIDDEN,
|
||||
NONE
|
||||
}
|
||||
|
||||
private enum IpAddressType {
|
||||
ALLOWED,
|
||||
FORBIDDEN,
|
||||
UNRECOGNIZED
|
||||
}
|
||||
|
||||
private enum ExistingPaymentAddressType {
|
||||
CURRENT_PROFILE,
|
||||
LEGACY_PROFILE,
|
||||
NONE
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void isPaymentAddressUpdateForbidden(final PhoneNumberType phoneNumberType,
|
||||
final IpAddressType ipAddressType,
|
||||
final ExistingPaymentAddressType existingPaymentAddressType,
|
||||
final boolean expectForbidden) {
|
||||
|
||||
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
||||
when(dynamicConfiguration.getPaymentsConfiguration())
|
||||
.thenReturn(new DynamicPaymentsConfiguration(List.of("+1"), List.of("US")));
|
||||
|
||||
@SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager =
|
||||
mock(DynamicConfigurationManager.class);
|
||||
|
||||
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
|
||||
switch (phoneNumberType) {
|
||||
case ALLOWED -> when(account.getNumberOptional())
|
||||
.thenReturn(Optional.of(PhoneNumberUtil.getInstance().format(
|
||||
PhoneNumberUtil.getInstance().getExampleNumber("DE"), PhoneNumberUtil.PhoneNumberFormat.E164)));
|
||||
|
||||
case FORBIDDEN -> when(account.getNumberOptional())
|
||||
.thenReturn(Optional.of(PhoneNumberUtil.getInstance().format(
|
||||
PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164)));
|
||||
|
||||
case NONE -> when(account.getNumberOptional()).thenReturn(Optional.empty());
|
||||
}
|
||||
|
||||
final AsnInfoProvider asnInfoProvider = mock(AsnInfoProvider.class);
|
||||
|
||||
switch (ipAddressType) {
|
||||
case ALLOWED -> when(asnInfoProvider.lookup(anyString())).thenReturn(Optional.of(new AsnInfo(123, "DE")));
|
||||
case FORBIDDEN -> when(asnInfoProvider.lookup(anyString())).thenReturn(Optional.of(new AsnInfo(123, "US")));
|
||||
case UNRECOGNIZED -> when(asnInfoProvider.lookup(anyString())).thenReturn(Optional.empty());
|
||||
}
|
||||
|
||||
final Optional<VersionedProfile> maybeProfile = switch (existingPaymentAddressType) {
|
||||
case CURRENT_PROFILE -> Optional.of(new VersionedProfile(TestRandomUtil.nextBytes(16),
|
||||
TestRandomUtil.nextBytes(16),
|
||||
TestRandomUtil.nextBytes(16),
|
||||
TestRandomUtil.nextBytes(16)));
|
||||
case LEGACY_PROFILE, NONE -> Optional.empty();
|
||||
};
|
||||
|
||||
final Optional<VersionedProfileV1> maybeLegacyProfile = switch (existingPaymentAddressType) {
|
||||
case LEGACY_PROFILE -> Optional.of(new VersionedProfileV1("version",
|
||||
TestRandomUtil.nextBytes(16),
|
||||
"avatar",
|
||||
TestRandomUtil.nextBytes(16),
|
||||
TestRandomUtil.nextBytes(16),
|
||||
TestRandomUtil.nextBytes(16),
|
||||
TestRandomUtil.nextBytes(16),
|
||||
TestRandomUtil.nextBytes(16)));
|
||||
case CURRENT_PROFILE, NONE -> Optional.empty();
|
||||
};
|
||||
|
||||
assertEquals(expectForbidden, ProfileHelper.isPaymentAddressUpdateForbidden(account,
|
||||
maybeProfile,
|
||||
maybeLegacyProfile,
|
||||
"127.0.0.1",
|
||||
asnInfoProvider,
|
||||
dynamicConfigurationManager));
|
||||
}
|
||||
|
||||
private static List<Arguments> isPaymentAddressUpdateForbidden() {
|
||||
return List.of(
|
||||
Arguments.argumentSet("Permitted phone number",
|
||||
PhoneNumberType.ALLOWED, IpAddressType.ALLOWED, ExistingPaymentAddressType.NONE, false),
|
||||
|
||||
Arguments.argumentSet("Forbidden phone number",
|
||||
PhoneNumberType.FORBIDDEN, IpAddressType.ALLOWED, ExistingPaymentAddressType.NONE, true),
|
||||
|
||||
Arguments.argumentSet("Forbidden phone number, has existing address",
|
||||
PhoneNumberType.FORBIDDEN, IpAddressType.ALLOWED, ExistingPaymentAddressType.CURRENT_PROFILE, false),
|
||||
|
||||
Arguments.argumentSet("Forbidden phone number, has existing address in legacy profile",
|
||||
PhoneNumberType.FORBIDDEN, IpAddressType.ALLOWED, ExistingPaymentAddressType.LEGACY_PROFILE, false),
|
||||
|
||||
Arguments.argumentSet("No phone number, permitted ASN region",
|
||||
PhoneNumberType.NONE, IpAddressType.ALLOWED, ExistingPaymentAddressType.NONE, false),
|
||||
|
||||
Arguments.argumentSet("No phone number, unrecognized ASN region",
|
||||
PhoneNumberType.NONE, IpAddressType.UNRECOGNIZED, ExistingPaymentAddressType.NONE, false),
|
||||
|
||||
Arguments.argumentSet("No phone number, forbidden ASN region",
|
||||
PhoneNumberType.NONE, IpAddressType.FORBIDDEN, ExistingPaymentAddressType.NONE, true),
|
||||
|
||||
Arguments.argumentSet("No phone number, forbidden ASN region, has existing address",
|
||||
PhoneNumberType.NONE, IpAddressType.FORBIDDEN, ExistingPaymentAddressType.CURRENT_PROFILE, false),
|
||||
|
||||
Arguments.argumentSet("No phone number, forbidden ASN region, has existing address in legacy profile",
|
||||
PhoneNumberType.NONE, IpAddressType.FORBIDDEN, ExistingPaymentAddressType.LEGACY_PROFILE, false)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user