gRPC API for external services credentials service
This commit is contained in:
parent
d0fdae3df7
commit
0b3af7d824
|
@ -121,6 +121,8 @@ import org.whispersystems.textsecuregcm.filters.RequestStatisticsFilter;
|
||||||
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
|
import org.whispersystems.textsecuregcm.filters.TimestampResponseFilter;
|
||||||
import org.whispersystems.textsecuregcm.grpc.AcceptLanguageInterceptor;
|
import org.whispersystems.textsecuregcm.grpc.AcceptLanguageInterceptor;
|
||||||
import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor;
|
import org.whispersystems.textsecuregcm.grpc.ErrorMappingInterceptor;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsAnonymousGrpcService;
|
||||||
|
import org.whispersystems.textsecuregcm.grpc.ExternalServiceCredentialsGrpcService;
|
||||||
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
|
import org.whispersystems.textsecuregcm.grpc.GrpcServerManagedWrapper;
|
||||||
import org.whispersystems.textsecuregcm.grpc.KeysAnonymousGrpcService;
|
import org.whispersystems.textsecuregcm.grpc.KeysAnonymousGrpcService;
|
||||||
import org.whispersystems.textsecuregcm.grpc.KeysGrpcService;
|
import org.whispersystems.textsecuregcm.grpc.KeysGrpcService;
|
||||||
|
@ -647,6 +649,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
new BasicCredentialAuthenticationInterceptor(new BaseAccountAuthenticator(accountsManager));
|
new BasicCredentialAuthenticationInterceptor(new BaseAccountAuthenticator(accountsManager));
|
||||||
|
|
||||||
final ServerBuilder<?> grpcServer = ServerBuilder.forPort(config.getGrpcPort())
|
final ServerBuilder<?> grpcServer = ServerBuilder.forPort(config.getGrpcPort())
|
||||||
|
.addService(ExternalServiceCredentialsGrpcService.createForAllExternalServices(config, rateLimiters))
|
||||||
|
.addService(ExternalServiceCredentialsAnonymousGrpcService.create(accountsManager, config))
|
||||||
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keys, rateLimiters), basicCredentialAuthenticationInterceptor))
|
.addService(ServerInterceptors.intercept(new KeysGrpcService(accountsManager, keys, rateLimiters), basicCredentialAuthenticationInterceptor))
|
||||||
.addService(new KeysAnonymousGrpcService(accountsManager, keys))
|
.addService(new KeysAnonymousGrpcService(accountsManager, keys))
|
||||||
.addService(new PaymentsGrpcService(currencyManager))
|
.addService(new PaymentsGrpcService(currencyManager))
|
||||||
|
|
|
@ -45,7 +45,7 @@ public class ArtController {
|
||||||
public ExternalServiceCredentials getAuth(final @Auth AuthenticatedAccount auth)
|
public ExternalServiceCredentials getAuth(final @Auth AuthenticatedAccount auth)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
final UUID uuid = auth.getAccount().getUuid();
|
final UUID uuid = auth.getAccount().getUuid();
|
||||||
rateLimiters.getArtPackLimiter().validate(uuid);
|
rateLimiters.forDescriptor(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS).validate(uuid);
|
||||||
return artServiceCredentialsGenerator.generateForUuid(uuid);
|
return artServiceCredentialsGenerator.generateForUuid(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ public class SecureBackupController {
|
||||||
}
|
}
|
||||||
final String username = info.credentials().username();
|
final String username = info.credentials().username();
|
||||||
// does this credential match the account id for the e164 provided in the request?
|
// does this credential match the account id for the e164 provided in the request?
|
||||||
boolean match = UUIDUtil.fromStringSafe(username).filter(uuidMatches).isPresent();
|
final boolean match = UUIDUtil.fromStringSafe(username).filter(uuidMatches).isPresent();
|
||||||
return match ? AuthCheckResponse.Result.MATCH : AuthCheckResponse.Result.NO_MATCH;
|
return match ? AuthCheckResponse.Result.MATCH : AuthCheckResponse.Result.NO_MATCH;
|
||||||
}
|
}
|
||||||
)));
|
)));
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.signal.chat.credentials.AuthCheckResult;
|
||||||
|
import org.signal.chat.credentials.CheckSvrCredentialsRequest;
|
||||||
|
import org.signal.chat.credentials.CheckSvrCredentialsResponse;
|
||||||
|
import org.signal.chat.credentials.ReactorExternalServiceCredentialsAnonymousGrpc;
|
||||||
|
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public class ExternalServiceCredentialsAnonymousGrpcService extends
|
||||||
|
ReactorExternalServiceCredentialsAnonymousGrpc.ExternalServiceCredentialsAnonymousImplBase {
|
||||||
|
|
||||||
|
private static final long MAX_SVR_PASSWORD_AGE_SECONDS = TimeUnit.DAYS.toSeconds(30);
|
||||||
|
|
||||||
|
private final ExternalServiceCredentialsGenerator svrCredentialsGenerator;
|
||||||
|
|
||||||
|
private final AccountsManager accountsManager;
|
||||||
|
|
||||||
|
|
||||||
|
public static ExternalServiceCredentialsAnonymousGrpcService create(
|
||||||
|
final AccountsManager accountsManager,
|
||||||
|
final WhisperServerConfiguration chatConfiguration) {
|
||||||
|
return new ExternalServiceCredentialsAnonymousGrpcService(
|
||||||
|
accountsManager,
|
||||||
|
ExternalServiceDefinitions.SVR.generatorFactory().apply(chatConfiguration, Clock.systemUTC())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
ExternalServiceCredentialsAnonymousGrpcService(
|
||||||
|
final AccountsManager accountsManager,
|
||||||
|
final ExternalServiceCredentialsGenerator svrCredentialsGenerator) {
|
||||||
|
this.accountsManager = requireNonNull(accountsManager);
|
||||||
|
this.svrCredentialsGenerator = requireNonNull(svrCredentialsGenerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<CheckSvrCredentialsResponse> checkSvrCredentials(final CheckSvrCredentialsRequest request) {
|
||||||
|
final List<String> tokens = request.getPasswordsList();
|
||||||
|
final List<ExternalServiceCredentialsSelector.CredentialInfo> credentials = ExternalServiceCredentialsSelector.check(
|
||||||
|
tokens,
|
||||||
|
svrCredentialsGenerator,
|
||||||
|
MAX_SVR_PASSWORD_AGE_SECONDS);
|
||||||
|
|
||||||
|
// the username associated with the provided number
|
||||||
|
final Optional<String> matchingUsername = accountsManager
|
||||||
|
.getByE164(request.getNumber())
|
||||||
|
.map(Account::getUuid)
|
||||||
|
.map(svrCredentialsGenerator::generateForUuid)
|
||||||
|
.map(ExternalServiceCredentials::username);
|
||||||
|
|
||||||
|
return Flux.fromIterable(credentials)
|
||||||
|
.reduce(CheckSvrCredentialsResponse.newBuilder(), ((builder, credentialInfo) -> {
|
||||||
|
final AuthCheckResult authCheckResult;
|
||||||
|
if (!credentialInfo.valid()) {
|
||||||
|
authCheckResult = AuthCheckResult.AUTH_CHECK_RESULT_INVALID;
|
||||||
|
} else {
|
||||||
|
final String username = credentialInfo.credentials().username();
|
||||||
|
// does this credential match the account id for the e164 provided in the request?
|
||||||
|
authCheckResult = matchingUsername.map(username::equals).orElse(false)
|
||||||
|
? AuthCheckResult.AUTH_CHECK_RESULT_MATCH
|
||||||
|
: AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH;
|
||||||
|
}
|
||||||
|
return builder.putMatches(credentialInfo.token(), authCheckResult);
|
||||||
|
}))
|
||||||
|
.map(CheckSvrCredentialsResponse.Builder::build);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.signal.chat.credentials.ExternalServiceType;
|
||||||
|
import org.signal.chat.credentials.GetExternalServiceCredentialsRequest;
|
||||||
|
import org.signal.chat.credentials.GetExternalServiceCredentialsResponse;
|
||||||
|
import org.signal.chat.credentials.ReactorExternalServiceCredentialsGrpc;
|
||||||
|
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public class ExternalServiceCredentialsGrpcService extends ReactorExternalServiceCredentialsGrpc.ExternalServiceCredentialsImplBase {
|
||||||
|
|
||||||
|
private final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType;
|
||||||
|
|
||||||
|
private final RateLimiters rateLimiters;
|
||||||
|
|
||||||
|
|
||||||
|
public static ExternalServiceCredentialsGrpcService createForAllExternalServices(
|
||||||
|
final WhisperServerConfiguration chatConfiguration,
|
||||||
|
final RateLimiters rateLimiters) {
|
||||||
|
return new ExternalServiceCredentialsGrpcService(
|
||||||
|
ExternalServiceDefinitions.createExternalServiceList(chatConfiguration, Clock.systemUTC()),
|
||||||
|
rateLimiters
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
ExternalServiceCredentialsGrpcService(
|
||||||
|
final Map<ExternalServiceType, ExternalServiceCredentialsGenerator> credentialsGeneratorByType,
|
||||||
|
final RateLimiters rateLimiters) {
|
||||||
|
this.credentialsGeneratorByType = requireNonNull(credentialsGeneratorByType);
|
||||||
|
this.rateLimiters = requireNonNull(rateLimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<GetExternalServiceCredentialsResponse> getExternalServiceCredentials(final GetExternalServiceCredentialsRequest request) {
|
||||||
|
final ExternalServiceCredentialsGenerator credentialsGenerator = this.credentialsGeneratorByType
|
||||||
|
.get(request.getExternalService());
|
||||||
|
if (credentialsGenerator == null) {
|
||||||
|
return Mono.error(Status.INVALID_ARGUMENT.asException());
|
||||||
|
}
|
||||||
|
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||||
|
return rateLimiters.forDescriptor(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS).validateReactive(authenticatedDevice.accountIdentifier())
|
||||||
|
.then(Mono.fromSupplier(() -> {
|
||||||
|
final ExternalServiceCredentials externalServiceCredentials = credentialsGenerator
|
||||||
|
.generateForUuid(authenticatedDevice.accountIdentifier());
|
||||||
|
return GetExternalServiceCredentialsResponse.newBuilder()
|
||||||
|
.setUsername(externalServiceCredentials.username())
|
||||||
|
.setPassword(externalServiceCredentials.password())
|
||||||
|
.build();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
import org.signal.chat.credentials.ExternalServiceType;
|
||||||
|
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.DirectoryV2ClientConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.PaymentsServiceConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration;
|
||||||
|
|
||||||
|
enum ExternalServiceDefinitions {
|
||||||
|
ART(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART, (chatConfig, clock) -> {
|
||||||
|
final ArtServiceConfiguration cfg = chatConfig.getArtServiceConfiguration();
|
||||||
|
return ExternalServiceCredentialsGenerator
|
||||||
|
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||||
|
.withUserDerivationKey(cfg.userAuthenticationTokenUserIdSecret())
|
||||||
|
.prependUsername(false)
|
||||||
|
.truncateSignature(false)
|
||||||
|
.build();
|
||||||
|
}),
|
||||||
|
DIRECTORY(ExternalServiceType.EXTERNAL_SERVICE_TYPE_DIRECTORY, (chatConfig, clock) -> {
|
||||||
|
final DirectoryV2ClientConfiguration cfg = chatConfig.getDirectoryV2Configuration().getDirectoryV2ClientConfiguration();
|
||||||
|
return ExternalServiceCredentialsGenerator
|
||||||
|
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||||
|
.withUserDerivationKey(cfg.userIdTokenSharedSecret())
|
||||||
|
.prependUsername(false)
|
||||||
|
.withClock(clock)
|
||||||
|
.build();
|
||||||
|
}),
|
||||||
|
PAYMENTS(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, (chatConfig, clock) -> {
|
||||||
|
final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration();
|
||||||
|
return ExternalServiceCredentialsGenerator
|
||||||
|
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||||
|
.prependUsername(true)
|
||||||
|
.build();
|
||||||
|
}),
|
||||||
|
SVR(ExternalServiceType.EXTERNAL_SERVICE_TYPE_SVR, (chatConfig, clock) -> {
|
||||||
|
final SecureValueRecovery2Configuration cfg = chatConfig.getSvr2Configuration();
|
||||||
|
return ExternalServiceCredentialsGenerator
|
||||||
|
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||||
|
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
|
||||||
|
.prependUsername(false)
|
||||||
|
.withDerivedUsernameTruncateLength(16)
|
||||||
|
.withClock(clock)
|
||||||
|
.build();
|
||||||
|
}),
|
||||||
|
STORAGE(ExternalServiceType.EXTERNAL_SERVICE_TYPE_STORAGE, (chatConfig, clock) -> {
|
||||||
|
final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration();
|
||||||
|
return ExternalServiceCredentialsGenerator
|
||||||
|
.builder(cfg.userAuthenticationTokenSharedSecret())
|
||||||
|
.prependUsername(true)
|
||||||
|
.build();
|
||||||
|
}),
|
||||||
|
;
|
||||||
|
|
||||||
|
private final ExternalServiceType externalService;
|
||||||
|
|
||||||
|
private final BiFunction<WhisperServerConfiguration, Clock, ExternalServiceCredentialsGenerator> generatorFactory;
|
||||||
|
|
||||||
|
ExternalServiceDefinitions(
|
||||||
|
final ExternalServiceType externalService,
|
||||||
|
final BiFunction<WhisperServerConfiguration, Clock, ExternalServiceCredentialsGenerator> factory) {
|
||||||
|
this.externalService = requireNonNull(externalService);
|
||||||
|
this.generatorFactory = requireNonNull(factory);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<ExternalServiceType, ExternalServiceCredentialsGenerator> createExternalServiceList(
|
||||||
|
final WhisperServerConfiguration chatConfiguration,
|
||||||
|
final Clock clock) {
|
||||||
|
return Arrays.stream(values())
|
||||||
|
.map(esd -> Pair.of(esd.externalService, esd.generatorFactory().apply(chatConfiguration, clock)))
|
||||||
|
.collect(Collectors.toMap(Pair::getKey, Pair::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public BiFunction<WhisperServerConfiguration, Clock, ExternalServiceCredentialsGenerator> generatorFactory() {
|
||||||
|
return generatorFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalServiceType externalService() {
|
||||||
|
return externalService;
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,7 +33,6 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
|
||||||
TURN("turnAllocate", false, new RateLimiterConfig(60, Duration.ofSeconds(1))),
|
TURN("turnAllocate", false, new RateLimiterConfig(60, Duration.ofSeconds(1))),
|
||||||
PROFILE("profile", false, new RateLimiterConfig(4320, Duration.ofSeconds(20))),
|
PROFILE("profile", false, new RateLimiterConfig(4320, Duration.ofSeconds(20))),
|
||||||
STICKER_PACK("stickerPack", false, new RateLimiterConfig(50, Duration.ofMinutes(72))),
|
STICKER_PACK("stickerPack", false, new RateLimiterConfig(50, Duration.ofMinutes(72))),
|
||||||
ART_PACK("artPack", false, new RateLimiterConfig(50, Duration.ofMinutes(72))),
|
|
||||||
USERNAME_LOOKUP("usernameLookup", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
USERNAME_LOOKUP("usernameLookup", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
||||||
USERNAME_SET("usernameSet", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
USERNAME_SET("usernameSet", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
||||||
USERNAME_RESERVE("usernameReserve", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
USERNAME_RESERVE("usernameReserve", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
||||||
|
@ -49,7 +48,9 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
|
||||||
PUSH_CHALLENGE_ATTEMPT("pushChallengeAttempt", true, new RateLimiterConfig(10, Duration.ofMinutes(144))),
|
PUSH_CHALLENGE_ATTEMPT("pushChallengeAttempt", true, new RateLimiterConfig(10, Duration.ofMinutes(144))),
|
||||||
PUSH_CHALLENGE_SUCCESS("pushChallengeSuccess", true, new RateLimiterConfig(2, Duration.ofHours(12))),
|
PUSH_CHALLENGE_SUCCESS("pushChallengeSuccess", true, new RateLimiterConfig(2, Duration.ofHours(12))),
|
||||||
CREATE_CALL_LINK("createCallLink", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
CREATE_CALL_LINK("createCallLink", false, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
||||||
INBOUND_MESSAGE_BYTES("inboundMessageBytes", true, new RateLimiterConfig(128 * 1024 * 1024, Duration.ofNanos(500_000)));
|
INBOUND_MESSAGE_BYTES("inboundMessageBytes", true, new RateLimiterConfig(128 * 1024 * 1024, Duration.ofNanos(500_000))),
|
||||||
|
EXTERNAL_SERVICE_CREDENTIALS("externalServiceCredentials", true, new RateLimiterConfig(100, Duration.ofMinutes(15))),
|
||||||
|
;
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
||||||
|
@ -157,10 +158,6 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
|
||||||
return forDescriptor(For.STICKER_PACK);
|
return forDescriptor(For.STICKER_PACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
public RateLimiter getArtPackLimiter() {
|
|
||||||
return forDescriptor(For.ART_PACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RateLimiter getUsernameLookupLimiter() {
|
public RateLimiter getUsernameLookupLimiter() {
|
||||||
return forDescriptor(For.USERNAME_LOOKUP);
|
return forDescriptor(For.USERNAME_LOOKUP);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
package org.signal.chat.credentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods for obtaining and verifying credentials for "external" services
|
||||||
|
* (i.e. services that are not a part of the chat server deployment).
|
||||||
|
* All methods of this service require authentication.
|
||||||
|
*/
|
||||||
|
service ExternalServiceCredentials {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates and returns an external service credentials for the caller.
|
||||||
|
*
|
||||||
|
* `UNAUTHENTICATED` status is returned if the call is made on unauthenticated channel.
|
||||||
|
*
|
||||||
|
* `INVALID_ARGUMENT` status is returned if service is not configured for the service type
|
||||||
|
* found in the request OR if `externalService` is not specified in the request.
|
||||||
|
*
|
||||||
|
* `RESOURCE_EXHAUSTED` status is returned if a rate limit for
|
||||||
|
* generating credentials has been exceeded, in which case a
|
||||||
|
* `retry-after` header containing an ISO 8601 duration string will be present
|
||||||
|
* in the response trailers.
|
||||||
|
*/
|
||||||
|
rpc GetExternalServiceCredentials(GetExternalServiceCredentialsRequest)
|
||||||
|
returns (GetExternalServiceCredentialsResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
service ExternalServiceCredentialsAnonymous {
|
||||||
|
/**
|
||||||
|
* Given a list of secure value recovery (SVR) service credentials and a phone number,
|
||||||
|
* checks, which of the provided credetials were generated by the user with the given phone number
|
||||||
|
* and have not yet expired.
|
||||||
|
*
|
||||||
|
* `UNAUTHENTICATED` status is returned if the call is made on unauthenticated channel.
|
||||||
|
*
|
||||||
|
* `INVALID_ARGUMENT` status is returned if request contains more than 10 passwords to be checked.
|
||||||
|
*/
|
||||||
|
rpc CheckSvrCredentials(CheckSvrCredentialsRequest)
|
||||||
|
returns (CheckSvrCredentialsResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExternalServiceType {
|
||||||
|
EXTERNAL_SERVICE_TYPE_UNSPECIFIED = 0;
|
||||||
|
EXTERNAL_SERVICE_TYPE_ART = 1;
|
||||||
|
EXTERNAL_SERVICE_TYPE_DIRECTORY = 2;
|
||||||
|
EXTERNAL_SERVICE_TYPE_PAYMENTS = 3;
|
||||||
|
EXTERNAL_SERVICE_TYPE_STORAGE = 4;
|
||||||
|
EXTERNAL_SERVICE_TYPE_SVR = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetExternalServiceCredentialsRequest {
|
||||||
|
/**
|
||||||
|
* A service to request credentials for.
|
||||||
|
*/
|
||||||
|
ExternalServiceType externalService = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetExternalServiceCredentialsResponse {
|
||||||
|
/**
|
||||||
|
* A username that can be presented to authenticate with the external service.
|
||||||
|
*/
|
||||||
|
string username = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A password that can be presented to authenticate with the external service.
|
||||||
|
*/
|
||||||
|
string password = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AuthCheckResult {
|
||||||
|
AUTH_CHECK_RESULT_UNSPECIFIED = 0;
|
||||||
|
/**
|
||||||
|
* The credentials could be used to make a call to SVR service by the user
|
||||||
|
* associated with the `CheckSvrCredentialsRequest.number` phone number.
|
||||||
|
*/
|
||||||
|
AUTH_CHECK_RESULT_MATCH = 1;
|
||||||
|
/**
|
||||||
|
* The credentials were generated by a different user.
|
||||||
|
*/
|
||||||
|
AUTH_CHECK_RESULT_NO_MATCH = 2;
|
||||||
|
/**
|
||||||
|
* This status indicates that the corresponding credentials token should no longer be used.
|
||||||
|
* This may be because it has expired or invalid, but it can also mean that there is a more
|
||||||
|
* recent token in the request which should be used instead.
|
||||||
|
*/
|
||||||
|
AUTH_CHECK_RESULT_INVALID = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckSvrCredentialsRequest {
|
||||||
|
/**
|
||||||
|
* A phone number in the E164 format to check the passwords against.
|
||||||
|
* Only passwords generated for the user associated with the given number will be marked as `AUTH_CHECK_RESULT_MATCH`.
|
||||||
|
*/
|
||||||
|
string number = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of credentials from previously made calls to `ExternalServiceCredentials.GetExternalServiceCredentials()`
|
||||||
|
* for `EXTERNAL_SERVICE_TYPE_SVR`. This list may contain credentials generated by different users.
|
||||||
|
*/
|
||||||
|
repeated string passwords = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each of the credentials tokens in the `CheckSvrCredentialsRequest` contains the result of the check.
|
||||||
|
*/
|
||||||
|
message CheckSvrCredentialsResponse {
|
||||||
|
|
||||||
|
map<string, AuthCheckResult> matches = 1;
|
||||||
|
}
|
|
@ -7,7 +7,6 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
|
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
@ -23,9 +22,9 @@ import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccou
|
||||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
import org.whispersystems.textsecuregcm.configuration.ArtServiceConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||||
|
|
||||||
@ExtendWith(DropwizardExtensionsSupport.class)
|
@ExtendWith(DropwizardExtensionsSupport.class)
|
||||||
|
@ -33,7 +32,6 @@ class ArtControllerTest {
|
||||||
private static final ArtServiceConfiguration ART_SERVICE_CONFIGURATION = new ArtServiceConfiguration(
|
private static final ArtServiceConfiguration ART_SERVICE_CONFIGURATION = new ArtServiceConfiguration(
|
||||||
randomSecretBytes(32), randomSecretBytes(32), Duration.ofDays(1));
|
randomSecretBytes(32), randomSecretBytes(32), Duration.ofDays(1));
|
||||||
private static final ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator(ART_SERVICE_CONFIGURATION);
|
private static final ExternalServiceCredentialsGenerator artCredentialsGenerator = ArtController.credentialsGenerator(ART_SERVICE_CONFIGURATION);
|
||||||
private static final RateLimiter rateLimiter = mock(RateLimiter.class);
|
|
||||||
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
private static final RateLimiters rateLimiters = mock(RateLimiters.class);
|
||||||
|
|
||||||
private static final ResourceExtension resources = ResourceExtension.builder()
|
private static final ResourceExtension resources = ResourceExtension.builder()
|
||||||
|
@ -47,9 +45,8 @@ class ArtControllerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetAuthToken() {
|
void testGetAuthToken() {
|
||||||
when(rateLimiters.getArtPackLimiter()).thenReturn(rateLimiter);
|
MockUtils.updateRateLimiterResponseToAllow(rateLimiters, RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS, AuthHelper.VALID_UUID);
|
||||||
|
final ExternalServiceCredentials token =
|
||||||
ExternalServiceCredentials token =
|
|
||||||
resources.getJerseyTest()
|
resources.getJerseyTest()
|
||||||
.target("/v1/art/auth")
|
.target("/v1/art/auth")
|
||||||
.request()
|
.request()
|
||||||
|
|
|
@ -268,7 +268,7 @@ abstract class SecureValueRecoveryControllerBaseTest {
|
||||||
return token(credentials(uuid, timeMillis));
|
return token(credentials(uuid, timeMillis));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String token(ExternalServiceCredentials credentials) {
|
private static String token(final ExternalServiceCredentials credentials) {
|
||||||
return credentials.username() + ":" + credentials.password();
|
return credentials.username() + ":" + credentials.password();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.apache.commons.lang3.RandomUtils;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.signal.chat.credentials.AuthCheckResult;
|
||||||
|
import org.signal.chat.credentials.CheckSvrCredentialsRequest;
|
||||||
|
import org.signal.chat.credentials.CheckSvrCredentialsResponse;
|
||||||
|
import org.signal.chat.credentials.ExternalServiceCredentialsAnonymousGrpc;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||||
|
import org.whispersystems.textsecuregcm.util.MutableClock;
|
||||||
|
|
||||||
|
class ExternalServiceCredentialsAnonymousGrpcServiceTest extends
|
||||||
|
SimpleBaseGrpcTest<ExternalServiceCredentialsAnonymousGrpcService, ExternalServiceCredentialsAnonymousGrpc.ExternalServiceCredentialsAnonymousBlockingStub> {
|
||||||
|
|
||||||
|
private static final UUID USER_UUID = UUID.randomUUID();
|
||||||
|
|
||||||
|
private static final String USER_E164 = PhoneNumberUtil.getInstance().format(
|
||||||
|
PhoneNumberUtil.getInstance().getExampleNumber("US"),
|
||||||
|
PhoneNumberUtil.PhoneNumberFormat.E164
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final MutableClock CLOCK = MockUtils.mutableClock(0);
|
||||||
|
|
||||||
|
private static final ExternalServiceCredentialsGenerator SVR_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||||
|
.builder(RandomUtils.nextBytes(32))
|
||||||
|
.withUserDerivationKey(RandomUtils.nextBytes(32))
|
||||||
|
.prependUsername(false)
|
||||||
|
.withDerivedUsernameTruncateLength(16)
|
||||||
|
.withClock(CLOCK)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AccountsManager accountsManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ExternalServiceCredentialsAnonymousGrpcService createServiceBeforeEachTest() {
|
||||||
|
return new ExternalServiceCredentialsAnonymousGrpcService(accountsManager, SVR_CREDENTIALS_GENERATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
Mockito.when(accountsManager.getByE164(USER_E164)).thenReturn(Optional.of(account(USER_UUID)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOneMatch() throws Exception {
|
||||||
|
final UUID user2 = UUID.randomUUID();
|
||||||
|
final UUID user3 = UUID.randomUUID();
|
||||||
|
assertExpectedCredentialCheckResponse(Map.of(
|
||||||
|
token(USER_UUID, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_MATCH,
|
||||||
|
token(user2, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH,
|
||||||
|
token(user3, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH
|
||||||
|
), day(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoMatch() throws Exception {
|
||||||
|
final UUID user2 = UUID.randomUUID();
|
||||||
|
final UUID user3 = UUID.randomUUID();
|
||||||
|
assertExpectedCredentialCheckResponse(Map.of(
|
||||||
|
token(user2, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH,
|
||||||
|
token(user3, day(1)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH
|
||||||
|
), day(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSomeInvalid() throws Exception {
|
||||||
|
final UUID user2 = UUID.randomUUID();
|
||||||
|
final UUID user3 = UUID.randomUUID();
|
||||||
|
final ExternalServiceCredentials user1Cred = credentials(USER_UUID, day(1));
|
||||||
|
final ExternalServiceCredentials user2Cred = credentials(user2, day(1));
|
||||||
|
final ExternalServiceCredentials user3Cred = credentials(user3, day(1));
|
||||||
|
|
||||||
|
final String fakeToken = token(new ExternalServiceCredentials(user2Cred.username(), user3Cred.password()));
|
||||||
|
assertExpectedCredentialCheckResponse(Map.of(
|
||||||
|
token(user1Cred), AuthCheckResult.AUTH_CHECK_RESULT_MATCH,
|
||||||
|
token(user2Cred), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH,
|
||||||
|
fakeToken, AuthCheckResult.AUTH_CHECK_RESULT_INVALID
|
||||||
|
), day(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSomeExpired() throws Exception {
|
||||||
|
final UUID user2 = UUID.randomUUID();
|
||||||
|
final UUID user3 = UUID.randomUUID();
|
||||||
|
assertExpectedCredentialCheckResponse(Map.of(
|
||||||
|
token(USER_UUID, day(100)), AuthCheckResult.AUTH_CHECK_RESULT_MATCH,
|
||||||
|
token(user2, day(100)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH,
|
||||||
|
token(user3, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID,
|
||||||
|
token(user3, day(20)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID
|
||||||
|
), day(110));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSomeHaveNewerVersions() throws Exception {
|
||||||
|
final UUID user2 = UUID.randomUUID();
|
||||||
|
final UUID user3 = UUID.randomUUID();
|
||||||
|
assertExpectedCredentialCheckResponse(Map.of(
|
||||||
|
token(USER_UUID, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID,
|
||||||
|
token(USER_UUID, day(20)), AuthCheckResult.AUTH_CHECK_RESULT_MATCH,
|
||||||
|
token(user2, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH,
|
||||||
|
token(user3, day(20)), AuthCheckResult.AUTH_CHECK_RESULT_NO_MATCH,
|
||||||
|
token(user3, day(10)), AuthCheckResult.AUTH_CHECK_RESULT_INVALID
|
||||||
|
), day(25));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertExpectedCredentialCheckResponse(
|
||||||
|
final Map<String, AuthCheckResult> expected,
|
||||||
|
final long nowMillis) throws Exception {
|
||||||
|
CLOCK.setTimeMillis(nowMillis);
|
||||||
|
final CheckSvrCredentialsRequest request = CheckSvrCredentialsRequest.newBuilder()
|
||||||
|
.setNumber(USER_E164)
|
||||||
|
.addAllPasswords(expected.keySet())
|
||||||
|
.build();
|
||||||
|
final CheckSvrCredentialsResponse response = unauthenticatedServiceStub().checkSvrCredentials(request);
|
||||||
|
final Map<String, AuthCheckResult> matchesMap = response.getMatchesMap();
|
||||||
|
assertEquals(expected, matchesMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String token(final UUID uuid, final long timeMillis) {
|
||||||
|
return token(credentials(uuid, timeMillis));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String token(final ExternalServiceCredentials credentials) {
|
||||||
|
return credentials.username() + ":" + credentials.password();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ExternalServiceCredentials credentials(final UUID uuid, final long timeMillis) {
|
||||||
|
CLOCK.setTimeMillis(timeMillis);
|
||||||
|
return SVR_CREDENTIALS_GENERATOR.generateForUuid(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long day(final int n) {
|
||||||
|
return Duration.ofDays(n).toMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Account account(final UUID uuid) {
|
||||||
|
final Account a = new Account();
|
||||||
|
a.setUuid(uuid);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertRateLimitExceeded;
|
||||||
|
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
|
||||||
|
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusUnauthenticated;
|
||||||
|
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.apache.commons.lang3.RandomUtils;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.signal.chat.credentials.ExternalServiceCredentialsGrpc;
|
||||||
|
import org.signal.chat.credentials.ExternalServiceType;
|
||||||
|
import org.signal.chat.credentials.GetExternalServiceCredentialsRequest;
|
||||||
|
import org.signal.chat.credentials.GetExternalServiceCredentialsResponse;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public class ExternalServiceCredentialsGrpcServiceTest
|
||||||
|
extends SimpleBaseGrpcTest<ExternalServiceCredentialsGrpcService, ExternalServiceCredentialsGrpc.ExternalServiceCredentialsBlockingStub> {
|
||||||
|
|
||||||
|
private static final ExternalServiceCredentialsGenerator ART_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||||
|
.builder(RandomUtils.nextBytes(32))
|
||||||
|
.withUserDerivationKey(RandomUtils.nextBytes(32))
|
||||||
|
.prependUsername(false)
|
||||||
|
.truncateSignature(false)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
private static final ExternalServiceCredentialsGenerator PAYMENTS_CREDENTIALS_GENERATOR = Mockito.spy(ExternalServiceCredentialsGenerator
|
||||||
|
.builder(RandomUtils.nextBytes(32))
|
||||||
|
.prependUsername(true)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RateLimiters rateLimiters;
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ExternalServiceCredentialsGrpcService createServiceBeforeEachTest() {
|
||||||
|
return new ExternalServiceCredentialsGrpcService(Map.of(
|
||||||
|
ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART, ART_CREDENTIALS_GENERATOR,
|
||||||
|
ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR
|
||||||
|
), rateLimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<Arguments> testSuccess() {
|
||||||
|
return Stream.of(
|
||||||
|
Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART, ART_CREDENTIALS_GENERATOR),
|
||||||
|
Arguments.of(ExternalServiceType.EXTERNAL_SERVICE_TYPE_PAYMENTS, PAYMENTS_CREDENTIALS_GENERATOR)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
public void testSuccess(
|
||||||
|
final ExternalServiceType externalServiceType,
|
||||||
|
final ExternalServiceCredentialsGenerator credentialsGenerator) throws Exception {
|
||||||
|
final RateLimiter limiter = mock(RateLimiter.class);
|
||||||
|
doReturn(limiter).when(rateLimiters).forDescriptor(eq(RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS));
|
||||||
|
doReturn(Mono.fromFuture(CompletableFuture.completedFuture(null))).when(limiter).validateReactive(eq(AUTHENTICATED_ACI));
|
||||||
|
final GetExternalServiceCredentialsResponse artResponse = authenticatedServiceStub().getExternalServiceCredentials(
|
||||||
|
GetExternalServiceCredentialsRequest.newBuilder()
|
||||||
|
.setExternalService(externalServiceType)
|
||||||
|
.build());
|
||||||
|
final Optional<Long> artValidation = credentialsGenerator.validateAndGetTimestamp(
|
||||||
|
new ExternalServiceCredentials(artResponse.getUsername(), artResponse.getPassword()));
|
||||||
|
assertTrue(artValidation.isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(ints = { -1, 0, 1000 })
|
||||||
|
public void testUnrecognizedService(final int externalServiceTypeValue) throws Exception {
|
||||||
|
assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||||
|
GetExternalServiceCredentialsRequest.newBuilder()
|
||||||
|
.setExternalServiceValue(externalServiceTypeValue)
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidRequest() throws Exception {
|
||||||
|
assertStatusException(Status.INVALID_ARGUMENT, () -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||||
|
GetExternalServiceCredentialsRequest.newBuilder()
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRateLimitExceeded() throws Exception {
|
||||||
|
final Duration retryAfter = MockUtils.updateRateLimiterResponseToFail(
|
||||||
|
rateLimiters, RateLimiters.For.EXTERNAL_SERVICE_CREDENTIALS, AUTHENTICATED_ACI, Duration.ofSeconds(100), false);
|
||||||
|
Mockito.reset(ART_CREDENTIALS_GENERATOR);
|
||||||
|
assertRateLimitExceeded(
|
||||||
|
retryAfter,
|
||||||
|
() -> authenticatedServiceStub().getExternalServiceCredentials(
|
||||||
|
GetExternalServiceCredentialsRequest.newBuilder()
|
||||||
|
.setExternalService(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART)
|
||||||
|
.build()),
|
||||||
|
ART_CREDENTIALS_GENERATOR
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUnauthenticatedCall() throws Exception {
|
||||||
|
assertStatusUnauthenticated(() -> unauthenticatedServiceStub().getExternalServiceCredentials(
|
||||||
|
GetExternalServiceCredentialsRequest.newBuilder()
|
||||||
|
.setExternalService(ExternalServiceType.EXTERNAL_SERVICE_TYPE_ART)
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `ExternalServiceDefinitions` enum is supposed to have entries for all values in `ExternalServiceType`,
|
||||||
|
* except for the `EXTERNAL_SERVICE_TYPE_UNSPECIFIED` and `UNRECOGNIZED`.
|
||||||
|
* This test makes sure that is the case.
|
||||||
|
*/
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(mode = EnumSource.Mode.EXCLUDE, names = { "UNRECOGNIZED", "EXTERNAL_SERVICE_TYPE_UNSPECIFIED" })
|
||||||
|
public void testHaveExternalServiceDefinitionForServiceTypes(final ExternalServiceType externalServiceType) throws Exception {
|
||||||
|
assertTrue(
|
||||||
|
Arrays.stream(ExternalServiceDefinitions.values()).anyMatch(v -> v.externalService() == externalServiceType),
|
||||||
|
"`ExternalServiceDefinitions` enum entry is missing for the `%s` value of `ExternalServiceType`".formatted(externalServiceType)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,18 @@ public final class GrpcTestUtils {
|
||||||
assertEquals(expected.getCode(), exception.getStatus().getCode());
|
assertEquals(expected.getCode(), exception.getStatus().getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void assertStatusInvalidArgument(final Executable serviceCall) {
|
||||||
|
assertStatusException(Status.INVALID_ARGUMENT, serviceCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertStatusUnauthenticated(final Executable serviceCall) {
|
||||||
|
assertStatusException(Status.UNAUTHENTICATED, serviceCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void assertStatusPermissionDenied(final Executable serviceCall) {
|
||||||
|
assertStatusException(Status.PERMISSION_DENIED, serviceCall);
|
||||||
|
}
|
||||||
|
|
||||||
public static void assertRateLimitExceeded(
|
public static void assertRateLimitExceeded(
|
||||||
final Duration expectedRetryAfter,
|
final Duration expectedRetryAfter,
|
||||||
final Executable serviceCall,
|
final Executable serviceCall,
|
||||||
|
|
Loading…
Reference in New Issue